Files
jason.woltjeandClaude Opus 5.5 f1ee9a9895 bus, business: agents land approved merges into the business working repository (row 137, #1631)
Row 137 round 4, Darkwing's candidate (agents/darkwing/work/queue-137/r4/,
build.patch 0cabe175, manifest 548ee477, 31 paths, base 63c82f1c4).
Filbert approved round 4 (comment 28809) and Dewey approved it (28816)
after round 3 changes (28801, 28805). Darkwing's round 4 ran 9/9 mutant
kills, bus 317, seat 150, land-work 86.

Round 5 is a rebase only (agents/sage/work/queue-137-r5/, build.patch
d6cbaaa8, manifest f73598fe). Row 150 changed docs/TOOLS.md, broker.mjs
and cli/README.md under it. The round 4 patch applied unchanged onto
435d1c616, and with index and @@ lines removed the two patches are
byte-identical. Filbert checked the interdiff (comment 28818).

A landing moves refs/heads/main in <dataRoot>/working/<business>/
<project>.git, never the project root or canonical refactor. The seed now
fetches the root as refs/heads/root and fast-forwards main only when it
is an ancestor, so a landed main survives the next push. Only a
cross-role merge decision lands. Its squash names the real channel:
"via the CLI" for the human, and "as its arbiter, via the broker" for an
arbiter's decision.resolve. Every other class refuses not-authorized
before the human check.

This carries row 139's two items. REQ-LAND-1: a gated merge decision
never lands. REQ-DEC-2: the host land op accepts an arbiter-resolved
merge. land-work's read.mjs stays human-only, and land-work.sh still
accepts only a human CLI resolution.

Sage's landing gate ran on 435d1c616 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 20:51:00Z to 20:55:34Z,
network sage-r137_default removed. Cherry-picked onto 5d98fb194, which
added only a docs/SESSIONS.md line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 15:56:00 -05:00

