import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import ts from "../web/node_modules/typescript/lib/typescript.js"; import { loadWasmProbe } from "./wasm-probe-runtime.mjs"; const root = fileURLToPath(new URL("../", import.meta.url)); const ids = ["Surface:surface.createRecompute", "Surface:surface.objectContractBatch", "Surface:surface.saveReopen"]; const selected = JSON.parse(process.env.FREECAD_BUSINESS_IDS ?? JSON.stringify(ids)); if (!Array.isArray(selected) || selected.some(id => !ids.includes(id))) { throw new Error("Unsupported Surface business IDs"); } const results = []; const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "surface-business-")); let kernel; async function stage(name, action) { const started = performance.now(); process.stderr.write(`${name}: entered\n`); try { return await action(); } finally { process.stderr.write(`${name}: completed in ${Math.round(performance.now() - started)}ms\n`); } } async function runCase(result, input, action) { const item = { input, assertions: [] }; result.cases.push(item); const check = (name, passed) => item.assertions.push({ name, passed: passed === true }); try { item.output = await action(check); } catch (error) { item.error = String(error.stack ?? error); check("request completes successfully", false); } } try { for (const name of ["facade-envelope", "surface-facade", "surface-object-contract-facade", "surface-save-facade"]) { const source = await fs.readFile(path.join(root, "web/src", `${name}.ts`), "utf8"); const compiled = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 }, }); await fs.writeFile(path.join(temporary, `${name}.js`), compiled.outputText); } await fs.writeFile(path.join(temporary, "package.json"), '{"type":"module"}\n'); const surface = await import(pathToFileURL(path.join(temporary, "surface-facade.js"))); const contract = await import(pathToFileURL(path.join(temporary, "surface-object-contract-facade.js"))); const save = await import(pathToFileURL(path.join(temporary, "surface-save-facade.js"))); ({ kernel } = await stage("load", () => loadWasmProbe( process.env.FREECAD_SURFACE_BUSINESS_BUILD ?? path.join(root, "freecad-source/build-web/configure/bin"), "FreeCADWebSurfaceRuntimeProbe", { printErr: text => process.stderr.write(`${text}\n`) }, ))); for (const capabilityId of selected) { const result = { capabilityId, status: "failed", cases: [] }; results.push(result); await stage(capabilityId, async () => { if (capabilityId === ids[0]) { // A changed operand must reach native code; fixture rejection is a business failure. for (const second of [surface.SURFACE_FIXTURE.second, { length: 2, width: 3, height: 4 }]) { const input = { ...surface.SURFACE_FIXTURE, second }; await runCase(result, input, async check => { const output = await surface.handleSurface(input, new AbortController().signal, kernel); check("native Surface::Cut created", output.objectType === "Surface::Cut"); check("three document objects recomputed", output.objectCount === 3 && output.recomputeCount === 3); check("native document released", output.resourcesReleased === true); return output; }); } } else if (capabilityId === ids[2]) { // The legacy probe is a baseline, not evidence of current-document persistence. for (const savePath of [save.SURFACE_SAVE_FIXTURE.path, "/tmp/SurfaceBusinessAlternate.FCStd"]) { const input = { ...save.SURFACE_SAVE_FIXTURE, path: savePath }; await runCase(result, input, async check => { const output = await save.handleSurfaceSave(input, new AbortController().signal, kernel); check("saved path matches requested path", output.path === input.path); check("native FCStd has nonzero size", Number.isInteger(output.size) && output.size > 0); check("native reopened Surface Cut", output.objectType === "Surface::Cut" && output.objectCount === 3); check("native round trip completed", output.roundTrip === true); check("native documents released", output.resourcesReleased === true); return output; }); } for (const kind of ["invalid-path", "cancelled"]) { const input = { ...save.SURFACE_SAVE_FIXTURE, path: kind === "invalid-path" ? "" : save.SURFACE_SAVE_FIXTURE.path }; await runCase(result, { payload: input, aborted: kind === "cancelled" }, async check => { const controller = new AbortController(); if (kind === "cancelled") controller.abort(); let error; try { await save.handleSurfaceSave(input, controller.signal, kernel); } catch (caught) { error = caught; } check("request rejected with expected code", error?.code === (kind === "cancelled" ? "cancelled" : "invalid_path")); return { code: error?.code, message: error?.message }; }); } } else { const record = { apiId: "obj:Surface:Surface::Sections", name: "Surface::Sections", family: "view-provider", sourcePath: "src/Mod/Surface/App/FeatureSections.h" }; for (const records of [[record], [record, { ...record, apiId: "obj:Surface:Unknown" }]]) { const input = contract.createSurfaceObjectContractRequest("surface-business", records).payload; await runCase(result, input, async check => { const output = await contract.handleSurfaceObjectContract(input, new AbortController().signal, kernel); const actual = output.records[0]; check("native record count matches request", output.records.length === records.length && output.count === records.length); check("Sections contract executed", actual.apiId === record.apiId && actual.status === "exposed" && actual.code === "native_object_contract"); check("desktop object and view provider match", actual.objectType === "Surface::Sections" && actual.viewProvider === "SurfaceGui::ViewProviderSections"); check("native NSections property exists", actual.hasNSections === true); check("native recompute executed", Number.isInteger(actual.recomputeCount) && actual.recomputeCount > 0); check("documents released", output.resourcesReleased === true && output.records.every(entry => entry.resourcesReleased === true)); if (records.length > 1) check("unknown object is not exposed", output.records[1].status === "not_implemented" && output.records[1].code === "unmatched_source_record"); return output; }); } } }); result.status = result.cases.every(item => item.assertions.length && item.assertions.every(check => check.passed)) ? "passed" : "failed"; if (result.status === "failed") result.error = result.cases.filter(item => item.assertions.some(check => !check.passed)) .map(item => item.error ?? item.assertions.filter(check => !check.passed).map(check => check.name).join(", ")).join("\n"); } } catch (error) { for (const capabilityId of selected.filter(id => !results.some(result => result.capabilityId === id))) { results.push({ capabilityId, status: "failed", error: String(error.stack ?? error), cases: [{ input: { capabilityId }, assertions: [{ name: "runtime and facade load", passed: false }] }] }); } } finally { await stage("cleanup", () => fs.rm(temporary, { recursive: true, force: true })); } const report = { schema: "freecad-web-business-cases/v1", results }; await new Promise((resolve, reject) => process.stdout.write(`${JSON.stringify(report)}\n`, error => error ? reject(error) : resolve())); // The standalone process owns the Emscripten pthread pool. process.exit(results.length === selected.length && results.every(result => result.status === "passed") ? 0 : 1);