import { test } from 'node:test' import assert from 'node:assert/strict' import { strToU8, zipSync } from 'fflate' import { createMockFacade } from '../src/facade/mockFacade' import { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from '../src/facade/projectSchema' import { PersistenceWriteQueue, ProjectAutosaveScheduler } from '../src/facade/projectStore' import { DependencyGraph } from '../src/facade/dependencyGraph' import { evaluateQuantityExpression, quantityFromNumber } from '../src/facade/units' import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, matchSubshapes, signatureForEdge, signatureForFace, signatureForVertex } from '../src/facade/topologyNaming' import { createSketch, solveSketch } from '../src/facade/sketcher' import { RecomputeCoordinator } from '../src/facade/recomputeEngine' import { inspectFcstdArchive } from '../src/facade/fcstd' import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime' import type { DocumentSnapshot, ShapeHandle } from '../src/facade/types' const recomputeDocumentFixture = (edges: DocumentSnapshot['dependencies'] = []): DocumentSnapshot => ({ id: 'doc-recompute', label: 'Recompute fixture', version: 1, dirty: true, readOnly: false, units: 'mm', tree: [ { id: 'root', label: 'Root', type: 'feature', state: 'dirty' }, { id: 'child', label: 'Child', type: 'feature', state: 'dirty' }, ], objects: [ { id: 'root', typeId: 'Part::Feature', properties: [] }, { id: 'child', typeId: 'Part::Feature', properties: [] }, ], dependencies: edges, recompute: { generation: 0, status: 'idle', objectStates: { root: 'touched', child: 'touched' }, dirtyObjects: ['root'], order: [], errors: [], }, }) test('facade exposes a stable initial document projection', () => { const facade = createMockFacade() const state = facade.getState() assert.equal(state.apiVersion, '0.1') assert.equal(state.document.id, 'doc-pump-housing') assert.equal(state.document.tree.find((item) => item.id === 'body')?.type, 'body') assert.equal(facade.gui.workbench.getActive(), 'Part Design') assert.equal(facade.geometry.capabilities().provider, 'Bitbybit OCCT') }) test('geometry boundary validates dimensions before invoking OCCT', () => { assert.throws(() => validateBoxInput({ width: 0, length: 2, height: 3, documentId: 'doc', documentVersion: 1 }), /width/) assert.throws(() => validateBoxInput({ width: 1, length: 2, height: 3, center: [0, Number.NaN, 0], documentId: 'doc', documentVersion: 1 }), /center/) assert.doesNotThrow(() => validateBoxInput({ width: 1, length: 2, height: 3, documentId: 'doc', documentVersion: 0 })) assert.throws(() => validateCylinderInput({ radius: 1, height: 2, direction: [0, 0, 0], documentId: 'doc', documentVersion: 1 }), /direction/) assert.throws(() => validateSphereInput({ radius: -1, documentId: 'doc', documentVersion: 1 }), /radius/) assert.throws(() => validateConeInput({ radius1: 0, radius2: 0, height: 2, documentId: 'doc', documentVersion: 1 }), /radius/) assert.throws(() => validatePlacementInput({ shape: { id: 'shape', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 1 }, placement: { translation: [0, 0, 0], rotationAxis: [0, 0, 0], rotationAngle: 0 }, documentId: 'doc', documentVersion: 2 }), /rotationAxis/) }) test('geometry handles reject forged document ownership', () => { const expected: ShapeHandle = { id: 'shape-one', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-one', documentVersion: 4 } assert.doesNotThrow(() => assertShapeHandleIntegrity({ ...expected }, expected)) assert.throws(() => assertShapeHandleIntegrity({ ...expected, documentVersion: 5 }, expected), /integrity/) assert.throws(() => assertShapeHandleIntegrity({ ...expected, id: 'shape-two' }, expected), /integrity/) assert.throws(() => assertShapeHandleIntegrity({ ...expected, documentId: 'doc-two' }, expected), /integrity/) }) test('boolean geometry rejects cross-document and future-version operands', () => { const first: ShapeHandle = { id: 'shape-one', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc-one', documentVersion: 2 } const second: ShapeHandle = { ...first, id: 'shape-two', documentVersion: 3 } assert.doesNotThrow(() => validateBooleanUnionInput({ shapes: [first, second], documentId: 'doc-one', documentVersion: 3 })) assert.throws(() => validateBooleanUnionInput({ shapes: [first, { ...second, documentId: 'doc-two' }], documentId: 'doc-one', documentVersion: 3 }), /another document/) assert.throws(() => validateBooleanUnionInput({ shapes: [first, second], documentId: 'doc-one', documentVersion: 2 }), /newer/) assert.throws(() => validateBooleanUnionInput({ shapes: [first], documentId: 'doc-one', documentVersion: 2 }), /at least 2/) assert.throws(() => validateFilletInput({ base: first, radius: 0, documentId: 'doc-one', documentVersion: 2 }), /radius/) assert.throws(() => validateChamferInput({ base: first, distance: 1, indexes: [-1], documentId: 'doc-one', documentVersion: 2 }), /indexes/) }) test('feature profiles reject degenerate and non-planar geometry', () => { const rectangle = { outer: [[-1, 0, -1], [1, 0, -1], [1, 0, 1], [-1, 0, 1]] as [number, number, number][] } assert.doesNotThrow(() => validatePlanarProfile(rectangle)) assert.doesNotThrow(() => validatePadInput({ profile: rectangle, length: 4, documentId: 'doc', documentVersion: 1 })) assert.throws(() => validatePlanarProfile({ outer: [[0, 0, 0], [1, 0, 0], [2, 0, 0]] }), /collinear/) assert.throws(() => validatePlanarProfile({ outer: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0.1, 1]] }), /coplanar/) assert.throws(() => validateRevolutionInput({ profile: rectangle, axisDirection: [0, 0, 0], documentId: 'doc', documentVersion: 1 }), /axisDirection/) }) test('Bitbybit face meshes are normalized into a facade-owned indexed asset', () => { const shape: ShapeHandle = { id: 'shape-test', kernel: 'bitbybit-occt', kind: 'solid', documentId: 'doc', documentVersion: 7 } const mesh = normalizeBitbybitMesh(shape, { faceList: [{ faceIndex: 1, vertexCoord: [0, 0, 0, 2, 0, 0, 0, 3, 0], normalCoord: [0, 0, 1, 0, 0, 1, 0, 0, 1], triIndexes: [0, 1, 2], numberOfTriangles: 1 }], edgeList: [], pointsList: [], } as never) assert.deepEqual([...mesh.positions], [0, 0, 0, 2, 0, 0, 0, 3, 0]) assert.deepEqual([...mesh.indices], [0, 1, 2]) assert.deepEqual(mesh.bounds, { min: [0, 0, 0], max: [2, 3, 0] }) assert.equal(mesh.topologyVersion, 7) assert.equal(mesh.subshapes?.length, 1) assert.equal(mesh.subshapes?.[0].status, 'stable') assert.throws(() => normalizeBitbybitMesh(shape, { faceList: [{ faceIndex: 2, vertexCoord: [0, 0, 0], normalCoord: [], triIndexes: [0, 1, 0], numberOfTriangles: 1 }], edgeList: [], pointsList: [], } as never), /out-of-range/) }) test('project schema is versioned and covers the FreeCAD document graph', () => { assert.equal(PROJECT_SCHEMA_VERSION, 4) assert.deepEqual(PROJECT_SCHEMA_MIGRATIONS.map((migration) => migration.version), [1, 2, 3, 4]) for (const table of ['projects', 'documents', 'objects', 'object_properties', 'dependencies', 'transactions', 'resources']) assert.match(PROJECT_SCHEMA_SQL, new RegExp(`CREATE TABLE IF NOT EXISTS ${table}`)) assert.match(PROJECT_SCHEMA_SQL, /CREATE INDEX IF NOT EXISTS objects_document_ordinal/) }) test('quantity expressions convert units and reject incompatible dimensions', () => { const result = evaluateQuantityExpression('1 in + 2 mm') assert.equal(result.value.dimension, 'length') assert.ok(Math.abs(result.value.value - 27.4) < 1e-9) const variables = new Map([['pad.Length', { value: 42, dimension: 'length' as const }]]) const reference = evaluateQuantityExpression('pad.Length * 2', variables) assert.deepEqual(reference.references, ['pad.Length']) assert.equal(reference.value.value, 84) assert.throws(() => evaluateQuantityExpression('1 in + 90 deg'), /Cannot add/) assert.throws(() => evaluateQuantityExpression('1 mm / 0'), /Division by zero/) assert.equal(evaluateQuantityExpression('2 + 3').value.value, 5) assert.equal(quantityFromNumber(7).dimension, 'dimensionless') assert.equal(evaluateQuantityExpression('50%').value.dimension, 'percent') assert.ok(Math.abs(evaluateQuantityExpression('sin(90 deg)').value.value - 1) < 1e-12) assert.equal(evaluateQuantityExpression('max(2 mm, 1 cm)').value.value, 10) assert.equal(evaluateQuantityExpression('clamp(15, 0, 10)').value.value, 10) assert.equal(evaluateQuantityExpression('round(2.6)').value.value, 3) assert.equal(evaluateQuantityExpression('2 mm ^ 2').value.dimension, 'area') assert.equal(evaluateQuantityExpression('2 mm ^ 3').value.dimension, 'volume') assert.equal(evaluateQuantityExpression('1 cm2 + 100 mm2').value.value, 200) assert.equal(evaluateQuantityExpression('2 ^ 3').value.value, 8) assert.throws(() => evaluateQuantityExpression('sin(2 mm)'), /angle or dimensionless/) assert.throws(() => evaluateQuantityExpression('unknown(1)'), /Unknown expression function/) assert.throws(() => evaluateQuantityExpression('2 mm ^ 0.5'), /dimensionless integer/) }) test('topology signatures are independent of transient face indexes and flag ambiguity', () => { const square = { vertexCoord: [0, 0, 0, 2, 0, 0, 2, 0, 2, 0, 0, 2], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] } const signature = signatureForFace(square) const sameSignature = signatureForFace({ ...square, triIndexes: [3, 0, 1, 1, 2, 3] }) assert.equal(signature.hash, sameSignature.hash) const first = createSubshapeRefs('shape-a', 1, [square]) assert.equal(first.refs[0].status, 'stable') const duplicate = createSubshapeRefs('shape-b', 2, [square, square]) assert.equal(duplicate.refs[0].status, 'ambiguous') assert.deepEqual(duplicate.refs[0].candidates, [duplicate.refs[0].persistentId, duplicate.refs[1].persistentId]) const matches = matchSubshapes([{ ref: first.refs[0], signature: first.signatures[0] }], [{ ref: duplicate.refs[0], signature: duplicate.signatures[0] }]) assert.equal(matches[0].status, 'stable') assert.equal(matches[0].previousId, first.refs[0].persistentId) }) test('edge and vertex topology signatures are orientation/index independent', () => { const edge = signatureForEdge([0, 0, 0], [2, 0, 0]) assert.equal(edge.hash, signatureForEdge([2, 0, 0], [0, 0, 0]).hash) assert.equal(signatureForVertex([1, 2, 3]).hash, signatureForVertex([1.000001, 2, 3], 1e-5).hash) const face = { vertexCoord: [0, 0, 0, 2, 0, 0, 2, 0, 2, 0, 0, 2], normalCoord: [], triIndexes: [0, 1, 2, 0, 2, 3] } const edges = createEdgeSubshapeRefs('shape-topology', 3, [face]) const vertices = createVertexSubshapeRefs('shape-topology', 3, [face]) assert.equal(edges.refs.length, 5) assert.equal(vertices.refs.length, 4) assert.ok(edges.refs.every((ref) => ref.kind === 'edge' && ref.persistentId.startsWith('topo-edge-'))) assert.ok(vertices.refs.every((ref) => ref.kind === 'vertex' && ref.persistentId.startsWith('topo-vertex-'))) }) test('basic sketch solver applies geometric constraints and reports remaining degrees of freedom', () => { const sketch = createSketch('sketch-test', [{ id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 0 } }], [ { id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' }, { id: 'length-1', type: 'distance', first: { geometryId: 'line-1', point: 'start' }, second: { geometryId: 'line-1', point: 'end' }, value: 5 }, ]) const result = solveSketch(sketch) assert.equal(result.status, 'under-constrained') assert.ok(result.residual <= 1e-7) assert.equal(result.snapshot.geometry[0].type, 'line') assert.equal((result.snapshot.geometry[0] as Extract).end.x, 5) const conflict = solveSketch(createSketch('conflict', [{ id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 1 } }], [ { id: 'block-1', type: 'block', geometryId: 'line-1' }, { id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' }, ])) assert.equal(conflict.status, 'conflicting') assert.ok(conflict.diagnostics.some((diagnostic) => diagnostic.code === 'SOLVER_NOT_CONVERGED')) }) test('Sketcher solver supports diameter, symmetric and tangent constraints at the model boundary', () => { const sketch = createSketch('advanced-constraints', [ { id: 'circle-1', type: 'circle', center: { x: 0, y: 1 }, radius: 1 }, { id: 'line-1', type: 'line', start: { x: -5, y: 0 }, end: { x: 5, y: 0 } }, { id: 'point-a', type: 'point', position: { x: -2, y: 2 } }, { id: 'point-b', type: 'point', position: { x: 4, y: 0 } }, { id: 'center', type: 'point', position: { x: 1, y: 1 } }, ], [ { id: 'diameter-1', type: 'diameter', geometryId: 'circle-1', value: 4 }, { id: 'tangent-1', type: 'tangent', firstGeometryId: 'line-1', secondGeometryId: 'circle-1' }, { id: 'symmetric-1', type: 'symmetric', first: { geometryId: 'point-a', point: 'position' }, second: { geometryId: 'point-b', point: 'position' }, center: { geometryId: 'center', point: 'position' } }, ]) const result = solveSketch(sketch) assert.ok(result.residual <= 1e-7) assert.equal((result.snapshot.geometry.find((geometry) => geometry.id === 'circle-1') as Extract).radius, 2) assert.equal((result.snapshot.geometry.find((geometry) => geometry.id === 'circle-1') as Extract).center.y, 2) }) test('sketcher operations are facade transactions and survive the project fallback store', async () => { const facade = createMockFacade() const before = facade.app.document.getActive().version const added = facade.app.sketcher.addGeometry('sketch', { id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 1, y: 0 } }) assert.equal(added.geometry.length, 1) facade.app.sketcher.addConstraint('sketch', { id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' }) const solved = facade.app.sketcher.solve('sketch') assert.equal(solved.status, 'under-constrained') assert.ok(facade.app.document.getActive().version >= before + 3) assert.match(String(facade.app.document.getObject('sketch')?.properties.find((property) => property.name === 'ConstraintStatus')?.value), /Under-constrained/) await facade.project.save() const loaded = await facade.project.load('doc-pump-housing') assert.equal(loaded?.objects.find((object) => object.id === 'sketch')?.sketch?.constraints[0].id, 'horizontal-1') }) test('Sketcher solve command is selection-aware and emits the standard command lifecycle', () => { const facade = createMockFacade() facade.gui.workbench.setActive('Sketcher') facade.selection.select('sketch') const events: string[] = [] facade.subscribe((event) => { if (event.type.startsWith('command.')) events.push(event.type) }) facade.gui.command.execute({ commandId: 'solve-sketch' }) assert.deepEqual(events, ['command.started', 'command.completed']) assert.equal(facade.app.document.getObject('sketch')?.sketch?.solver.status, 'solved') facade.selection.clear() assert.equal(facade.gui.command.getState('solve-sketch').status, 'disabled') }) test('dependency graph propagates dirty state and orders dependencies', () => { const graph = new DependencyGraph([ { sourceId: 'pocket', targetId: 'pad', relation: 'link' }, { sourceId: 'fillet', targetId: 'pocket', relation: 'link' }, { sourceId: 'body', targetId: 'fillet', relation: 'link' }, ], ['pad', 'pocket', 'fillet', 'body']) const plan = graph.plan(['pad']) assert.deepEqual(plan.cycles, []) assert.deepEqual(plan.order, ['pad', 'pocket', 'fillet', 'body']) assert.deepEqual(plan.levels, [['pad'], ['pocket'], ['fillet'], ['body']]) assert.deepEqual(plan.affected, ['pad', 'pocket', 'fillet', 'body']) graph.addEdge({ sourceId: 'pad', targetId: 'body', relation: 'expression' }) assert.deepEqual(graph.findCycles(new Set(['pad', 'pocket', 'fillet', 'body'])), [['pad', 'pocket', 'fillet', 'body']]) const branched = new DependencyGraph([{ sourceId: 'left', targetId: 'root', relation: 'link' }, { sourceId: 'right', targetId: 'root', relation: 'link' }], ['root', 'left', 'right']) assert.deepEqual(branched.plan(['root']).levels, [['root'], ['left', 'right']]) }) test('generation-aware recompute cancels an older run before accepting its result', async () => { let version = 1 const coordinator = new RecomputeCoordinator(async (object, _document, context) => { if (context.generation === 1) { await new Promise((resolve, reject) => { const timer = setTimeout(resolve, 30) context.signal.addEventListener('abort', () => { clearTimeout(timer) const error = new Error('cancelled') error.name = 'AbortError' reject(error) }, { once: true }) }) } return { status: 'success', updatedObject: object } }, () => version) const document = recomputeDocumentFixture() const first = coordinator.run(document) const second = coordinator.run(document) assert.equal((await first).status, 'cancelled') const accepted = await second assert.equal(accepted.status, 'completed') assert.equal(accepted.generation, 2) assert.deepEqual(accepted.completed, ['root']) version = 2 }) test('recompute rejects results from an obsolete document version', async () => { let version = 1 const coordinator = new RecomputeCoordinator(async () => { version = 2 return { status: 'success' } }, () => version) const result = await coordinator.run(recomputeDocumentFixture()) assert.equal(result.status, 'stale') assert.deepEqual(result.dirtyObjects, ['root']) }) test('recompute failure marks dependent objects as upstream-failed', async () => { const document = recomputeDocumentFixture([{ sourceId: 'child', targetId: 'root', relation: 'link' }]) const coordinator = new RecomputeCoordinator(async (object) => object.id === 'root' ? { status: 'failed', errors: [{ objectId: object.id, code: 'TEST_FAILURE', message: 'expected failure' }] } : { status: 'success' }, () => 1) const result = await coordinator.run(document) assert.equal(result.status, 'failed') assert.deepEqual(result.failed, ['root']) assert.deepEqual(result.skipped, ['child']) assert.equal(result.objectStates.child, 'upstream-failed') assert.ok(result.errors.some((error) => error.code === 'UPSTREAM_FAILED')) }) test('facade async recompute commits only an accepted generation', async () => { const facade = createMockFacade() facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 51 }) const result = await facade.app.document.recomputeAsync() assert.equal(result.status, 'completed') assert.equal(facade.app.document.getActive().recompute?.generation, result.generation) assert.equal(facade.app.document.getActive().recompute?.dirtyObjects.length, 0) }) test('persistence writes are serialized and continue after a failed write', async () => { const queue = new PersistenceWriteQueue() let inFlight = 0 let maximumInFlight = 0 const order: string[] = [] const write = (id: string, shouldFail = false) => queue.run(async () => { inFlight += 1 maximumInFlight = Math.max(maximumInFlight, inFlight) await new Promise((resolve) => setTimeout(resolve, 2)) order.push(id) inFlight -= 1 if (shouldFail) throw new Error('expected write failure') return id }) const results = await Promise.allSettled([write('one'), write('two', true), write('three')]) assert.equal(maximumInFlight, 1) assert.deepEqual(order, ['one', 'two', 'three']) assert.equal(results[0].status, 'fulfilled') assert.equal(results[1].status, 'rejected') assert.equal(results[2].status, 'fulfilled') }) test('autosave scheduler coalesces idle document changes and flushes the latest version', async () => { const savedVersions: number[] = [] const scheduler = new ProjectAutosaveScheduler(async (document) => { savedVersions.push(document.version); return { documentId: document.id, documentVersion: document.version, persistedAt: Date.now(), mode: 'sqlite-memory' } }, 5) const document = (version: number) => ({ id: 'doc', label: 'Autosave', version, dirty: true, readOnly: false, units: 'mm', tree: [], objects: [] }) scheduler.schedule(document(1)) scheduler.schedule(document(2)) await new Promise((resolve) => setTimeout(resolve, 15)) assert.deepEqual(savedVersions, [2]) scheduler.schedule(document(3)) const result = await scheduler.flush() assert.equal(result?.documentVersion, 3) scheduler.cancel() }) test('workbench and selection changes are event driven', () => { const facade = createMockFacade() const events: string[] = [] const unsubscribe = facade.subscribe((event) => events.push(event.type)) facade.gui.workbench.setActive('Sketcher') facade.selection.select('sketch') unsubscribe() assert.equal(facade.getState().activeWorkbench, 'Sketcher') assert.equal(facade.selection.getObjectId(), 'sketch') assert.ok(events.includes('state.changed')) assert.ok(events.includes('notice')) }) test('commands and tasks preserve apply/cancel lifecycle', () => { const facade = createMockFacade() facade.gui.command.execute({ commandId: 'create-sketch' }) assert.equal(facade.task.getActive()?.status, 'preview') facade.task.update({ support: 'XY_Plane' }) assert.equal(facade.task.getActive()?.draft.support, 'XY_Plane') facade.task.cancel() assert.equal(facade.task.getActive()?.status, 'cancelled') }) test('disabled commands return a reason instead of mutating the document', () => { const facade = createMockFacade() const before = facade.getState().document.version const state = facade.gui.command.getState('pad') assert.equal(state.status, 'enabled') facade.selection.clear() facade.gui.workbench.setActive('Sketcher') const disabled = facade.gui.command.getState('pad') assert.equal(disabled.status, 'disabled') assert.match(disabled.reason || '', /Part Design|compatible object/) facade.gui.command.execute({ commandId: 'pad' }) assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_DISABLED') assert.equal(facade.getState().document.version, before) }) test('manifest commands without a Bitbybit executor are explicitly unsupported', () => { const facade = createMockFacade() const before = facade.getState().document.version const state = facade.gui.command.getState('linear-pattern') assert.equal(state.status, 'disabled') assert.match(state.reason || '', /business executor/) facade.gui.command.execute({ commandId: 'linear-pattern' }) assert.equal(facade.getState().diagnostics.at(-1)?.code, 'COMMAND_UNIMPLEMENTED') assert.equal(facade.getState().document.version, before) }) test('command events carry a document-scoped request context', () => { const facade = createMockFacade() const events: string[] = [] let requestId = '' facade.subscribe((event) => { events.push(event.type); if (event.type === 'command.started') requestId = event.context.requestId }) facade.gui.command.execute({ commandId: 'create-sketch' }) assert.ok(events.includes('command.started')) assert.ok(events.includes('command.completed')) assert.match(requestId, /^req-/) }) test('document mutations use the facade history boundary', () => { const facade = createMockFacade() facade.gui.command.execute({ commandId: 'new-document' }) assert.equal(facade.app.document.getActive().label, 'Untitled document') assert.equal(facade.history.canUndo(), true) facade.history.undo() assert.equal(facade.app.document.getActive().label, 'Pump Housing') assert.equal(facade.history.canRedo(), true) facade.history.redo() assert.equal(facade.app.document.getActive().label, 'Untitled document') }) test('typed properties are validated, versioned and undoable through the document boundary', () => { const facade = createMockFacade() const before = facade.app.document.getActive().version assert.equal(facade.app.document.getObject('pad')?.properties.find((property) => property.name === 'Length')?.value, 42) facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 50 }) assert.equal(facade.app.document.getActive().version, before + 1) assert.equal(facade.app.document.getActive().tree.find((item) => item.id === 'pad')?.state, 'dirty') assert.equal(facade.app.document.getObject('pad')?.properties.find((property) => property.name === 'Status')?.value, 'Touched') assert.throws(() => facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: -1 }), /negative/) assert.equal(facade.app.document.getActive().version, before + 1) facade.history.undo() assert.equal(facade.app.document.getObject('pad')?.properties.find((property) => property.name === 'Length')?.value, 42) facade.history.redo() assert.equal(facade.app.document.getObject('pad')?.properties.find((property) => property.name === 'Length')?.value, 50) }) test('expressions are evaluated in the facade and create document dependencies', () => { const facade = createMockFacade() facade.app.document.setExpression({ objectId: 'pad', propertyName: 'Length', expression: '1 in + 2 mm' }) const property = facade.app.document.getObject('pad')?.properties.find((candidate) => candidate.name === 'Length') assert.equal(property?.expression, '1 in + 2 mm') assert.ok(Math.abs(Number(property?.value) - 27.4) < 1e-9) assert.equal(facade.getState().document.recompute?.objectStates.pad, 'touched') facade.app.document.setExpression({ objectId: 'pocket', propertyName: 'Length', expression: 'pad.Length * 2' }) const edge = facade.app.document.getDependencies().find((candidate) => candidate.sourceId === 'pocket' && candidate.targetId === 'pad' && candidate.relation === 'expression') assert.equal(edge?.propertyName, 'Length') const result = facade.app.document.recompute() assert.equal(result.status, 'completed') assert.deepEqual(result.order, ['pad', 'pocket', 'fillet', 'body']) assert.equal(facade.getState().document.recompute?.dirtyObjects.length, 0) assert.equal(facade.app.document.getObject('pocket')?.properties.find((candidate) => candidate.name === 'Length')?.value, 54.8) }) test('expression cycles fail recompute without silently accepting a result', () => { const facade = createMockFacade() facade.app.document.setExpression({ objectId: 'pad', propertyName: 'Length', expression: 'pocket.Length' }) facade.app.document.setExpression({ objectId: 'pocket', propertyName: 'Length', expression: 'pad.Length' }) const result = facade.app.document.recompute() assert.equal(result.status, 'failed') assert.ok(result.errors.every((error) => error.code === 'DEPENDENCY_CYCLE')) assert.equal(facade.getState().document.recompute?.objectStates.pad, 'error') assert.equal(facade.getState().document.recompute?.objectStates.pocket, 'error') }) test('view properties do not mark the geometry object touched', () => { const facade = createMockFacade() facade.app.document.setProperty({ objectId: 'fillet', propertyName: 'Visibility', value: false }) assert.equal(facade.app.document.getActive().tree.find((item) => item.id === 'fillet')?.state, 'valid') assert.equal(facade.app.document.getObject('fillet')?.properties.find((property) => property.name === 'Visibility')?.value, false) }) test('project persistence remains behind the facade contract', async () => { const facade = createMockFacade() facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 55 }) const saved = await facade.project.save() assert.equal(saved.documentId, 'doc-pump-housing') const loaded = await facade.project.load('doc-pump-housing') assert.equal(loaded?.label, 'Pump Housing') assert.equal(loaded?.objects.find((object) => object.id === 'pad')?.properties.find((property) => property.name === 'Length')?.value, 55) assert.equal(facade.project.capabilities().mode, 'sqlite-memory') const recovery = await facade.project.recovery('doc-pump-housing') assert.equal(recovery.integrity, 'ok') assert.equal(recovery.lastSavedVersion, loaded?.version) }) test('recovery report identifies missing snapshots in the transient fallback store', async () => { const facade = createMockFacade() const recovery = await facade.project.recovery('missing-document') assert.equal(recovery.integrity, 'unavailable') assert.equal(recovery.lastSavedVersion, null) assert.ok(recovery.warnings.some((warning) => /No saved snapshot/.test(warning))) }) test('FCStd inspection reports recognized, proxy, and Python-backed objects without executing code', () => { const documentXml = ` ` const archive = zipSync({ 'Document.xml': strToU8(documentXml), 'GuiDocument.xml': strToU8(''), 'Macro/unsafe.py': strToU8('raise RuntimeError("must not run")'), }) const inspection = inspectFcstdArchive(archive) assert.equal(inspection.label, 'Imported assembly') assert.equal(inspection.schemaVersion, '4') assert.equal(inspection.objects.find((object) => object.name === 'Body')?.label, 'Main body') assert.equal(inspection.compatibility.recognizedObjects, 1) assert.equal(inspection.compatibility.proxyObjects, 1) assert.equal(inspection.compatibility.blockedObjects, 1) assert.equal(inspection.compatibility.level, 'blocked') assert.equal(inspection.compatibility.codeExecutionBlocked, true) assert.deepEqual(inspection.compatibility.unknownTypeIds, ['Vendor::CustomFeature']) assert.ok(inspection.entries.some((entry) => entry.role === 'script')) assert.equal(inspection.proxyDocument.readOnly, true) assert.equal(inspection.proxyDocument.objects.length, 3) assert.equal(inspection.proxyDocument.objects[1].properties.find((property) => property.name === 'ImportSupport')?.value, 'proxy') }) test('FCStd inspection rejects missing metadata, traversal paths, and suspicious compression ratios', () => { assert.throws(() => inspectFcstdArchive(zipSync({ 'GuiDocument.xml': strToU8('') })), /Document\.xml/) assert.throws(() => inspectFcstdArchive(zipSync({ '../Document.xml': strToU8('') })), /Unsafe FCStd entry path/) const compressed = zipSync({ 'Document.xml': strToU8(`${' '.repeat(20_000)}`) }) assert.throws(() => inspectFcstdArchive(compressed, { maxCompressionRatio: 2 }), /compression ratio/) }) test('FCStd inspection is available only through the facade project boundary', () => { const facade = createMockFacade() const archive = zipSync({ 'Document.xml': strToU8('') }) const inspection = facade.project.fcstd.inspect(archive) assert.equal(inspection.format, 'FCStd') assert.equal(inspection.compatibility.level, 'metadata-compatible') assert.equal(inspection.compatibility.readOnly, true) }) test('project resources use content identity and reference counting', async () => { const facade = createMockFacade() const bytes = new Uint8Array([1, 2, 3, 5, 8]) const first = await facade.project.resource.put(bytes, 'application/octet-stream') const second = await facade.project.resource.put(bytes, 'application/octet-stream') assert.equal(first.hash, second.hash) assert.equal(second.refCount, 2) bytes[0] = 99 assert.deepEqual([...((await facade.project.resource.get(first.hash)) || [])], [1, 2, 3, 5, 8]) await facade.project.resource.release(first.hash) assert.deepEqual([...((await facade.project.resource.get(first.hash)) || [])], [1, 2, 3, 5, 8]) await facade.project.resource.release(first.hash) assert.equal(await facade.project.resource.get(first.hash), null) }) test('Part Design feature tasks commit a document object and remain undoable', () => { const facade = createMockFacade() const before = facade.app.document.getActive() facade.gui.command.execute({ commandId: 'pad' }) assert.equal(facade.task.getActive()?.commandId, 'pad') facade.task.update({ length: 42, reversed: false }) facade.task.apply() const committed = facade.app.document.getActive() const created = committed.tree.find((item) => item.id === 'pad001') assert.equal(created?.label, 'Pad') assert.equal(committed.version, before.version + 1) assert.equal(facade.selection.getObjectId(), 'pad001') assert.equal(committed.dirty, true) facade.history.undo() assert.equal(facade.app.document.getActive().tree.some((item) => item.id === 'pad001'), false) facade.history.redo() assert.equal(facade.app.document.getActive().tree.some((item) => item.id === 'pad001'), true) })