208 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// What a landing checks and runs (src/landing.mjs, row 137, #1631): the
// root-file rule, the mosaic-stack check, the gate's environment, its steps
// and its runs, and the CLI land-work.sh calls. The gates here run in fake
// trees with small packages and scripts; none runs this repository's suites.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { gitEnv } from "../src/checkout.mjs";
import { gateEnv, gateSteps, isMosaicStack, MOSAIC_ROOT_COMMIT, rootHits, runGate } from "../src/landing.mjs";
const REPO = fileURLToPath(new URL("../../..", import.meta.url));
const CLI = join(REPO, "packages", "seat", "src", "landing.mjs");
function tmp(t) {
const dir = mkdtempSync(join(tmpdir(), "mosaic-landing-"));
t.after(() => rmSync(dir, { recursive: true, force: true }));
return dir;
}
function git(cwd, ...args) {
const r = spawnSync("git", ["-c", "user.name=t", "-c", "[email protected]", ...args], { cwd, encoding: "utf8", env: gitEnv() });
assert.equal(r.status, 0, `git ${args.join(" ")}: ${r.stderr}`);
return r.stdout.trim();
}
function write(path, text) {
mkdirSync(join(path, ".."), { recursive: true });
writeFileSync(path, text);
}
// A test file that passes, or fails when `pass` is false.
const nodeTest = (pass) => `import { test } from "node:test";\nimport assert from "node:assert/strict";\ntest("t", () => assert.ok(${pass}));\n`;
// A tree with: packages a and c with tests, b without, queue (skipped);
// scripts test-one.sh and test-two.sh, and other.sh, which isn't a step.
function tree(t, { two = "echo two\n" } = {}) {
const w = tmp(t);
write(join(w, "packages", "c", "tests", "c.test.mjs"), nodeTest(true));
write(join(w, "packages", "a", "tests", "a.test.mjs"), nodeTest(true));
write(join(w, "packages", "b", "src", "b.mjs"), "export {};\n");
write(join(w, "packages", "queue", "tests", "q.test.mjs"), nodeTest(false));
write(join(w, "scripts", "test-two.sh"), two);
write(join(w, "scripts", "test-one.sh"), 'printf "%s\\n" "${GATE_SEEN-unset}" > "$PWD/one.env"\n');
write(join(w, "scripts", "other.sh"), "exit 9\n");
const logs = join(w, "logs");
mkdirSync(logs);
return { w, logs };
}
// The test runner's own context would turn an inner `node --test` into its child.
const env = (extra = {}) => ({ ...gateEnv(), ...extra });
test("rootHits: a top-level file, a new top-level directory, src/ and v1/ are the root; paths under known directories aren't", () => {
const top = ["docs", "packages", "scripts", "src", "v1", "README.md"];
assert.deepEqual(rootHits(["packages/a/x.mjs", "docs/y.md", "NEW.md", "newdir/z", "src/a", "v1/b", "README.md"], top), ["NEW.md", "newdir/z", "src/a", "v1/b", "README.md"]);
assert.deepEqual(rootHits([], top), []);
});
test("gateEnv keeps the named variables that are strings and drops everything else", () => {
const kept = gateEnv({ PATH: "/bin", HOME: "/h", TMPDIR: "/t", DOCKER_HOST: "unix:///d", MOSAIC_CONFIG: "/c", ANTHROPIC_API_KEY: "k", NODE_TEST_CONTEXT: "child-v8", TZ: undefined });
assert.deepEqual(kept, { PATH: "/bin", HOME: "/h", TMPDIR: "/t", DOCKER_HOST: "unix:///d" });
});
test("isMosaicStack: a checkout whose HEAD descends from the root commit and has node_modules; nothing else", async (t) => {
const objects = join(git(REPO, "rev-parse", "--path-format=absolute", "--git-common-dir"), "objects");
const dir = tmp(t);
const ours = join(dir, "ours");
git(dir, "init", "-q", ours);
// Borrow this repository's objects rather than copy its history.
writeFileSync(join(ours, ".git", "objects", "info", "alternates"), `${objects}\n`);
const rootTree = git(ours, "rev-parse", `${MOSAIC_ROOT_COMMIT}^{tree}`);
const child = git(ours, "commit-tree", rootTree, "-p", MOSAIC_ROOT_COMMIT, "-m", "child");
git(ours, "update-ref", "refs/heads/main", child);
git(ours, "symbolic-ref", "HEAD", "refs/heads/main");
assert.equal(await isMosaicStack(ours), false, "no node_modules");
writeFileSync(join(ours, "node_modules"), "");
assert.equal(await isMosaicStack(ours), false, "node_modules is a file");
rmSync(join(ours, "node_modules"));
mkdirSync(join(ours, "node_modules"));
assert.equal(await isMosaicStack(ours), true);
const other = join(dir, "other");
git(dir, "init", "-q", other);
writeFileSync(join(other, "README"), "x\n");
git(other, "add", "README");
git(other, "commit", "-q", "-m", "x");
mkdirSync(join(other, "node_modules"));
assert.equal(await isMosaicStack(other), false, "an unrelated history");
const plain = join(dir, "plain");
mkdirSync(join(plain, "node_modules"), { recursive: true });
assert.equal(await isMosaicStack(plain), false, "not a repository");
assert.equal(await isMosaicStack(join(dir, "missing")), false);
});
test("gateSteps: each package with tests but queue in name order, then each scripts/test-*.sh", (t) => {
const { w } = tree(t);
assert.deepEqual(gateSteps(w), [
{ name: "pkg-a", dir: join("packages", "a"), command: "node --test tests/*.test.mjs" },
{ name: "pkg-c", dir: join("packages", "c"), command: "node --test tests/*.test.mjs" },
{ name: "script-one", dir: ".", command: "bash scripts/test-one.sh" },
{ name: "script-two", dir: ".", command: "bash scripts/test-two.sh" },
]);
assert.deepEqual(gateSteps(tmp(t)), []);
});
test("runGate: every step passes, each with its own log and a summary line, under the environment it was given", async (t) => {
const { w, logs } = tree(t);
const lines = [];
const r = await runGate({ worktree: w, logs, env: env({ GATE_SEEN: "given" }), report: (l) => lines.push(l) });
assert.deepEqual(r, { ok: true });
assert.deepEqual(lines.map((l) => l.split(" ")[0]), ["pkg-a", "pkg-c", "script-one", "script-two", "GATE_PASSED"]);
assert.match(lines[0], /^pkg-a rc=0 ℹ pass 1 ℹ fail 0 $/);
assert.equal(lines[2], "script-one rc=0 ");
assert.equal(readFileSync(join(logs, "gate.log"), "utf8"), lines.map((l) => `${l}\n`).join(""));
for (const s of ["pkg-a", "pkg-c", "script-one", "script-two"]) assert.ok(existsSync(join(logs, `${s}.log`)), s);
assert.equal(readFileSync(join(logs, "script-two.log"), "utf8"), "two\n");
assert.equal(readFileSync(join(w, "one.env"), "utf8"), "given\n");
});
test("runGate: the first failing step stops the gate and names its log", async (t) => {
const { w, logs } = tree(t, { two: "echo broke\nexit 3\n" });
// test-z.sh sorts after test-two.sh, so only a stop keeps it from running.
write(join(w, "scripts", "test-z.sh"), "touch ran-z\n");
const r = await runGate({ worktree: w, logs, env: env() });
assert.deepEqual(r, { ok: false, step: "script-two", log: join(logs, "script-two.log") });
assert.match(readFileSync(join(logs, "gate.log"), "utf8"), /script-two rc=3 \n$/);
assert.equal(existsSync(join(w, "ran-z")), false);
// A failing node suite fails its package's step.
const f = tree(t);
write(join(f.w, "packages", "a", "tests", "a.test.mjs"), nodeTest(false));
const p = await runGate({ worktree: f.w, logs: f.logs, env: env() });
assert.deepEqual(p, { ok: false, step: "pkg-a", log: join(f.logs, "pkg-a.log") });
assert.match(readFileSync(join(f.logs, "gate.log"), "utf8"), /^pkg-a rc=1 ℹ pass 0 ℹ fail 1 \n$/);
});
test("runGate: an abort stops the running step and everything in its group, and runs nothing after it", async (t) => {
const { w, logs } = tree(t, { two: 'sleep 300 &\necho $! > "$PWD/sleep.pid"\nwait\n' });
write(join(w, "scripts", "test-z.sh"), "touch ran-z\n");
const ac = new AbortController();
const pid = join(w, "sleep.pid");
const gate = runGate({ worktree: w, logs, env: env(), signal: ac.signal });
while (!existsSync(pid) || !readFileSync(pid, "utf8").trim()) await new Promise((r) => setTimeout(r, 20));
ac.abort();
assert.deepEqual(await gate, { ok: false, aborted: true });
// The orphaned sleep may stay a zombie for a moment before init reaps it.
const sleeper = Number(readFileSync(pid, "utf8"));
const gone = () => {
try {
return readFileSync(`/proc/${sleeper}/stat`, "utf8").split(") ")[1][0] === "Z";
} catch {
return true;
}
};
const until = Date.now() + 5000;
while (!gone() && Date.now() < until) await new Promise((r) => setTimeout(r, 20));
assert.ok(gone(), `sleep ${sleeper} still runs`);
assert.equal(existsSync(join(w, "ran-z")), false);
assert.doesNotMatch(readFileSync(join(logs, "gate.log"), "utf8"), /script-two|GATE_PASSED/);
// A signal that has already aborted runs nothing.
const f = tree(t);
const done = new AbortController();
done.abort();
assert.deepEqual(await runGate({ worktree: f.w, logs: f.logs, env: env(), signal: done.signal }), { ok: false, aborted: true });
assert.equal(existsSync(join(f.logs, "gate.log")), false);
});
// The CLI land-work.sh calls.
function cli(...args) {
return spawnSync(process.execPath, [CLI, ...args], { encoding: "utf8", env: env() });
}
test("the CLI: root-file prints the hits and exits 2, or exits 0; gate exits 0, or 2 with the step and its log; anything else is usage", (t) => {
const dir = tmp(t);
writeFileSync(join(dir, "top"), ["docs", "packages", ""].join("\0"));
writeFileSync(join(dir, "bad"), ["docs/a.md", "NEW.md", "src/x", ""].join("\0"));
writeFileSync(join(dir, "good"), ["docs/a.md", "packages/p/b.mjs"].join("\0"));
let r = cli("root-file", join(dir, "top"), join(dir, "bad"));
assert.equal(r.status, 2);
assert.equal(r.stdout, "NEW.md\nsrc/x\n");
r = cli("root-file", join(dir, "top"), join(dir, "good"));
assert.equal(r.status, 0);
assert.equal(r.stdout, "");
const pass = tree(t);
r = cli("gate", pass.w, pass.logs);
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout, "");
assert.match(r.stderr, /GATE_PASSED\n$/);
const fail = tree(t, { two: "exit 5\n" });
r = cli("gate", fail.w, fail.logs);
assert.equal(r.status, 2);
assert.equal(r.stdout, `script-two\t${join(fail.logs, "script-two.log")}\n`);
for (const args of [[], ["gate", pass.w], ["root-file", "x"], ["land"]]) {
r = cli(...args);
assert.equal(r.status, 4, args.join(" "));
assert.match(r.stderr, /^usage: landing\.mjs /);
}
});