#!/usr/bin/env node import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { Worker } from "node:worker_threads"; import { fileURLToPath, pathToFileURL } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const build = path.resolve(process.argv[2] ?? path.join(root, "freecad-source/build-web/configure/bin")); const out = fs.mkdtempSync(path.join(os.tmpdir(), "freecad-p6-sandbox-family-facade-")); execFileSync(path.join(root, "web/node_modules/.bin/tsc"), ["--target", "ES2022", "--module", "ESNext", "--moduleResolution", "Bundler", "--strict", "--skipLibCheck", "--outDir", out, "--declaration", "false", "--noEmit", "false", "web/src/facade-envelope.ts", "web/src/sandbox-python-family-facade.ts"], { cwd: root, stdio: "inherit" }); const facade = await import(pathToFileURL(path.join(out, "sandbox-python-family-facade.js"))); const envelope = await import(pathToFileURL(path.join(out, "facade-envelope.js"))); const worker = new Worker(path.join(root, "tools/sandbox-python-family-facade-smoke-worker.mjs"), { workerData: { buildDirectory: build, facadePath: pathToFileURL(path.join(out, "sandbox-python-family-facade.js")).href, envelopePath: pathToFileURL(path.join(out, "facade-envelope.js")).href } }); const receive = () => new Promise((resolve, reject) => { const onMessage = (value) => { worker.off("error", reject); resolve(value); }; worker.once("message", onMessage); worker.once("error", reject); }); const ready = await receive(); const records = [ { apiId: "py:Sandbox:class:Drawexporter", name: "Drawexporter", family: "generic-object" }, { apiId: "py:Sandbox:class:SandboxWorkbench", name: "SandboxWorkbench", family: "generic-object" }, { apiId: "py:Sandbox:function:isDraftShape2DView", name: "isDraftShape2DView", family: "view-resource" }, ]; const successRequest = facade.createSandboxPythonFamilyRequest("sandbox-family-success-1", records); worker.postMessage(successRequest); const success = await receive(); worker.postMessage(successRequest); const duplicate = await receive(); const cancelRequest = facade.createSandboxPythonFamilyRequest("sandbox-family-cancel-1", records); const cancelledPromise = new Promise((resolve, reject) => { const onMessage = (value) => { if (value.requestId === cancelRequest.requestId) { worker.off("error", reject); resolve(value); } }; worker.on("message", onMessage); worker.once("error", reject); }); worker.postMessage(cancelRequest); worker.postMessage(envelope.createCancel("sandbox-family-cancel-control", cancelRequest.requestId, "sandbox-python-family")); const cancelled = await cancelledPromise; const crossRequest = facade.createSandboxPythonFamilyRequest("sandbox-family-cross-1", [{ ...records[0], apiId: "py:BIM:class:Drawexporter" }]); worker.postMessage(crossRequest); const crossModule = await receive(); worker.postMessage({ kind: "reset", requestId: "sandbox-family-reset-1" }); const reset = await receive(); worker.postMessage(facade.createSandboxPythonFamilyRequest("sandbox-family-after-reset-1", records.slice(0, 1))); const afterReset = await receive(); await worker.terminate(); assert.equal(ready.kind, "ready"); assert.equal(success.kind, "result"); assert.equal(success.result.schema, facade.SANDBOX_PYTHON_FAMILY_SCHEMA); assert.equal(success.result.records.length, 3); assert.deepEqual(success.result.records.map((record) => record.status), ["not_implemented", "restricted", "restricted"]); assert.equal(success.result.resourcesReleased, true); assert.equal(duplicate.error.code, "duplicate_request"); assert.equal(cancelled.error.code, "cancelled"); assert.equal(crossModule.result.records[0].status, "not_implemented"); assert.equal(crossModule.result.records[0].code, "unmatched_source_record"); assert.deepEqual(reset, { kind: "reset", requestId: "sandbox-family-reset-1" }); assert.equal(afterReset.kind, "result"); const trace = { ready, success, duplicate, cancelled, crossModule, reset, afterReset }; const traceSha256 = crypto.createHash("sha256").update(JSON.stringify(trace)).digest("hex"); const report = { schema: "freecad-web-p6-sandbox-tier1-readonly-family-facade/v1", taskId: "FW-P6-FPA-F1-SANDBOX", module: "Sandbox", command: facade.SANDBOX_PYTHON_FAMILY_COMMAND, result: "pass", envelopeSchema: envelope.FACADE_SCHEMA, typedSchema: facade.SANDBOX_PYTHON_FAMILY_SCHEMA, trace, traceSha256, sequencing: { requestCount: 5, responseCount: 7, maxActive: 1, cancelAccepted: true, resetAccepted: true }, schemaChecks: { nativeSource: success.result.source === "freecad-source", resourcesReleased: success.result.resourcesReleased === true, terminalStatuses: success.result.records.every((record) => ["exposed", "restricted", "nonportable", "not_implemented"].includes(record.status)), noNativePointers: !JSON.stringify(trace).match(/pointer/i) }, artifacts: { native: "docs/p6/sandbox-tier1-readonly-family-runtime.json", worker: "web/src/sandbox-python.worker.ts", facade: "web/src/sandbox-python-family-facade.ts" } }; fs.writeFileSync(path.join(root, "docs/p6/sandbox-tier1-readonly-family-facade.json"), `${JSON.stringify(report, null, 2)}\n`); fs.writeFileSync(path.join(root, "docs/freecad-p6-sandbox-tier1-readonly-family-facade.log"), `${JSON.stringify({ result: report.result, traceSha256, cancel: cancelled.error.code, reset: reset.requestId })}\n`); console.log(JSON.stringify({ result: report.result, traceSha256, cancel: cancelled.error.code, reset: reset.requestId }));