Files
mesh-catalog/modules/gitea/test/bundle-import.test.ts
T
jschoubben 896bd88380
mesh/repo-check head 896bd88380b3 merged into trunk a21da33d4857 beside mesh-controller#441@d51324df — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.s…
mesh/merge-gate head 896bd88380b3 merged into trunk a21da33d4857 — pass: group feat/r4e composed: every machine composes with the change as it did without …
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792707174627079192 (novox/mesh-catalog at 89e15c7b into main, do…
mesh/delivery-group group feat/r4e delivered: every member is delivered
mesh/check failed: ace: novox/mesh-catalog#457: modules/gitea/go.mod names mesh-sdk 104533e8c0c9, which is neither on mesh-sdk's trunk nor the group's…
Write mesh/check from the checker's owed state, read by a watch (R2-1)
R4e round 2 finding R2-1, ADR 0521: the delivery plan draws no edge that
orders the forge module before the forge bridge, and the forge module's
durable consumer keeps its deliver policy when its filters change, so a
check.status the bridge emitted before the forge module consumed it was lost.
An in-memory revision guard also forgot itself on a restart.

The forge module now reads mesh-delivery-checker.owed, which the checker
announces readable, by a watch: the watch hands over the latest value of every
key whenever the module starts, so a status written while it was away is
written then, and only the latest. Each mesh/check is written once per
revision, an older one dropped; a forge refusal is asked again on a bound
until written or overtaken; a refused read is said once and asked again until
granted. No tool writes mesh/check, and no check.status is consumed.
2026-10-22 21:45:53 +02:00

112 lines
6.9 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 writes mesh/check from the checker's owed state, and serves no tool that writes it** (ADR 0521; R4e round 2
// finding R2-1; finding K1). The bundle is imported whole, as the runtime imports it, while its read of the checker's owed
// is refused (the grant not yet composed): the watch is asked again, and once granted it is handed the bucket as the bus
// keeps it, the latest revision of each key, then an older revision delivered late. The forge stand-in sees the latest
// written, and nothing after it.
test("a bundle whose read of the checker's owed is granted late writes the latest mesh/check, 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) => { bound.push(pattern); return () => {}; },
publish: async () => {}, request: async () => { throw new Error("no responders"); },
handle: async () => () => {}, close: async () => {},
}));
const key = "novox/mesh-sdk.12.${sha}.status=mesh/check";
const owed = (state, text) => ({ v: 1, act: "status", context: "mesh/check", state, text, target: "https://view.example/c" });
const watched = [];
let granted = 0;
const { useStateChannel } = await import("@novox/mesh-sdk/state");
useStateChannel({
ask: async () => null,
watch: async (params, deliver) => {
watched.push(params.state);
if (watched.length === 1) throw new Error('gitea keeps and reads no state called "mesh-delivery-checker.owed"');
granted++;
// Two revisions were written while the read was refused; the bucket hands the latest, then a late older one.
await deliver({ state: params.state, key, op: "put", value: owed("success", "pass at 4"), revision: 4, current: true });
await deliver({ state: params.state, key, op: "put", value: owed("pending", "pending at 3"), revision: 3, current: false });
},
});
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 < 300 && !(granted && statuses.length); i++) await new Promise((r) => setTimeout(r, 20));
await new Promise((r) => setTimeout(r, 200));
console.log(JSON.stringify({ tools, bound, watched, 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"), 15000);
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[]; watched: 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.some((p) => p.includes("check.status")), `bound: ${got.bound}`);
assert.deepEqual(got.watched, ["mesh-delivery-checker.owed", "mesh-delivery-checker.owed"], "refused, then asked again");
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":"success".*pass at 4/);
assert.match(err, /mesh-delivery-checker\.owed could not be bound/, "the refusal is said");
});