import { isDeepStrictEqual } from 'node:util' import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const report = JSON.parse(await readFile(resolve(root, 'config/freecad-property-linklisthidden-failure.json'), 'utf8')) const fail = (message) => { throw new Error(`FreeCAD PropertyLinkListHidden failure check failed: ${message}`) } if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linklisthidden-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkListHidden') fail('baseline is invalid') if (report.object?.name !== 'SketchProbe' || report.object.typeId !== 'Sketcher::SketchObject' || report.property?.name !== 'Exports' || report.property.typeId !== 'App::PropertyLinkListHidden') fail('fixture identity changed') const expectedObjects = [ { name: 'SketchProbe', typeId: 'Sketcher::SketchObject' }, { name: 'ExportA', typeId: 'Part::Feature' }, { name: 'ExportB', typeId: 'Part::Feature' }, ] const initial = report.initial if (!isDeepStrictEqual(initial?.value, ['ExportA', 'ExportB']) || initial.propertyTypeId !== 'App::PropertyLinkListHidden' || !isDeepStrictEqual(initial.propertyStatus, ['26']) || !isDeepStrictEqual(initial.editorMode, ['Hidden']) || !isDeepStrictEqual(initial.ownerState, ['Up-to-date']) || initial.statusString !== 'Valid' || initial.ownerShapeNull !== true || !isDeepStrictEqual(initial.ownerOutList, []) || !isDeepStrictEqual(initial.targetInLists, { ExportA: [], ExportB: [] }) || typeof initial.targetShapeHashes?.ExportA !== 'string' || typeof initial.targetShapeHashes?.ExportB !== 'string' || !isDeepStrictEqual(initial.objectSet, expectedObjects)) fail('initial hidden-link state is invalid') const assertBoundary = (snapshot, label) => { if (snapshot?.propertyTypeId !== 'App::PropertyLinkListHidden' || snapshot.ownerShapeNull !== true || !isDeepStrictEqual(snapshot.ownerOutList, []) || !isDeepStrictEqual(snapshot.targetInLists, { ExportA: [], ExportB: [] }) || !isDeepStrictEqual(snapshot.targetShapeHashes, initial.targetShapeHashes) || !isDeepStrictEqual(snapshot.objectSet, expectedObjects)) fail(`${label} polluted hidden dependencies, Shapes, or objects`) } const expectedFailures = new Map([ ['string-scalar', ['TypeError', 'Type must be App.DocumentObject or None, not str']], ['string-item', ['TypeError', 'Type must be App.DocumentObject or None, not str']], ['integer-scalar', ['TypeError', 'Type must be App.DocumentObject or None, not int']], ['integer-item', ['TypeError', 'Type must be App.DocumentObject or None, not int']], ['cross-document', ['ValueError', 'PropertyLinkList does not support external object']], ]) if (report.failures?.length !== expectedFailures.size) fail('failure case count changed') for (const entry of report.failures) { const expected = expectedFailures.get(entry.id) if (!expected || entry.exception?.type !== expected[0] || entry.exception.message !== expected[1] || entry.valuePreserved !== true || entry.documentPreserved !== true || !isDeepStrictEqual(entry.before, initial) || !isDeepStrictEqual(entry.after, initial)) fail(`${entry.id} did not reject atomically`) } const editor = report.editorReadOnly if (editor?.classification !== 'editor-read-only-python-setter-accepted' || !isDeepStrictEqual(editor.editorMode, ['ReadOnly', 'Hidden']) || editor.exception !== null || editor.pythonBypassesEditorReadOnly !== true || !isDeepStrictEqual(editor.before.value, ['ExportA', 'ExportB']) || !isDeepStrictEqual(editor.after.value, ['ExportB']) || !isDeepStrictEqual(editor.after.propertyStatus, ['ReadOnly', '26']) || !isDeepStrictEqual(editor.after.editorMode, ['ReadOnly', 'Hidden']) || !isDeepStrictEqual(editor.after.ownerState, ['Touched']) || !isDeepStrictEqual(editor.restoredEditorMode, ['Hidden'])) fail('ReadOnly Python acceptance boundary changed') assertBoundary(editor.before, 'ReadOnly before') assertBoundary(editor.after, 'ReadOnly after') const immutable = report.immutable if (immutable?.classification !== 'immutable-python-setter-accepted' || immutable.exception !== null || immutable.pythonBypassesImmutable !== true || !isDeepStrictEqual(immutable.before.value, ['ExportA', 'ExportB']) || !isDeepStrictEqual(immutable.after.value, ['ExportB']) || !isDeepStrictEqual(immutable.before.propertyStatus, ['Immutable', '26']) || !isDeepStrictEqual(immutable.after.propertyStatus, ['Immutable', '26']) || !isDeepStrictEqual(immutable.after.ownerState, ['Touched']) || !isDeepStrictEqual(immutable.restoredStatus, ['26'])) fail('Immutable Python acceptance boundary changed') assertBoundary(immutable.before, 'Immutable before') assertBoundary(immutable.after, 'Immutable after') const transaction = report.transaction if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-linklisthidden-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || !isDeepStrictEqual(transaction.before?.value, ['ExportA', 'ExportB']) || !isDeepStrictEqual(transaction.edited?.value, ['ExportB', 'ExportA', 'ExportB']) || !isDeepStrictEqual(transaction.afterAbortBeforeRecompute?.value, ['ExportA', 'ExportB']) || !isDeepStrictEqual(transaction.afterAbortBeforeRecompute.ownerState, ['Touched']) || transaction.recomputeAfterAbort !== true || !isDeepStrictEqual(transaction.afterAbort?.ownerState, ['Up-to-date']) || transaction.afterAbort.statusString !== 'Valid' || transaction.restored !== true || !isDeepStrictEqual(transaction.before, transaction.afterAbort)) fail('transaction abort and recompute recovery changed') for (const [name, snapshot] of Object.entries({ before: transaction.before, edited: transaction.edited, afterAbortBeforeRecompute: transaction.afterAbortBeforeRecompute, afterAbort: transaction.afterAbort })) assertBoundary(snapshot, `transaction ${name}`) if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-property-setter' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is incomplete') if (report.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(report.documentIntegrity.initialObjects, expectedObjects) || !isDeepStrictEqual(report.documentIntegrity.finalObjects, expectedObjects) || !isDeepStrictEqual(report.documentIntegrity.foreignDocumentObjects, [{ name: 'ForeignExport', typeId: 'Part::Feature' }])) fail('failure cases polluted either document') console.log(JSON.stringify({ status: 'freecad-property-linklisthidden-failure-pass', failures: report.failures.map(({ id, exception }) => ({ id, exception })), readOnlyPythonAccepted: editor.pythonBypassesEditorReadOnly, immutablePythonAccepted: immutable.pythonBypassesImmutable, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))