Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
114 lines
6.7 KiB
JavaScript
114 lines
6.7 KiB
JavaScript
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 { once } from "node:events";
|
|
import test from "node:test";
|
|
import { createDeploymentHttpServer } from "../../../tools/web/deployment-http-server.mjs";
|
|
|
|
const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
const canonical = (value) => JSON.stringify(value, (_, item) => item && !Array.isArray(item) && typeof item === "object" ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
|
|
|
|
test("async HTTP provider lifecycle (transport double, not Blender evidence)", async (t) => {
|
|
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "operator-http-test-"));
|
|
const options = { workspaceRoot, pythonBinary: path.resolve(import.meta.dirname, "../fixtures/server-operator-runner.py") };
|
|
const server = createDeploymentHttpServer({ root: workspaceRoot, serverOperator: options });
|
|
server.listen(0, "127.0.0.1");
|
|
await once(server, "listening");
|
|
const url = `http://127.0.0.1:${server.address().port}/api/server-operator-job`;
|
|
const requests = [];
|
|
async function call(action, request, body) {
|
|
const response = await fetch(action === "read" ? `${url}?jobId=${request.jobId}&requestSha256=${request.requestSha256}` : url, {
|
|
method: action === "read" ? "GET" : "POST",
|
|
headers: { "X-Server-Operator-Action": action },
|
|
body: action === "read" ? undefined : body ?? JSON.stringify(request),
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
return { code: response.status, ...await response.json() };
|
|
}
|
|
function envelope(mode, overrides = {}) {
|
|
const source = Buffer.from("transport-source");
|
|
const properties = { mode };
|
|
const request = { schemaVersion: 1, route: "SERVER_NATIVE", jobId: crypto.randomUUID(), sourceBlendSha256: digest(source), sourceBlendByteLength: source.length, sourceRevision: 1, blenderBuild: { version: "test", buildSha256: "a".repeat(64) }, operator: "object.test", properties, propertiesSha256: digest(canonical(properties)), timeoutMs: 1000, memoryBytes: 64 * 1024 * 1024, resultBinding: { kind: "snapshot", key: "test" }, context: "BACKGROUND", ...overrides };
|
|
request.requestSha256 = digest(canonical(request));
|
|
const json = Buffer.from(JSON.stringify(request));
|
|
const header = Buffer.alloc(16);
|
|
header.write("WBJ1"); header.writeUInt32LE(1, 4); header.writeUInt32LE(json.length, 8); header.writeUInt32LE(source.length, 12);
|
|
requests.push(request);
|
|
return { request, body: Buffer.concat([header, json, source]) };
|
|
}
|
|
async function terminal(request) {
|
|
for (let i = 0; i < 80; i++) {
|
|
const reply = await call("read", request);
|
|
if (reply.result.status !== "RUNNING") return reply.result;
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
assert.fail("job did not reach a terminal state");
|
|
}
|
|
try {
|
|
await t.test("deployment reserves the API and rejects invalid methods/actions before execution", async () => {
|
|
const invalid = await call("unknown", {});
|
|
assert.equal(invalid.code, 400);
|
|
for (const action of ["describe", "submit", "cancel"]) {
|
|
const response = await fetch(url, { headers: { "X-Server-Operator-Action": action } });
|
|
assert.equal(response.status, 405);
|
|
assert.equal(response.headers.get("allow"), "POST");
|
|
assert.equal(response.headers.get("cache-control"), "no-store");
|
|
assert.equal(response.headers.get("cross-origin-opener-policy"), "same-origin");
|
|
}
|
|
assert.equal((await fetch(`${url}?jobId=missing&requestSha256=missing`)).status, 404);
|
|
assert.equal((await fetch(new URL("/index.html", url), { method: "POST" })).status, 405);
|
|
const malformed = await call("submit", {}, Buffer.from("invalid envelope"));
|
|
assert.equal(malformed.code, 400);
|
|
const versioned = envelope("success"); versioned.body.writeUInt32LE(2, 4);
|
|
assert.equal((await call("submit", versioned.request, versioned.body)).code, 400);
|
|
assert.equal((await call("read", versioned.request)).code, 404);
|
|
assert.deepEqual(fs.readdirSync(workspaceRoot), []);
|
|
});
|
|
await t.test("202, concurrent reads, success, immutable terminal state and cleanup", async () => {
|
|
const { request, body } = envelope("success");
|
|
const submitted = await call("submit", request, body);
|
|
assert.equal(submitted.code, 202); assert.equal(submitted.result.status, "RUNNING");
|
|
assert.equal((await call("read", request)).result.status, "RUNNING");
|
|
assert.equal((await call("submit", request, body)).code, 409);
|
|
const replies = await Promise.all([terminal(request), terminal(request)]);
|
|
assert.deepEqual(replies[0], replies[1]);
|
|
assert.equal(replies[0].status, "SUCCEEDED");
|
|
assert.equal(Buffer.from(replies[0].resultBlendBase64, "base64").toString(), "transport-result");
|
|
assert.equal((await call("cancel", request)).result.status, "SUCCEEDED");
|
|
assert.deepEqual(fs.readdirSync(workspaceRoot), []);
|
|
});
|
|
await t.test("cancel is idempotent; no late success or result bytes", async () => {
|
|
const { request, body } = envelope("slow");
|
|
await call("submit", request, body);
|
|
await Promise.all([call("cancel", request), call("read", request)]);
|
|
assert.equal((await call("cancel", request)).result.status, "CANCELLED");
|
|
const result = await terminal(request);
|
|
assert.equal(result.status, "CANCELLED"); assert.equal(result.resultBlendBase64, undefined);
|
|
assert.deepEqual(fs.readdirSync(workspaceRoot), []);
|
|
});
|
|
for (const mode of ["signal", "slow", "malformed", "bad-hash", "bad-binding"]) {
|
|
await t.test(`${mode} fails closed and cleans without polling`, async () => {
|
|
const { request, body } = envelope(mode);
|
|
assert.equal((await call("submit", request, body)).code, 202);
|
|
await new Promise((resolve) => setTimeout(resolve, mode === "slow" ? 1600 : 650));
|
|
assert.deepEqual(fs.readdirSync(workspaceRoot), []);
|
|
const result = await terminal(request);
|
|
assert.equal(result.status, "FAILED"); assert.equal(result.resultBlendBase64, undefined);
|
|
});
|
|
}
|
|
await t.test("missing executable does not crash the HTTP provider", async () => {
|
|
options.pythonBinary = path.join(workspaceRoot, "missing-python");
|
|
const { request, body } = envelope("success");
|
|
assert.equal((await call("submit", request, body)).code, 202);
|
|
assert.equal((await terminal(request)).status, "FAILED");
|
|
});
|
|
} finally {
|
|
for (const request of requests) await call("cancel", request).catch(() => {});
|
|
server.closeAllConnections();
|
|
await new Promise((resolve) => server.close(resolve));
|
|
fs.rmSync(workspaceRoot, { recursive: true, force: true });
|
|
}
|
|
});
|