feat: complete production naming and reference lifecycle gates
This commit is contained in:
39
tests/externalPartDesignLinks.test.ts
Normal file
39
tests/externalPartDesignLinks.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { ExternalPartDesignLinkRegistry, type ExternalSourceEvidence } from '../src/facade/externalPartDesignLinks'
|
||||
|
||||
const source = (revision = 1): ExternalSourceEvidence => ({
|
||||
documentId: 'XLinkSource',
|
||||
path: '/projects/XLinkSource.FCStd',
|
||||
revision,
|
||||
objects: {
|
||||
SourceBox: { objectId: 'SourceBox', length: revision === 1 ? 4 : 8, width: revision === 1 ? 5 : 9, height: 6, volume: revision === 1 ? 120 : 432, surfaceArea: revision === 1 ? 148 : 348, shapeId: `source-${revision}` },
|
||||
SecondBox: { objectId: 'SecondBox', length: 3, width: 7, height: revision === 1 ? 2 : 4, volume: revision === 1 ? 42 : 84, surfaceArea: revision === 1 ? 82 : 122, shapeId: `second-${revision}` },
|
||||
},
|
||||
})
|
||||
|
||||
test('external PartDesign links close, cache, relink and preserve history', () => {
|
||||
const registry = new ExternalPartDesignLinkRegistry(source())
|
||||
assert.equal(registry.snapshot().properties.XLinkSubList.length, 2)
|
||||
registry.closeSource()
|
||||
assert.deepEqual(registry.snapshot().properties.XLinkList, [])
|
||||
assert.equal(registry.snapshot().binder.cacheState, 'cached')
|
||||
registry.markSourceMissing()
|
||||
const restored = ExternalPartDesignLinkRegistry.deserialize(registry.serialize())
|
||||
assert.equal(restored.snapshot().source.status, 'missing')
|
||||
restored.relinkSource(source())
|
||||
restored.editSource(source(2))
|
||||
assert.equal(restored.snapshot().binder.area, 54)
|
||||
restored.undo()
|
||||
assert.equal(restored.snapshot().binder.area, 30)
|
||||
restored.redo()
|
||||
assert.equal(restored.snapshot().binder.area, 54)
|
||||
})
|
||||
|
||||
test('external PartDesign links reject divergent relink and unresolved edits', () => {
|
||||
const registry = new ExternalPartDesignLinkRegistry(source())
|
||||
registry.closeSource()
|
||||
assert.throws(() => registry.editSource(source(2)), /open and resolved/)
|
||||
assert.throws(() => registry.relinkSource({ ...source(), path: '/projects/Other.FCStd' }), /identity/)
|
||||
assert.throws(() => new ExternalPartDesignLinkRegistry({ ...source(), objects: { ...source().objects, SourceBox: { ...source().objects.SourceBox, volume: 1 } } }), /geometry evidence/)
|
||||
})
|
||||
@@ -17,7 +17,7 @@ import { runTopologyMutationReplay } from '../src/facade/topologyReplay'
|
||||
import { captureNativeTopologyHistory, captureNativeTopologyHistoryStages, captureSignatureTopologyHistory, composeNativeTopologyHistoryLineage } from '../src/facade/topologyHistory'
|
||||
import { createNativeOcctStepHistoryBridge, mapNativeOcctHistoryRecords } from '../src/facade/nativeHistoryProvider'
|
||||
import { DirectNativeOcctHistoryProvider, NativeOcctHistoryCoordinator, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION, nativeNamingAbiCapabilities, type NativeOcctHistoryProvider } from '../src/facade/nativeHistoryProtocol'
|
||||
import { probeFreeCadPrivateNamingAbi } from '../src/facade/nativeNamingAbi'
|
||||
import { captureFreeCadPrivateNamingEvidence, classifyFreeCadPrivateNamingHistory, createFreeCadPrivateNamingAbiRequest, probeFreeCadPrivateNamingAbi, selectFreeCadPrivateNamingHistoryRecords } from '../src/facade/nativeNamingAbi'
|
||||
import { NativeOcctHistoryWorkerProvider } from '../src/facade/nativeHistoryWorkerClient'
|
||||
import { assessResourceQuota, planResourceSweep } from '../src/facade/resourcePolicy'
|
||||
import { applySketchAutoConstraints, cloneSketch, createSketch, deleteSketchGeometry, dragSketchPoint, editBsplineGeometry, editSketchBspline, extendSketchLine, FREECAD_SKETCHER_CONSTRAINT_TYPES, FREECAD_SKETCHER_GEOMETRY_TYPES, FREECAD_SKETCHER_INTERNAL_ALIGNMENT_TYPES, replaySketchEditorEvents, setSketchConstruction, SketchEditorInteractionSession, sketchGeometrySignature, solveSketch, splitSketchLine, suggestSketchAutoConstraints, trimSketchLine, validateSketchGeometry, type SketchGeometry } from '../src/facade/sketcher'
|
||||
@@ -705,6 +705,79 @@ test('native naming ABI capabilities forbid synthetic FreeCAD tokens unless a pr
|
||||
assert.deepEqual(nativeNamingAbiCapabilities({ ...base, naming: { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' } }), { mappedNameRef: 'optional', stringHasher: 'opaque-preserved', elementMap2: 'opaque-preserved', tokenGeneration: 'native-only' })
|
||||
})
|
||||
|
||||
test('FreeCAD naming ABI excludes native history sources without an external transport input', () => {
|
||||
const records = [
|
||||
{ relation: 'modified' as const, source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
|
||||
{ relation: 'generated' as const, source: 'tool', kind: 'face', sourceIndex: 0, resultIndex: 1 },
|
||||
{ relation: 'modified' as const, source: 'base-input', kind: 'edge', sourceIndex: 0, resultIndex: 2 },
|
||||
{ relation: 'generated' as const, source: 'object', sourceId: 'native-cylinder', kind: 'face', sourceIndex: 0, resultIndex: 3 },
|
||||
]
|
||||
const baseInput = { inputId: 'base-input', objectId: 'base', role: 'object', step: 'ISO-10303-21; base', objectTag: 17 }
|
||||
assert.deepEqual(selectFreeCadPrivateNamingHistoryRecords(records, [baseInput]), [records[0], records[2]])
|
||||
assert.deepEqual(selectFreeCadPrivateNamingHistoryRecords(records, [baseInput, { inputId: 'tool-input', role: 'tool' }]), [records[0], records[1], records[2]])
|
||||
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'hole-naming', documentId: 'doc', documentVersion: 1, operationId: 'hole', operation: 'hole', stageId: 'hole:stage', resultObjectId: 'hole:result', resultObjectTag: 23,
|
||||
inputs: [baseInput], stages: [{ stageId: 'hole:stage', operation: 'hole', inputIds: ['base-input'], ordinal: 0 }],
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', records, hasModified: true, hasGenerated: true, hasDeleted: false },
|
||||
})
|
||||
assert.deepEqual(request.history.records.map(({ source, resultIndex }) => ({ source, resultIndex })), [{ source: 'object', resultIndex: 0 }, { source: 'base-input', resultIndex: 2 }])
|
||||
})
|
||||
|
||||
test('FreeCAD naming ABI preserves non-unique native provenance instead of selecting the first source', () => {
|
||||
const inputs = [
|
||||
{ inputId: 'base', objectId: 'base-object', role: 'object', stageId: 'base-stage', step: 'ISO-10303-21; base', objectTag: 17 },
|
||||
{ inputId: 'tool', objectId: 'tool-object', role: 'tool', stageId: 'tool-stage', step: 'ISO-10303-21; tool', objectTag: 19 },
|
||||
]
|
||||
const records = [
|
||||
{ relation: 'modified' as const, source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 },
|
||||
{ relation: 'modified' as const, source: 'tool', kind: 'face', sourceIndex: 0, resultIndex: 0 },
|
||||
{ relation: 'generated' as const, source: 'object', kind: 'edge', sourceIndex: 1, resultIndexes: [1, 1] },
|
||||
{ relation: 'generated' as const, source: 'native-profile', sourceId: 'native-profile', kind: 'edge', sourceIndex: 0, resultIndex: 2 },
|
||||
]
|
||||
const classification = classifyFreeCadPrivateNamingHistory(records, inputs)
|
||||
assert.deepEqual(classification.records.map(({ kind, resultIndex }) => ({ kind, resultIndex })), [{ kind: 'edge', resultIndex: 1 }])
|
||||
assert.equal(classification.externalRecordCount, 3)
|
||||
assert.equal(classification.internalRecordCount, 1)
|
||||
assert.equal(classification.duplicateRecordCount, 1)
|
||||
assert.equal(classification.relationConflictCount, 0)
|
||||
assert.deepEqual(classification.ambiguities, [{
|
||||
resultKind: 'face', resultIndex: 0, candidates: [
|
||||
{ inputId: 'base', objectId: 'base-object', persistentId: 'Face1', stageId: 'base-stage', relation: 'modified', sourceKind: 'face', sourceIndex: 0 },
|
||||
{ inputId: 'tool', objectId: 'tool-object', persistentId: 'Face1', stageId: 'tool-stage', relation: 'modified', sourceKind: 'face', sourceIndex: 0 },
|
||||
],
|
||||
}])
|
||||
|
||||
const request = createFreeCadPrivateNamingAbiRequest({
|
||||
requestId: 'ambiguous', documentId: 'doc', documentVersion: 1, operationId: 'common', operation: 'common', stageId: 'common:stage', resultObjectId: 'common:result', resultObjectTag: 23,
|
||||
inputs, stages: [{ stageId: 'common:stage', operation: 'common', inputIds: ['base', 'tool'], ordinal: 0 }],
|
||||
history: { provider: 'occt-native', occtVersion: '8.0.0', records: records.slice(0, 2), hasModified: true, hasGenerated: false, hasDeleted: false },
|
||||
})
|
||||
let called = false
|
||||
const module = {
|
||||
freecadNamingAbiVersion: () => 1,
|
||||
freecadNamingCapabilitiesJson: () => JSON.stringify({ schemaVersion: 1, freecadVersion: '1.1.1', sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', mappedNameRef: true, stringHasher: true, elementMap2: true, operations: ['common'] }),
|
||||
freecadNamingEvidenceJson: () => { called = true; return '{}' },
|
||||
}
|
||||
const evidence = captureFreeCadPrivateNamingEvidence(module, request)
|
||||
assert.equal(called, false)
|
||||
assert.equal(evidence?.status, 'ambiguous')
|
||||
assert.deepEqual(evidence?.mappedNames?.[0].candidates, [
|
||||
{ objectId: 'base-object', persistentId: 'Face1', stageId: 'base-stage' },
|
||||
{ objectId: 'tool-object', persistentId: 'Face1', stageId: 'tool-stage' },
|
||||
])
|
||||
|
||||
const downstream = classifyFreeCadPrivateNamingHistory(
|
||||
[{ relation: 'modified', source: 'object', kind: 'face', sourceIndex: 0, resultIndex: 0 }],
|
||||
[{ inputId: 'object', objectId: 'common:result', role: 'object', stageId: 'common:stage', step: 'ISO-10303-21; common', namingEvidence: evidence }],
|
||||
)
|
||||
assert.equal(downstream.records.length, 0)
|
||||
assert.deepEqual(downstream.ambiguities[0].candidates.map(({ objectId, persistentId }) => ({ objectId, persistentId })), [
|
||||
{ objectId: 'base-object', persistentId: 'Face1' },
|
||||
{ objectId: 'tool-object', persistentId: 'Face1' },
|
||||
])
|
||||
})
|
||||
|
||||
test('versioned FreeCAD private naming ABI is capability-gated and attaches validated native evidence', async () => {
|
||||
const elementMap2 = parseElementMap2([
|
||||
'BeginElementMap v1', '1 PostfixCount 1', 'Edge', 'MapCount 1', 'ElementMap 1 1 1', 'Edge',
|
||||
@@ -737,7 +810,13 @@ test('versioned FreeCAD private naming ABI is capability-gated and attaches vali
|
||||
assert.equal(capturedRequest?.resultObjectTag, 23)
|
||||
assert.equal((capturedRequest?.inputs as Array<{ objectTag?: number; namingEvidence?: { stageId: string } }>)[0].objectTag, 17)
|
||||
assert.equal((capturedRequest?.inputs as Array<{ objectId?: string }>)[0].objectId, 'base')
|
||||
assert.equal((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string } }>)[0].namingEvidence?.stageId, 'source:stage:0')
|
||||
assert.deepEqual((capturedRequest?.inputs as Array<{ namingEvidence?: { stageId: string; status: string; mappedNames?: unknown[] } }>)[0].namingEvidence, {
|
||||
schemaVersion: 1,
|
||||
stageId: 'source:stage:0',
|
||||
resultObjectId: 'source',
|
||||
status: 'final-shape-only',
|
||||
reason: 'Native provider returned final Shape without MappedNameRef/StringHasher evidence.',
|
||||
})
|
||||
assert.notEqual((capturedRequest?.inputs as Array<{ namingEvidence?: unknown }>)[0].namingEvidence, inputEvidence)
|
||||
await assert.rejects(() => provider.capture({ protocolVersion: 1, requestId: 'naming-invalid-tag', documentId: 'doc', documentVersion: 2, operationId: 'cut-native', operation: 'cut', objectStep: 'ISO-10303-21; object', toolStep: 'ISO-10303-21; tool', resultObjectTag: 0 }, new AbortController().signal), /positive safe integer/)
|
||||
|
||||
@@ -4974,6 +5053,11 @@ test('FCStd LinkSub uses native multi-subelement XML and round-trips without JSO
|
||||
assert.match(writtenXml, /<LinkSub value="pad" count="2"><Sub value="Edge1"\/><Sub value="Edge7"\/><\/LinkSub>/)
|
||||
const roundTripped = inspectFcstdArchive(zipSync({ 'Document.xml': written })).objects.find((object) => object.name === 'fillet')?.properties[0]
|
||||
assert.deepEqual(roundTripped?.subElements, ['Edge1', 'Edge7'])
|
||||
const edgeRef: TopoRefValue = { schemaVersion: 1, objectId: 'pad', kind: 'edge', persistentId: 'Edge1', topologyVersion: 2, generation: 1, status: 'stable' }
|
||||
const withRef = (ref: TopoRefValue) => serializeFcstdMetadataArchive({ ...document, objects: [document.objects[0], { ...document.objects[1], properties: [{ ...document.objects[1].properties[0], value: { schemaVersion: 1, objectId: 'pad', subElements: [ref] } }] }] })
|
||||
assert.match(new TextDecoder().decode(unzipSync(withRef(edgeRef))['Document.xml']), /<Sub value="Edge1"\/>/)
|
||||
assert.throws(() => withRef({ ...edgeRef, status: 'ambiguous', candidates: ['Edge1', 'Edge2'] }), /stable topology status/)
|
||||
assert.throws(() => withRef({ ...edgeRef, persistentId: 'web-edge-id' }), /lossless native EdgeN/)
|
||||
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(xml.replace('count="2"', 'count="3"')) })), /LinkSub count/)
|
||||
})
|
||||
|
||||
@@ -4990,6 +5074,9 @@ test('FCStd LinkSubList uses native obj/sub children and preserves structured en
|
||||
const summary = inspectFcstdArchive(serializeFcstdMetadataArchive(document)).objects.find((object) => object.name === 'holder')?.properties[0]
|
||||
assert.deepEqual(summary?.linkSubs, [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }])
|
||||
assert.deepEqual(decodeFcstdPropertyValue(summary!), { value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: 'Face1' }, { objectId: 'pad', subElement: 'Edge3' }, { objectId: 'body', subElement: '' }] }, decoded: true })
|
||||
const faceRef: TopoRefValue = { schemaVersion: 1, objectId: 'pad', kind: 'face', persistentId: 'Face1', topologyVersion: 2, generation: 1, status: 'ambiguous', candidates: ['Face1', 'Face2'] }
|
||||
const ambiguousDocument = { ...document, objects: [{ ...document.objects[0], properties: [{ ...document.objects[0].properties[0], value: { schemaVersion: 1, entries: [{ objectId: 'pad', subElement: faceRef }] } }] }, ...document.objects.slice(1)] }
|
||||
assert.throws(() => serializeFcstdMetadataArchive(ambiguousDocument), /stable topology status/)
|
||||
const malformed = '<Document><Objects><Object name="holder" type="Part::Feature"/></Objects><ObjectData><Object name="holder"><Properties><Property name="References" type="App::PropertyLinkSubList"><LinkSubList count="2"><Link obj="pad" sub="Face1"/></LinkSubList></Property></Properties></Object></ObjectData></Document>'
|
||||
assert.throws(() => inspectFcstdArchive(zipSync({ 'Document.xml': strToU8(malformed) })), /LinkSubList count/)
|
||||
})
|
||||
|
||||
@@ -73,13 +73,13 @@ test('private naming boundary checker cross-validates all authoritative status f
|
||||
'config/freecad-private-naming-source-readiness.json',
|
||||
'config/freecad-naming-sdk-plan.json',
|
||||
]) assert.match(source, new RegExp(path.replaceAll('.', '\\.')))
|
||||
assert.match(source, /shippedWorkerImplementation !== 'not-linked'/)
|
||||
assert.match(source, /task\.status !== 'in_progress'/)
|
||||
assert.match(source, /shippedWorkerImplementation !== 'freecad-linked'/)
|
||||
assert.match(source, /task\.status !== 'completed'/)
|
||||
assert.match(source, /systemExactEvaluation\?\.exact !== false/)
|
||||
assert.match(source, /production\?\.workerLinked !== false/)
|
||||
assert.match(source, /production\?\.workerLinked !== true/)
|
||||
assert.match(source, /candidateAbi\?\.scope !== 'isolated-non-production'/)
|
||||
assert.match(source, /callback\.startsWith\('freecadNamingCandidate'\)/)
|
||||
assert.match(source, /FreeCAD private naming Worker bridge/)
|
||||
assert.match(source, /productionLinkedComponents/)
|
||||
assert.match(source, /sdkPlan\.productionPublication !== false/)
|
||||
})
|
||||
|
||||
@@ -157,12 +157,12 @@ test('SDK readiness verifies the complete candidate wasm closure and keeps it pr
|
||||
assert.match(sdkOverlay, /worktree add --detach/)
|
||||
assert.match(sdkOverlay, /git -C "\$\{FREECAD_SOURCE_DIR\}" diff --quiet/)
|
||||
assert.match(sdkConfigure, /FreeCADBase|BUILD_PART/)
|
||||
assert.match(sdkConfigure, /callbacks: \[\]/)
|
||||
assert.match(sdkConfigure, /freecadNamingEvidenceJson/)
|
||||
assert.match(sdkBuild, /cmake --build "\$\{BUILD_DIR\}" --target Part/)
|
||||
assert.match(sdkBuild, /libFreeCADBase\.a/)
|
||||
assert.match(sdkBuild, /ElementMap\.cpp\.o/)
|
||||
assert.match(sdkBuild, /productionManifestGenerated: false/)
|
||||
assert.match(sdkBuild, /availability: 'unavailable'/)
|
||||
assert.match(sdkBuild, /availability: 'available'/)
|
||||
assert.match(bridgeBuild, /libHacl_Hash_SHA2\.a/)
|
||||
assert.match(bridgeBuild, /--preload-file/)
|
||||
assert.match(bridgeBuild, /FREECAD_NAMING_BRIDGE_REPORT/)
|
||||
@@ -214,4 +214,11 @@ test('real wasm and offline smoke lanes rebuild and execute the source prerequis
|
||||
assert.match(real, /probe:freecad-attachment-modes/)
|
||||
assert.match(real, /name\.startsWith\('test:chrome-'\)/)
|
||||
assert.match(real, /requestedPhase === 'execute' \? chromeExecution : chromeChecks/)
|
||||
assert.match(real, /test:freecad-naming-production/)
|
||||
assert.match(real, /test:freecad-isomorphic-provenance/)
|
||||
assert.match(real, /probe:freecad-native-property-semantics/)
|
||||
assert.match(offline, /publish:occt-history/)
|
||||
assert.match(offline, /check:freecad-naming-production/)
|
||||
assert.match(offline, /check:freecad-isomorphic-provenance/)
|
||||
assert.match(offline, /check:freecad-native-property-semantics/)
|
||||
})
|
||||
|
||||
@@ -38,6 +38,7 @@ test('offline profile covers runtime, native sources, browsers and host recovery
|
||||
'camotics-worktree',
|
||||
'linuxcnc-worktree',
|
||||
'occt-history-dist',
|
||||
'freecad-naming-wasm-sdk',
|
||||
'planegcs-dist',
|
||||
'qt6-wasm-core-downloads',
|
||||
'qt6-wasm-core-source',
|
||||
@@ -46,3 +47,11 @@ test('offline profile covers runtime, native sources, browsers and host recovery
|
||||
'debian-package-cache',
|
||||
]) assert.ok(ids.has(id), id)
|
||||
})
|
||||
|
||||
test('offline profile restores the linked naming SDK and production Worker', () => {
|
||||
const byId = new Map(config.archives.map((resource) => [resource.id, resource]))
|
||||
assert.equal(byId.get('freecad-naming-wasm-sdk')?.source, '.cache/toolchains/freecad-naming-sdk')
|
||||
assert.equal(byId.get('freecad-naming-wasm-sdk')?.required, true)
|
||||
assert.equal(byId.get('occt-history-dist')?.source, 'native/occt-history/dist')
|
||||
assert.equal(byId.get('occt-history-dist')?.required, true)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user