import { existsSync, mkdtempSync, rmSync } from 'node:fs' import { readFile, writeFile } from 'node:fs/promises' import { spawnSync } from 'node:child_process' import { join, resolve } from 'node:path' import { tmpdir } from 'node:os' const root = resolve(new URL('..', import.meta.url).pathname) const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd') const sysroot = resolve(root, '.cache/freecad/sysroot') const scriptPath = resolve(root, 'scripts/freecad-property-direction-failure.py') const outputPath = resolve(root, 'config/freecad-property-direction-failure.json') if (!existsSync(executable)) throw new Error(`FreeCAD PropertyDirection executable is missing: ${executable}`) const baseEnv = { ...process.env, PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`, LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`, } const runProbe = ({ mode = 'main', target = outputPath, timeout = 120_000 } = {}) => spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], { cwd: root, encoding: 'utf8', timeout, maxBuffer: 32 * 1024 * 1024, env: { ...baseEnv, FREECAD_PROPERTY_DIRECTION_FAILURE_OUTPUT: target, FREECAD_PROPERTY_DIRECTION_FAILURE_MODE: mode, }, }) const execution = runProbe({ timeout: Number(process.env.FREECAD_PROPERTY_DIRECTION_FAILURE_TIMEOUT_MS || 120_000) }) const output = `${execution.stdout || ''}\n${execution.stderr || ''}` if (execution.error || execution.status !== 0 || !output.includes('FREECAD_PROPERTY_DIRECTION_FAILURE_RESULT=')) throw new Error(`FreeCAD PropertyDirection failure probe failed with status ${execution.status}: ${execution.error?.message || 'process failed'}\n${output.trim()}`) const report = JSON.parse(await readFile(outputPath, 'utf8')) const isolatedRoot = mkdtempSync(join(tmpdir(), 'freecad-property-direction-')) try { report.nonFiniteConsumerBoundaries = [] const isolatedTimeout = Number(process.env.FREECAD_PROPERTY_DIRECTION_NON_FINITE_TIMEOUT_MS || 5_000) for (const hostId of ['Mirroring', 'ProjectOnSurface']) { for (const valueId of ['nan', 'positive-infinity', 'negative-infinity']) { const caseId = `${hostId}-${valueId}` const attempts = [] for (let attempt = 1; attempt <= 2; attempt += 1) { const target = join(isolatedRoot, `${caseId}-${attempt}.json`) const startedAt = Date.now() const isolated = runProbe({ mode: `non-finite:${hostId}:${valueId}`, target, timeout: isolatedTimeout }) const isolatedOutput = `${isolated.stdout || ''}\n${isolated.stderr || ''}`.trim() const timedOut = isolated.error?.code === 'ETIMEDOUT' let completedCase = null if (!timedOut && isolated.status === 0 && isolatedOutput.includes('FREECAD_PROPERTY_DIRECTION_FAILURE_ISOLATED_RESULT=') && existsSync(target)) { completedCase = JSON.parse(await readFile(target, 'utf8')).case } attempts.push({ attempt, timeoutMs: isolatedTimeout, elapsedMs: Date.now() - startedAt, timedOut, exitStatus: isolated.status, signal: isolated.signal, errorCode: isolated.error?.code || null, progressMarkerObserved: isolatedOutput.includes(`FREECAD_PROPERTY_DIRECTION_PROGRESS=${caseId}`), completionMarkerObserved: isolatedOutput.includes('FREECAD_PROPERTY_DIRECTION_FAILURE_ISOLATED_RESULT='), diagnostic: isolatedOutput.split('\n').filter((line) => line && !line.startsWith('FreeCAD 0.')).slice(-8), completedCase, outcome: timedOut ? 'timeout' : completedCase ? 'completed' : 'process-exited-without-completion', }) } const outcomes = [...new Set(attempts.map(({ outcome }) => outcome))] const stable = outcomes.length === 1 && attempts.every(({ progressMarkerObserved }) => progressMarkerObserved) report.nonFiniteConsumerBoundaries.push({ id: caseId, mode: `non-finite:${hostId}:${valueId}`, attemptCount: attempts.length, attempts, stable, classification: !stable ? 'native-nondeterministic' : outcomes[0] === 'timeout' ? 'native-synchronous-consumer-timeout' : outcomes[0] === 'process-exited-without-completion' ? 'native-process-exit-without-probe-completion' : 'native-consumer-returned', }) } } report.status = report.nonFiniteConsumerBoundaries.every(({ stable, classification }) => stable && classification !== 'native-consumer-returned') ? 'pass' : 'fail' await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`) } finally { rmSync(isolatedRoot, { recursive: true, force: true }) } console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-direction-failure.json', acceptedCoercions: report.setterBoundaries.acceptedCoercions.map(({ id, accepted, after }) => ({ id, accepted, after })), setterFailures: report.setterBoundaries.failures.map(({ id, exception, valuePreserved }) => ({ id, exception, valuePreserved })), consumerFailures: report.consumerFailures.map(({ id, setterException, recomputeResult, afterRecompute }) => ({ id, setterException, recomputeResult, state: afterRecompute.state })), nonFiniteConsumerBoundaries: report.nonFiniteConsumerBoundaries.map(({ id, classification, attemptCount, stable }) => ({ id, classification, attemptCount, stable })), dependencyFailures: report.dependencyFailures.map(({ id, recomputeResult, afterRecompute }) => ({ id, recomputeResult, state: afterRecompute.state })), transactionRestored: report.transaction.restored, }, null, 2))