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-area-failure.json'), 'utf8')) const fail = (message) => { throw new Error(`FreeCAD PropertyArea failure check failed: ${message}`) } const same = (left, right) => JSON.stringify(left) === JSON.stringify(right) if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-area-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline is invalid') if (report.object?.name !== 'AreaProbe' || report.object.typeId !== 'Measure::MeasureArea' || report.property?.name !== 'Area' || report.property.typeId !== 'App::PropertyArea' || report.input?.name !== 'Elements' || report.input.typeId !== 'App::PropertyLinkSubList') fail('native object/property identity changed') if (report.initial?.numeric !== 10 || report.initial.statusString !== 'Valid' || !same(report.initial.state, ['Up-to-date']) || report.failures?.length !== 2) fail('initial state or failure case set changed') const expectedFailures = new Map([ ['unsupported-point', { name: 'AreaPoint', subElement: 'Vertex1', shapeType: 'Vertex' }], ['unsupported-line', { name: 'AreaLine', subElement: 'Edge1', shapeType: 'Edge' }], ]) for (const entry of report.failures) { const expected = expectedFailures.get(entry.id) if (!expected || entry.source?.name !== expected.name || entry.source.typeId !== 'Part::Feature' || entry.source.subElement !== expected.subElement || entry.source.shapeType !== expected.shapeType) fail(`${entry.id} source fixture changed`) if (entry.expectedDiagnostic !== 'Cannot calculate area' || entry.assignmentException !== null || entry.recomputeException !== null || entry.recomputeResult !== true) fail(`${entry.id} native failure phase changed`) if (entry.after?.numeric !== 10 || entry.after.statusString !== 'Cannot calculate area' || !same(entry.after.state, ['Touched', 'Invalid']) || !same(entry.after.elements, [{ object: expected.name, subElements: [expected.subElement] }])) fail(`${entry.id} did not enter the locked invalid state`) if (entry.valuePreserved !== true || entry.objectsPreserved !== true || entry.recoveredExactly !== true || entry.recovered?.numeric !== 10 || entry.recovered.statusString !== 'Valid' || !same(entry.recovered.state, ['Up-to-date']) || !same(entry.recovered.elements, entry.before.elements) || !same(entry.beforeObjects, entry.afterObjects)) fail(`${entry.id} did not recover without document pollution`) } const disabled = report.disabled if (disabled?.classification !== 'editor-readonly-python-mutable-until-immutable' || disabled.editorReadOnlyRequested !== '20 mm^2' || disabled.immutableRequested !== '30 mm^2' || !same(disabled.propertyStatus, ['24', '27']) || !same(disabled.editorMode, ['ReadOnly'])) fail('read-only property boundary changed') if (disabled.editorReadOnlyException !== null || disabled.pythonBypassesEditorReadOnly !== true || disabled.editorReadOnlyAfter?.numeric !== 20 || !disabled.immutableStatus?.includes('Immutable') || disabled.immutableException?.type !== 'AttributeError' || disabled.immutableException.message !== "Object attribute 'Area' is read-only") fail('editor read-only and Immutable behavior changed') if (disabled.immutableAfter?.numeric !== 20 || disabled.immutableValuePreserved !== true || disabled.valueRestored !== true || disabled.restored?.numeric !== 10 || disabled.restored.statusString !== 'Valid' || disabled.objectsPreserved !== true || !same(disabled.restoredStatus, ['24', '27']) || !same(disabled.restoredEditorMode, ['ReadOnly'])) fail('disabled state did not restore cleanly') const transaction = report.transaction if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-area-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort?.id !== 0) fail('transaction lifecycle changed') if (transaction.before?.numeric !== 10 || transaction.edited?.numeric !== 75.398223877 || transaction.afterAbort?.numeric !== 10 || transaction.afterRecompute?.numeric !== 10 || transaction.afterAbort.statusString !== 'Touched' || transaction.afterRecompute.statusString !== 'Valid' || transaction.restored !== true || transaction.objectsRestored !== true || !same(transaction.before.elements, transaction.afterRecompute.elements) || !same(transaction.objectsBefore, transaction.objectsAfter)) fail('transaction abort did not restore Elements and derived Area') if (report.cancellationBoundary?.supported !== false || report.cancellationBoundary.classification !== 'synchronous-measure-recompute' || report.cancellationBoundary.reason !== 'no-native-cancel-hook' || report.cancellationBoundary.replacement !== 'abort-active-document-transaction') fail('cancellation boundary is not explicit') if (report.documentIntegrity?.objectsPreserved !== true || report.documentIntegrity.objectCount !== 5 || !same(report.documentIntegrity.initialObjects, report.documentIntegrity.finalObjects)) fail('failure, disabled or cancellation cases polluted the document') console.log(JSON.stringify({ status: 'freecad-property-area-failure-pass', failures: report.failures.map(({ id, after }) => ({ id, state: after.state, diagnostic: after.statusString })), disabled: { editorMode: disabled.editorMode, pythonBypassesEditorReadOnly: disabled.pythonBypassesEditorReadOnly, immutableException: disabled.immutableException }, transactionRestored: transaction.restored, documentObjectsPreserved: report.documentIntegrity.objectsPreserved, cancellationBoundary: report.cancellationBoundary, }, null, 2))