144 lines
6.9 KiB
JavaScript
144 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const facadePath = process.argv[2];
|
|
const buildDirectory = path.resolve(process.argv[3] || "freecad-source/build-web/cloud-configure-r2/bin");
|
|
if (!facadePath) throw new Error("compiled cloud-facade module path is required");
|
|
const facade = await import(facadePath);
|
|
const envelope = await import(path.resolve(path.dirname(facadePath), "facade-envelope.js"));
|
|
const {
|
|
CLOUD_ACTION_SCHEMA,
|
|
CLOUD_COMMAND,
|
|
CLOUD_FIXTURE,
|
|
createCloudRequest,
|
|
handleCloudConfigure,
|
|
isCloudRequest,
|
|
} = facade;
|
|
const { FacadeSerialQueue, createCancel, isFacadeResponse } = envelope;
|
|
|
|
globalThis.__dirname = `${buildDirectory}${path.sep}`;
|
|
globalThis.require = (await import("node:module")).createRequire(import.meta.url);
|
|
const loaderUrl = pathToFileURL(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.js")).href;
|
|
const loader = await import(loaderUrl);
|
|
const kernel = await loader.default({
|
|
wasmBinary: fs.readFileSync(path.join(buildDirectory, "FreeCADWebCloudKernelProbe.wasm")),
|
|
});
|
|
|
|
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
const outputPath = path.join(root, "docs", "p6", "cloud-facade.json");
|
|
const schemaPath = path.join(root, "docs", "p6", "cloud-facade.schema.json");
|
|
const logPath = path.join(root, "docs", "freecad-p6-cloud-e-worker.log");
|
|
const queue = new FacadeSerialQueue();
|
|
const trace = [];
|
|
let active = 0;
|
|
let maxActive = 0;
|
|
|
|
async function dispatch(request) {
|
|
trace.push({ direction: "request", requestId: request.requestId, command: request.command });
|
|
const response = await queue.dispatch(request, async (payload, signal) => {
|
|
active += 1;
|
|
maxActive = Math.max(maxActive, active);
|
|
trace.push({ direction: "handler-start", requestId: request.requestId });
|
|
try {
|
|
return await handleCloudConfigure(payload, signal, kernel);
|
|
} finally {
|
|
active -= 1;
|
|
trace.push({ direction: "handler-end", requestId: request.requestId });
|
|
}
|
|
});
|
|
trace.push({ direction: "response", requestId: response.requestId, kind: response.kind, code: response.kind === "error" ? response.error.code : null });
|
|
return response;
|
|
}
|
|
|
|
const success = await dispatch(createCloudRequest("cloud-success"));
|
|
const expectedError = await dispatch(createCloudRequest("cloud-error", { ...CLOUD_FIXTURE, url: "" }));
|
|
const duplicate = await dispatch(createCloudRequest("cloud-success"));
|
|
const cancelRequest = createCloudRequest("cloud-cancel");
|
|
const cancelPromise = dispatch(cancelRequest);
|
|
trace.push({ direction: "cancel", requestId: "cloud-cancel-message", cancelOf: cancelRequest.requestId });
|
|
const cancelAccepted = queue.cancel(createCancel("cloud-cancel-message", cancelRequest.requestId, "cloud-config"));
|
|
const cancelled = await cancelPromise;
|
|
const afterCancel = await dispatch(createCloudRequest("cloud-after-cancel"));
|
|
|
|
assert.equal(CLOUD_COMMAND, "cloud.configureRecompute");
|
|
assert.equal(isCloudRequest(createCloudRequest("schema-test")), true);
|
|
assert.equal(isCloudRequest({ ...createCloudRequest("schema-test"), command: "other" }), false);
|
|
assert.equal(success.kind, "result");
|
|
assert.equal(success.result.schema, CLOUD_ACTION_SCHEMA);
|
|
assert.equal(success.result.configuration.setterCount, 6);
|
|
assert.equal(success.result.recompute.count, 1);
|
|
assert.equal(success.result.recompute.hasError, 0);
|
|
assert.equal(success.result.recompute.summary, Object.values(CLOUD_FIXTURE).slice(2).join("|"));
|
|
assert.equal(expectedError.kind, "error");
|
|
assert.equal(expectedError.error.code, "invalid_argument");
|
|
assert.equal(expectedError.error.diagnostic.field, "url");
|
|
assert.equal(duplicate.kind, "error");
|
|
assert.equal(duplicate.error.code, "duplicate_request");
|
|
assert.equal(cancelAccepted, true);
|
|
assert.equal(cancelled.kind, "error");
|
|
assert.equal(cancelled.error.code, "cancelled");
|
|
assert.equal(afterCancel.kind, "result");
|
|
assert.deepEqual(afterCancel.result, success.result);
|
|
assert.equal(maxActive, 1);
|
|
assert.equal(active, 0);
|
|
for (const response of [success, expectedError, duplicate, cancelled, afterCancel]) assert.equal(isFacadeResponse(response), true);
|
|
|
|
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
|
|
assert.equal(schema.$id, "https://freecad.org/schemas/freecad-web-cloud-configure-recompute-v1.json");
|
|
assert.equal(schema.$defs.request.properties.command.const, CLOUD_COMMAND);
|
|
assert.equal(schema.$defs.typedResult.properties.schema.const, CLOUD_ACTION_SCHEMA);
|
|
assert.equal(schema.$defs.payload.additionalProperties, false);
|
|
|
|
const inputSha256 = crypto.createHash("sha256").update(JSON.stringify(CLOUD_FIXTURE)).digest("hex");
|
|
const resultSha256 = crypto.createHash("sha256").update(JSON.stringify(success.result)).digest("hex");
|
|
const report = {
|
|
schema: "freecad-web-p6-cloud-facade/v1",
|
|
task_id: "FW-P6-CLD-E",
|
|
module: "Cloud",
|
|
action: CLOUD_COMMAND,
|
|
status: "pass",
|
|
source_revision: "b5a086fda59b0325fe0996147e6e9b344414f9bc",
|
|
input_sha256: inputSha256,
|
|
result_sha256: resultSha256,
|
|
responses: { success, expectedError, duplicate, cancelled, afterCancel },
|
|
trace,
|
|
sequencing: { maxActive, activeAfter: active, requestCount: 5, responseCount: 5, cancelAccepted },
|
|
state: {
|
|
beforeCancel: success.result.recompute.summary,
|
|
afterCancel: afterCancel.result.recompute.summary,
|
|
unchangedAfterCancel: success.result.recompute.summary === afterCancel.result.recompute.summary,
|
|
},
|
|
schemaChecks: {
|
|
request: true,
|
|
result: true,
|
|
expectedError: true,
|
|
duplicate: true,
|
|
cancel: true,
|
|
noNativePointers: !JSON.stringify({ responses: { success, expectedError, duplicate, cancelled, afterCancel }, trace }).match(/pointer|native/i),
|
|
},
|
|
commands: [
|
|
"npx tsc --target ES2022 --module ESNext --moduleResolution Bundler --strict --skipLibCheck --outDir /tmp/freecad-p6-cloud-facade --declaration false --noEmit false src/facade-envelope.ts src/cloud-facade.ts",
|
|
"node tools/run_p6_cloud_facade_smoke.mjs /tmp/freecad-p6-cloud-facade/cloud-facade.js freecad-source/build-web/cloud-configure-r2/bin"
|
|
],
|
|
artifacts: {
|
|
schema: path.relative(root, schemaPath),
|
|
workerLog: path.relative(root, logPath),
|
|
wasm: "freecad-source/build-web/cloud-configure-r2/bin/FreeCADWebCloudKernelProbe.wasm",
|
|
},
|
|
boundaries: [
|
|
"TypeScript validates and sequences envelope data; all Cloud configuration and recompute result fields come from the WebAssembly C ABI.",
|
|
"The Facade and Worker never expose a native pointer.",
|
|
"Only the locked configure/recompute action is included."
|
|
],
|
|
generated_at: new Date().toISOString(),
|
|
};
|
|
|
|
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
fs.writeFileSync(logPath, `${trace.map((event, index) => `${String(index + 1).padStart(2, "0")} ${JSON.stringify(event)}`).join("\n")}\nresult=pass\nmaxActive=${maxActive}\n`);
|
|
console.log(JSON.stringify({ result: report.status, requests: report.sequencing.requestCount, responses: report.sequencing.responseCount, maxActive }));
|