packages/queue, scripts/queue-commit.sh, scripts/git-hooks and scripts/test-queue.sh, plus docs/plans/BRIEF-TEMPLATE.md. There is no queue.json yet, so verify skips until the genesis commit after A2. Darkwing built it, and Filbert reviewed R0 (6933b885, changes requested) and r1 (e464be6c, approved). The 20 files match manifest 85a8a453. The nine suites passed on an index export, including the new queue suite. test-queue.sh joins the suite list in AGENTS.md. Lead decisions 20, 23 and 26. Co-Authored-By: Claude Opus 5.5 <[email protected]>
558 lines
24 KiB
JavaScript
558 lines
24 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 commit-tree and before update-ref: refused, nothing published", (t) => {
|
|
const r = ready(t);
|
|
note(r);
|
|
const h = r.head();
|
|
r.on("git", `if [ "$phase $sub" = "after commit-tree" ] && 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 canary makes update-ref fail", (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, 1, res.err);
|
|
assert.match(res.err, new RegExp(`moved since ${h}`));
|
|
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);
|
|
// Both guard checks ran in full (two canary runs each), so only update-ref caught the move.
|
|
assert.equal(r.calls("git").filter((c) => c === "before hook").length, 4);
|
|
});
|
|
|
|
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/);
|
|
});
|