Built by filbert, approved by darkwing in round 1 (#1508 comment 26651). Manifest fe7da3ef, 10 paths. Suites green on an index export. Co-Authored-By: Claude Opus 5.5 <[email protected]>
603 lines
26 KiB
JavaScript
603 lines
26 KiB
JavaScript
// scripts/queue-commit.sh and the queue guard (8.12): F1, the bootstrap (F3)
|
|
// and the general cases. Every run is in a scratch repository with the
|
|
// scripts committed; nothing touches this checkout's .git.
|
|
//
|
|
// Schedules use PATH shims for git and node. Each shim calls
|
|
// <ctl>/on-git or <ctl>/on-node, when present, before and after the real
|
|
// command, and the call blocks, so an action runs at an exact point in the
|
|
// procedure. Actions run with the original PATH, so they reach real git.
|
|
import { strict as assert } from "node:assert";
|
|
import { spawn } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { test } from "node:test";
|
|
import { sleepMs } from "../src/io.mjs";
|
|
import { cli, scratchRepo, sh } from "./helpers.mjs";
|
|
|
|
const QJSON = "docs/plans/queue.json";
|
|
const QMD = "docs/plans/QUEUE.md";
|
|
const FIX = `git reset -q -- ${QJSON} ${QMD}`;
|
|
const GUARD = "mosaic queue guard: refused";
|
|
const REAL_GIT = sh("sh", ["-c", "command -v git"]).stdout.trim();
|
|
|
|
function q(s) {
|
|
return `'${String(s).replaceAll("'", "'\\''")}'`;
|
|
}
|
|
|
|
function sha256(buf) {
|
|
return createHash("sha256").update(buf).digest("hex");
|
|
}
|
|
|
|
// A repository with the scripts, the guard installed by sage and, unless
|
|
// `bootstrap` is set, genesis committed through queue-commit.sh.
|
|
function ready(t, { bootstrap = false } = {}) {
|
|
const repo = scratchRepo(t, { scripts: true });
|
|
const ctl = join(repo.base, "ctl");
|
|
const shimDir = join(repo.base, "shims");
|
|
mkdirSync(ctl);
|
|
mkdirSync(shimDir);
|
|
const origPath = repo.env.PATH;
|
|
for (const [name, real] of [["git", REAL_GIT], ["node", process.execPath]]) {
|
|
const on = join(ctl, `on-${name}`);
|
|
writeFileSync(join(shimDir, name), [
|
|
"#!/bin/sh",
|
|
`if [ -x ${q(on)} ]; then PATH=${q(origPath)} ${q(on)} before "$@" </dev/null >>${q(join(ctl, "log"))} 2>&1; fi`,
|
|
`${q(real)} "$@"; s=$?`,
|
|
`if [ -x ${q(on)} ]; then PATH=${q(origPath)} ${q(on)} after "$s" "$@" </dev/null >>${q(join(ctl, "log"))} 2>&1; fi`,
|
|
"exit $s",
|
|
"",
|
|
].join("\n"), { mode: 0o755 });
|
|
}
|
|
const r = {
|
|
...repo,
|
|
ctl,
|
|
hook: join(repo.gitDir, "hooks/pre-commit"),
|
|
// Runs queue-commit.sh through the shims.
|
|
qc(args, env = {}) {
|
|
const res = sh("bash", [join(repo.root, "scripts/queue-commit.sh"), ...args], {
|
|
cwd: repo.root, env: { ...repo.env, PATH: `${shimDir}:${origPath}`, ...env }, allowFail: true,
|
|
});
|
|
return { code: res.status, out: res.stdout, err: res.stderr };
|
|
},
|
|
// Installs an action. `body` is bash; $phase is before or after, $status
|
|
// the exit status after, $sub the git subcommand, $@ the arguments.
|
|
on(name, body) {
|
|
writeFileSync(join(ctl, `on-${name}`), [
|
|
"#!/bin/bash",
|
|
"phase=$1; shift; status=",
|
|
'if [ "$phase" = after ]; then status=$1; shift; fi',
|
|
'a=("$@"); i=0; while [ "${a[i]:-}" = -C ]; do i=$((i+2)); done; sub=${a[i]:-}',
|
|
`CTL=${q(ctl)}; R=${q(repo.root)}`,
|
|
"unset GIT_INDEX_FILE",
|
|
'once() { [ -e "$CTL/once-$1" ] && return 1; : > "$CTL/once-$1"; }',
|
|
'echo "$phase $sub" >> "$CTL/calls-$(basename "$0")"',
|
|
body,
|
|
"",
|
|
].join("\n"), { mode: 0o755 });
|
|
},
|
|
off(name) {
|
|
rmSync(join(ctl, `on-${name}`), { force: true });
|
|
},
|
|
calls(name) {
|
|
const p = join(ctl, `calls-on-${name}`);
|
|
return existsSync(p) ? readFileSync(p, "utf8").split("\n").filter(Boolean) : [];
|
|
},
|
|
ctlFile(name) {
|
|
return readFileSync(join(ctl, name), "utf8").trim();
|
|
},
|
|
head() {
|
|
return repo.g("rev-parse", "HEAD").trim();
|
|
},
|
|
blob(rev, path) {
|
|
return sh("git", ["-C", repo.root, "cat-file", "blob", `${rev}:${path}`], { env: repo.env }).stdout;
|
|
},
|
|
revAt(rev) {
|
|
return JSON.parse(r.blob(rev, QJSON)).revision;
|
|
},
|
|
};
|
|
const inst = r.qc(["--install-hook", "--by", "sage"]);
|
|
assert.equal(inst.code, 0, inst.err);
|
|
if (!bootstrap) {
|
|
genesis(r);
|
|
const c = r.qc(["--genesis", "-m", "queue genesis"]);
|
|
assert.equal(c.code, 0, c.err);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
function genesis(r) {
|
|
const g = cli(r, ["genesis", "--op", "genesis-2026-09-26", "--root", r.root, "--branch", "refactor", "--map", "agents/sage/work/queue-migration-map.md"], { by: "sage" });
|
|
assert.equal(g.code, 0, g.err);
|
|
}
|
|
|
|
let noteN = 0;
|
|
function note(r, id = 6) {
|
|
const res = cli(r, ["note", String(id), `note ${++noteN}`, "--op", `note-op-${noteN}`], { by: "darkwing" });
|
|
assert.equal(res.code, 0, res.err);
|
|
return res;
|
|
}
|
|
|
|
function stageFile(r, name, text = "source\n") {
|
|
writeFileSync(join(r.root, name), text);
|
|
r.g("add", "--", name);
|
|
}
|
|
|
|
function assertUntouched(r, head, res, why) {
|
|
assert.equal(res.code, 2, `${why}: ${res.err}`);
|
|
assert.equal(r.head(), head, why);
|
|
assert.ok(!r.calls("git").includes("before update-ref"), `${why}: update-ref ran`);
|
|
}
|
|
|
|
// --- F1 ---
|
|
|
|
test("F1: an ordinary commit after update-ref is refused until step 8; then it commits and the queue stays at C", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
r.on("git", `if [ "$phase $sub $status" = "after update-ref 0" ]; then
|
|
echo src > "$R/src.txt"; git -C "$R" add src.txt
|
|
git -C "$R" commit -q -m ordinary 2> "$CTL/commit-err"; echo $? > "$CTL/commit-rc"
|
|
fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
assert.notEqual(r.ctlFile("commit-rc"), "0");
|
|
assert.match(r.ctlFile("commit-err"), new RegExp(GUARD));
|
|
assert.match(r.ctlFile("commit-err"), new RegExp(FIX.replaceAll(".", "\\.")));
|
|
const c = r.head();
|
|
assert.equal(r.revAt(c), 1);
|
|
r.off("git");
|
|
r.g("commit", "-q", "-m", "ordinary");
|
|
assert.equal(r.g("rev-parse", "HEAD^").trim(), c);
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
assert.equal(r.blob("HEAD", "src.txt"), "src\n");
|
|
});
|
|
|
|
// A commit paused in its editor after its guard passed against H. Whether
|
|
// git holds index.lock during the editor depends on the form: git 2.55
|
|
// doesn't for plain `commit -e` and does for `commit -e -- path`. Step 8
|
|
// reconciles when the lock is free and exits 3 when it isn't; either way
|
|
// the paused commit loses at its own HEAD update.
|
|
async function pausedCommit(t, form) {
|
|
const r = ready(t);
|
|
note(r);
|
|
stageFile(r, "src.txt");
|
|
const h = r.head();
|
|
const started = join(r.ctl, "editor-started");
|
|
const go = join(r.ctl, "editor-go");
|
|
const editor = join(r.ctl, "editor.sh");
|
|
writeFileSync(editor, `#!/bin/sh\n: > ${q(started)}\nwhile [ ! -e ${q(go)} ]; do sleep 0.05; done\necho "ordinary" > "$1"\n`, { mode: 0o755 });
|
|
const child = spawn("git", ["-C", r.root, "commit", "-e", "-q", ...form], { env: { ...r.env, GIT_EDITOR: editor }, stdio: ["ignore", "pipe", "pipe"] });
|
|
let childErr = "";
|
|
child.stderr.on("data", (d) => { childErr += d; });
|
|
const exited = new Promise((resolve) => child.on("exit", resolve));
|
|
for (let i = 0; i < 200 && !existsSync(started); i++) sleepMs(50);
|
|
assert.ok(existsSync(started), "the editor never started");
|
|
const locked = existsSync(join(r.gitDir, "index.lock"));
|
|
t.diagnostic(`git commit -e${form.map((a) => ` ${a}`).join("")}: index.lock ${locked ? "held" : "free"} during the editor`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
writeFileSync(go, "");
|
|
const code = await exited;
|
|
assert.equal(res.code, locked ? 3 : 0, `index.lock ${locked ? "held" : "free"} during the editor: ${res.err}`);
|
|
if (locked) assert.match(res.err, /another git process holds \.git\/index\.lock/);
|
|
const c = r.head();
|
|
assert.equal(r.g("rev-parse", "HEAD^").trim(), h);
|
|
assert.equal(r.revAt(c), 1);
|
|
assert.notEqual(code, 0);
|
|
assert.match(childErr, new RegExp(`cannot lock ref 'HEAD': is at ${c} but expected ${h}`));
|
|
assert.equal(r.head(), c, "the old queue landed on top of C");
|
|
if (locked) r.g("reset", "-q", "--", "docs/plans/queue.json", "docs/plans/QUEUE.md");
|
|
assert.equal(r.g("diff", "--cached", "--name-only").trim(), "src.txt");
|
|
r.g("commit", "-q", "-m", "ordinary");
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
return locked;
|
|
}
|
|
|
|
test("F1: a plain `commit -e` whose guard ran before update-ref fails at its own HEAD update", async (t) => {
|
|
await pausedCommit(t, []);
|
|
});
|
|
|
|
test("F1: a `commit -e -- path` whose guard ran before update-ref fails at its own HEAD update", async (t) => {
|
|
await pausedCommit(t, ["--", "src.txt"]);
|
|
});
|
|
|
|
test("F1: step 8 with index.lock held exits 3, and ordinary commits stay refused until the printed command runs", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
r.on("git", `if [ "$phase $sub" = "before ls-files" ] && once lock; then : > "$R/.git/index.lock"; fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 3, res.err);
|
|
assert.match(res.err, /holds \.git\/index\.lock/);
|
|
assert.ok(res.err.includes(`run: ${FIX}`) || res.err.includes(`until this runs: ${FIX}`));
|
|
const c = r.head();
|
|
assert.equal(r.revAt(c), 1);
|
|
r.off("git");
|
|
unlinkSync(join(r.gitDir, "index.lock"));
|
|
stageFile(r, "src.txt");
|
|
const refused = sh("git", ["-C", r.root, "commit", "-q", "-m", "ordinary"], { env: r.env, allowFail: true });
|
|
assert.notEqual(refused.status, 0);
|
|
assert.match(refused.stderr, new RegExp(GUARD));
|
|
assert.equal(r.head(), c);
|
|
sh("sh", ["-c", FIX], { cwd: r.root, env: r.env });
|
|
r.g("commit", "-q", "-m", "ordinary");
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
assert.equal(r.g("rev-parse", "HEAD^").trim(), c);
|
|
});
|
|
|
|
test("F1: HEAD moving after the step-7 guard check and before update-ref: refused, nothing published", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "before update-ref" ] && once move; then
|
|
echo other > "$R/other.txt"; git -C "$R" add other.txt; git -C "$R" commit -q -m other
|
|
fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 1, res.err);
|
|
assert.match(res.err, new RegExp(`refs/heads/refactor moved since ${h}; nothing published; start again`));
|
|
assert.equal(r.g("rev-parse", "HEAD^").trim(), h);
|
|
assert.equal(r.revAt("HEAD"), 0);
|
|
assert.equal(r.g("log", "-1", "--format=%s").trim(), "other");
|
|
});
|
|
|
|
test("F1: H is recorded before the canary, so HEAD moving during the step-1 canary is refused at step 7", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "before hook" ] && once move; then
|
|
echo other > "$R/other.txt"; git -C "$R" add other.txt; git -C "$R" commit -q -m other
|
|
fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 2, res.err);
|
|
assert.match(res.err, new RegExp(`refused \\(step7\\): HEAD moved since ${h} was recorded \\(another commit landed\\); run queue-commit.sh again`));
|
|
assert.equal(r.g("log", "-1", "--format=%s").trim(), "other");
|
|
assert.equal(r.g("rev-parse", "HEAD^").trim(), h);
|
|
assert.equal(r.revAt("HEAD"), 0);
|
|
// A commit outside the queue files passes the step-1 canary. The step-7
|
|
// check refuses before its canary, so update-ref never runs.
|
|
assert.equal(r.calls("git").filter((c) => c === "before hook").length, 2);
|
|
assert.ok(!r.calls("git").includes("before update-ref"));
|
|
});
|
|
|
|
// Another queue commit landing after H is recorded. The canary's index
|
|
// holds H's tree and the hook diffs it with the live HEAD, so before
|
|
// 2026-10-04 the clean run failed and the script blamed the guard.
|
|
const NESTED = 'bash "$R/scripts/queue-commit.sh" -m nested >> "$CTL/nested.log" 2>&1; echo $? > "$CTL/nested-exit"';
|
|
|
|
test("F1: a queue commit landing after H is recorded: step 1 says HEAD moved, not the guard", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "after symbolic-ref" ] && once move; then ${NESTED}; fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(r.ctlFile("nested-exit"), "0", r.ctlFile("nested.log"));
|
|
assert.equal(res.code, 2, res.err);
|
|
assert.match(res.err, new RegExp(`refused \\(step1\\): HEAD moved since ${h} was recorded \\(another commit landed\\); run queue-commit.sh again`));
|
|
assert.doesNotMatch(res.err, /not running the queue guard/);
|
|
assert.equal(r.calls("git").filter((c) => c === "before hook").length, 0);
|
|
assert.equal(r.g("log", "-1", "--format=%s").trim(), "nested");
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
// Running it again, as the message says, finds the queue already committed.
|
|
r.off("git");
|
|
note(r);
|
|
const again = r.qc(["-m", "queue rev 2"]);
|
|
assert.equal(again.code, 0, again.err);
|
|
assert.equal(r.revAt("HEAD"), 2);
|
|
});
|
|
|
|
test("F1: a queue commit landing between the HEAD check and the canary: the failed clean run is reported as HEAD moved", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "before hook" ] && once move; then ${NESTED}; fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(r.ctlFile("nested-exit"), "0", r.ctlFile("nested.log"));
|
|
assert.equal(res.code, 2, res.err);
|
|
assert.match(res.err, new RegExp(`refused \\(step1\\): HEAD moved since ${h} was recorded \\(another commit landed\\); run queue-commit.sh again`));
|
|
assert.doesNotMatch(res.err, /not running the queue guard/);
|
|
// The clean run did run, and failed on the moved HEAD.
|
|
assert.equal(r.calls("git").filter((c) => c === "before hook").length, 1);
|
|
assert.ok(r.calls("git").includes("after hook"));
|
|
assert.equal(r.g("log", "-1", "--format=%s").trim(), "nested");
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
});
|
|
|
|
test("F1: a shared-index change during the procedure is not committed", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
r.on("git", `if [ "$phase $sub" = "before commit-tree" ]; then echo late > "$R/late.txt"; git -C "$R" add late.txt; fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
assert.deepEqual(r.g("diff-tree", "-r", "--name-only", "HEAD^", "HEAD").trim().split("\n"), [QMD, QJSON]);
|
|
assert.equal(r.g("diff", "--cached", "--name-only").trim(), "late.txt");
|
|
});
|
|
|
|
test("F1: a queue path staged after update-ref: step 8 stops and touches nothing", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
r.on("git", `if [ "$phase $sub $status" = "after update-ref 0" ]; then
|
|
git -C "$R" add docs/plans/queue.json; sha256sum "$R/.git/index" | cut -d' ' -f1 > "$CTL/index-sha"
|
|
fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 3, res.err);
|
|
assert.match(res.err, /someone staged a queue path; the index was not touched/);
|
|
assert.equal(sha256(readFileSync(join(r.gitDir, "index"))), r.ctlFile("index-sha"));
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
});
|
|
|
|
test("F1: a missing or a different hook refuses", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
const bytes = readFileSync(r.hook);
|
|
r.on("git", "");
|
|
unlinkSync(r.hook);
|
|
assertUntouched(r, h, r.qc(["-m", "x"]), "missing");
|
|
assert.match(r.qc(["-m", "x"]).err, /queue guard is not installed/);
|
|
writeFileSync(r.hook, `${bytes}\n# edited\n`, { mode: 0o755 });
|
|
const diff = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, diff, "different");
|
|
assert.match(diff.err, /differs from scripts\/git-hooks\/pre-commit/);
|
|
const inst = r.qc(["--install-hook", "--by", "sage"]);
|
|
assert.equal(inst.code, 2);
|
|
assert.match(inst.err, /a different pre-commit hook exists/);
|
|
});
|
|
|
|
test("F1: same bytes without the exec bit, a symlinked hook, and core.hooksPath in the local or global scope each refuse before update-ref", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
const bytes = readFileSync(r.hook);
|
|
r.on("git", "");
|
|
chmodSync(r.hook, 0o644);
|
|
let res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "no exec bit");
|
|
assert.match(res.err, /not executable/);
|
|
chmodSync(r.hook, 0o755);
|
|
const target = join(r.base, "hook-copy");
|
|
writeFileSync(target, bytes, { mode: 0o755 });
|
|
unlinkSync(r.hook);
|
|
symlinkSync(target, r.hook);
|
|
res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "symlink");
|
|
assert.match(res.err, /is a symlink/);
|
|
unlinkSync(r.hook);
|
|
writeFileSync(r.hook, bytes, { mode: 0o755 });
|
|
r.g("config", "core.hooksPath", ".githooks");
|
|
res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "local hooksPath");
|
|
assert.match(res.err, /core\.hooksPath is set \(local/);
|
|
r.g("config", "--unset", "core.hooksPath");
|
|
r.g("config", "--global", "core.hooksPath", "/nonexistent-hooks");
|
|
res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "global hooksPath");
|
|
assert.match(res.err, /core\.hooksPath is set \(global/);
|
|
r.g("config", "--global", "--unset", "core.hooksPath");
|
|
res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
});
|
|
|
|
test("F1: the canary refuses a hook that git would not run", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "before hook" ] && once chmod; then chmod 0644 "$R/.git/hooks/pre-commit"; fi`);
|
|
const res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "canary");
|
|
assert.match(res.err, /refused \(step1\): the canary's clean run failed/);
|
|
});
|
|
|
|
test("F1: the guard deactivated after step 1 is refused at the step-7 recheck", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "after commit-tree" ]; then chmod 0644 "$R/.git/hooks/pre-commit"; fi`);
|
|
const res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "step 7");
|
|
assert.match(res.err, /refused \(step7\): \.git\/hooks\/pre-commit is not executable/);
|
|
assert.ok(r.calls("git").includes("after commit-tree"));
|
|
});
|
|
|
|
// --- bootstrap (F3) ---
|
|
|
|
test("bootstrap: implementation-only HEAD, the guard, genesis, the --genesis commit, then an extending commit", (t) => {
|
|
const r = ready(t, { bootstrap: true });
|
|
const impl = r.head();
|
|
assert.equal(sh("git", ["-C", r.root, "cat-file", "-e", `HEAD:${QJSON}`], { env: r.env, allowFail: true }).status, 128);
|
|
assert.equal(readFileSync(r.hook, "utf8"), r.blob("HEAD", "scripts/git-hooks/pre-commit"));
|
|
genesis(r);
|
|
const g = r.qc(["--genesis", "-m", "queue genesis"]);
|
|
assert.equal(g.code, 0, g.err);
|
|
assert.match(g.err, /ok verify --snapshot rev 0: pair valid, view current, genesis alone/);
|
|
assert.equal(r.g("rev-parse", "HEAD^").trim(), impl);
|
|
assert.equal(r.revAt("HEAD"), 0);
|
|
assert.equal(r.g("status", "--porcelain").trim(), "");
|
|
note(r);
|
|
const c = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(c.code, 0, c.err);
|
|
assert.match(c.err, /extends the base/);
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
assert.equal(r.g("status", "--porcelain").trim(), "");
|
|
});
|
|
|
|
test("bootstrap: --genesis with a base present, no base without --genesis, an op before the first commit, a changed map, another branch", (t) => {
|
|
const r = ready(t, { bootstrap: true });
|
|
let res = r.qc(["-m", "x"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /HEAD has no docs\/plans\/queue\.json; the first queue commit needs --genesis/);
|
|
genesis(r);
|
|
res = r.qc(["-m", "x"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /needs --genesis/);
|
|
const op = cli(r, ["note", "6", "early", "--op", "note-early-1"], { by: "darkwing" });
|
|
assert.equal(op.code, 2);
|
|
assert.match(op.err, /genesis not committed/);
|
|
// The map changed in HEAD after genesis read it.
|
|
const mapPath = join(r.root, "agents/sage/work/queue-migration-map.md");
|
|
const mapText = readFileSync(mapPath, "utf8");
|
|
writeFileSync(mapPath, `${mapText}\nEdited.\n`);
|
|
r.g("add", "agents/sage/work/queue-migration-map.md");
|
|
r.g("commit", "-q", "-m", "map edit");
|
|
res = r.qc(["--genesis", "-m", "x"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /genesis read map blob [0-9a-f]{40}, but HEAD's agents\/sage\/work\/queue-migration-map\.md is/);
|
|
writeFileSync(mapPath, mapText);
|
|
r.g("add", "agents/sage/work/queue-migration-map.md");
|
|
r.g("commit", "-q", "-m", "map restored");
|
|
r.g("checkout", "-q", "-b", "other");
|
|
res = r.qc(["--genesis", "-m", "x"]);
|
|
assert.equal(res.code, 2);
|
|
r.g("checkout", "-q", "refactor");
|
|
res = r.qc(["--genesis", "-m", "queue genesis"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
res = r.qc(["--genesis", "-m", "x"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /--genesis, but HEAD already has docs\/plans\/queue\.json/);
|
|
});
|
|
|
|
test("bootstrap: the archived tests and validator run outside any repository", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
r.on("node", `if [ "$phase" = before ] && { [ "$1" = --test ] || [ "$3" = --snapshot ]; }; then
|
|
{ echo "$1|$(pwd -P)|\${GIT_CEILING_DIRECTORIES:-}|$(git rev-parse --show-toplevel >/dev/null 2>&1; echo $?)"; } >> "$CTL/node-where"
|
|
fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
const lines = r.ctlFile("node-where").split("\n");
|
|
assert.equal(lines.length, 2);
|
|
for (const line of lines) {
|
|
const [first, cwd, ceiling, rc] = line.split("|");
|
|
assert.ok(first === "--test" || first === "packages/queue/src/cli.mjs", first);
|
|
assert.ok(!cwd.startsWith(`${r.root}/`) && cwd !== r.root, cwd);
|
|
assert.ok(cwd.startsWith(`${ceiling}/`), `${cwd} under ${ceiling}`);
|
|
assert.notEqual(rc, "0", "a repository was found around the archive");
|
|
}
|
|
});
|
|
|
|
// --- general ---
|
|
|
|
test("general: an unrelated staged file stays staged, and the committed blobs are the snapshot bytes", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
stageFile(r, "src.txt");
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
const m = /snapshot rev 1: queue\.json ([0-9a-f]{64}), QUEUE\.md ([0-9a-f]{64})/.exec(res.out);
|
|
assert.ok(m, res.out);
|
|
assert.equal(sha256(r.blob("HEAD", QJSON)), m[1]);
|
|
assert.equal(sha256(r.blob("HEAD", QMD)), m[2]);
|
|
assert.equal(r.blob("HEAD", QJSON), readFileSync(join(r.root, QJSON), "utf8"));
|
|
assert.equal(r.g("diff", "--cached", "--name-only").trim(), "src.txt");
|
|
assert.equal(sh("git", ["-C", r.root, "cat-file", "-e", "HEAD:src.txt"], { env: r.env, allowFail: true }).status, 128);
|
|
});
|
|
|
|
test("general: a queue write after the snapshot is not committed", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const cliPath = join(r.root, "packages/queue/src/cli.mjs");
|
|
r.on("node", `if [ "$phase $status $2" = "after 0 snapshot" ] && once write; then
|
|
MOSAIC_AGENT_NAME=darkwing node ${q(cliPath)} note 6 late --op note-late-1 > "$CTL/late-out" 2>&1
|
|
fi`);
|
|
const res = r.qc(["-m", "queue rev 1"]);
|
|
assert.equal(res.code, 0, res.err);
|
|
assert.match(r.ctlFile("late-out"), /ok note-late-1 rev 2/);
|
|
assert.equal(r.revAt("HEAD"), 1);
|
|
assert.equal(JSON.parse(readFileSync(join(r.root, QJSON), "utf8")).revision, 2);
|
|
r.off("node");
|
|
const again = r.qc(["-m", "queue rev 2"]);
|
|
assert.equal(again.code, 0, again.err);
|
|
assert.equal(r.revAt("HEAD"), 2);
|
|
});
|
|
|
|
test("general: a snapshot whose log does not extend the base refuses", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
// A bypass commit of rev 1, then the working files rolled back to rev 0
|
|
// and the loss accepted: the working log forks from HEAD's.
|
|
r.g("add", QJSON, QMD);
|
|
r.g("commit", "-q", "--no-verify", "-m", "bypass");
|
|
writeFileSync(join(r.root, QJSON), r.blob("HEAD^", QJSON));
|
|
writeFileSync(join(r.root, QMD), r.blob("HEAD^", QMD));
|
|
const acc = cli(r, ["accept-history", "--op", "accept-1", "--reason", "test rollback", "--yes"], { by: "sage" });
|
|
assert.equal(acc.code, 0, acc.err);
|
|
const h = r.head();
|
|
r.on("git", "");
|
|
const res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "fork");
|
|
assert.match(res.err, /the snapshot's log does not extend the base's \(base rev 1, snapshot rev 1\)/);
|
|
});
|
|
|
|
test("general: install-hook privilege, repair of a same-bytes hook, and its refusals", (t) => {
|
|
const r = ready(t, { bootstrap: true });
|
|
let res = r.qc(["--install-hook"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /no actor/);
|
|
res = r.qc(["--install-hook"], { MOSAIC_AGENT_NAME: "darkwing" });
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /privileged \(jason or sage\), not darkwing/);
|
|
chmodSync(r.hook, 0o644);
|
|
res = r.qc(["--install-hook"], { MOSAIC_AGENT_NAME: "jason" });
|
|
assert.equal(res.code, 0, res.err);
|
|
assert.match(res.out, /the same bytes were already there/);
|
|
assert.equal(sh("stat", ["-c", "%a", r.hook]).stdout.trim(), "755");
|
|
const bytes = readFileSync(r.hook);
|
|
unlinkSync(r.hook);
|
|
const target = join(r.base, "hook-copy");
|
|
writeFileSync(target, bytes, { mode: 0o755 });
|
|
symlinkSync(target, r.hook);
|
|
res = r.qc(["--install-hook", "--by", "sage"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /is a symlink/);
|
|
unlinkSync(r.hook);
|
|
r.g("config", "core.hooksPath", ".githooks");
|
|
res = r.qc(["--install-hook", "--by", "sage"]);
|
|
assert.equal(res.code, 2);
|
|
assert.match(res.err, /core\.hooksPath is set/);
|
|
assert.ok(!existsSync(r.hook));
|
|
});
|
|
|
|
test("general: environment overrides, a linked worktree and usage", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
for (const k of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"]) {
|
|
const res = r.qc(["-m", "x"], { [k]: join(r.root, ".git") });
|
|
assert.equal(res.code, 2, k);
|
|
assert.match(res.err, new RegExp(`${k} is set`));
|
|
}
|
|
const wt = join(r.base, "wt");
|
|
r.g("worktree", "add", "-q", "-b", "wt", wt);
|
|
const w = sh("bash", [join(wt, "scripts/queue-commit.sh"), "-m", "x"], { cwd: wt, env: r.env, allowFail: true });
|
|
assert.equal(w.status, 2);
|
|
assert.match(w.stderr, /linked worktree/);
|
|
for (const args of [[], ["-m", ""], ["--by", "sage", "-m", "x"], ["--install-hook", "-m", "x"], ["--bogus"], ["-m"]]) {
|
|
assert.equal(r.qc(args).code, 4, JSON.stringify(args));
|
|
}
|
|
assert.equal(r.head(), h);
|
|
});
|
|
|
|
test("general: a queue path staged before the run refuses at step 1", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.g("add", QMD);
|
|
r.on("git", "");
|
|
const res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "staged");
|
|
assert.match(res.err, /the shared index has staged changes to docs\/plans\/queue\.json or docs\/plans\/QUEUE\.md/);
|
|
assert.ok(res.err.includes(FIX));
|
|
assert.ok(!r.calls("git").includes("before commit-tree"));
|
|
});
|
|
|
|
test("general: HEAD's queue tests failing in the archive refuse", (t) => {
|
|
const r = ready(t);
|
|
writeFileSync(join(r.root, "packages/queue/tests/fail.test.mjs"), 'import { test } from "node:test";\ntest("fails", () => { throw new Error("archived failure"); });\n');
|
|
r.g("add", "packages/queue/tests/fail.test.mjs");
|
|
r.g("commit", "-q", "-m", "failing test");
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", "");
|
|
const res = r.qc(["-m", "x"]);
|
|
assertUntouched(r, h, res, "archive tests");
|
|
assert.match(res.err, /archived failure/);
|
|
assert.match(res.err, /refused: HEAD's queue tests failed in the archive/);
|
|
});
|