Lands Gizmoduck's round 1 candidate,ed4193316on gizmoduck/row235-e (one commit on62ddc96cc), approved by Duckworth (duckworth-0089, REVIEW-r1d9ecec514on duckworth/reviews, #1741 comment 30022). 13 paths, manifest dad377ca8fe4. No 235 path changed between62ddc96ccand this commit's parent. Patch-id c355d2fc457d, the same as 62ddc96cc..ed4193316. broker.mjs keeps row 213's import and decision.raise creative refusal as landed. Creative roles (mos-1163, mos-1164, sage-1908): #hostDefinition refuses host-creative-role at the raise and at the step 1 recheck, and #hostAccess refuses it at host.run before reading the grant record, which stays for hosts.revoke. Duckworth checked from the code that a creative role can't raise, approve or spend a host grant, and ruled the upsert mutant equivalent. Evidence: Gizmoduck's runs on my slot. B1-B37 on 5df4b52c5, and run 2 on 534f2254a (10:12:52-10:14:51Z): grant-host 9/9, grant-executor 37/37 and grant-enroll 24/24, each run whole, with B16, B23, B27, B38 and B39 killed. B1-B39: 38 killed, and B17 is equivalent (sage-1904). Gate on my slot, 10:30:55-10:42:43Z on29ce1e360plus the 13 paths: bus 596/0 (its first whole run on this head, duckworth-0089 N1), business 200/0, cli 666/0, and every scripts/test-*.sh green (auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 and 28, release 14, task 98, all 0 failed).
105 lines
6.0 KiB
JavaScript
105 lines
6.0 KiB
JavaScript
// Row 234 (#1740, r6 E4): `mosaic hosts status|check|remove`, and row 235 (#1741) revoke and sweep. A
|
|
// fake transport records the calls; no bus
|
|
// runs here. The bus side of each verb is in packages/bus/tests/grant-enroll.test.mjs.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { main } from "../src/cli.mjs";
|
|
import { io, tmp } from "./helpers.mjs";
|
|
|
|
const MOSAIC = fileURLToPath(new URL("../../../scripts/mosaic", import.meta.url));
|
|
const ROWS = [
|
|
{ host: "web1", address: "10.1.1.53", port: 22, user: "deploy", sudo: true, state: "ready", hostKeys: ["SHA256:a"],
|
|
installerFingerprint: "SHA256:b", checkedAt: "2026-10-21T00:00:00.000Z", hostLineLeft: [] },
|
|
{ host: "web2", address: "10.1.1.54", port: 2222, user: "app", sudo: false, state: "pending-install", hostKeys: ["SHA256:c"],
|
|
installerFingerprint: "SHA256:d", checkedAt: null, hostLineLeft: ["mosaic-" + "0".repeat(32)] },
|
|
];
|
|
|
|
function deps(calls) {
|
|
const answer = (who) => (business) => async (verb, args) => {
|
|
calls.push({ who, business, verb, args });
|
|
if (verb === "hosts.status") return ROWS;
|
|
if (verb === "hosts.check") return { host: args.host, state: "ready", checkedAt: "2026-10-21T00:00:00.000Z" };
|
|
return { host: args.host, removed: true };
|
|
};
|
|
return { system: { dataRoot: "/nonexistent" }, transport: answer("human"), testerTransport: answer("tester") };
|
|
}
|
|
|
|
test("row 234: hosts status prints one line per host, and --json the rows", async () => {
|
|
const calls = [];
|
|
const x = io();
|
|
await main(["hosts", "--business", "acme"], x, deps(calls));
|
|
assert.deepEqual(calls, [{ who: "human", business: "acme", verb: "hosts.status", args: {} }]);
|
|
assert.equal(x.out.text,
|
|
"web1\tready\[email protected]:22\tsudo\t2026-10-21T00:00:00.000Z\n" +
|
|
`web2\tpending-install\[email protected]:2222\tno-sudo\tunchecked\tline left: mosaic-${"0".repeat(32)}\n`);
|
|
const y = io();
|
|
await main(["hosts", "status", "--json", "--tester", "--business", "acme"], y, deps(calls));
|
|
assert.deepEqual(JSON.parse(y.out.text), ROWS);
|
|
assert.equal(calls[1].who, "tester");
|
|
});
|
|
|
|
test("row 234: hosts check and remove send the host id, as the human or the tester", async () => {
|
|
const calls = [];
|
|
const x = io();
|
|
await main(["hosts", "check", "web1", "--business", "acme"], x, deps(calls));
|
|
await main(["hosts", "remove", "web2", "--tester", "--business", "acme"], x, deps(calls));
|
|
assert.deepEqual(calls.map(({ who, verb, args }) => ({ who, verb, args })), [
|
|
{ who: "human", verb: "hosts.check", args: { host: "web1" } },
|
|
{ who: "tester", verb: "hosts.remove", args: { host: "web2" } },
|
|
]);
|
|
});
|
|
|
|
test("row 234: a malformed hosts command is a usage error and calls nothing", async () => {
|
|
const calls = [];
|
|
for (const argv of [["hosts", "check"], ["hosts", "remove"], ["hosts", "status", "web1"], ["hosts", "check", "web1", "web2"],
|
|
["hosts", "purge", "web1"], ["hosts", "check", "web1", "--json"], ["hosts", "--business"], ["hosts", "--force"]])
|
|
await assert.rejects(main([...argv], io(), deps(calls)), (e) => (assert.equal(e.exitCode, 4, argv.join(" ")), assert.match(e.message, /mosaic hosts/), true));
|
|
assert.equal(calls.length, 0);
|
|
});
|
|
|
|
test("row 234: a bus refusal exits 2 with its code", async () => {
|
|
const refusing = { system: { dataRoot: "/nonexistent" }, transport: () => async () => { throw new Error("host-in-use"); } };
|
|
await assert.rejects(main(["hosts", "remove", "web1", "--business", "acme"], io(), refusing), (e) => (assert.equal(e.exitCode, 2), assert.match(e.message, /host-in-use/), true));
|
|
});
|
|
|
|
test("row 234: scripts/mosaic sends hosts to packages/cli, not the seat CLI", (t) => {
|
|
const root = tmp(t, "mosaic-cli-hosts-route-");
|
|
const env = { PATH: process.env.PATH, HOME: join(root, "home"), MOSAIC_CONFIG: join(root, "none.json") };
|
|
const r = spawnSync(MOSAIC, ["hosts", "purge", "web1"], { env, encoding: "utf8" });
|
|
assert.equal(r.status, 4);
|
|
assert.match(r.stderr, /^mosaic: mosaic hosts \[status\]/);
|
|
assert.doesNotMatch(r.stderr, /mosaic launch </);
|
|
});
|
|
|
|
test("row 235: hosts revoke sends agent, host and reason, and sweep the host, as the human or the tester", async () => {
|
|
const calls = [];
|
|
const x = io();
|
|
await main(["hosts", "revoke", "ops", "web1", "--reason", "left the team", "--business", "acme"], x, deps(calls));
|
|
await main(["hosts", "sweep", "web1", "--tester", "--business", "acme"], x, deps(calls));
|
|
assert.deepEqual(calls.map(({ who, verb, args }) => ({ who, verb, args })), [
|
|
{ who: "human", verb: "hosts.revoke", args: { agent: "ops", host: "web1", reason: "left the team" } },
|
|
{ who: "tester", verb: "hosts.sweep", args: { host: "web1" } },
|
|
]);
|
|
});
|
|
|
|
test("row 235 (N10): revoke without --reason, --reason on another verb, and inherited names are usage errors", async () => {
|
|
const calls = [];
|
|
for (const argv of [["hosts", "revoke", "ops", "web1"], ["hosts", "revoke", "web1", "--reason", "r"], ["hosts", "revoke", "ops", "web1", "x", "--reason", "r"],
|
|
["hosts", "sweep"], ["hosts", "sweep", "web1", "web2"], ["hosts", "check", "web1", "--reason", "r"], ["hosts", "revoke", "ops", "web1", "--reason"],
|
|
["hosts", "revoke", "ops", "web1", "--reason", "a", "--reason", "b"], ["hosts", "constructor", "web1"], ["hosts", "toString", "web1"],
|
|
["hosts", "hasOwnProperty"], ["hosts", "__proto__", "web1"]])
|
|
await assert.rejects(main([...argv], io(), deps(calls)), (e) => (assert.equal(e.exitCode, 4, argv.join(" ")), assert.match(e.message, /mosaic hosts/), true));
|
|
assert.equal(calls.length, 0);
|
|
});
|
|
|
|
test("row 235: a status line shows the tags of a revoke in progress", async () => {
|
|
const rows = [{ ...ROWS[0], revoking: ["mosaic-" + "a".repeat(32)] }];
|
|
const x = io();
|
|
const d = { ...deps([]), transport: () => async () => rows };
|
|
await main(["hosts", "--business", "acme"], x, d);
|
|
assert.equal(x.out.text, `web1\tready\[email protected]:22\tsudo\t2026-10-21T00:00:00.000Z\trevoking: mosaic-${"a".repeat(32)}\n`);
|
|
});
|