Merge branch 'feat/modules-name-no-node' into bulk-c

This commit is contained in:
jochen
2026-10-22 21:38:21 +02:00
39 changed files with 292 additions and 240 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ func tools(settingsFile string, now func() time.Time) []stdio.Tool {
Input: map[string]any{"type": "object", "properties": map[string]any{
"object": str("a stored document's key: a scan's key, as scan.added names it, or the key node-files.upload answered"),
"path": str("an absolute path of a file on the machine node names"),
"node": str("the machine path is on, such as g14"),
"node": str("the machine path is on, such as node-b"),
"url": str("an http or https address of a document or a page"),
"ask": str("the ask id of an earlier print that was answered as started: alone, it reads that print's outcome"),
"copies": num("1 to 99 (default 1)"),
@@ -813,7 +813,7 @@ func (m *MemoryCarrier) unwritten(key, project, path string, err error) {
}
// OlderBeside is where the older of two versions too large to share one file is kept: beside the file, named for
// the node it was changed on ("MEMORY.md" from shanks: "MEMORY.older-from-shanks.md").
// the node it was changed on ("MEMORY.md" from node-d: "MEMORY.older-from-node-d.md").
func OlderBeside(path, node string) string {
ext := filepath.Ext(path)
return strings.TrimSuffix(path, ext) + ".older-from-" + node + ext
@@ -900,7 +900,7 @@ func (m *MemoryCarrier) Take(key, op string, value json.RawMessage, revision uin
}
// Only noted here, never applied: the runtime hands the watch's values over one at a time and gives the whole
// current set ten seconds, so a watch whose handler reads the state, waits for a look or writes a file never
// starts on a node with a few hundred memory files (issue 367, seen on shanks on 2026-10-13). Drain applies
// starts on a node with a few hundred memory files (issue 367, seen on node-d on 2026-10-13). Drain applies
// what was noted, the newest revision of each key.
m.queueMu.Lock()
if m.queue == nil {
@@ -117,7 +117,7 @@ func proposalTools(p Paths) []stdio.Tool {
Input: map[string]any{
"scope": str("mesh (every node) or node (the nodes given, or this one)"),
"nodes": str("for scope node: a comma-separated list of nodes; absent is this node"),
"name": str("the section's name: lower-case letters, digits and -, e.g. handoff-from-g14"),
"name": str("the section's name: lower-case letters, digits and -, e.g. handoff-from-node-b"),
"text": str("the section's text, markdown, as every session will read it"),
"remove": map[string]any{"type": "boolean", "description": "propose removing the approved section of this name and scope instead"},
},
@@ -173,7 +173,7 @@ func questionTools(p Paths, notLaunched error) []stdio.Tool {
}, "required": []string{"label", "means"}}},
"why": str("why the recommended option is the one you recommend, at most 120 bytes"),
"on_expiry": str("what you do if nobody answers before it expires, e.g. 'I leave the pull request open'"),
"by": str("required: your session's name, as who@node or who, e.g. fix-589@g14"),
"by": str("required: your session's name, as who@node or who, e.g. fix-589@node-b"),
"expires_minutes": map[string]any{"type": "integer",
"description": "minutes until it expires, 5 to 1430; 60 when absent"},
},
@@ -351,7 +351,7 @@ func AskQuestion(p Paths, in QuestionInput, store QuestionStore, bus AskBus, now
by = who
}
if by == "" {
return refuse("it does not say who asks: give by, your session's name as who@node, such as fix-589@g14")
return refuse("it does not say who asks: give by, your session's name as who@node, such as fix-589@node-b")
}
life := in.Expires
if life == 0 {
+1
View File
@@ -1,3 +1,4 @@
MESH_LAB_FORGE=${config:forge}
MESH_LAB_FORGE_OWNER=${config:forge-owner}
MESH_LAB_FORGE_BINDING=${dir:state}/package-registry.json
MESH_LAB_FORGE_SECRET=${dir:state}/package-registry.secret
+6
View File
@@ -37,6 +37,12 @@
"description": "The forge the lab's machines take their packages from, as its address: the operator's, with no default (novox/hq ADR 0112).",
"trusted": true,
"required": true
},
"forge-owner": {
"type": "string",
"description": "The owner on the forge the mesh's repositories and npm packages are under, such as the organisation that holds them: the operator's, with no default (novox/hq ADR 0507 §9).",
"trusted": true,
"required": true
}
},
"resources": [
+2 -2
View File
@@ -60,8 +60,8 @@ test("no run starts while the base image is rebuilt", () => {
});
test("a rebuild is mesh-lab's base build from the forge's main, with no secret in its script", () => {
const s = baseScript("/work/base-x", "https://forge.invalid", "/work/.base-x.git-credentials");
assert.match(s, /clone --quiet --depth 1 'https:\/\/forge.invalid\/novox\/mesh-lab.git'/);
const s = baseScript("/work/base-x", "https://forge.invalid", "mesh-org", "/work/.base-x.git-credentials");
assert.match(s, /clone --quiet --depth 1 'https:\/\/forge.invalid\/mesh-org\/mesh-lab.git'/);
assert.match(s, /src\/cli.ts base build/);
assert.ok(s.indexOf("rm -f '/work/.base-x.git-credentials'") < s.indexOf("npm ci"), "the credential outlives the clone");
});
+26
View File
@@ -0,0 +1,26 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
// The forge's owner differs between meshes (novox/hq ADR 0507 §9, issue 1838): the lab takes it as a config value
// the operator gives, with no default, and no file of the lab names it.
// 1838/AC4
const here = join(import.meta.dirname, "..");
test("the forge's owner is a required config value with no default, and reaches the lab's env-file", () => {
const manifest = JSON.parse(readFileSync(join(here, "module.json"), "utf8")) as { config: Record<string, Record<string, unknown>> };
const owner = manifest.config["forge-owner"];
assert.ok(owner, "the manifest declares no forge-owner");
assert.equal(owner.required, true);
assert.ok(!("default" in owner), `forge-owner has the default ${JSON.stringify(owner.default)}`);
assert.match(readFileSync(join(here, "files", "lab.env"), "utf8"), /^MESH_LAB_FORGE_OWNER=\$\{config:forge-owner\}$/m);
});
test("no file of the lab's code names an owner on the forge", () => {
for (const f of readdirSync(join(here, "tools")).filter((f) => f.endsWith(".ts"))) {
const src = readFileSync(join(here, "tools", f), "utf8");
assert.doesNotMatch(src, /\}\/[a-z][\w.-]*\/\$\{repo\}|\}\/[a-z][\w.-]*\/mesh-lab\.git|\/api\/packages\/[a-z][\w.-]*\/npm\//, f);
}
});
+65 -65
View File
@@ -92,8 +92,8 @@ test("a run fails at its setup, naming the checkout, when nothing of it can be i
// checkouts are.
test("a run checks out mesh-tools-go and says where its checkouts are", () => {
assert.ok(REPOSITORIES.includes("mesh-tools-go"));
const s = script("/work", "run-x", "http://forge", ["test/integration/genesis-keeps-its-bus.test.ts"], {});
assert.match(s, /novox\/mesh-tools-go\.git' '\/work\/run-x\/mesh-tools-go'/);
const s = script("/work", "run-x", "http://forge", "mesh-org", ["test/integration/genesis-keeps-its-bus.test.ts"], {});
assert.match(s, /mesh-org\/mesh-tools-go\.git' '\/work\/run-x\/mesh-tools-go'/);
assert.match(s, /export MESH_LAB_REPOS='\/work\/run-x'/);
assert.doesNotMatch(s, /cd '\/work\/run-x'\/\$repo && npm ci/);
for (const repo of NPM_REPOSITORIES) assert.ok(s.includes(`'/work/run-x/${repo}'/package-lock.json`), repo);
@@ -113,22 +113,22 @@ function aBinding(as: string, secret: string): { binding: string; secret: string
}
test("the forge credential is the bound account and its sealed secret, for the forge's own origin", () => {
const b = aBinding("lab-shanks", "s3cret-of-forty-characters");
assert.equal(forgeCredential("http://novox.internal:20000", b.binding, b.secret),
"http://lab-shanks:s3cret-of-forty-characters@novox.internal:20000");
assert.equal(forgeCredential("http://novox.internal:20000/", b.binding, ""), undefined);
const b = aBinding("lab-node-d", "s3cret-of-forty-characters");
assert.equal(forgeCredential("http://node-c.internal:20000", b.binding, b.secret),
"http://lab-node-d:s3cret-of-forty-characters@node-c.internal:20000");
assert.equal(forgeCredential("http://node-c.internal:20000/", b.binding, ""), undefined);
assert.equal(forgeCredential("", b.binding, b.secret), undefined);
assert.equal(forgeCredential("http://novox.internal:20000", "/nowhere", b.secret), undefined);
assert.equal(forgeCredential("http://node-c.internal:20000", "/nowhere", b.secret), undefined);
});
test("a run clones through git's credential store, and its script carries no secret", () => {
const b = aBinding("lab-shanks", "s3cret-of-forty-characters");
const credential = forgeCredential("http://novox.internal:20000", b.binding, b.secret);
const b = aBinding("lab-node-d", "s3cret-of-forty-characters");
const credential = forgeCredential("http://node-c.internal:20000", b.binding, b.secret);
const path = credentialPath("/work", "run-x");
const s = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}, path);
const s = script("/work", "run-x", "http://node-c.internal:20000", "mesh-org", ["t.test.ts"], {}, path);
assert.ok(credential);
assert.doesNotMatch(s, /s3cret/);
assert.match(s, /git -c credential\.helper= -c 'credential\.helper=store --file=\/work\/\.run-x\.git-credentials' clone --quiet --depth 50 --branch 'main' 'http:\/\/novox\.internal:20000\/novox\/mesh-tools-go\.git'/);
assert.match(s, /git -c credential\.helper= -c 'credential\.helper=store --file=\/work\/\.run-x\.git-credentials' clone --quiet --depth 50 --branch 'main' 'http:\/\/node-c\.internal:20000\/mesh-org\/mesh-tools-go\.git'/);
assert.match(s, /export GIT_TERMINAL_PROMPT=0/);
assert.equal(path.startsWith("/work/run-x/"), false, "the credential is kept out of the run's own directory");
assert.equal(credentialArgs(undefined), "");
@@ -139,10 +139,10 @@ test("a run clones through git's credential store, and its script carries no sec
test("git presents the stored credential to the forge, and only when one is given", () => {
const dir = mkdtempSync(join(tmpdir(), "lab-git-"));
const store = join(dir, "git-credentials");
writeFileSync(store, "http://lab-shanks:pw@forge.invalid:20000\n", { mode: 0o600 });
writeFileSync(store, "http://lab-node-d:pw@forge.invalid:20000\n", { mode: 0o600 });
const fill = spawnSync("sh", ["-c", `printf 'protocol=http\\nhost=forge.invalid:20000\\n\\n' | git ${credentialArgs(store)}credential fill`],
{ encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
assert.match(fill.stdout, /username=lab-shanks/);
assert.match(fill.stdout, /username=lab-node-d/);
assert.match(fill.stdout, /password=pw/);
const other = spawnSync("sh", ["-c", `printf 'protocol=http\\nhost=elsewhere.invalid\\n\\n' | git ${credentialArgs(store)}credential fill`],
{ encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
@@ -168,7 +168,7 @@ test("a stopped script removes the credential, whatever ends it", () => {
});
test("the script removes the credential as soon as its clones are done", () => {
const s = script("/work", "run-x", "http://forge", ["t.test.ts"], {}, "/work/.run-x.git-credentials");
const s = script("/work", "run-x", "http://forge", "mesh-org", ["t.test.ts"], {}, "/work/.run-x.git-credentials");
const lastClone = s.lastIndexOf("clone --quiet");
const removed = s.indexOf("rm -f '/work/.run-x.git-credentials'", lastClone);
const building = s.indexOf("npm ci");
@@ -252,9 +252,9 @@ test("the lab's tools bundle names its bound credential and binding in its env",
assert.equal(env.MESH_LAB_FORGE_BINDING, m.binds["npm-package-registry"]);
});
// The next setup failure of novox/hq issue 624 (run-2026-10-13T13-15-26-634Z on shanks): npm 12 refused
// The next setup failure of novox/hq issue 624 (run-2026-10-13T13-15-26-634Z on node-d): npm 12 refused
// mesh-tools' locked @novox/mesh-sdk with EALLOWREMOTE. The lab named the registry by its forge setting's
// address (http://novox.internal:20000/…), the lock file holds the address the forge writes every tarball
// address (http://node-c.internal:20000/…), the lock file holds the address the forge writes every tarball
// URL at (https://git.novox.be/…), and npm 12 installs a locked tarball only from its configured registry.
// A run names the registry by the address the registry names itself by.
import { npmRegistry } from "../tools/runs.ts";
@@ -270,54 +270,54 @@ function aRegistry(answer: () => Response | Promise<Response>): { get: typeof fe
const packument = (tarball: string) => new Response(JSON.stringify({
name: "@novox/mesh-sdk", "dist-tags": { latest: "0.1.14" },
versions: { "0.1.1": { dist: { tarball: "http://old.example/api/packages/novox/npm/x.tgz" } }, "0.1.14": { dist: { tarball } } },
versions: { "0.1.1": { dist: { tarball: "http://old.example/api/packages/mesh-org/npm/x.tgz" } }, "0.1.14": { dist: { tarball } } },
}), { status: 200, headers: { "content-type": "application/json" } });
test("a run names the package registry by the address the registry gives its tarballs", async () => {
const r = aRegistry(() => packument("https://forge.example.org/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"));
const got = await npmRegistry("http://forge.example.org:3000/", r.get, "https://forge.example.org/api/packages/novox/npm/");
assert.deepEqual(got, { registry: "https://forge.example.org/api/packages/novox/npm/" });
assert.deepEqual(r.asked, ["http://forge.example.org:3000/api/packages/novox/npm/@novox%2fmesh-sdk"]);
const r = aRegistry(() => packument("https://forge.example.org/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"));
const got = await npmRegistry("http://forge.example.org:3000/", "mesh-org", r.get, "https://forge.example.org/api/packages/mesh-org/npm/");
assert.deepEqual(got, { registry: "https://forge.example.org/api/packages/mesh-org/npm/" });
assert.deepEqual(r.asked, ["http://forge.example.org:3000/api/packages/mesh-org/npm/@novox%2fmesh-sdk"]);
// The forge's own scheme is taken too, with its port.
const plain = await npmRegistry("http://forge.example.org:3000",
aRegistry(() => packument("http://forge.example.org:3000/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz")).get);
assert.deepEqual(plain, { registry: "http://forge.example.org:3000/api/packages/novox/npm/" });
const plain = await npmRegistry("http://forge.example.org:3000", "mesh-org",
aRegistry(() => packument("http://forge.example.org:3000/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz")).get);
assert.deepEqual(plain, { registry: "http://forge.example.org:3000/api/packages/mesh-org/npm/" });
});
// What the registry answers may come over plain HTTP, so it never decides where packages come from: only
// the forge's public root or its own address, scheme, host and port, under the npm path, with nothing after
// it (review of mesh-catalog #275, ADR 0346).
test("a registry that does not give an address the lab takes leaves the forge's, and the reason is said", async () => {
const sdk = "/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz";
const sdk = "/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz";
for (const [answer, why] of [
[() => new Response("not found", { status: 404 }), /it answered 404/],
[() => new Response("{}", { status: 200 }), /no tarball/],
[() => { throw new Error("connect ECONNREFUSED"); }, /ECONNREFUSED/],
[() => packument(`https://forge.example.org/elsewhere/mesh-sdk-0.1.14.tgz`), /not under \/api\/packages\/novox\/npm\//],
[() => packument(`https://forge.example.org/elsewhere/mesh-sdk-0.1.14.tgz`), /not under \/api\/packages\/mesh-org\/npm\//],
[() => packument(`https://attacker.example.net${sdk}`), /at https:\/\/attacker\.example\.net, which is neither the forge's public root nor its own address/],
[() => packument(`https://attacker.example.net/x?u=https://novox.internal${sdk}`), /query or a fragment/],
[() => packument(`https://novox.internal${sdk}?x=1`), /query or a fragment/],
[() => packument(`https://novox.internal${sdk}#frag`), /query or a fragment/],
[() => packument(`https://attacker.example.net/#https://novox.internal${sdk}`), /query or a fragment/],
[() => packument(`ftp://novox.internal${sdk}`), /at ftp:\/\/novox\.internal, which is neither/],
[() => packument(`file:///api/packages/novox/npm/x.tgz`), /which is neither/],
[() => packument(`https://novox.internal/api/packages/novox/npm/\nregistry=https://attacker.example.net/\nallow-remote=all`), /whitespace or a control character/],
[() => packument(`https://user:pw@novox.internal${sdk}`), /user or password/],
[() => packument(`https://attacker.example.net/x?u=https://node-c.internal${sdk}`), /query or a fragment/],
[() => packument(`https://node-c.internal${sdk}?x=1`), /query or a fragment/],
[() => packument(`https://node-c.internal${sdk}#frag`), /query or a fragment/],
[() => packument(`https://attacker.example.net/#https://node-c.internal${sdk}`), /query or a fragment/],
[() => packument(`ftp://node-c.internal${sdk}`), /at ftp:\/\/node-c\.internal, which is neither/],
[() => packument(`file:///api/packages/mesh-org/npm/x.tgz`), /which is neither/],
[() => packument(`https://node-c.internal/api/packages/mesh-org/npm/\nregistry=https://attacker.example.net/\nallow-remote=all`), /whitespace or a control character/],
[() => packument(`https://user:pw@node-c.internal${sdk}`), /user or password/],
] as [() => Response, RegExp][]) {
const got = await npmRegistry("http://novox.internal:20000", aRegistry(answer).get);
assert.equal(got.registry, "http://novox.internal:20000/api/packages/novox/npm/", String(why));
const got = await npmRegistry("http://node-c.internal:20000", "mesh-org", aRegistry(answer).get);
assert.equal(got.registry, "http://node-c.internal:20000/api/packages/mesh-org/npm/", String(why));
assert.match(got.problem ?? "", /did not give an address the lab takes/);
assert.match(got.problem ?? "", why);
}
});
test("a run's npm configuration names the registry it was given, and the forge's own address only without one", () => {
const given = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}, undefined,
"https://forge.example.org/api/packages/novox/npm/");
assert.match(given, / '@novox:registry=https:\/\/forge\.example\.org\/api\/packages\/novox\/npm\/' > '\/work\/run-x\/\.npmrc'\n/);
assert.doesNotMatch(given, /@novox:registry=http:\/\/novox\.internal/);
const none = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {});
assert.match(none, /'@novox:registry=http:\/\/novox\.internal:20000\/api\/packages\/novox\/npm\/'/);
const given = script("/work", "run-x", "http://node-c.internal:20000", "mesh-org", ["t.test.ts"], {}, undefined,
"https://forge.example.org/api/packages/mesh-org/npm/");
assert.match(given, / '@novox:registry=https:\/\/forge\.example\.org\/api\/packages\/mesh-org\/npm\/' > '\/work\/run-x\/\.npmrc'\n/);
assert.doesNotMatch(given, /@novox:registry=http:\/\/node-c\.internal/);
const none = script("/work", "run-x", "http://node-c.internal:20000", "mesh-org", ["t.test.ts"], {});
assert.match(none, /'@novox:registry=http:\/\/node-c\.internal:20000\/api\/packages\/mesh-org\/npm\/'/);
});
// The public root the forge serves in the lab's binding (novox/hq ADR 0346) is the one other origin a
@@ -325,20 +325,20 @@ test("a run's npm configuration names the registry it was given, and the forge's
import { publicRootOf } from "../tools/runs.ts";
test("a tarball is taken at the forge's public root, and at no other port or scheme of its host", async () => {
const sdk = "/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz";
const root = "https://git.example.org/api/packages/novox/npm/";
const sdk = "/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz";
const root = "https://git.example.org/api/packages/mesh-org/npm/";
const at = (tarball: string, publicRoot?: string) =>
npmRegistry("http://novox.internal:20000", aRegistry(() => packument(tarball)).get, publicRoot);
npmRegistry("http://node-c.internal:20000", "mesh-org", aRegistry(() => packument(tarball)).get, publicRoot);
assert.deepEqual(await at(`https://git.example.org${sdk}`, root), { registry: root });
for (const tarball of [`https://git.example.org:8443${sdk}`, `http://git.example.org${sdk}`, `https://example.org${sdk}`]) {
const got = await at(tarball, root);
assert.equal(got.registry, "http://novox.internal:20000/api/packages/novox/npm/", tarball);
assert.equal(got.registry, "http://node-c.internal:20000/api/packages/mesh-org/npm/", tarball);
assert.match(got.problem ?? "", /neither the forge's public root nor its own address/, tarball);
}
// Without a public root, the forge's public name is not trusted on its own word.
assert.match((await at(`https://git.example.org${sdk}`)).problem ?? "", /neither/);
// And the forge's own address only at its own port.
assert.match((await at(`http://novox.internal${sdk}`, root)).problem ?? "", /neither/);
assert.match((await at(`http://node-c.internal${sdk}`, root)).problem ?? "", /neither/);
});
test("the public root is read from the binding, and only an https root at the npm path is taken", () => {
@@ -346,39 +346,39 @@ test("the public root is read from the binding, and only an https root at the np
const binding = (root: unknown) => {
const f = join(dir, `b-${Math.random()}.json`);
writeFileSync(f, JSON.stringify({ provision: "npm-package-registry", at: "home", serves: { scheme: "http", port: 3000,
"npm-path": "/api/packages/novox/npm/", ...(root === undefined ? {} : { "public-root": root }) } }));
"npm-path": "/api/packages/mesh-org/npm/", ...(root === undefined ? {} : { "public-root": root }) } }));
return f;
};
assert.deepEqual(publicRootOf(binding("https://git.example.org/api/packages/novox/npm/")),
{ root: "https://git.example.org/api/packages/novox/npm/" });
assert.deepEqual(publicRootOf(binding("https://git.example.org:8443/api/packages/novox/npm/")),
{ root: "https://git.example.org:8443/api/packages/novox/npm/" });
assert.deepEqual(publicRootOf(binding("https://git.example.org/api/packages/mesh-org/npm/"), "mesh-org"),
{ root: "https://git.example.org/api/packages/mesh-org/npm/" });
assert.deepEqual(publicRootOf(binding("https://git.example.org:8443/api/packages/mesh-org/npm/"), "mesh-org"),
{ root: "https://git.example.org:8443/api/packages/mesh-org/npm/" });
for (const [root, why] of [
[undefined, /serves no public-root/],
["http://git.example.org/api/packages/novox/npm/", /is not https:\/\/<host>/],
["http://git.example.org/api/packages/mesh-org/npm/", /is not https:\/\/<host>/],
["https://git.example.org/elsewhere/", /is not https:\/\/<host>/],
["https://git.example.org/api/packages/novox/npm/?x=1", /is not https:\/\/<host>/],
["https://u:p@git.example.org/api/packages/novox/npm/", /is not https:\/\/<host>/],
["https://git.example.org/api/packages/novox/npm/\nallow-remote=all", /whitespace or a control character/],
["https://${bound:route:name-web}/api/packages/novox/npm/", /not a URL|is not https/],
["https://git.example.org/api/packages/mesh-org/npm/?x=1", /is not https:\/\/<host>/],
["https://u:p@git.example.org/api/packages/mesh-org/npm/", /is not https:\/\/<host>/],
["https://git.example.org/api/packages/mesh-org/npm/\nallow-remote=all", /whitespace or a control character/],
["https://${bound:route:name-web}/api/packages/mesh-org/npm/", /not a URL|is not https/],
] as [unknown, RegExp][]) {
const got = publicRootOf(binding(root));
const got = publicRootOf(binding(root), "mesh-org");
assert.equal(got.root, undefined, String(root));
assert.match(got.problem ?? "", why, String(root));
}
assert.match(publicRootOf(join(dir, "none.json")).problem ?? "", /could not be read/);
assert.match(publicRootOf(undefined).problem ?? "", /no npm-package-registry binding/);
assert.match(publicRootOf(join(dir, "none.json"), "mesh-org").problem ?? "", /could not be read/);
assert.match(publicRootOf(undefined, "mesh-org").problem ?? "", /no npm-package-registry binding/);
rmSync(dir, { recursive: true, force: true });
});
// A forge setting that is not http(s) is never a trusted origin (review of #280): a `file:` tarball's origin
// is "null", as is a `file:` forge's, and the two must not match.
test("a forge setting that is not http(s) trusts no origin of its own", async () => {
const got = await npmRegistry("file:///srv/forge", aRegistry(() => packument("file:///api/packages/novox/npm/x.tgz")).get);
const got = await npmRegistry("file:///srv/forge", "mesh-org", aRegistry(() => packument("file:///api/packages/mesh-org/npm/x.tgz")).get);
assert.match(got.problem ?? "", /neither the forge's public root nor its own address/);
const root = "https://git.example.org/api/packages/novox/npm/";
const viaRoot = await npmRegistry("ftp://novox.internal", aRegistry(() =>
packument("ftp://novox.internal/api/packages/novox/npm/x.tgz")).get, root);
const root = "https://git.example.org/api/packages/mesh-org/npm/";
const viaRoot = await npmRegistry("ftp://node-c.internal", "mesh-org", aRegistry(() =>
packument("ftp://node-c.internal/api/packages/mesh-org/npm/x.tgz")).get, root);
assert.match(viaRoot.problem ?? "", /neither/);
});
@@ -401,7 +401,7 @@ test("every file takes its text from a template in files/", () => {
});
// Defends novox/hq issue 693: a run whose only test skipped as NOT REACHED read "0 passed, 0 failed", exit 0,
// and lab_status said passed. The log's tail, as run-2026-10-14T00-21-44-014Z on shanks wrote it.
// and lab_status said passed. The log's tail, as run-2026-10-14T00-21-44-014Z on node-d wrote it.
const oldLog = [
"STOPPED at step 16 of 24, base: \t\t\t\tMake sure you have all the referenced content and try again.",
"\ufe63 genesis adopts its bus as the nats module in place: one bus, its streams kept, the node connected (588152.016874ms) # ADR 0333 §5 NOT REACHED: the first run stopped at step 16 of 24, base, before the broker step:",
+23 -13
View File
@@ -8,7 +8,7 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
checkDir, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, publicRootOf, readStatus, REPOSITORIES, running, start,
checkDir, repositoryURL, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, publicRootOf, readStatus, REPOSITORIES, running, start,
stop, tail, writeCredential, baseBuildLog, buildingBase, listBaseBuilds, startBaseBuild, credentialPath, launchRun,
launchRebuild, BASE_MAX_AGE_DAYS, describeBase, readBaseImage, baseProblem,
} from "./runs.js";
@@ -19,13 +19,17 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
// ADR 0192): read from the env-file the mesh fills, at each call, so a changed setting is used
// without restarting the runtime. MESH_LAB_FORGE itself still wins, for a hand-run instance.
const forgeOf = (): string => (env.MESH_LAB_FORGE ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE")).replace(/\/+$/, "");
// The owner on the forge the mesh's repositories and packages are under, the operator's too (novox/hq ADR 0507
// §9), read the same way.
const ownerOf = (): string => (env.MESH_LAB_FORGE_OWNER ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE_OWNER")).trim();
const noOwner = "the lab's forge owner is not set: config for lab, {\"forge-owner\": \"<owner>\"}";
// The forge credential the mesh binds and seals to the lab (novox/hq issue 624), read at each call.
// The registry's public root the forge serves in the lab's binding (novox/hq ADR 0346), read at each call.
const bindingOf = (): string | undefined =>
env.MESH_LAB_FORGE_BINDING ?? (wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE_BINDING") || undefined);
const registryOf = async (forge: string) => {
const root = publicRootOf(bindingOf());
const registry = await npmRegistry(forge, fetch, root.root);
const registryOf = async (forge: string, owner: string) => {
const root = publicRootOf(bindingOf(), owner);
const registry = await npmRegistry(forge, owner, fetch, root.root);
return registry.problem && root.problem ? { ...registry, problem: `${registry.problem}; ${root.problem}` } : registry;
};
const credentialOf = (forge: string): string | undefined => forgeCredential(forge,
@@ -39,6 +43,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
run: async () => {
const forge = forgeOf();
if (!forge) return { ok: false, output: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
const owner = ownerOf();
if (!owner) return { ok: false, output: noOwner };
// What earlier checks left is cleared, and what could not be is said; this check works in a fresh
// directory of its own either way, and removes it when it is done.
const leftovers = clearChecks(work);
@@ -46,13 +52,13 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
let check: ReturnType<typeof spawnSync>;
let problems: string[];
try {
const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", `${forge}/novox/mesh-lab.git`, dir], { encoding: "utf8" });
const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", repositoryURL(forge, owner, "mesh-lab"), dir], { encoding: "utf8" });
if (clone.status !== 0) return { ok: false, output: [String(clone.stderr).trim(), ...leftovers].join("\n") };
spawnSync("npm", ["ci", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, encoding: "utf8" });
check = spawnSync("node", ["--experimental-strip-types", "src/cli.ts", "check"], { cwd: dir, encoding: "utf8" });
problems = setupProblems(forge, `${dir}/setup`, credentialOf(forge));
problems = setupProblems(forge, owner, `${dir}/setup`, credentialOf(forge));
// And the address a run names the package registry by (npmRegistry): said when the registry does not say it.
const registry = await registryOf(forge);
const registry = await registryOf(forge, owner);
if (registry.problem) problems.push(registry.problem);
} finally {
rmSync(dir, { recursive: true, force: true });
@@ -91,6 +97,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
run: async (args) => {
const forge = forgeOf();
if (!forge) return { started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
const owner = ownerOf();
if (!owner) return { started: false, reason: noOwner };
const tests = String(args.tests ?? "").split(",").map((s) => s.trim()).filter(Boolean);
if (tests.length === 0) return { started: false, reason: "name at least one bed test file" };
let refs: Record<string, string> = {};
@@ -105,8 +113,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
if (stranger.length > 0) return { started: false, reason: `the lab does not build ${stranger.join(", ")}` };
// **One job at a time, not on a base image past its bound, nor while it is rebuilt** (novox/hq issue 698,
// review 256): the registry asked first, then the check and the start with nothing awaited between.
const launched = await launchRun(work, { registry: () => registryOf(forge), base: () => readBaseImage() },
(registry) => start(work, forge, tests, refs, credentialOf(forge), registry));
const launched = await launchRun(work, { registry: () => registryOf(forge, owner), base: () => readBaseImage() },
(registry) => start(work, forge, owner, tests, refs, credentialOf(forge), registry));
if (!launched.started) return launched;
return { started: true, run: launched.job, base: describeBase(launched.base), registry: launched.registry,
...(launched.problem ? { problem: launched.problem } : {}) };
@@ -134,10 +142,12 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
if (args.rebuild !== true) return reading;
const forge = forgeOf();
if (!forge) return { ...reading, started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"<url>\"}" };
const owner = ownerOf();
if (!owner) return { ...reading, started: false, reason: noOwner };
// Not under a run, nor beside another rebuild: the registry asked first, then the check and the start with
// nothing awaited between (review 256).
const launched = await launchRebuild(work, { registry: () => registryOf(forge), base: () => before },
(registry) => startBaseBuild(work, forge, credentialOf(forge), registry));
const launched = await launchRebuild(work, { registry: () => registryOf(forge, owner), base: () => before },
(registry) => startBaseBuild(work, forge, owner, credentialOf(forge), registry));
if (launched.started === false) return { ...reading, started: false, reason: launched.reason };
return { ...reading, started: true, build: launched.job };
},
@@ -174,7 +184,7 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] {
* answer for, and a checkout it installs with nothing installable (installPlan). Empty when a run's setup
* would go through.
*/
export function setupProblems(forge: string, dir: string, credential?: string): string[] {
export function setupProblems(forge: string, owner: string, dir: string, credential?: string): string[] {
const problems: string[] = [];
spawnSync("rm", ["-rf", dir]);
// The credential a run would offer, in a file of its own outside the check's clones, removed after.
@@ -186,7 +196,7 @@ export function setupProblems(forge: string, dir: string, credential?: string):
});
try {
for (const repo of REPOSITORIES) {
const url = `${forge}/novox/${repo}.git`;
const url = repositoryURL(forge, owner, repo);
const remote = git(["ls-remote", "--heads", url, "main"]);
if (remote.status !== 0 || !remote.stdout.includes("refs/heads/main")) {
const said = remote.stderr.trim().split("\n")[0] ?? "";
+22 -13
View File
@@ -60,8 +60,11 @@ export function installStep(root: string, build: boolean): string {
fi`;
}
/** Where the forge serves the mesh's npm packages, under its own address (gitea's npm registry for the novox org). */
export const NPM_PATH = "/api/packages/novox/npm/";
/**
* Where the forge serves the mesh's npm packages, under its own address: gitea's npm registry for the owner the
* mesh's repositories are under, the operator's config value (novox/hq ADR 0507 §9).
*/
export const npmPath = (owner: string): string => `/api/packages/${encodeURIComponent(owner)}/npm/`;
/**
* The registry's public root, as the forge serves it in the lab's npm-package-registry binding
@@ -70,7 +73,8 @@ export const NPM_PATH = "/api/packages/novox/npm/";
* nothing else. Undefined, with why, when the binding has none or it is not that shape — a forge on a
* machine with no public domain serves none (ADR 0346 §3).
*/
export function publicRootOf(bindingFile: string | undefined): { root?: string; problem?: string } {
export function publicRootOf(bindingFile: string | undefined, owner: string): { root?: string; problem?: string } {
const NPM_PATH = npmPath(owner);
if (!bindingFile) return { problem: "the lab has no npm-package-registry binding" };
let said: unknown;
try {
@@ -112,8 +116,9 @@ export function publicRootOf(bindingFile: string | undefined): { root?: string;
* reaches the run's npm configuration but a scheme, a host and a port. Anything else falls back to the
* forge's own address and says why, so a run still starts and lab_check names it.
*/
export async function npmRegistry(forge: string, get: typeof fetch = fetch, publicRoot?: string):
export async function npmRegistry(forge: string, owner: string, get: typeof fetch = fetch, publicRoot?: string):
Promise<{ registry: string; problem?: string }> {
const NPM_PATH = npmPath(owner);
const asked = `${forge.replace(/\/+$/, "")}${NPM_PATH}`;
const url = `${asked}@novox%2fmesh-sdk`;
const fallback = (why: string) => ({
@@ -453,6 +458,10 @@ export function running(work: string): RunStatus | undefined {
return listRuns(work).find((s) => !["passed", "failed", "stopped", "lost"].includes(s.state));
}
/** A repository's clone address on the forge: under the owner the mesh's repositories are under (ADR 0507 §9). */
export const repositoryURL = (forge: string, owner: string, repo: string): string =>
`${forge}/${encodeURIComponent(owner)}/${repo}.git`;
const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
/**
@@ -461,8 +470,8 @@ const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
* The environment is the one the lab's README describes for a run against sibling checkouts, pointed
* at this run's own tree, so what is built and claimed is exactly what was checked out.
*/
export function script(work: string, id: string, forge: string, tests: string[], refs: Record<string, string>,
credential?: string, registry = `${forge}${NPM_PATH}`): string {
export function script(work: string, id: string, forge: string, owner: string, tests: string[], refs: Record<string, string>,
credential?: string, registry = `${forge}${npmPath(owner)}`): string {
const dir = runDir(work, id);
const setState = (state: string) =>
`node -e ${shellQuote(
@@ -471,7 +480,7 @@ export function script(work: string, id: string, forge: string, tests: string[],
const clones = REPOSITORIES.map((repo) => {
const ref = refs[repo] ?? "main";
return [
`git ${credentialArgs(credential)}clone --quiet --depth 50 --branch ${shellQuote(ref)} ${shellQuote(`${forge}/novox/${repo}.git`)} ${shellQuote(join(dir, repo))}`,
`git ${credentialArgs(credential)}clone --quiet --depth 50 --branch ${shellQuote(ref)} ${shellQuote(repositoryURL(forge, owner, repo))} ${shellQuote(join(dir, repo))}`,
`echo "${repo} $(git -C ${shellQuote(join(dir, repo))} rev-parse HEAD)" >> ${shellQuote(join(dir, "commits.txt"))}`,
].join("\n");
}).join("\n");
@@ -515,7 +524,7 @@ node --experimental-strip-types src/cli.ts suite ${tests.map(shellQuote).join("
}
/** start begins a run and returns at once with its status. */
export function start(work: string, forge: string, tests: string[], refs: Record<string, string>,
export function start(work: string, forge: string, owner: string, tests: string[], refs: Record<string, string>,
credential?: string, registry?: string): RunStatus {
const id = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`;
const dir = runDir(work, id);
@@ -525,7 +534,7 @@ export function start(work: string, forge: string, tests: string[], refs: Record
// What a crashed or rebooted run left behind goes before this run writes its own.
cleanCredentials(work);
const credentials = writeCredential(credentialPath(work, id), credential);
writeFileSync(join(dir, "run.sh"), script(work, id, forge, tests, refs, credentials, registry), { mode: 0o700 });
writeFileSync(join(dir, "run.sh"), script(work, id, forge, owner, tests, refs, credentials, registry), { mode: 0o700 });
// The wrapper records how the run ended, then removes the checkouts and keeps the log and status: a
// run's tree is its own, and the next run starts from fresh ones (novox/hq ADR 0172).
@@ -715,13 +724,13 @@ export function baseBuildLog(work: string, id: string, lines = 40): string {
* baseScript is what one rebuild runs: mesh-lab's main from the forge, installed the way a run installs it,
* then its `base build`, which replaces the image and reads back that it works before publishing it.
*/
export function baseScript(dir: string, forge: string, credential?: string, registry = `${forge}${NPM_PATH}`): string {
export function baseScript(dir: string, forge: string, owner: string, credential?: string, registry = `${forge}${npmPath(owner)}`): string {
const lab = join(dir, "mesh-lab");
return `set -euo pipefail
export GIT_TERMINAL_PROMPT=0
${credentialGuard(credential)}
cd ${shellQuote(dir)}
git ${credentialArgs(credential)}clone --quiet --depth 1 ${shellQuote(`${forge}/novox/mesh-lab.git`)} ${shellQuote(lab)}
git ${credentialArgs(credential)}clone --quiet --depth 1 ${shellQuote(repositoryURL(forge, owner, "mesh-lab"))} ${shellQuote(lab)}
${credential ? `rm -f ${shellQuote(credential)}` : ""}
echo "mesh-lab $(git -C ${shellQuote(lab)} rev-parse HEAD)"
printf '%s\\n' ${shellQuote(`@novox:registry=${registry}`)} > ${shellQuote(join(dir, ".npmrc"))}
@@ -733,7 +742,7 @@ node --experimental-strip-types src/cli.ts base build
}
/** startBaseBuild rebuilds the base image in the background and answers at once with the build. */
export function startBaseBuild(work: string, forge: string, credential?: string, registry?: string): BaseBuild {
export function startBaseBuild(work: string, forge: string, owner: string, credential?: string, registry?: string): BaseBuild {
const id = `base-${new Date().toISOString().replace(/[:.]/g, "-")}`;
const dir = join(work, id);
mkdirSync(dir, { recursive: true });
@@ -742,7 +751,7 @@ export function startBaseBuild(work: string, forge: string, credential?: string,
// What a crashed or rebooted run or rebuild left behind goes before this one writes its own.
cleanCredentials(work);
const credentials = writeCredential(credentialPath(work, id), credential);
writeFileSync(join(dir, "build.sh"), baseScript(dir, forge, credentials, registry), { mode: 0o700 });
writeFileSync(join(dir, "build.sh"), baseScript(dir, forge, owner, credentials, registry), { mode: 0o700 });
const wrapper = `bash ${shellQuote(join(dir, "build.sh"))} > ${shellQuote(join(dir, "build.log"))} 2>&1; code=$?
node -e ${shellQuote(
`const f=${JSON.stringify(buildStatusPath(work, id))};const s=JSON.parse(require("fs").readFileSync(f,"utf8"));s.state=process.argv[1]==="0"?"built":"failed";s.exit=Number(process.argv[1]);s.ended=new Date().toISOString();require("fs").writeFileSync(f,JSON.stringify(s,null,2))`,
@@ -16,17 +16,17 @@ func effortAsk(n int, status string, docs ...Section) ProposeAsk {
// novox/hq issue 844: an active research effort takes new versions freely, each reviewed as a proposal.
func TestAnActiveEffortTakesANewVersionWithANewDocument(t *testing.T) {
w := imported(t, newMemStore())
r, err := w.k.ReserveNumber("author@g14", KindResearch, "an effort")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "an effort")
if err != nil {
t.Fatal(err)
}
overview := Section{Name: "00-overview.md", Text: "What an inference layer would be."}
w.accepted(t, "author@g14", effortAsk(r.Number, StatusActive, overview))
w.accepted(t, "author@node-b", effortAsk(r.Number, StatusActive, overview))
key := Key{KindResearch, r.Number}
reviews := len(w.judge.reviews)
next := effortAsk(r.Number, StatusActive, overview, Section{Name: "01-findings.md", Text: "A new finding."})
next.Why = "findings added"
if _, err := w.k.Propose("author@g14", next); err != nil {
if _, err := w.k.Propose("author@node-b", next); err != nil {
t.Fatalf("an active effort refused a new version: %v", err)
}
e := mustGet(t, w.k.Store, key)
@@ -56,24 +56,24 @@ func TestAnActiveEffortTakesANewVersionWithANewDocument(t *testing.T) {
func TestAGraduatedEffortAndADecisionStillTakeOnlyAnInsight(t *testing.T) {
w := imported(t, newMemStore())
r, err := w.k.ReserveNumber("author@g14", KindResearch, "an effort")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "an effort")
if err != nil {
t.Fatal(err)
}
overview := Section{Name: "00-overview.md", Text: "What an inference layer would be."}
grad := effortAsk(r.Number, StatusGraduated, overview)
grad.Links = append(grad.Links, LinkAsk{Kind: LinkBecame, To: "0290"})
w.accepted(t, "author@g14", grad)
w.accepted(t, "author@node-b", grad)
next := effortAsk(r.Number, StatusGraduated, overview, Section{Name: "01-findings.md", Text: "More."})
next.Links = grad.Links
if _, err := w.k.Propose("author@g14", next); err == nil || !strings.Contains(err.Error(), "progressive insight") {
if _, err := w.k.Propose("author@node-b", next); err == nil || !strings.Contains(err.Error(), "progressive insight") {
t.Fatalf("a graduated effort took a plain version: %v", err)
}
n := w.reserved(t, "author@g14")
w.accepted(t, "author@g14", valid(n))
n := w.reserved(t, "author@node-b")
w.accepted(t, "author@node-b", valid(n))
p := valid(n)
p.Sections[2].Text = "Changed."
if _, err := w.k.Propose("author@g14", p); err == nil || !strings.Contains(err.Error(), "progressive insight") {
if _, err := w.k.Propose("author@node-b", p); err == nil || !strings.Contains(err.Error(), "progressive insight") {
t.Fatalf("a decision took a plain version: %v", err)
}
}
@@ -82,34 +82,34 @@ func TestAGraduatedEffortAndADecisionStillTakeOnlyAnInsight(t *testing.T) {
func TestANewVersionOfAnOldActiveEffortIsHeldToTodaysRules(t *testing.T) {
w := imported(t, newMemStore())
w.clock = time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
r, err := w.k.ReserveNumber("author@g14", KindResearch, "an old effort")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "an old effort")
if err != nil {
t.Fatal(err)
}
overview := Section{Name: "00-overview.md", Text: "What an inference layer would be."}
first := effortAsk(r.Number, StatusActive, overview)
first.Date = "2026-10-01"
w.accepted(t, "author@g14", first)
w.accepted(t, "author@node-b", first)
w.clock = time.Date(2026, 10, 15, 9, 0, 0, 0, time.UTC)
next := effortAsk(r.Number, StatusActive, overview, Section{Name: "01-findings.md", Text: "The control plane wrote them."})
next.Date = "2026-10-01"
if _, err := w.k.Propose("author@g14", next); err == nil || !strings.Contains(err.Error(), "retired-word") {
if _, err := w.k.Propose("author@node-b", next); err == nil || !strings.Contains(err.Error(), "retired-word") {
t.Fatalf("a retired word in new text of an old effort: %v", err)
}
}
func TestAChangesVerdictLeavesTheServedVersionServedAndGraduatingNeedsABecameLink(t *testing.T) {
w := imported(t, newMemStore())
r, err := w.k.ReserveNumber("author@g14", KindResearch, "an effort")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "an effort")
if err != nil {
t.Fatal(err)
}
overview := Section{Name: "00-overview.md", Text: "What an inference layer would be."}
w.accepted(t, "author@g14", effortAsk(r.Number, StatusActive, overview))
w.accepted(t, "author@node-b", effortAsk(r.Number, StatusActive, overview))
key := Key{KindResearch, r.Number}
w.judge.review = `{"verdict":"changes","findings":["01-findings.md: say where the finding comes from"]}`
next := effortAsk(r.Number, StatusActive, overview, Section{Name: "01-findings.md", Text: "A new finding."})
if _, err := w.k.Propose("author@g14", next); err != nil {
if _, err := w.k.Propose("author@node-b", next); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
@@ -119,7 +119,7 @@ func TestAChangesVerdictLeavesTheServedVersionServedAndGraduatingNeedsABecameLin
t.Fatalf("after changes: pending %v, current %+v", e.Pending != nil, e.Current.Sections)
}
grad := effortAsk(r.Number, StatusGraduated, overview)
if _, err := w.k.Propose("author@g14", grad); err == nil || !strings.Contains(err.Error(), "became") {
if _, err := w.k.Propose("author@node-b", grad); err == nil || !strings.Contains(err.Error(), "became") {
t.Fatalf("graduated without a became link: %v", err)
}
}
@@ -128,18 +128,18 @@ func TestAChangesVerdictLeavesTheServedVersionServedAndGraduatingNeedsABecameLin
func TestAnOldActiveEffortWithAMarkedInsightTakesANewVersion(t *testing.T) {
w := imported(t, newMemStore())
w.clock = time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
r, err := w.k.ReserveNumber("author@g14", KindResearch, "an old effort")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "an old effort")
if err != nil {
t.Fatal(err)
}
overview := Section{Name: "00-overview.md", Text: "What an inference layer would be.\n\n**Progressive insight — 2026-10-05.** A fact was corrected."}
first := effortAsk(r.Number, StatusActive, overview)
first.Date = "2026-10-01"
w.accepted(t, "author@g14", first)
w.accepted(t, "author@node-b", first)
w.clock = time.Date(2026, 10, 15, 9, 0, 0, 0, time.UTC)
next := effortAsk(r.Number, StatusActive, overview, Section{Name: "01-findings.md", Text: "A new finding."})
next.Date = "2026-10-01"
if _, err := w.k.Propose("author@g14", next); err != nil {
if _, err := w.k.Propose("author@node-b", next); err != nil {
t.Fatalf("a new version with an earlier marked insight: %v", err)
}
w.reviewAll(t)
@@ -55,11 +55,11 @@ func whoCalled(t *testing.T, params map[string]any) string {
// **Who called is the caller the runtime proved** (hq issue 365): read from the call's runtime-written `_meta`,
// never from its arguments; Unproven where the runtime named none.
func TestTheCallerIsTheOneTheRuntimeProved(t *testing.T) {
if got := whoCalled(t, map[string]any{"name": "who", "arguments": map[string]any{"caller": "novox.node-tools"},
"_meta": map[string]any{stdio.MetaCaller: "shanks.node-tools"}}); got != "shanks.node-tools" {
if got := whoCalled(t, map[string]any{"name": "who", "arguments": map[string]any{"caller": "node-c.node-tools"},
"_meta": map[string]any{stdio.MetaCaller: "node-d.node-tools"}}); got != "node-d.node-tools" {
t.Errorf("callerOf answered %q, want the runtime's caller", got)
}
if got := whoCalled(t, map[string]any{"name": "who", "arguments": map[string]any{"caller": "novox.node-tools"}}); got != Unproven {
if got := whoCalled(t, map[string]any{"name": "who", "arguments": map[string]any{"caller": "node-c.node-tools"}}); got != Unproven {
t.Errorf("a call the runtime named no caller for answered %q, want %s", got, Unproven)
}
}
@@ -33,14 +33,14 @@ func TestAnInsightToAnOlderRecordIsJudgedByTheRulesThatHeldIt(t *testing.T) {
}
p := insightTo(t, w, Key{KindDecision, 11})
p.Sections[0].Text += "\n\nThe control plane wrote them."
if _, err := w.k.Propose("corrector@ace", p); err != nil {
if _, err := w.k.Propose("corrector@node-a", p); err != nil {
t.Fatalf("an insight to ADR 0011 was refused by rules that never held it: %v", err)
}
// The same words in a record of today are refused.
n := w.reserved(t, "author@g14")
n := w.reserved(t, "author@node-b")
q := valid(n)
q.Sections[2].Text = "The control plane wrote them."
if _, err := w.k.Propose("author@g14", q); err == nil || !strings.Contains(err.Error(), "- retired-word:") {
if _, err := w.k.Propose("author@node-b", q); err == nil || !strings.Contains(err.Error(), "- retired-word:") {
t.Fatalf("a retired word in a record of today: %v", err)
}
}
@@ -73,11 +73,11 @@ func TestEachDatedRuleHoldsFromItsDay(t *testing.T) {
// A new record cannot be dated back past the rules: it is of the day its number was reserved, or later.
func TestANewRecordIsNotDatedBeforeItsReservation(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
n := w.reserved(t, "author@node-b")
p := valid(n)
p.Date = "2026-01-01"
p.Sections[2].Text = "The control plane wrote them."
_, err := w.k.Propose("author@g14", p)
_, err := w.k.Propose("author@node-b", p)
if err == nil || !strings.Contains(err.Error(), "- date:") {
t.Fatalf("a new record dated before its reservation: %v", err)
}
@@ -91,7 +91,7 @@ func TestAnInsightToADecisionOfOctoberKeepsItsWords(t *testing.T) {
t.Fatal(err)
}
for _, n := range []int{276, 240, 244, 272} {
if _, err := w.k.Propose("corrector@ace", insightTo(t, w, Key{KindDecision, n})); err != nil {
if _, err := w.k.Propose("corrector@node-a", insightTo(t, w, Key{KindDecision, n})); err != nil {
t.Errorf("an insight to ADR %04d was refused: %v", n, err)
}
}
@@ -178,10 +178,10 @@ func TestTheOperatorIsToldOncePerTopicAndReturnDraftsAgain(t *testing.T) {
}
told := len(w.told)
k := Key{KindDecision, 240}
if _, err := w.k.Return("operator@shanks", Key{KindDecision, 276}, "x"); err == nil {
if _, err := w.k.Return("operator@node-d", Key{KindDecision, 276}, "x"); err == nil {
t.Fatal("a record whose author wrote its clauses was returned")
}
if _, err := w.k.Return("operator@shanks", k, "clause 2 says more than the section"); err != nil {
if _, err := w.k.Return("operator@node-d", k, "clause 2 says more than the section"); err != nil {
t.Fatal(err)
}
r := mustGet(t, w.k.Store, k).Current
@@ -258,7 +258,7 @@ func TestARefreshRepairsOnlyRecordsNobodyChangedSinceTheImportAndChangesNothingT
e85 := mustGet(t, s, Key{KindDecision, 85})
d := e85.Current.Clone()
d.Title = "A secret is a provision, corrected"
if err := s.Save(ctx, Version{By: "someone@g14", At: at, Why: "a correction by hand", Record: d}, true, false, e85.Revision); err != nil {
if err := s.Save(ctx, Version{By: "someone@node-b", At: at, Why: "a correction by hand", Record: d}, true, false, e85.Revision); err != nil {
t.Fatal(err)
}
before := countVersions(t, s)
@@ -381,7 +381,7 @@ func TestARefreshRepairsARecordTheKnowledgeBaseDraftedAndKeepsItsClauses(t *test
t.Fatalf("0004 after the refresh: %+v %+v", e4.Clauses, e4.Drafting)
}
// A record someone returned is someone's change: left alone.
if _, err := w.k.Return("operator@shanks", Key{KindDecision, 2}, "clause 2 says more than the record"); err != nil {
if _, err := w.k.Return("operator@node-d", Key{KindDecision, 2}, "clause 2 says more than the record"); err != nil {
t.Fatal(err)
}
again, _ := w.k.Import("import@test", ImportAsk{Refresh: true}, at487())
@@ -17,7 +17,7 @@ import (
func adr0343() *Record {
r := &Record{Kind: KindDecision, Number: 343, Status: StatusProposed, Date: "2026-10-13", NeedsOperator: true,
Title: "A session's memory follows the operator: the agent module carries each project's memory folder to every node, and a file changed on two keeps both",
Author: "fix-367@g14",
Author: "fix-367@node-b",
Brief: &Brief{Headline: "Your agents' memory follows you to every machine",
Summary: "What an agent learns in a project's memory is copied to every machine that runs the agent, so a session " +
"anywhere starts with it. A file changed on two machines keeps both versions and warns you; secrets in memory " +
@@ -62,7 +62,7 @@ func TestADecisionAskReadsAtAGlance(t *testing.T) {
}
}
details := strings.Split(a.Details, "\n")
if len(details) != 2 || !strings.Contains(details[0], "ADR 0343, proposed by fix-367@g14; the knowledge base's own review approved it.") ||
if len(details) != 2 || !strings.Contains(details[0], "ADR 0343, proposed by fix-367@node-b; the knowledge base's own review approved it.") ||
!strings.Contains(details[0], "Fingerprint "+fingerprint(r.Digest())+".") ||
details[1] != "Read its 13 clauses with the knowledge base's get, number 343." {
t.Errorf("the details:\n%s", a.Details)
@@ -35,7 +35,7 @@ func TestTheHighestToolAnswersTheHighestNumberHeldPerKind(t *testing.T) {
t.Fatalf("%+v", h)
}
// A number reserved, and later withdrawn, is spent: it is the highest.
w.reserved(t, "author@g14") // 300
w.reserved(t, "author@node-b") // 300
w.clock = w.clock.Add(DefaultReservedFor + time.Hour)
if _, err := w.k.WithdrawStale(); err != nil {
t.Fatal(err)
@@ -12,14 +12,14 @@ import (
func TestThreeVersionsAreKeptAndTheFirstAndThirdCompareExactly(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@g14") // version 1: the reservation
n := w.reserved(t, "author@node-b") // version 1: the reservation
p := valid(n)
if _, err := w.k.Propose("author@g14", p); err != nil { // version 2
if _, err := w.k.Propose("author@node-b", p); err != nil { // version 2
t.Fatal(err)
}
p.Clauses[1] = "**A batch is delivered by one walk.** At most one commit per repository."
p.Why = "the bound named"
if _, err := w.k.Propose("author@g14", p); err != nil { // version 3
if _, err := w.k.Propose("author@node-b", p); err != nil { // version 3
t.Fatal(err)
}
h, err := w.k.History(Key{KindDecision, n}, 0, 0, 0)
@@ -27,7 +27,7 @@ func TestThreeVersionsAreKeptAndTheFirstAndThirdCompareExactly(t *testing.T) {
t.Fatal(err)
}
if len(h.Versions) != 3 || h.Versions[0].Status != StatusReserved || h.Versions[1].Why != "proposed" ||
h.Versions[2].Why != "the bound named" || !h.Versions[2].Served || h.Versions[0].By != "author@g14" {
h.Versions[2].Why != "the bound named" || !h.Versions[2].Served || h.Versions[0].By != "author@node-b" {
t.Fatalf("history: %+v", h.Versions)
}
d, err := w.k.History(Key{KindDecision, n}, 1, 3, 0)
@@ -114,15 +114,15 @@ func TestDiffIsExactOnSmallTexts(t *testing.T) {
// history is bounded like the other answers: its work, and its answer, with a cut notice (the review of #206).
func TestHistoryIsBounded(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
n := w.reserved(t, "author@node-b")
p := valid(n)
big := strings.Repeat("A line of reasoning that is long enough to count.\n", 3000)
p.Sections[0].Text = big
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatal(err)
}
p.Sections[0].Text = strings.ReplaceAll(big, "long enough", "longer than")
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatal(err)
}
h, err := w.k.History(Key{KindDecision, n}, 2, 3, 0)
@@ -82,7 +82,7 @@ func TestARecordWhoseClausesAreLinkedByNumberIsNeverReturned(t *testing.T) {
}
w.reviewAll(t)
before := mustGet(t, s, k)
_, err := w.k.Return("operator@shanks", k, "clause 1 says more than the section")
_, err := w.k.Return("operator@node-d", k, "clause 1 says more than the section")
if err == nil {
t.Fatal("a record a proposed record links by clause was returned")
}
@@ -101,7 +101,7 @@ func TestARecordWhoseClausesAreLinkedByNumberIsNeverReturned(t *testing.T) {
p2 := valid(n2)
p2.Links = append(p2.Links, LinkAsk{Kind: LinkChanges, To: "0240 §1", From: 2})
w2.accepted(t, "author@test", p2)
if _, err := w2.k.Return("operator@shanks", k2, "again"); err == nil || !strings.Contains(err.Error(), "§1 is changed by ADR "+itoa4(n2)+" §2") {
if _, err := w2.k.Return("operator@node-d", k2, "again"); err == nil || !strings.Contains(err.Error(), "§1 is changed by ADR "+itoa4(n2)+" §2") {
t.Fatalf("a record with a changed clause was returned, or the refusal does not name the change: %v", err)
}
})
@@ -39,15 +39,15 @@ func (a *asked) ask(key string, body any) (json.RawMessage, error) {
return nil, errors.New("claude_code_judge takes question, schema and model; not within")
}
if _, long := b["within"]; long && a.pending > 0 {
return json.RawMessage(`{"node":"novox","ticket":"t1","pending":true}`), nil
return json.RawMessage(`{"node":"node-c","ticket":"t1","pending":true}`), nil
}
return json.RawMessage(`{"answer":{"verdict":"approve","findings":[]},"model":"sonnet","node":"novox"}`), nil
return json.RawMessage(`{"answer":{"verdict":"approve","findings":[]},"model":"sonnet","node":"node-c"}`), nil
}
if key == "claude-code.claude_code_judged@novox" && b["ticket"] == "t1" {
if key == "claude-code.claude_code_judged@node-c" && b["ticket"] == "t1" {
if a.pending--; a.pending > 0 {
return json.RawMessage(`{"node":"novox","ticket":"t1","pending":true}`), nil
return json.RawMessage(`{"node":"node-c","ticket":"t1","pending":true}`), nil
}
return json.RawMessage(`{"answer":{"verdict":"approve","findings":[]},"model":"sonnet","node":"novox","ticket":"t1"}`), nil
return json.RawMessage(`{"answer":{"verdict":"approve","findings":[]},"model":"sonnet","node":"node-c","ticket":"t1"}`), nil
}
return nil, fmt.Errorf("nothing answers %s", key)
}
@@ -68,7 +68,7 @@ func withinOf(t *testing.T, b map[string]any) time.Duration {
func TestALongRecordsReviewGetsTheLargerBound(t *testing.T) {
short, long := ReviewQuestion(aRecord(2, 200), nil), ReviewQuestion(aRecord(12, 2000), nil)
a := &asked{}
j := judgementOver(a.ask, "novox")
j := judgementOver(a.ask, "node-c")
for _, q := range []string{short, long} {
if _, _, err := j(q, reviewSchema, "sonnet"); err != nil {
t.Fatal(err)
@@ -88,14 +88,14 @@ func TestALongRecordsReviewGetsTheLargerBound(t *testing.T) {
// judgement's.
func TestAPendingJudgementIsCollectedByItsTicket(t *testing.T) {
a := &asked{pending: 3}
answer, by, err := judgementOver(a.ask, "novox")(ReviewQuestion(aRecord(12, 2000), nil), reviewSchema, "sonnet")
answer, by, err := judgementOver(a.ask, "node-c")(ReviewQuestion(aRecord(12, 2000), nil), reviewSchema, "sonnet")
if err != nil {
t.Fatal(err)
}
if string(answer) != `{"verdict":"approve","findings":[]}` || by != "the agent on novox (sonnet)" {
if string(answer) != `{"verdict":"approve","findings":[]}` || by != "the agent on node-c (sonnet)" {
t.Fatalf("%s by %s", answer, by)
}
if strings.Join(a.keys, " ") != "claude-code.claude_code_judge@novox claude-code.claude_code_judged@novox claude-code.claude_code_judged@novox claude-code.claude_code_judged@novox" {
if strings.Join(a.keys, " ") != "claude-code.claude_code_judge@node-c claude-code.claude_code_judged@node-c claude-code.claude_code_judged@node-c claude-code.claude_code_judged@node-c" {
t.Fatalf("asked %q", a.keys)
}
}
@@ -104,7 +104,7 @@ func TestAPendingJudgementIsCollectedByItsTicket(t *testing.T) {
// it fits the old bound, and fails as before when it does not.
func TestAnAgentModuleThatTakesNoWithinIsAskedWithoutIt(t *testing.T) {
a := &asked{refuse: true}
if _, _, err := judgementOver(a.ask, "novox")(ReviewQuestion(aRecord(12, 2000), nil), reviewSchema, "sonnet"); err != nil {
if _, _, err := judgementOver(a.ask, "node-c")(ReviewQuestion(aRecord(12, 2000), nil), reviewSchema, "sonnet"); err != nil {
t.Fatal(err)
}
if len(a.bodies) != 2 || withinOf(t, a.bodies[1]) != 0 {
@@ -95,7 +95,7 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) {
m.Build.Artifacts[0].Env["MESH_KNOWLEDGE_STATUS"] == "" || !reflect.DeepEqual(m.Build.Artifacts[0].Loads, []string{Module}) {
t.Fatalf("build %+v", m.Build)
}
for _, never := range []string{"/home/", "jochen", "shanks", "novox.be"} {
for _, never := range []string{"/home/", "jochen", "node-d", "node-c.be"} {
if strings.Contains(strings.ToLower(raw), never) {
t.Errorf("module.json names %q", never)
}
@@ -15,7 +15,7 @@ import (
func TestAnAskThatCannotBeMadeIsTriedABoundedNumberOfTimes(t *testing.T) {
w := imported(t, newMemStore())
w.k.Asks = nil
n := proposed(t, w, "author@g14", true)
n := proposed(t, w, "author@node-b", true)
for i := 0; i < 6*48; i++ { // two days, every ten minutes
w.reviewAll(t)
w.clock = w.clock.Add(10 * time.Minute)
@@ -45,8 +45,8 @@ func (f *failingSaves) Save(ctx context.Context, v Version, current, clearPendin
func TestARecordWhoseReviewCannotBeKeptBlocksNoOtherAndIsTriedBoundedly(t *testing.T) {
s := &failingSaves{memStore: newMemStore()}
w := imported(t, s)
a := proposed(t, w, "author@g14", false)
b := proposed(t, w, "author@g14", false)
a := proposed(t, w, "author@node-b", false)
b := proposed(t, w, "author@node-b", false)
s.key, s.fail = Key{KindDecision, a}, true
errs := 0
for i := 0; i < 6*48; i++ {
@@ -78,7 +78,7 @@ func TestARecordWhoseReviewCannotBeKeptBlocksNoOtherAndIsTriedBoundedly(t *testi
// sent is sent again at the next step, since health no longer stands for it.
func TestARecordThatCouldNotBeReviewedLeavesTheModuleHealthyAndIsTold(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
w.judge.reviewFail = true
notify := w.k.Notify
failing := true
@@ -109,18 +109,18 @@ func TestARecordThatCouldNotBeReviewedLeavesTheModuleHealthyAndIsTold(t *testing
// A progressive insight waiting for its review is replaced only by its author, proven.
func TestAPendingInsightIsReplacedOnlyByItsAuthor(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
w.accepted(t, "author@g14", valid(n))
n := w.reserved(t, "author@node-b")
w.accepted(t, "author@node-b", valid(n))
p := valid(n)
p.Sections[2].Text = "Merges land sooner, by about a minute."
p.Insight = "it said only that merges land sooner"
if _, err := w.k.Propose("corrector@ace", p); err != nil {
if _, err := w.k.Propose("corrector@node-a", p); err != nil {
t.Fatal(err)
}
q := valid(n)
q.Sections[2].Text = "Merges land later."
q.Insight = "it said merges land sooner"
for _, other := range []string{"author@g14", "other@novox", Unproven} {
for _, other := range []string{"author@node-b", "other@node-c", Unproven} {
if _, err := w.k.Propose(other, q); err == nil || !strings.Contains(err.Error(), "only its author") {
t.Fatalf("a pending insight replaced by %s: %v", other, err)
}
@@ -128,7 +128,7 @@ func TestAPendingInsightIsReplacedOnlyByItsAuthor(t *testing.T) {
if e := mustGet(t, w.k.Store, Key{KindDecision, n}); e.Pending.Section("Consequences") != p.Sections[2].Text {
t.Fatalf("the pending insight was replaced: %q", e.Pending.Section("Consequences"))
}
if _, err := w.k.Propose("corrector@ace", q); err != nil {
if _, err := w.k.Propose("corrector@node-a", q); err != nil {
t.Fatalf("its author's revision: %v", err)
}
}
@@ -17,7 +17,7 @@ func TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
inZone(t)
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
key := Key{KindDecision, n}
lastWhy := func() string {
vs, err := s.Versions(context.Background(), key)
@@ -97,7 +97,7 @@ func TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid(t *testing.T) {
func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
key := Key{KindDecision, n}
runOut := func(from int) {
t.Helper()
@@ -125,7 +125,7 @@ func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) {
t.Fatalf("a version written while it waits: told %v, cleared %v", w.toldKeys, w.cleared)
}
// Proposed again: the notice is cleared, saying so.
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
if _, err := w.k.Propose("author@node-b", valid(n)); err != nil {
t.Fatal(err)
}
w.clock = w.clock.Add(time.Second)
@@ -142,7 +142,7 @@ func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) {
// Proposed again and accepted at last: that notice is cleared too, and nothing is cleared twice, also after
// a restart.
w.judge.reviewFail = false
w.accepted(t, "author@g14", valid(n))
w.accepted(t, "author@node-b", valid(n))
if len(w.cleared) != 2 || w.cleared[1][0] != w.toldKeys[1] || !strings.Contains(w.cleared[1][1], "proposed again") {
t.Fatalf("accepted: cleared %v", w.cleared)
}
@@ -172,7 +172,7 @@ func inZone(t *testing.T) {
func TestAClearThatFailsIsSentAgain(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
w.judge.reviewFail = true
for try := 1; try <= MostReviewTries; try++ {
w.reviewAll(t)
@@ -184,7 +184,7 @@ func TestAClearThatFailsIsSentAgain(t *testing.T) {
prefix := fmt.Sprintf("knowledge.unreviewed.decision-%d.", n)
refused := 0
w.k.Clear = func(key, resolved string) error { refused++; return errors.New("the channel is down") }
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
if _, err := w.k.Propose("author@node-b", valid(n)); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
@@ -30,11 +30,11 @@ func TestTheReviewIsShownEveryLinkKindProposeAccepts(t *testing.T) {
// A research effort's own status is shown apart from its state in the knowledge base.
func TestTheReviewIsShownAResearchEffortsStatusApartFromItsState(t *testing.T) {
w := imported(t, newMemStore())
r, err := w.k.ReserveNumber("author@g14", KindResearch, "a vendor-neutral inference layer")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "a vendor-neutral inference layer")
if err != nil {
t.Fatal(err)
}
if _, err := w.k.Propose("author@g14", ProposeAsk{Kind: KindResearch, Number: r.Number, Title: "A vendor-neutral inference layer",
if _, err := w.k.Propose("author@node-b", ProposeAsk{Kind: KindResearch, Number: r.Number, Title: "A vendor-neutral inference layer",
Date: "2026-10-11", Deciders: []string{"jochen"}, Status: StatusActive,
Sections: []Section{{Name: "00-overview.md", Text: "What an inference layer the mesh controls would be."}},
Links: []LinkAsk{{Kind: LinkTouches, To: "0290"}, {Kind: LinkTouches, To: "issue 365"}}}); err != nil {
@@ -31,7 +31,7 @@ func proposed(t *testing.T, w *world, author string, needsOperator bool) int {
func TestAProposalIsReviewedByTheKnowledgeBaseItselfAndAcceptedOnApprove(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
// Proposed, waiting for its review: nothing of it holds yet.
e := mustGet(t, s, Key{KindDecision, n})
if e.Current.Status != StatusProposed || e.Current.Judging == nil || e.Current.Judging.State != JudgingDue || len(w.judge.reviews) != 0 {
@@ -49,7 +49,7 @@ func TestAProposalIsReviewedByTheKnowledgeBaseItselfAndAcceptedOnApprove(t *test
t.Errorf("the review's question lacks %q", want)
}
}
for _, never := range []string{"ADR 0131", "ADR 0011", "author@g14"} {
for _, never := range []string{"ADR 0131", "ADR 0011", "author@node-b"} {
if strings.Contains(q, never) {
t.Errorf("the review's question holds %q, which the record does not link", never)
}
@@ -89,7 +89,7 @@ func itoaKey(n int) string { return Key{KindDecision, n}.String() }
func TestOnChangesTheRecordStaysProposedAndItsAuthorsRevisionIsReviewedAgain(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
w.judge.review = `{"verdict":"changes","findings":["§2: name the bound","Consequences: say who waits"]}`
w.reviewAll(t)
e := mustGet(t, s, Key{KindDecision, n})
@@ -108,11 +108,11 @@ func TestOnChangesTheRecordStaysProposedAndItsAuthorsRevisionIsReviewedAgain(t *
}
p := valid(n)
p.Sections[2].Text = "Merges land sooner; nobody waits longer than ninety seconds."
if _, err := w.k.Propose("reviewer@novox", p); err == nil || !strings.Contains(err.Error(), "only its author") {
if _, err := w.k.Propose("reviewer@node-c", p); err == nil || !strings.Contains(err.Error(), "only its author") {
t.Fatalf("a revision by someone else: %v", err)
}
w.judge.review = ""
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
@@ -137,7 +137,7 @@ func TestAJudgementThatCannotBeHadNeverApproves(t *testing.T) {
} {
t.Run(c.name, func(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
w.judge.review, w.judge.reviewFail = c.answer, c.answer == ""
for try := 1; try <= MostReviewTries; try++ {
if said := w.reviewAll(t); len(said) != 1 || !strings.Contains(said[0], "not reviewed") {
@@ -174,7 +174,7 @@ func TestAJudgementThatCannotBeHadNeverApproves(t *testing.T) {
}
// Its author proposes it again, and that version is reviewed.
w.judge.review, w.judge.reviewFail = "", false
w.accepted(t, "author@g14", valid(n))
w.accepted(t, "author@node-b", valid(n))
})
}
}
@@ -182,11 +182,11 @@ func TestAJudgementThatCannotBeHadNeverApproves(t *testing.T) {
// A record revised while its judgement was asked is not decided on the version judged: the newest is judged next.
func TestARecordRevisedWhileItIsJudgedIsJudgedAgain(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
p := valid(n)
p.Sections[2].Text = "Merges land sooner, and the walk is shorter."
w.judge.during = func() {
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Error(err)
}
}
@@ -224,11 +224,11 @@ func TestARecordProposedUnprovenIsNeverReviewed(t *testing.T) {
// A record that supersedes another moves it and every clause of it to superseded.
func TestASupersedingRecordSupersedesTheOlderOnAcceptance(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
n := w.reserved(t, "author@node-b")
p := valid(n)
p.Links = []LinkAsk{{Kind: LinkSupersedes, To: "0276"}}
p.Sections[0].Text = "The merge window is replaced."
w.accepted(t, "author@g14", p)
w.accepted(t, "author@node-b", p)
old := mustGet(t, w.k.Store, Key{KindDecision, 276}).Current
if old.Status != StatusSuperseded || old.Clause(4).Status != ClauseSuperseded || old.Extra["superseded-by"] != itoaKey(n) {
t.Fatalf("0276: %s, §4 %+v", old.Status, old.Clause(4))
@@ -242,7 +242,7 @@ func TestASupersedingRecordSupersedesTheOlderOnAcceptance(t *testing.T) {
func TestARecordThatNeedsTheOperatorIsAcceptedOnlyOnTheWarrant(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := proposed(t, w, "author@g14", true)
n := proposed(t, w, "author@node-b", true)
w.reviewAll(t)
if e := mustGet(t, s, Key{KindDecision, n}); e.Current.Status != StatusProposed || len(w.router.asked) != 1 {
t.Fatalf("after the review: %s, asks %d", e.Current.Status, len(w.router.asked))
@@ -293,7 +293,7 @@ func TestARecordThatNeedsTheOperatorIsAcceptedOnlyOnTheWarrant(t *testing.T) {
func TestAnAskThatCannotBePublishedLeavesTheRecordProposed(t *testing.T) {
w := imported(t, newMemStore())
w.k.Asks = nil
n := proposed(t, w, "author@g14", true)
n := proposed(t, w, "author@node-b", true)
said := w.reviewAll(t)
e := mustGet(t, w.k.Store, Key{KindDecision, n})
if len(said) != 1 || !strings.Contains(said[0], "cannot be asked") || e.Current.Status != StatusProposed || e.Current.Ask != nil ||
@@ -305,7 +305,7 @@ func TestAnAskThatCannotBePublishedLeavesTheRecordProposed(t *testing.T) {
// A warrant for an ask whose record changed after it was asked does not hold: the act it bound is another version.
func TestAWarrantForAnotherVersionDoesNotAccept(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", true)
n := proposed(t, w, "author@node-b", true)
w.reviewAll(t)
// The record is changed behind the ask, keeping the ask: what the operator was shown is not what is held.
ctx := context.Background()
@@ -327,7 +327,7 @@ func TestAWarrantForAnotherVersionDoesNotAccept(t *testing.T) {
func TestTheOperatorsDeclineLeavesTheRecordProposed(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", true)
n := proposed(t, w, "author@node-b", true)
w.reviewAll(t)
w.router.answer(w.router.asked[0].ID, OptionDecline, w.clock.Add(time.Minute))
if _, err := w.k.Reconcile(); err != nil {
@@ -343,16 +343,16 @@ func TestTheOperatorsDeclineLeavesTheRecordProposed(t *testing.T) {
// version is served.
func TestAnAcceptedRecordChangesOnlyByAnInsightReviewedLikeAProposal(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
w.accepted(t, "author@g14", valid(n))
n := w.reserved(t, "author@node-b")
w.accepted(t, "author@node-b", valid(n))
p := valid(n)
p.Sections[2].Text = "Merges land sooner, by about a minute."
if _, err := w.k.Propose("author@g14", p); err == nil || !strings.Contains(err.Error(), "progressive insight") {
if _, err := w.k.Propose("author@node-b", p); err == nil || !strings.Contains(err.Error(), "progressive insight") {
t.Fatalf("an accepted record rewritten: %v", err)
}
p.Insight = "it said only that merges land sooner"
w.judge.review = `{"verdict":"changes","findings":["Consequences: say what a minute is measured from"]}`
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
@@ -363,7 +363,7 @@ func TestAnAcceptedRecordChangesOnlyByAnInsightReviewedLikeAProposal(t *testing.
}
w.judge.review = ""
p.Sections[2].Text = "Merges land sooner, by about a minute from the last merge."
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
@@ -394,7 +394,7 @@ func TestNoVerbTakesAReview(t *testing.T) {
func TestReserveAsksTheTrackerAndANumberNeverProposedIsWithdrawn(t *testing.T) {
w := newWorld(t, newMemStore(), fixture())
r, err := w.k.ReserveNumber("author@g14", KindResearch, "how a knowledge base is asked")
r, err := w.k.ReserveNumber("author@node-b", KindResearch, "how a knowledge base is asked")
if err != nil {
t.Fatal(err)
}
@@ -402,7 +402,7 @@ func TestReserveAsksTheTrackerAndANumberNeverProposedIsWithdrawn(t *testing.T) {
w.tracker.asked[0]["for"] != "how a knowledge base is asked" {
t.Fatalf("%+v %v", r, w.tracker.asked)
}
if _, err := w.k.ReserveNumber("author@g14", "design", "x"); err == nil {
if _, err := w.k.ReserveNumber("author@node-b", "design", "x"); err == nil {
t.Fatal("a design number reserved")
}
w.clock = w.clock.Add(DefaultReservedFor + time.Hour)
@@ -410,10 +410,10 @@ func TestReserveAsksTheTrackerAndANumberNeverProposedIsWithdrawn(t *testing.T) {
t.Fatalf("withdrew %d (%v)", n, err)
}
e := mustGet(t, w.k.Store, Key{KindResearch, 50})
if e.Current.Status != StatusWithdrawn || e.Current.ReservedBy != "author@g14" {
if e.Current.Status != StatusWithdrawn || e.Current.ReservedBy != "author@node-b" {
t.Fatalf("%+v", e.Current)
}
if _, err := w.k.Propose("author@g14", ProposeAsk{Kind: KindResearch, Number: 50, Title: "late", Status: StatusActive,
if _, err := w.k.Propose("author@node-b", ProposeAsk{Kind: KindResearch, Number: 50, Title: "late", Status: StatusActive,
Sections: []Section{{Name: "00-overview.md", Text: "x"}}}); err == nil {
t.Fatal("a withdrawn number took a proposal")
}
@@ -423,7 +423,7 @@ func TestReserveAsksTheTrackerAndANumberNeverProposedIsWithdrawn(t *testing.T) {
// for an answer nobody was shown (ADR 0397, issue 926); offered, it waits again; answered, it is decided as ever.
func TestAnAskTheRouterHoldsIsShownHeldWithTheReason(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", true)
n := proposed(t, w, "author@node-b", true)
w.reviewAll(t)
id := w.router.asked[0].ID
key := Key{KindDecision, n}
@@ -39,7 +39,7 @@ would receive them whole.
1. **Keep the flat list; rely on the client to defer it.** Rejected: a property of one client, and it
leaves the ambiguity and the stale list.
2. **One flat tool per assignment** (`ace_postgres_list_databases`). Removes the ambiguity, multiplies
2. **One flat tool per assignment** (`node-a_postgres_list_databases`). Removes the ambiguity, multiplies
the list, and runs into the API's tool-name limit (letters, digits, `_`, `-`, 64 characters).
3. **A small fixed set of tools that walk the mesh's own structure**, with the full address as an
argument: the mesh's seats; a machine's node seats and assignments; a search; a description; a
@@ -27,7 +27,7 @@ connections lasted, and then the public name went down
**The same fault, four days later, undetected for five days.** One container had restarted 2286 times
against a database it could no longer find, while the mesh reported the machine as doing what it was
told. Inside it, `novox.internal` was an address that had not existed for five days
told. Inside it, `node-c.internal` was an address that had not existed for five days
([issue 135](../04-ISSUES/135-a-containers-mesh-names-are-not-compared/00-report.md)). Forty-eight
other containers were current, none of them corrected — each had been recreated for some other
reason and picked up the roster on the way.
@@ -128,7 +128,7 @@ catalogue along for a controller change; `plans stop`), 200. The first merge han
machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0
the build machine; tier 1 the controller and the proxy that packages its source. The handler
returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for
builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and
builder on node-c to be applied* until the machine reported; then tier 1 was asked, both built, and
the plan read done — three minutes, read through the console with `plans`, the receive loop taking
reports throughout. What the day between decision and proof taught is in issues
[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md),
@@ -13,10 +13,10 @@ import (
func TestADecisionNothingWouldCiteIsNotAccepted(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@g14")
n := w.reserved(t, "author@node-b")
p := valid(n)
p.Homes = nil
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatal(err)
}
judged := len(w.judge.reviews)
@@ -38,7 +38,7 @@ func TestADecisionNothingWouldCiteIsNotAccepted(t *testing.T) {
}
// Named with its home, it is accepted, and the module stays healthy.
p.Homes = []string{testHome}
w.accepted(t, "author@g14", p)
w.accepted(t, "author@node-b", p)
if h := w.k.Health(nil); !h.Healthy {
t.Fatalf("unhealthy after a decision with its home was accepted: %s", h.Why)
}
@@ -53,18 +53,18 @@ func TestADecisionNothingWouldCiteIsNotAccepted(t *testing.T) {
func TestARecordAcceptedCitingANewDecisionLetsItBeAcceptedAndDroppingTheLinkIsLoud(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@g14")
n := w.reserved(t, "author@node-b")
a := valid(n)
a.Homes = nil
if _, err := w.k.Propose("author@g14", a); err != nil {
if _, err := w.k.Propose("author@node-b", a); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
m := w.reserved(t, "author@g14")
m := w.reserved(t, "author@node-b")
b := valid(m)
b.Links = append(b.Links, LinkAsk{Kind: LinkCites, To: itoa4(n)})
w.accepted(t, "author@g14", b)
w.accepted(t, "author@g14", a) // its revision: an accepted record links it now
w.accepted(t, "author@node-b", b)
w.accepted(t, "author@node-b", a) // its revision: an accepted record links it now
if h := w.k.Health(nil); !h.Healthy {
t.Fatalf("unhealthy with both accepted, one citing the other: %s", h.Why)
}
@@ -75,7 +75,7 @@ func TestARecordAcceptedCitingANewDecisionLetsItBeAcceptedAndDroppingTheLinkIsLo
// A correction of the citing record drops its link: the rule, run after the acceptance, says so.
b.Links = b.Links[:len(b.Links)-1]
b.Insight = "it cited " + itoaKey(n)
w.accepted(t, "author@g14", b)
w.accepted(t, "author@node-b", b)
h := w.k.Health(nil)
if !h.Healthy || !strings.Contains(h.Why, "the rule cited") || !strings.Contains(h.Why, itoaKey(n)) {
t.Fatalf("a decision left cited by nothing: the health should stay healthy and name it, not %+v", h)
@@ -99,10 +99,10 @@ func TestARecordAcceptedCitingANewDecisionLetsItBeAcceptedAndDroppingTheLinkIsLo
t.Fatal("the acceptance was undone")
}
// A record that cites it heals it.
k := w.reserved(t, "author@g14")
k := w.reserved(t, "author@node-b")
c := valid(k)
c.Links = append(c.Links, LinkAsk{Kind: LinkCites, To: itoa4(n)})
w.accepted(t, "author@g14", c)
w.accepted(t, "author@node-b", c)
if h := w.k.Health(nil); strings.Contains(h.Why, itoaKey(n)+" is accepted and nothing cites it") {
t.Fatalf("still said after a record cited it: %s", h.Why)
}
@@ -14,7 +14,7 @@ func TestAReservedRecordIsWithdrawnAtOnce(t *testing.T) {
w := imported(t, s)
n := w.reserved(t, "author@test")
key := Key{KindDecision, n}
out, err := w.k.WithdrawRecord("operator@shanks", key, "never proposed")
out, err := w.k.WithdrawRecord("operator@node-d", key, "never proposed")
if err != nil {
t.Fatal(err)
}
@@ -45,7 +45,7 @@ func TestAProposedRecordNeedingTheOperatorIsWithdrawnWithItsAskEndedAndItsReview
t.Fatalf("not waiting for the operator: %+v", before.Current)
}
why := "replaced by research on moving designs into the knowledge base"
out, err := w.k.WithdrawRecord("operator@shanks", key, why)
out, err := w.k.WithdrawRecord("operator@node-d", key, why)
if err != nil {
t.Fatal(err)
}
@@ -73,7 +73,7 @@ func TestAProposedRecordNeedingTheOperatorIsWithdrawnWithItsAskEndedAndItsReview
t.Fatal(err)
}
last := vs[len(vs)-1]
if last.By != "operator@shanks" || !strings.Contains(last.Why, why) {
if last.By != "operator@node-d" || !strings.Contains(last.Why, why) {
t.Fatalf("the new version is not by the caller with the why: %+v", last)
}
})
@@ -85,7 +85,7 @@ func TestAnAcceptedRecordRefusesWithdrawNamingWhy(t *testing.T) {
n := w.reserved(t, "author@test")
key := Key{KindDecision, n}
w.accepted(t, "author@test", valid(n))
if _, err := w.k.WithdrawRecord("operator@shanks", key, "no longer wanted"); err == nil || !strings.Contains(err.Error(), "accepted") {
if _, err := w.k.WithdrawRecord("operator@node-d", key, "no longer wanted"); err == nil || !strings.Contains(err.Error(), "accepted") {
t.Fatalf("err %v", err)
}
e := mustGet(t, w.k.Store, key)
@@ -100,10 +100,10 @@ func TestAWithdrawnRecordRefusesWithdrawAgain(t *testing.T) {
w := imported(t, s)
n := w.reserved(t, "author@test")
key := Key{KindDecision, n}
if _, err := w.k.WithdrawRecord("operator@shanks", key, "first"); err != nil {
if _, err := w.k.WithdrawRecord("operator@node-d", key, "first"); err != nil {
t.Fatal(err)
}
if _, err := w.k.WithdrawRecord("operator@shanks", key, "again"); err == nil || !strings.Contains(err.Error(), "withdrawn") {
if _, err := w.k.WithdrawRecord("operator@node-d", key, "again"); err == nil || !strings.Contains(err.Error(), "withdrawn") {
t.Fatalf("err %v", err)
}
})
@@ -113,10 +113,10 @@ func TestWithdrawNeedsWhyAndRefusesARecordItHoldsNoneOf(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported(t, s)
n := w.reserved(t, "author@test")
if _, err := w.k.WithdrawRecord("operator@shanks", Key{KindDecision, n}, " "); err == nil {
if _, err := w.k.WithdrawRecord("operator@node-d", Key{KindDecision, n}, " "); err == nil {
t.Fatal("an empty why was accepted")
}
if _, err := w.k.WithdrawRecord("operator@shanks", Key{KindDecision, 999999}, "why"); err == nil {
if _, err := w.k.WithdrawRecord("operator@node-d", Key{KindDecision, 999999}, "why"); err == nil {
t.Fatal("a record it holds none of was accepted")
}
})
@@ -11,10 +11,10 @@ import (
func TestARevisionIsTakenOnlyFromTheRecordsAuthor(t *testing.T) {
w := imported(t, newMemStore())
n := proposed(t, w, "author@g14", false)
n := proposed(t, w, "author@node-b", false)
p := valid(n)
p.Sections[2].Text = "Merges land a little sooner."
if _, err := w.k.Propose("other@novox", p); err == nil || !strings.Contains(err.Error(), "only its author") {
if _, err := w.k.Propose("other@node-c", p); err == nil || !strings.Contains(err.Error(), "only its author") {
t.Fatalf("a revision by someone else: %v", err)
}
if _, err := w.k.Propose(Unproven, p); err == nil || !strings.Contains(err.Error(), "only its author") {
@@ -23,18 +23,18 @@ func TestARevisionIsTakenOnlyFromTheRecordsAuthor(t *testing.T) {
if mustGet(t, w.k.Store, Key{KindDecision, n}).Versions != 2 {
t.Fatal("a refused revision wrote a version")
}
if _, err := w.k.Propose("author@g14", p); err != nil {
if _, err := w.k.Propose("author@node-b", p); err != nil {
t.Fatalf("the author's revision: %v", err)
}
}
func TestAReservedNumberIsProposedOnlyByWhoReservedIt(t *testing.T) {
w := imported(t, newMemStore())
n := w.reserved(t, "author@g14")
if _, err := w.k.Propose("other@novox", valid(n)); err == nil || !strings.Contains(err.Error(), "reserved by author@g14") {
n := w.reserved(t, "author@node-b")
if _, err := w.k.Propose("other@node-c", valid(n)); err == nil || !strings.Contains(err.Error(), "reserved by author@node-b") {
t.Fatalf("a proposal under another's number: %v", err)
}
if _, err := w.k.Propose("author@g14", valid(n)); err != nil {
if _, err := w.k.Propose("author@node-b", valid(n)); err != nil {
t.Fatal(err)
}
}
+3 -3
View File
@@ -106,17 +106,17 @@ what is said is decided by the state now and by when a thing happened, never by
what arrives meanwhile goes out, coalesced, when the minute ends.
**A message reads in one glance** (novox/hq ADR 0253). Its title is the condition's headline — a few
plain words such as "openrazer not working on g14" — prefixed "Urgent:", "Still open:" or "Now urgent:"
plain words such as "openrazer not working on node-b" — prefixed "Urgent:", "Still open:" or "Now urgent:"
where that applies; its body is the condition's explanation (what happened, what it means, whether the
operator needs to act) and since when, in the operator's time zone. No key, id, commit, command or markup:
those stay in the controller's `conditions` and in this module's history. A clearance is one line, the
condition's resolved line and how long it was open: "Resolved: openrazer works again on g14, after 26 min".
condition's resolved line and how long it was open: "Resolved: openrazer works again on node-b, after 26 min".
A condition from a controller older than plain words, or a module's notice without a headline, is said from
its summary, with code spans taken out (`cmd/messenger/words.go`).
**A condition's message says which issue tracks it** (novox/hq ADR 0285, issue 400). Below the explanation,
one line from the issue tracker's `tracking` verb, asked outside the work at most once a minute and used for
ten minutes: "Tracked as issue 386: fix-386 on g14 is fixing it since 21:10.", "…: nobody is working on it.",
ten minutes: "Tracked as issue 386: fix-386 on node-b is fixing it since 21:10.", "…: nobody is working on it.",
"…: its fix is merged and on its way.", "…: it waits for your answer.", up to three issues and then a count;
"No issue tracks this yet." when no issue links the condition's key; "Whether an issue tracks this could not
be checked." when the tracker gave no answer in ten minutes. "Nothing for you to do." becomes "Needs you:
+1 -1
View File
@@ -100,7 +100,7 @@ func (c Condition) When(event string) time.Time {
return time.Time{}
}
// SubjectWords is the subject as a person reads it: "machine ace", "plan 41", "provider keycloak".
// SubjectWords is the subject as a person reads it: "machine node-a", "plan 41", "provider keycloak".
func (c Condition) SubjectWords() string {
parts := []string{}
if c.Scope != "" {
+2 -2
View File
@@ -145,7 +145,7 @@ func randomRun(text string) (Refusal, bool) {
}
// isPath: absolute, home-relative or dot-relative, or two separators deep. A mesh address names one
// machine and one tool (`ace/postgres.query`) and has one; a ratio ("3/4") has digits only.
// machine and one tool (`node-a/postgres.query`) and has one; a ratio ("3/4") has digits only.
func isPath(w string) bool {
if w == "" {
return false
@@ -162,7 +162,7 @@ func isPath(w string) bool {
}
// isHostName: two names or more, the last a top-level one. A condition key's last name is its kind
// (`machine.ace.silent`), which none of these is.
// (`machine.node-a.silent`), which none of these is.
func isHostName(w string) bool {
w = strings.ToLower(w)
if w == "localhost" {
+4 -4
View File
@@ -675,14 +675,14 @@ func tools(h *Holder, l *listening) []stdio.Tool {
"Roles and words only: an address, a path or a secret is refused. Deduplicated, capped and routed " +
"like the controller's conditions.",
Input: map[string]any{
"key": str("what makes it the same message the next time, e.g. backup.ace.failed"),
"key": str("what makes it the same message the next time, e.g. backup.node-a.failed"),
"severity": map[string]any{"type": "string", "enum": []string{Urgent, Warning}},
"summary": str("one line in the mesh's words"),
"subject": str("what it is about: a machine's role, a module, a plan"),
"headline": str("a few plain words the operator reads as the title, e.g. backup failed on ace: no ids, keys or commands"),
"headline": str("a few plain words the operator reads as the title, e.g. backup failed on node-a: no ids, keys or commands"),
"explanation": str("one or two plain sentences: what happened, what it means for the operator, and whether " +
"they need to act"),
"resolved": str("the one short line said when it is over, e.g. backups run again on ace"),
"resolved": str("the one short line said when it is over, e.g. backups run again on node-a"),
"clear": map[string]any{"type": "boolean", "description": "it is over: the message is edited to say so"},
},
Run: func(a map[string]any) (any, error) {
@@ -696,7 +696,7 @@ func tools(h *Holder, l *listening) []stdio.Tool {
"words are, and offering Details like any message. Never escalated, never kept open, never reminded; " +
"its own cap of 10 a channel an hour, apart from the conditions' and notify's.",
Input: map[string]any{
"headline": str("a few plain words the operator reads as the title, e.g. new mail arrived for ace"),
"headline": str("a few plain words the operator reads as the title, e.g. new mail arrived for node-a"),
"lines": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "one or two plain lines"},
"subject": str("what it is about: a machine's role, a module, a plan"),
},
+1 -1
View File
@@ -326,7 +326,7 @@ func issueState(is TrackedIssue, now time.Time, loc *time.Location) string {
return "nobody is working on it"
}
// workerWords is who@node as said: "fix-400 on g14".
// workerWords is who@node as said: "fix-400 on node-b".
func workerWords(worker string) string {
who, node, ok := strings.Cut(worker, "@")
if !ok {
+2 -2
View File
@@ -9,7 +9,7 @@ package main
// controller where the condition is made, so every channel says the same thing. A condition from a
// controller older than them, or a module's notice without a headline, is said from its summary,
// with what the operator cannot read taken out (plainSummary).
// - A clearance is one short line: "Resolved: openrazer works again on g14, after 26 min".
// - A clearance is one short line: "Resolved: openrazer works again on node-b, after 26 min".
// - A reminder, an escalation and a reopening say so in the title, before the same headline.
// - Times are the operator's (the setting time-zone; this machine's zone when not given), and said as a
// clock time today, a weekday this week, a date before that.
@@ -168,7 +168,7 @@ func fallbackHeadline(r *Record) string {
return capital(what) + " on " + about
}
// machineOf is the machine a record's subject words name: "machine ace" or "x on ace".
// machineOf is the machine a record's subject words name: "machine node-a" or "x on node-a".
func machineOf(subject string) string {
if _, on, ok := strings.Cut(subject, " on "); ok {
return on
+3 -3
View File
@@ -55,7 +55,7 @@ func tools(monitor Monitor, users func(context.Context) ([]byte, error), written
}},
{Name: "nats_connections",
Description: "Every client connected to the bus, named by the mesh user it is (a machine's host `node.<machine>`, a machine's runtime `<machine>.node-tools`, the controller, a person): address, uptime, round trip, traffic and pending bytes. Narrowed by a user or connection name, sorted by out_bytes (default), in_bytes, pending, msgs or subscriptions.",
Input: map[string]any{"user": str("part of a user or connection name, e.g. g14"), "sort": str("out_bytes, in_bytes, pending, msgs or subscriptions"), "limit": num("at most this many (default 50)")},
Input: map[string]any{"user": str("part of a user or connection name, e.g. node-b"), "sort": str("out_bytes, in_bytes, pending, msgs or subscriptions"), "limit": num("at most this many (default 50)")},
Run: func(a map[string]any) (any, error) {
connz, err := get("/connz?auth=1&limit=1024")
if err != nil {
@@ -115,7 +115,7 @@ func tools(monitor Monitor, users func(context.Context) ([]byte, error), written
}},
{Name: "nats_users",
Description: "The bus's users as the controller composed them — never a password hash. Without a user, each with how many grants it holds; with part of a user's name, its publish and subscribe grants in full.",
Input: map[string]any{"user": str("part of a user name, e.g. g14.node-tools or controller")},
Input: map[string]any{"user": str("part of a user name, e.g. node-b.node-tools or controller")},
Run: func(a map[string]any) (any, error) {
c, err := conf()
if err != nil {
@@ -125,7 +125,7 @@ func tools(monitor Monitor, users func(context.Context) ([]byte, error), written
}},
{Name: "nats_user_can",
Description: "Whether a bus user may publish or subscribe to a subject, and the grant that allows it — the question behind a request that timed out because the bus refused it. Also whether the running server holds it: `in_force` is true only when the user list on the machine allows it and the server reloaded its configuration after that list was written (novox/hq issue 353: a list sent is not a list the bus enforces until it reloads).",
Input: map[string]any{"user": str("the user exactly, e.g. g14.node-tools"), "action": str("publish or subscribe"), "subject": str("a concrete subject, e.g. $KV.claude-code_servers.all.x")},
Input: map[string]any{"user": str("the user exactly, e.g. node-b.node-tools"), "action": str("publish or subscribe"), "subject": str("a concrete subject, e.g. $KV.claude-code_servers.all.x")},
Run: func(a map[string]any) (any, error) {
c, err := conf()
if err != nil {