Row 137 round 4, Darkwing's candidate (agents/darkwing/work/queue-137/r4/, build.patch 0cabe175, manifest 548ee477, 31 paths, base63c82f1c4). 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 onto435d1c616, 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 on435d1c616plus 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 onto5d98fb194, which added only a docs/SESSIONS.md line. Co-Authored-By: Claude Opus 5.5 <[email protected]>
208 lines
10 KiB
JavaScript
208 lines
10 KiB
JavaScript
// 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 /);
|
||
}
|
||
});
|