Review finding K1 of R4e, issue 1301 S4 (ADR 0503 §3, ADR 0513 §4: the forge requires mesh/check alone). gitea_check_status assumed only the bridge could call it, but a tool is told no caller and node-tools invokes every tool, so any session could set mesh/check to success and merge past the checker: the fault issue 293 closed for the two statuses before it. gitea now consumes mesh-delivery-forge-bridge.check.status, a subject the bus grants to the bridge's own account alone, and check.ts writes mesh/check from it: the source is checked, the head's whole sha and the state, and a revision older than one already written on the head is dropped so a redelivered stale pending never hides a verdict; a forge refusal throws, so the event is delivered again. gitea_check_status is removed, and gitea_commit_status keeps refusing mesh/check.
96 lines
6.0 KiB
TypeScript
96 lines
6.0 KiB
TypeScript
// **The bundle's import ends while a bind is refused** (novox/hq issue 1793). The launcher imports index.js and then answers
|
|
// node-tools' handshake, which has 10 s before the bundle is killed and never started again. A bind awaited at the top
|
|
// of index.ts, refused for longer, ended the bundle at 00:28:37 on 2026-10-22. Run in a child: the polls' timers keep a
|
|
// process alive, and the child says "imported" and exits as soon as the import resolves.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawn } from "node:child_process";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
|
|
test("importing the bundle resolves while the broker keeps refusing the binds", async () => {
|
|
const index = pathToFileURL(join(here, "..", "dist", "index.js")).href;
|
|
const script = `
|
|
const { useBroker } = await import("@novox/mesh-sdk/messaging");
|
|
useBroker(() => ({
|
|
subscribe: async () => { throw new Error("binding gitea's consumer novox_gitea: nats: no responders available for request"); },
|
|
publish: async () => {}, request: async () => { throw new Error("no responders"); },
|
|
handle: async () => () => {}, close: async () => {},
|
|
}));
|
|
await import(${JSON.stringify(index)});
|
|
console.log("imported");
|
|
process.exit(0);`;
|
|
const env = { ...process.env, MESH_GITEA_TOKEN: "x", MESH_GITEA_URL: "http://127.0.0.1:1" };
|
|
for (const k of ["MESH_GITEA_STATE_DIR", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD", "MESH_SERVED_MODULE"]) delete env[k];
|
|
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
|
|
let out = "";
|
|
let err = "";
|
|
child.stdout.on("data", (d) => (out += d));
|
|
child.stderr.on("data", (d) => (err += d));
|
|
const ended = new Promise<void>((r) => child.on("exit", () => r()));
|
|
const timer = setTimeout(() => child.kill("SIGKILL"), 3000);
|
|
await ended;
|
|
clearTimeout(timer);
|
|
assert.match(out, /imported/, `the import did not resolve within 3 s while a bind was refused; stderr: ${err.slice(0, 600)}`);
|
|
});
|
|
|
|
// **The bundle binds the forge bridge's check.status and serves no tool that writes mesh/check** (review finding K1 of R4e;
|
|
// issue 1301 S4; ADR 0503 §3). The bundle is imported whole, as the runtime imports it; the consumer it binds is handed the
|
|
// bridge's event and one from another source, and the forge stand-in sees mesh/check written for the bridge's alone.
|
|
test("the bundle writes mesh/check from the bridge's check.status alone, and serves no gitea_check_status", async () => {
|
|
const index = pathToFileURL(join(here, "..", "dist", "index.js")).href;
|
|
const sha = "0123456789abcdef0123456789abcdef01234567";
|
|
const script = `
|
|
const http = await import("node:http");
|
|
const statuses = [];
|
|
const forge = http.createServer((req, res) => {
|
|
let b = ""; req.on("data", (d) => (b += d)); req.on("end", () => {
|
|
if (req.method === "POST" && /\\/statuses\\//.test(req.url)) statuses.push(req.url + " " + b);
|
|
res.setHeader("content-type", "application/json"); res.end(req.method === "GET" ? "[]" : "{}");
|
|
});
|
|
});
|
|
await new Promise((r) => forge.listen(0, "127.0.0.1", r));
|
|
process.env.MESH_GITEA_URL = "http://127.0.0.1:" + forge.address().port;
|
|
const bound = {};
|
|
const { useBroker } = await import("@novox/mesh-sdk/messaging");
|
|
useBroker(() => ({
|
|
subscribe: async (pattern, handler) => { bound[pattern] = handler; return () => {}; },
|
|
publish: async () => {}, request: async () => { throw new Error("no responders"); },
|
|
handle: async () => () => {}, close: async () => {},
|
|
}));
|
|
await import(${JSON.stringify(index)});
|
|
const { collectTools } = await import("@novox/mesh-sdk/tools");
|
|
const tools = collectTools().flatMap((r) => r.tools.map((t) => t.name));
|
|
for (let i = 0; i < 100 && !bound["mesh-delivery-forge-bridge.check.status"]; i++) await new Promise((r) => setTimeout(r, 20));
|
|
const h = bound["mesh-delivery-forge-bridge.check.status"];
|
|
const env = (source, state) => ({ key: "check.status", node: "n1",
|
|
headers: { "x-event-id": "e", "x-source": source, "x-node": "n1", "x-time": "", "content-type": "application/json" },
|
|
body: { owner: "novox", repo: "mesh-sdk", sha: ${JSON.stringify(sha)}, state, description: "by " + source, revision: 1 } });
|
|
if (h) { await h(env("node-tools", "success")); await h(env("mesh-delivery-forge-bridge", "pending")); }
|
|
console.log(JSON.stringify({ tools, bound: Object.keys(bound), statuses }));
|
|
process.exit(0);`;
|
|
const env = { ...process.env, MESH_GITEA_TOKEN: "x" };
|
|
for (const k of ["MESH_GITEA_STATE_DIR", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD", "MESH_SERVED_MODULE", "MESH_GITEA_URL"]) delete env[k];
|
|
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
|
|
let out = "";
|
|
let err = "";
|
|
child.stdout.on("data", (d) => (out += d));
|
|
child.stderr.on("data", (d) => (err += d));
|
|
const ended = new Promise<void>((r) => child.on("exit", () => r()));
|
|
const timer = setTimeout(() => child.kill("SIGKILL"), 8000);
|
|
await ended;
|
|
clearTimeout(timer);
|
|
const line = out.split("\n").find((l) => l.startsWith("{"));
|
|
assert.ok(line, `the bundle said nothing; stdout: ${out.slice(0, 400)} stderr: ${err.slice(0, 600)}`);
|
|
const got = JSON.parse(line!) as { tools: string[]; bound: string[]; statuses: string[] };
|
|
assert.ok(got.tools.includes("gitea_commit_status"), `the bundle's tools: ${got.tools}`);
|
|
assert.ok(!got.tools.includes("gitea_check_status"), "no tool writes mesh/check");
|
|
assert.ok(got.bound.includes("mesh-delivery-forge-bridge.check.status"), `bound: ${got.bound}`);
|
|
assert.equal(got.statuses.length, 1, `statuses written: ${got.statuses}`);
|
|
assert.match(got.statuses[0], new RegExp(`/repos/novox/mesh-sdk/statuses/${sha} .*"context":"mesh/check"`));
|
|
assert.match(got.statuses[0], /"state":"pending".*by mesh-delivery-forge-bridge/);
|
|
});
|