Revert the revert of G's mesh-sdk merge (#191): G's sdk stands again over reader release 3
Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
Vendored
+11
-2
@@ -1,8 +1,17 @@
|
||||
// A tools bundle written against the protocol: the module's own tool, a seat's verb, and one that fails.
|
||||
// A tools bundle written against the protocol: the module's own tool, a seat's verb, one that fails, and one that says
|
||||
// who called it as the runtime proved it and refuses a caller its verb's list does not name.
|
||||
import { serveStdio } from "../../dist/stdio/index.js";
|
||||
import { callerOf, callIdOf, refusal } from "../../dist/callers/index.js";
|
||||
|
||||
const verbs = { "node-lamp.on": ["mesh-delivery-forge-bridge"] };
|
||||
|
||||
await serveStdio("lamp", [
|
||||
{ name: "brightness", description: "set it", input: { level: { type: "number", description: "0 to 10" } }, run: async (a) => ({ level: a.level }) },
|
||||
{ name: "node-lamp.on", description: "the seat's verb", input: {}, run: async () => ({ on: true }) },
|
||||
{ name: "node-lamp.on", description: "the seat's verb", input: {}, run: async (a) => {
|
||||
const no = refusal(verbs, "node-lamp.on", callerOf(a));
|
||||
if (no) throw no;
|
||||
return { on: true };
|
||||
} },
|
||||
{ name: "fail", description: "it throws", input: {}, run: async () => { throw new Error("the bulb is gone"); } },
|
||||
{ name: "who", description: "who called", input: {}, run: async (a) => ({ caller: callerOf(a), call: callIdOf(a) }) },
|
||||
]);
|
||||
|
||||
Vendored
+4
-1
@@ -1,8 +1,11 @@
|
||||
// A launched bundle whose tool emits (novox/hq ADR 0193): the emit travels to the runtime over stdio.
|
||||
// A launched bundle whose tool emits (novox/hq ADR 0193): the emit travels to the runtime over stdio, and so does an
|
||||
// event it says on a seat (ADR 0259 §3).
|
||||
import { registerModuleTools } from "../../dist/tools/index.js";
|
||||
import { emit } from "../../dist/events/index.js";
|
||||
import { seatPublish } from "../../dist/runtime/index.js";
|
||||
import { serveRegisteredOverStdio } from "../../dist/stdio/index.js";
|
||||
registerModuleTools("chime", () => [
|
||||
{ name: "ring", description: "rings and says so", input: {}, run: async () => { await emit("rang", { times: 1 }); return { rang: true }; } },
|
||||
{ name: "peal", description: "says it on its seat", input: {}, run: async () => { await seatPublish("mesh.seat.bell.event.pealed.anchor", { times: 2 }, "p-1"); return { pealed: true }; } },
|
||||
]);
|
||||
await serveRegisteredOverStdio();
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// A launched bundle whose finding writer's transport is the bundle's (design 47 G §559): the writer's publish asks the
|
||||
// runtime for an acknowledged publish, its consumer of the asks a subscription that delivers only new messages.
|
||||
import { bundleTransport } from "../../dist/views/index.js";
|
||||
import { registerModuleTools } from "../../dist/tools/index.js";
|
||||
import { serveRegisteredOverStdio } from "../../dist/stdio/index.js";
|
||||
|
||||
const t = bundleTransport("keeper", "anchor");
|
||||
await t.listen("mesh-conditions.findings-asked", (b) => process.stderr.write(`asked ${JSON.stringify(b)}\n`));
|
||||
registerModuleTools("keeper", () => [
|
||||
{
|
||||
name: "say", description: "says a finding", input: {},
|
||||
run: async () => {
|
||||
try {
|
||||
await t.publish("finding-written", { kind: "k" }, "id-1");
|
||||
return { said: true };
|
||||
} catch (e) {
|
||||
return { said: false, why: String(e.message) };
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
await serveRegisteredOverStdio();
|
||||
@@ -661,4 +661,366 @@ test("the provider loop is named for what it is, and its old name stays an alias
|
||||
assert.equal(pkg.exports["./provisioner"], pkg.exports["./provider-loop"]);
|
||||
// The events and tools are what the controller and the operator read on the bus: their names stay.
|
||||
assert.equal(loop.FAILING_EVENT, "provisioner.failing");
|
||||
// Tests of milestone G, merged here to keep the module within its test budget (design 47 G §419):
|
||||
await newTest1(); // callers and verb callers judge a proved caller by the Go side's rules
|
||||
await newTest2(); // a first view follows its owner's events, asks again on a gap and by ha
|
||||
await newTest3(); // the finding writer writes, clears and states findings as the Go writer
|
||||
await newTest4(); // the finding writer owns its kinds, keeps a views reader's findings in
|
||||
await newTest5(); // a bundle reads its tool runner's capability words once, reads an unkno
|
||||
await newTest6(); // a TypeScript bundle's value over one call's limit fails at once, namin
|
||||
});
|
||||
|
||||
import { allows, refusal, parseCallers, parseVerbCallers, parseConfiguredVerbCallers, readDefinitionCallers, moduleOf, UNPROVEN } from "../dist/callers/index.js";
|
||||
import { Follower, statusTools, keepStatus, statusKey, viewWaitsFinding, type Answer, type Status } from "../dist/views/index.js";
|
||||
|
||||
// callers and verb callers judge a proved caller by the Go side's rules (ADR 0459 §2, ADR 0627 §2)
|
||||
async function newTest1(): Promise<void> {
|
||||
const rows: [string, string[], string, boolean][] = [
|
||||
["a module on any machine", ["mesh-delivery-forge-bridge"], "ace.mesh-delivery-forge-bridge", true],
|
||||
["exactly a machine's module", ["ace.mesh-delivery-forge-bridge"], "g14.mesh-delivery-forge-bridge", false],
|
||||
["exactly a person", ["person.jochen"], "person.jochen", true],
|
||||
["a module entry is not a person", ["jochen"], "person.jochen", false],
|
||||
["a wildcard is no entry", ["*", ">"], "ace.anything", false],
|
||||
["an unproven caller never matches", ["unproven"], UNPROVEN, false],
|
||||
];
|
||||
for (const [what, list, caller, want] of rows) assert.equal(allows(list, caller), want, what);
|
||||
assert.deepEqual(parseCallers('{"callers":["a","person.b"]}'), ["a", "person.b"]);
|
||||
// design 47 G §336, §392: a definition's callers are a list of the format's forms; text and a bare name are refused
|
||||
assert.deepEqual(readDefinitionCallers("list", ["operator", "invokers", "seat:mesh-grants", "holder:node-backup@same-node"]),
|
||||
["operator", "invokers", "seat:mesh-grants", "holder:node-backup@same-node"]);
|
||||
assert.throws(() => readDefinitionCallers("list", "a person"), /list: callers are text/);
|
||||
assert.throws(() => readDefinitionCallers("list", ["mesh-grants"]), /list: the callers entry "mesh-grants" is none of/);
|
||||
// design 47 G §391, §456: a verb with no callers takes its class's default; no class reads as read; its own list is kept
|
||||
const byClass: [string | undefined, string[]][] = [
|
||||
[undefined, ["invokers", "persons", "sessions"]], ["read", ["invokers", "persons", "sessions"]],
|
||||
["act", ["operator", "sessions"]], ["warrant", ["operator"]],
|
||||
];
|
||||
for (const [cls, want] of byClass) assert.deepEqual(readDefinitionCallers("list", undefined, cls), want, String(cls));
|
||||
assert.deepEqual(readDefinitionCallers("register", ["seat:mesh-broker"], "act"), ["seat:mesh-broker"]);
|
||||
assert.throws(() => parseCallers('{"callers":["a"],"more":1}'), /unknown field "more"/);
|
||||
|
||||
const verbs = parseVerbCallers('{"pull":["*"],"merge-pull":["mesh-delivery-forge-bridge"],"comment":["operator"]}', ["pull", "merge-pull", "comment"], {});
|
||||
assert.equal(refusal(verbs, "pull", "ace.anyone"), undefined);
|
||||
assert.match(String(refusal(verbs, "pull", UNPROVEN)), /carries no caller the tool runner proved/);
|
||||
assert.equal(refusal(verbs, "merge-pull", "novox.mesh-delivery-forge-bridge"), undefined);
|
||||
assert.match(String(refusal(verbs, "merge-pull", "person.jochen")), /refused for person.jochen: not on the holder's caller list for merge-pull/);
|
||||
assert.equal(refusal(verbs, "comment", "person.jochen"), undefined);
|
||||
assert.ok(refusal(verbs, "comment", "ace.mesh-delivery-forge-bridge"));
|
||||
assert.ok(refusal(verbs, "status", "ace.anyone"), "a verb the list does not name refuses everyone");
|
||||
assert.throws(() => parseVerbCallers('{"nope":["*"]}', ["pull"], {}), /names the verb "nope", which the holder does not serve/);
|
||||
const defaults = { pull: ["*"] };
|
||||
assert.equal(parseVerbCallers(" ", ["pull"], defaults), defaults);
|
||||
assert.equal(parseConfiguredVerbCallers(null, ["pull"], defaults), defaults);
|
||||
assert.deepEqual(parseConfiguredVerbCallers('{"pull":["a"]}', ["pull"], defaults), { pull: ["a"] });
|
||||
assert.deepEqual(parseConfiguredVerbCallers({ pull: ["b"] }, ["pull"], defaults), { pull: ["b"] });
|
||||
assert.throws(() => parseConfiguredVerbCallers("{", ["pull"], defaults), /not a JSON object/);
|
||||
assert.equal(moduleOf("ace.mesh-delivery-forge-bridge"), "mesh-delivery-forge-bridge");
|
||||
}
|
||||
|
||||
// a first view follows its owner's events, asks again on a gap and by hand, and its two tools read and steer it (ADR 0615)
|
||||
async function newTest2(): Promise<void> {
|
||||
const created = "2026-11-09T10:00:00.000Z";
|
||||
let held: Record<string, unknown> = { a: 1, b: 2, c: 3 };
|
||||
let revision = 4;
|
||||
let down = 2;
|
||||
const asked: string[] = [];
|
||||
const raised: Status[] = [];
|
||||
const cleared: Status[] = [];
|
||||
const pauses: number[] = [];
|
||||
let clock = Date.parse(created);
|
||||
// The owner answers two keys a page, and is down for its first two asks.
|
||||
const ask = async (after: string): Promise<Answer> => {
|
||||
asked.push(after);
|
||||
if (down > 0) {
|
||||
down--;
|
||||
throw new Error("no responders");
|
||||
}
|
||||
const keys = Object.keys(held).sort().filter((k) => k > after);
|
||||
const page = keys.slice(0, 2);
|
||||
return { v: 1, revision, created, values: Object.fromEntries(page.map((k) => [k, held[k]])), next: keys.length > 2 ? page[1] : "" };
|
||||
};
|
||||
const kept = new Map<string, Status>();
|
||||
const st = {
|
||||
get: async (k: string) => (kept.has(k) ? { key: k, value: kept.get(k)!, revision: 1 } : null),
|
||||
put: async (k: string, v: Status) => (kept.set(k, structuredClone(v)), 1),
|
||||
delete: async (k: string) => void kept.delete(k),
|
||||
keys: async () => [...kept.keys()].sort(),
|
||||
watch: async () => {},
|
||||
};
|
||||
const f = new Follower({
|
||||
owner: "mesh-catalog", tool: "mesh-catalog.revision", state: "catalog",
|
||||
ask, said: keepStatus(st), raise: (s) => void raised.push(s), clear: (s) => void cleared.push(s),
|
||||
now: () => new Date(clock), bound: 1000, wait: 10,
|
||||
pause: async (ms) => { pauses.push(ms); clock += 1000; },
|
||||
});
|
||||
// An event before the first view is answered is passed over: the answer holds it.
|
||||
assert.deepEqual(await f.apply({ key: "a", revision: 4, created, value: 1 }), { gap: false, why: "" });
|
||||
await f.start();
|
||||
assert.deepEqual(pauses, [10, 20], "a failed ask waits, doubling");
|
||||
assert.equal(raised.length, 1, "past its bound it raises its condition");
|
||||
assert.equal(cleared.length, 1, "the answer clears it");
|
||||
assert.deepEqual(Object.fromEntries(f.picture()), { a: 1, b: 2, c: 3 }, "every page is taken");
|
||||
assert.deepEqual(asked.slice(-2), ["", "b"]);
|
||||
let s = kept.get(statusKey("mesh-catalog", "catalog"))!;
|
||||
assert.equal(s.revision, 4);
|
||||
assert.equal(s.asks, 3);
|
||||
assert.equal(s.raised, false);
|
||||
assert.equal(s.keys, 3);
|
||||
|
||||
// The next revision is taken; one at or below the picture's is passed over.
|
||||
assert.deepEqual(await f.apply({ key: "d", revision: 5, created, value: 4 }), { gap: false, why: "" });
|
||||
assert.deepEqual(await f.apply({ key: "a", revision: 3, created, value: 0 }), { gap: false, why: "" });
|
||||
assert.equal(f.get("d"), 4);
|
||||
assert.equal(f.get("a"), 1);
|
||||
assert.equal(kept.get(statusKey("mesh-catalog", "catalog"))!.revision, 5, "the kept status follows the events");
|
||||
|
||||
// A revision past the next one is a gap: hearing it asks the first view once more.
|
||||
held = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 };
|
||||
revision = 7;
|
||||
const before = asked.length;
|
||||
await f.hear({ key: "f", revision: 7, created, value: 6 });
|
||||
await f.ready;
|
||||
for (let i = 0; i < 10 && f.status().revision !== 7; i++) await Promise.resolve();
|
||||
assert.ok(asked.length > before, "a gap asks again");
|
||||
assert.equal(f.status().revision, 7);
|
||||
assert.equal(f.status().why?.startsWith("a gap:"), true);
|
||||
assert.equal(f.get("e"), 5);
|
||||
|
||||
// The two tools: read lists the kept status; again records the hand act first and asks by hand with the caller.
|
||||
const acts: unknown[] = [];
|
||||
const [read, again] = statusTools("gitea", "gitea_first_views", "gitea_first_view_again", [f], st, async (a) => void acts.push(a));
|
||||
const listed = (await read.run({})) as { "first-views": Status[] };
|
||||
assert.equal(listed["first-views"].length, 1);
|
||||
assert.equal(listed["first-views"][0].tool, "mesh-catalog.revision");
|
||||
await assert.rejects(again.run({ owner: "mesh-catalog", state: "catalog" }), /names the owner, the state and why/);
|
||||
await assert.rejects(again.run({ owner: "x", state: "catalog", why: "w" }), /holds no first view of x's catalog/);
|
||||
const [, refused] = statusTools("gitea", "r", "g", [f], st, undefined);
|
||||
await assert.rejects(refused.run({ owner: "mesh-catalog", state: "catalog", why: "w" }), /nothing was asked again/);
|
||||
const byHandBefore = f.status()["by-hand"];
|
||||
await again.run({ owner: "mesh-catalog", state: "catalog", why: "the picture is doubted" });
|
||||
for (let i = 0; i < 10 && f.status()["by-hand"] === byHandBefore; i++) await Promise.resolve();
|
||||
assert.equal(acts.length, 1);
|
||||
assert.equal(f.status()["by-hand"], byHandBefore + 1);
|
||||
assert.match(f.status().why ?? "", /^by hand: the picture is doubted \(unproven\)$/);
|
||||
// Another created is the state made again: a gap too.
|
||||
f.close();
|
||||
assert.equal((await f.apply({ key: "a", revision: 1, created: "2026-11-09T11:00:00.000Z", value: 9 })).gap, true);
|
||||
|
||||
// Design 47 *A part with no view says it waits*: the status says the view held, the one waited for and since when;
|
||||
// past its bound the part's view-waits is raised once, and holding the view clears it.
|
||||
const ends: (() => void)[] = [];
|
||||
const bounds: number[] = [];
|
||||
const wRaised: Status[] = [];
|
||||
const wCleared: Status[] = [];
|
||||
const settle = async () => { for (let i = 0; i < 20; i++) await Promise.resolve(); };
|
||||
const w = new Follower({
|
||||
owner: "o", tool: "o.revision", state: "catalog", ask, now: () => new Date(created), said: keepStatus(st),
|
||||
raiseWaits: (s) => void wRaised.push(s), clearWaits: (s) => void wCleared.push(s),
|
||||
waitsBound: (ms) => (bounds.push(ms), new Promise<void>((resolve) => ends.push(resolve))),
|
||||
});
|
||||
await w.holds("trunk.11");
|
||||
await w.waitFor("trunk.12");
|
||||
await w.waitFor("trunk.12"); // the same view again: nothing new, no second bound
|
||||
assert.deepEqual(bounds, [5 * 60 * 1000], "one five-minute bound");
|
||||
s = w.status();
|
||||
assert.equal(s.view, "trunk.11");
|
||||
assert.equal(s["waits-for"], "trunk.12");
|
||||
assert.equal(s["waiting-since"], created);
|
||||
assert.equal(s["waits-raised"], false);
|
||||
assert.equal(kept.get(statusKey("o", "catalog"))!["waits-for"], "trunk.12", "the read tool's state says it");
|
||||
ends.shift()!();
|
||||
await settle();
|
||||
assert.equal(wRaised.length, 1, "past its bound view-waits is raised");
|
||||
assert.equal(wRaised[0]["waits-for"], "trunk.12");
|
||||
const fd = viewWaitsFinding("gitea", w.status(), new Date(created));
|
||||
assert.equal(fd.kind, "view-waits");
|
||||
assert.equal(fd.id, "gitea.o.catalog");
|
||||
assert.match(fd.summary, /^gitea waits for o's view trunk\.12 since 2026-11-09T10:00:00Z and acts on nothing new until it holds it; it holds trunk\.11$/);
|
||||
await w.holds("trunk.12");
|
||||
s = w.status();
|
||||
assert.equal(s.view, "trunk.12");
|
||||
assert.equal(s["waits-for"], undefined);
|
||||
assert.equal(s["waits-raised"], undefined);
|
||||
assert.equal(wCleared.length, 1, "holding the view clears it");
|
||||
await w.waitFor("trunk.13");
|
||||
await w.holds("trunk.13");
|
||||
ends.shift()!(); // the bound of a view now held raises nothing
|
||||
await settle();
|
||||
assert.equal(wRaised.length, 1, "a view held before its bound raises nothing");
|
||||
assert.equal(wCleared.length, 1, "nothing raised, nothing cleared");
|
||||
}
|
||||
|
||||
// the finding writer writes, clears and states findings as the Go writer does
|
||||
async function newTest3(): Promise<void> {
|
||||
const { Writer, OBSERVE_RETRY_MS } = await import("../dist/views/index.js");
|
||||
const store = new Map<string, any>();
|
||||
const st: any = {
|
||||
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
|
||||
put: async (k: string, v: any) => (store.set(k, structuredClone(v)), 1),
|
||||
delete: async (k: string) => void store.delete(k),
|
||||
keys: async () => [...store.keys()].sort(),
|
||||
};
|
||||
let clock = Date.parse("2026-11-11T12:00:00Z");
|
||||
const sent: { subject: string; body: any }[] = [];
|
||||
let refuse = 0;
|
||||
const waits: number[] = [];
|
||||
let ask: ((b: unknown) => void) | undefined;
|
||||
let consumers = 0;
|
||||
let observes = 0;
|
||||
const logs: string[] = [];
|
||||
const w = new Writer({
|
||||
module: "m",
|
||||
asked: "s.findings-asked",
|
||||
state: st,
|
||||
|
||||
observe: async () => {
|
||||
if (++observes <= 2) throw new Error("not yet");
|
||||
return { findings: [{ kind: "k", scope: "module", id: "a", severity: "warning", summary: "now", source: "p", subject: { scope: "module", id: "a" } }], pending: [{ key: "module.wait.k", since: "2026-11-11T11:00:00Z", bound: "5m" }] };
|
||||
},
|
||||
publish: async (subject, body) => {
|
||||
if (refuse > 0) { refuse--; throw new Error("no ack"); }
|
||||
sent.push({ subject, body });
|
||||
},
|
||||
listen: async (s, h) => { assert.equal(s, "s.findings-asked"); consumers++; ask = h; return () => {}; },
|
||||
log: (l) => logs.push(l),
|
||||
now: () => new Date(clock),
|
||||
wait: async (ms) => { waits.push(ms); clock += ms; return true; },
|
||||
});
|
||||
const f = { kind: "k", scope: "module", id: "a", severity: "warning" as const, summary: "one", source: "p", subject: { scope: "module", id: "a" } };
|
||||
const first = new Date(clock).toISOString();
|
||||
await w.write(f);
|
||||
clock += 3600_000;
|
||||
await w.write({ ...f, run: "other" }); // nothing it says changed
|
||||
await w.write({ ...f, summary: "two" });
|
||||
assert.deepEqual(sent.map((s) => s.subject), ["finding-written", "finding-written"]);
|
||||
assert.equal(sent[1].body.since, first);
|
||||
assert.equal(sent[1].body.observer, "m");
|
||||
await w.clear("module", "never", "k", "held by the keeper");
|
||||
assert.equal(sent[2].subject, "finding-cleared");
|
||||
assert.equal(sent[2].body.why, "held by the keeper");
|
||||
// an unacknowledged event is sent again for a minute, then logged naming the finding
|
||||
refuse = 1000;
|
||||
const t0 = clock;
|
||||
await w.write({ ...f, id: "b" });
|
||||
assert.ok(clock - t0 >= 60_000 && clock - t0 < 70_000);
|
||||
assert.ok(logs.some((l) => l.includes("module.b.k") && l.startsWith("error")));
|
||||
refuse = 0;
|
||||
// a start states what is observed now, retrying observe every minute, and deletes the keys not stated or pending
|
||||
store.set("module.old.k", f);
|
||||
waits.length = 0;
|
||||
await w.start();
|
||||
for (let i = 0; i < 50 && sent.length < 4; i++) await new Promise((r) => setImmediate(r));
|
||||
const stated = sent[3];
|
||||
assert.equal(stated.subject, "findings-stated");
|
||||
assert.deepEqual(waits.slice(0, 2), [OBSERVE_RETRY_MS, OBSERVE_RETRY_MS]);
|
||||
assert.equal(stated.body.findings.length, 1);
|
||||
assert.equal(stated.body.findings[0].since, first);
|
||||
assert.equal(stated.body.pending[0].key, "module.wait.k");
|
||||
assert.deepEqual([...store.keys()].sort(), ["module.a.k"]);
|
||||
// an ask for another module is not answered, one for this module is, with its id; a reconnect makes the consumer again
|
||||
ask!({ ask: "a1", at: "", by: "keeper", module: "other" });
|
||||
ask!({ ask: "a2", at: "", by: "keeper", module: "m" });
|
||||
for (let i = 0; i < 50 && sent.length < 5; i++) await new Promise((r) => setImmediate(r));
|
||||
assert.equal(sent.length, 5);
|
||||
assert.equal(sent[4].body.asked, "a2");
|
||||
await w.reconnected();
|
||||
for (let i = 0; i < 50 && sent.length < 6; i++) await new Promise((r) => setImmediate(r));
|
||||
assert.equal(consumers, 2);
|
||||
assert.equal(sent.length, 6);
|
||||
w.close();
|
||||
// one act at a time: a write asked during a statement waits, so the statement never deletes the key it made
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((r) => (release = r));
|
||||
const w2 = new Writer({
|
||||
module: "m", state: st,
|
||||
observe: async () => { await gate; return { findings: [], pending: [] }; },
|
||||
publish: async () => {}, listen: async () => () => {},
|
||||
});
|
||||
const stating = w2.state();
|
||||
const writing = w2.write({ ...f, id: "during" });
|
||||
for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r));
|
||||
assert.equal(store.has("module.during.k"), false);
|
||||
release();
|
||||
await Promise.all([stating, writing]);
|
||||
assert.equal(store.has("module.during.k"), true);
|
||||
}
|
||||
|
||||
// the finding writer owns its kinds, keeps a views reader's findings in its statement and refuses an owner of no form
|
||||
async function newTest4(): Promise<void> {
|
||||
const { Writer, VIEW_KINDS, checkFinding, escapeName, viewFindingBody, writerFindings, viewWaitsFinding } = await import("../dist/views/index.js");
|
||||
const store = new Map<string, any>();
|
||||
const st: any = {
|
||||
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
|
||||
put: async (k: string, v: any) => (store.set(k, structuredClone(v)), 1),
|
||||
delete: async (k: string) => void store.delete(k),
|
||||
keys: async () => [...store.keys()].sort(),
|
||||
};
|
||||
const sent: { subject: string; body: any }[] = [];
|
||||
const w = new Writer({
|
||||
module: "m", asked: "s.findings-asked", state: st, kinds: ["k"],
|
||||
observe: async () => ({ findings: [], pending: [] }),
|
||||
publish: async (subject, body) => void sent.push({ subject, body }),
|
||||
listen: async () => () => {},
|
||||
});
|
||||
const f = { kind: "k", scope: "module", id: "a", severity: "warning" as const, summary: "one", source: "p", subject: { scope: "module", id: "a" } };
|
||||
await assert.rejects(w.write({ ...f, kind: "other" }), /"other"/);
|
||||
await assert.rejects(w.clear("module", "a", "other", "why"), /"other"/);
|
||||
await assert.rejects(w.write({ ...f, owner: "Not A Form" }), /owner/);
|
||||
// a body whose wire form lacks a field the contract names fails at its first write, naming it (design 47 G A-3)
|
||||
const { subject: _s, ...noSubject } = f;
|
||||
await assert.rejects(w.write(noSubject as any), /subject/);
|
||||
assert.throws(() => checkFinding({ ...f, kind: "a.b" }), /dot/);
|
||||
await assert.rejects(w.write({ ...f, "waiting-on": "x y" }), /waiting-on/);
|
||||
await w.write({ ...f, owner: "me@node-1" });
|
||||
await w.write({ ...f, kind: VIEW_KINDS[0], id: "m.o.s" });
|
||||
store.set("module.x.other", f);
|
||||
await w.state();
|
||||
assert.deepEqual([...store.keys()].sort(), ["module.m.o.s.view-waits", "module.x.other"].sort());
|
||||
const stated = sent.at(-1)!;
|
||||
assert.equal(stated.subject, "findings-stated");
|
||||
assert.ok(stated.body.kinds.includes("k") && stated.body.kinds.includes("view-waits"));
|
||||
assert.equal(stated.body.findings.length, 1);
|
||||
// a views reader's findings are laid out by the writer from the part's plain ones (design 47 G §687)
|
||||
assert.equal(escapeName("a b.c"), "a=20b=2ec");
|
||||
const vf = viewWaitsFinding("m", { owner: "o", state: "s", "waiting-since": "2026-11-11T12:00:00Z", "waits-for": "r7", view: "" } as any, new Date("2026-11-11T12:06:00Z"));
|
||||
const laid = viewFindingBody(vf, "m", "n1");
|
||||
assert.deepEqual([laid.kind, laid.scope, laid.id, laid.severity, laid.source], ["view-waits", "module", "m=2eo=2es", "warning", "views"]);
|
||||
assert.deepEqual(laid.subject, { scope: "module", id: "m", machine: "n1" });
|
||||
assert.equal(viewFindingBody({ ...vf, kind: "view-too-large" }, "m", "n1").severity, "urgent");
|
||||
const vw = writerFindings(w, "m", "n1");
|
||||
await vw.put(vf);
|
||||
assert.equal(store.has("module.m=2eo=2es.view-waits"), true);
|
||||
await vw.delete(vf);
|
||||
assert.equal(store.has("module.m=2eo=2es.view-waits"), false);
|
||||
}
|
||||
|
||||
// a bundle reads its tool runner's capability words once, reads an unknown call as none and names a word not stated (design 47 G §576)
|
||||
async function newTest5(): Promise<void> {
|
||||
const { useRuntime, requireCapability } = await import("../dist/runtime/index.js");
|
||||
const was = process.env.MESH_NODE;
|
||||
process.env.MESH_NODE = "ace";
|
||||
let asked = 0;
|
||||
useRuntime(async (method) => {
|
||||
asked++;
|
||||
assert.equal(method, "mesh/capabilities");
|
||||
return { words: ["finding-calls"] };
|
||||
});
|
||||
await requireCapability("finding-calls");
|
||||
await assert.rejects(requireCapability("put-sessions"), /"put-sessions".*ace|ace.*"put-sessions"/);
|
||||
assert.equal(asked, 1);
|
||||
useRuntime(async () => { throw Object.assign(new Error("no mesh/capabilities"), { code: -32601 }); });
|
||||
await assert.rejects(requireCapability("finding-calls"), /"finding-calls"/);
|
||||
let tries = 0;
|
||||
useRuntime(async () => { if (++tries === 1) throw new Error("pipe closed"); return { words: ["finding-calls"] }; });
|
||||
await assert.rejects(requireCapability("finding-calls"), /pipe closed/);
|
||||
await requireCapability("finding-calls"); // a failed read is asked again
|
||||
useRuntime(undefined);
|
||||
if (was === undefined) delete process.env.MESH_NODE;
|
||||
else process.env.MESH_NODE = was;
|
||||
}
|
||||
|
||||
// a TypeScript bundle's value over one call's limit fails at once, naming the limit (design 47 G §643)
|
||||
async function newTest6(): Promise<void> {
|
||||
const { state, CHUNK_BYTES } = await import("../dist/state/index.js");
|
||||
await assert.rejects(state("s").put("k", { text: "x".repeat(CHUNK_BYTES) }), /512 KiB/);
|
||||
}
|
||||
|
||||
+69
-1
@@ -26,7 +26,7 @@ test("a bundle on stdio lists its tools and answers a call, a seat's verb among
|
||||
assert.equal((await ask(1, "initialize", { protocolVersion: "2025-03-26", capabilities: {} })).result.serverInfo.name, "lamp");
|
||||
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
|
||||
const listed = (await ask(2, "tools/list")).result.tools;
|
||||
assert.deepEqual(listed.map((t: any) => t.name), ["brightness", "node-lamp.on", "fail"]);
|
||||
assert.deepEqual(listed.map((t: any) => t.name), ["brightness", "node-lamp.on", "fail", "who"]);
|
||||
assert.deepEqual(listed[0].inputSchema, { type: "object", properties: { level: { type: "number", description: "0 to 10" } } });
|
||||
const called = (await ask(3, "tools/call", { name: "brightness", arguments: { level: 7 } })).result;
|
||||
assert.deepEqual(JSON.parse(called.content[0].text), { level: 7 });
|
||||
@@ -34,6 +34,21 @@ test("a bundle on stdio lists its tools and answers a call, a seat's verb among
|
||||
assert.equal(failed.isError, true);
|
||||
assert.equal(failed.content[0].text, "the bulb is gone");
|
||||
assert.equal((await ask(5, "tools/call", { name: "nothing" })).error.code, -32602);
|
||||
// The caller the runtime proved in `_meta` is handed to the tool, never one in the arguments (issue 365).
|
||||
const proved = { "mesh/caller": "ace.mesh-delivery-forge-bridge", "mesh/call": "c-17" };
|
||||
const who = (await ask(6, "tools/call", { name: "who", arguments: { caller: "person.mallory" }, _meta: proved })).result;
|
||||
assert.deepEqual(JSON.parse(who.content[0].text), { caller: "ace.mesh-delivery-forge-bridge", call: "c-17" });
|
||||
const unnamed = (await ask(7, "tools/call", { name: "who", arguments: { caller: "person.mallory" } })).result;
|
||||
assert.deepEqual(JSON.parse(unnamed.content[0].text), { caller: "unproven", call: "" });
|
||||
const malformed = (await ask(8, "tools/call", { name: "who", arguments: {}, _meta: { "mesh/caller": "a b", "mesh/call": "x y" } })).result;
|
||||
assert.deepEqual(JSON.parse(malformed.content[0].text), { caller: "unproven", call: "" });
|
||||
// A seat's verb judges its callers by the proof: the named module is answered, another and an unproven one refused.
|
||||
assert.deepEqual(JSON.parse((await ask(9, "tools/call", { name: "node-lamp.on", arguments: {}, _meta: proved })).result.content[0].text), { on: true });
|
||||
const other = (await ask(10, "tools/call", { name: "node-lamp.on", arguments: {}, _meta: { "mesh/caller": "ace.mallory" } })).result;
|
||||
assert.equal(other.isError, true);
|
||||
assert.equal(other.content[0].text, "node-lamp.on: refused for ace.mallory: not on the holder's caller list for node-lamp.on");
|
||||
const none = (await ask(11, "tools/call", { name: "node-lamp.on", arguments: {} })).result;
|
||||
assert.match(none.content[0].text, /carries no caller the tool runner proved/);
|
||||
} finally {
|
||||
child.stdin.end();
|
||||
child.kill();
|
||||
@@ -100,6 +115,14 @@ test("a launched tool's emit is asked of the runtime, and the call answers once
|
||||
send({ jsonrpc: "2.0", id: asked.id, result: {} });
|
||||
const answered = await until((m) => m.id === 2);
|
||||
assert.deepEqual(JSON.parse(answered.result.content[0].text), { rang: true });
|
||||
// A seat's event is asked of the runtime as a seat's publish, its subject whole and its id kept (ADR 0259 §3).
|
||||
send({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "peal", arguments: {} } });
|
||||
const said = await until((m) => m.method === "mesh/seat.publish");
|
||||
assert.ok(said, "the seat's event was not asked of the runtime");
|
||||
assert.deepEqual(said.params, { subject: "mesh.seat.bell.event.pealed.anchor", body: { times: 2 }, id: "p-1" });
|
||||
assert.equal(seen.find((m) => m.id === 3), undefined, "the call answered before its seat's event was published");
|
||||
send({ jsonrpc: "2.0", id: said.id, result: {} });
|
||||
assert.deepEqual(JSON.parse((await until((m) => m.id === 3)).result.content[0].text), { pealed: true });
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
@@ -309,4 +332,49 @@ test("a launched bundle announces what it keeps as it is imported, and binds no
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
// A test of milestone G, merged here to keep the module within its test budget (design 47 G §419):
|
||||
await aBundlesFindingWriter();
|
||||
});
|
||||
|
||||
// a bundle's finding writer publishes with ack and listens with deliver new through the runtime channel (design 47 G §559)
|
||||
async function aBundlesFindingWriter(): Promise<void> {
|
||||
const child = spawn(process.execPath, [fileURLToPath(new URL("./fixtures/stdio-finding-calls.mjs", import.meta.url))],
|
||||
{ stdio: ["pipe", "pipe", "inherit"], env: { ...process.env, MESH_SERVED_MODULE: "keeper" } });
|
||||
const seen: any[] = [];
|
||||
let buffered = "";
|
||||
child.stdout.on("data", (d) => {
|
||||
buffered += d.toString();
|
||||
let at: number;
|
||||
while ((at = buffered.indexOf("\n")) >= 0) {
|
||||
seen.push(JSON.parse(buffered.slice(0, at)));
|
||||
buffered = buffered.slice(at + 1);
|
||||
}
|
||||
});
|
||||
const until = async (match: (m: any) => boolean) => {
|
||||
for (let i = 0; i < 150 && !seen.some(match); i++) await new Promise((r) => setTimeout(r, 20));
|
||||
return seen.find(match);
|
||||
};
|
||||
const send = (m: unknown) => child.stdin.write(JSON.stringify(m) + "\n");
|
||||
try {
|
||||
// the capability words are read once, before the first call that needs one (design 47 G §576)
|
||||
const caps = await until((m) => m.method === "mesh/capabilities");
|
||||
send({ jsonrpc: "2.0", id: caps.id, result: { words: ["finding-calls"] } });
|
||||
const sub = await until((m) => m.method === "mesh/subscribe");
|
||||
assert.deepEqual(sub.params, { pattern: "mesh-conditions.findings-asked", deliver: "new" });
|
||||
send({ jsonrpc: "2.0", id: sub.id, result: {} });
|
||||
send({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {} } });
|
||||
await until((m) => m.id === 1);
|
||||
send({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "say", arguments: {} } });
|
||||
const pub = await until((m) => m.method === "mesh/publish");
|
||||
assert.equal(pub.params.ack, true);
|
||||
assert.equal(pub.params.key, "finding-written");
|
||||
assert.equal(pub.params.headers["x-event-id"], "id-1");
|
||||
// The runtime answers an unacknowledged publish with {error}: the writer's publish fails, so it sends again.
|
||||
send({ jsonrpc: "2.0", id: pub.id, result: { error: "no stream" } });
|
||||
const answered = await until((m) => m.id === 2);
|
||||
assert.equal(seen.filter((m) => m.method === "mesh/capabilities").length, 1, "the words are asked once");
|
||||
assert.deepEqual(JSON.parse(answered.result.content[0].text), { said: false, why: "the bus did not acknowledge finding-written: no stream" });
|
||||
} finally {
|
||||
child.kill();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user