Files
Web_FreeCAD_Bitbybit/scripts/check-freecad-property-linksubhidden-failure.mjs
wangdequan 7bd7ff5055
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled
feat: close hidden link property evidence
2026-08-17 17:31:34 -04:00

58 lines
8.6 KiB
JavaScript

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-linksubhidden-failure.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD PropertyLinkSubHidden failure check failed: ${message}`) }
if (report.schemaVersion !== 1 || report.status !== 'pass' || report.baselineId !== 'freecad-1.1.1-property-linksubhidden-failure' || report.freecadVersion !== '1.1.1' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || report.propertyType !== 'App::PropertyLinkSubHidden' || report.caseCount !== 1) fail('baseline is invalid')
const entry = report.case
const expectedObjects = [
{ name: 'LinkGroupProbe', typeId: 'App::LinkGroup' },
{ name: 'ColorSourceA', typeId: 'Part::Feature' },
{ name: 'ColorSourceB', typeId: 'Part::Feature' },
]
if (entry?.objectTypeId !== 'App::LinkGroup' || entry.objectName !== 'LinkGroupProbe' || entry.propertyName !== 'ColoredElements' || entry.propertyTypeId !== 'App::PropertyLinkSubHidden' || !isDeepStrictEqual(entry.baseline, { object: 'ColorSourceA', subElements: ['Face1', 'Face3'] })) fail('native case identity changed')
const initial = entry.initial
if (!isDeepStrictEqual(initial?.value, entry.baseline) || initial.propertyTypeId !== 'App::PropertyLinkSubHidden' || !isDeepStrictEqual(initial.propertyStatus, ['LockDynamic', '26']) || !isDeepStrictEqual(initial.editorMode, ['Hidden']) || !isDeepStrictEqual(initial.ownerState, ['Up-to-date']) || initial.statusString !== 'Valid' || !isDeepStrictEqual(initial.ownerOutList, []) || !isDeepStrictEqual(initial.targetInLists, { ColorSourceA: [], ColorSourceB: [] }) || !isDeepStrictEqual(initial.objectSet, expectedObjects) || typeof initial.targetShapeHashes?.ColorSourceA !== 'string' || typeof initial.targetShapeHashes?.ColorSourceB !== 'string') fail('initial hidden sub-link state is invalid')
const assertBoundary = (snapshot, label) => {
if (snapshot?.propertyTypeId !== 'App::PropertyLinkSubHidden' || !isDeepStrictEqual(snapshot.ownerOutList, []) || !isDeepStrictEqual(snapshot.targetInLists, { ColorSourceA: [], ColorSourceB: [] }) || !isDeepStrictEqual(snapshot.targetShapeHashes, initial.targetShapeHashes) || !isDeepStrictEqual(snapshot.objectSet, expectedObjects)) fail(`${label} polluted hidden dependencies, Shapes, or objects`)
}
const failureNames = new Set(entry.failures?.map(({ name }) => name))
if (!['scalar', 'wrongSubList', 'unknownTuple'].every((name) => failureNames.has(name)) || entry.failures.length !== 3) fail('invalid input case inventory changed')
const scalar = entry.failures.find(({ name }) => name === 'scalar')
if (scalar?.accepted !== false || scalar.error?.type !== 'TypeError' || !/DocumentObject|NoneType/.test(scalar.error.message) || scalar.valuePreserved !== true || scalar.documentPreserved !== true || !isDeepStrictEqual(scalar.before, initial) || !isDeepStrictEqual(scalar.after, initial)) fail('scalar input was not rejected atomically')
const wrongSubList = entry.failures.find(({ name }) => name === 'wrongSubList')
if (wrongSubList?.accepted !== true || wrongSubList.error !== null || wrongSubList.valuePreserved !== false || !isDeepStrictEqual(wrongSubList.after.value, { object: 'ColorSourceA', subElements: ['Face1'] }) || wrongSubList.after.ownerState?.join(',') !== 'Touched') fail('tuple shorthand boundary changed')
const unknownTuple = entry.failures.find(({ name }) => name === 'unknownTuple')
if (unknownTuple?.accepted !== true || unknownTuple.error !== null || unknownTuple.valuePreserved !== false || !isDeepStrictEqual(unknownTuple.after.value, { object: 'ColorSourceA', subElements: ['MissingFace'] }) || unknownTuple.after.ownerState?.join(',') !== 'Touched') fail('unknown sub-element reference boundary changed')
for (const [label, snapshot] of [['scalar before', scalar.before], ['scalar after', scalar.after], ['wrongSubList before', wrongSubList.before], ['wrongSubList after', wrongSubList.after], ['unknownTuple before', unknownTuple.before], ['unknownTuple after', unknownTuple.after]]) assertBoundary(snapshot, label)
if (entry.crossDocument?.accepted !== false || entry.crossDocument.error?.type !== 'ValueError' || entry.crossDocument.error.message !== 'PropertyLinkSub does not support external object' || entry.crossDocument.valuePreserved !== true || entry.crossDocument.documentPreserved !== true || !isDeepStrictEqual(entry.crossDocument.before, initial) || !isDeepStrictEqual(entry.crossDocument.after, initial)) fail('cross-document input boundary changed')
assertBoundary(entry.crossDocument.before, 'cross-document before')
assertBoundary(entry.crossDocument.after, 'cross-document after')
const editor = entry.readOnly
if (editor?.accepted !== true || editor.error !== null || editor.pythonBypassesEditorReadOnly !== true || !isDeepStrictEqual(editor.before.value, entry.baseline) || !isDeepStrictEqual(editor.after.value, { object: 'ColorSourceB', subElements: ['Face2'] }) || !isDeepStrictEqual(editor.after.propertyStatus, ['ReadOnly', 'LockDynamic', '26']) || !isDeepStrictEqual(editor.after.editorMode, ['ReadOnly', 'Hidden']) || !isDeepStrictEqual(editor.after.ownerState, ['Touched'])) fail('ReadOnly Python acceptance boundary changed')
assertBoundary(editor.before, 'ReadOnly before')
assertBoundary(editor.after, 'ReadOnly after')
const immutable = entry.immutable
if (immutable?.accepted !== false || immutable.error?.type !== 'AttributeError' || immutable.error.message !== "Object attribute 'ColoredElements' is read-only" || immutable.pythonBypassesImmutable !== false || !isDeepStrictEqual(immutable.before.value, entry.baseline) || !isDeepStrictEqual(immutable.after.value, entry.baseline) || !isDeepStrictEqual(immutable.before.propertyStatus, ['Immutable', 'LockDynamic', '26']) || !isDeepStrictEqual(immutable.after.propertyStatus, ['Immutable', 'LockDynamic', '26']) || !isDeepStrictEqual(immutable.after.ownerState, ['Up-to-date'])) fail('Immutable Python rejection boundary changed')
assertBoundary(immutable.before, 'Immutable before')
assertBoundary(immutable.after, 'Immutable after')
const transaction = entry.transaction
if (transaction?.undoMode !== 1 || transaction.pendingAfterEdit !== true || transaction.pendingAfterAbort !== false || transaction.activeAfterEdit?.name !== 'property-linksubhidden-cancel' || !(transaction.activeAfterEdit.id > 0) || transaction.activeAfterAbort?.name !== '' || transaction.activeAfterAbort.id !== 0 || !isDeepStrictEqual(transaction.before?.value, entry.baseline) || !isDeepStrictEqual(transaction.edited?.value, { object: 'ColorSourceB', subElements: ['Face5'] }) || !isDeepStrictEqual(transaction.afterAbortBeforeRecompute?.value, entry.baseline) || !isDeepStrictEqual(transaction.afterAbortBeforeRecompute.ownerState, ['Touched']) || transaction.recomputeAfterAbort !== true || !isDeepStrictEqual(transaction.afterAbort?.ownerState, ['Up-to-date']) || transaction.afterAbort.statusString !== 'Valid' || transaction.restored !== true) 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.disabled?.classification !== 'editor-read-only-python-mutable-until-immutable' || report.disabled.readOnlySetter !== 'accepted' || report.disabled.immutableSetter !== 'rejected') fail('disabled state boundary is incomplete')
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 (entry.documentIntegrity?.objectsPreserved !== true || !isDeepStrictEqual(entry.documentIntegrity.initialObjects, expectedObjects) || !isDeepStrictEqual(entry.documentIntegrity.finalObjects, expectedObjects) || !isDeepStrictEqual(entry.documentIntegrity.foreignDocumentObjects, [{ name: 'ExternalSource', typeId: 'Part::Feature' }])) fail('failure cases polluted either document')
console.log(JSON.stringify({ status: 'freecad-property-linksubhidden-failure-pass', invalidCases: [...failureNames], crossDocument: entry.crossDocument, readOnlyPythonAccepted: editor.pythonBypassesEditorReadOnly, immutableRejected: !immutable.pythonBypassesImmutable, transactionRestored: transaction.restored, cancellationBoundary: report.cancellationBoundary }, null, 2))