cli, seat, harness: git socket hardening and refusal tests, row 100 (#1594)

Row 93 follow-up: SIGTERM then SIGKILL for host git, rollback-failed,
a total body deadline, fetch-aborted for a reviewer gone mid-fetch,
working refs first in have, and tests for five untested refusals.
Dewey approved in 28226 and Sage in 28225; Sage's full gate passed
16:57:32-17:01:32Z. The host side is live at the next bus restart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 12:15:52 -05:00
co-authored by Claude Opus 5.5
parent c8302a0675
commit 664199f794
8 changed files with 619 additions and 62 deletions
+108 -1
View File
@@ -8,7 +8,7 @@ import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { gitEnv } from "../src/checkout.mjs";
@@ -87,6 +87,34 @@ async function refuses(w, fn, code) {
assert.doesNotMatch(w.refs(), /refs\/mosaic\//, "no temp ref is left");
}
// The .lock files under a repository's refs.
function locks(repo) {
const found = [];
const walk = (d) => {
for (const name of readdirSync(d)) {
const p = join(d, name);
if (lstatSync(p).isDirectory()) walk(p);
else if (name.endsWith(".lock")) found.push(p.slice(repo.length + 1));
}
};
walk(repo);
return found;
}
// A `git` first in PATH that runs `body` (bash) with REAL as the real git,
// and the env that puts it there.
const REAL_GIT = spawnSync("sh", ["-c", "command -v git"], { encoding: "utf8" }).stdout.trim();
function wrapGit(w, body) {
const bin = join(w.dir, "bin");
mkdirSync(bin);
writeFileSync(join(bin, "git"), `#!/bin/bash
REAL='${REAL_GIT}'
${body}
`);
chmodSync(join(bin, "git"), 0o755);
return { ...process.env, PATH: `${bin}:${process.env.PATH}` };
}
async function lands(w, fn) {
const root = fingerprint(w.root);
const r = await fn();
@@ -236,6 +264,85 @@ test("a record that throws moves the work ref back, or removes a new one", async
assert.equal(w.refs(), `refs/heads/main ${w.first}\n${REF} ${c1}`);
});
test("a work ref that can't go back refuses rollback-failed, naming the ref and the commit", async (t) => {
const w = world(t);
git(w.clone, "checkout", "-q", "-b", "feat");
const c1 = commit(w.clone, "two");
// record() fails after taking the ref's lock, so moving the ref back fails.
const lock = join(w.repo, `${REF}.lock`);
const stuck = async () => {
writeFileSync(lock, "");
throw new Error("not recorded");
};
const message = (c) => (e) => e instanceof WorkingError && e.code === "rollback-failed" && e.message.includes(`${REF} still names ${c}`) && /not recorded/.test(e.message);
// A new ref that can't be removed.
await assert.rejects(w.go(w.bundle(), { record: stuck }), message(c1));
assert.equal(w.refs(), `refs/heads/main ${w.first}\n${REF} ${c1}`);
rmSync(lock);
// A ref that can't go back to the one before.
git(w.clone, "update-ref", "refs/remotes/working/work/vikunja-1-2", c1);
const c2 = commit(w.clone, "three");
await assert.rejects(w.go(w.bundle(), { record: stuck }), message(c2));
assert.equal(w.refs(), `refs/heads/main ${w.first}\n${REF} ${c2}`);
rmSync(lock);
assert.deepEqual(locks(w.repo), []);
});
test("a root that can't be read refuses root-unreadable", async (t) => {
const w = world(t);
git(w.clone, "checkout", "-q", "-b", "feat");
commit(w.clone, "two");
await refuses(w, () => w.go(w.bundle(), { root: join(w.dir, "no-root") }), "root-unreadable");
});
test("a git past its time gets SIGTERM, leaves no ref lock, and the next push lands", async (t) => {
const w = world(t);
git(w.clone, "checkout", "-q", "-b", "feat");
const c1 = commit(w.clone, "two");
await w.go(w.bundle());
git(w.clone, "update-ref", "refs/remotes/working/work/vikunja-1-2", c1);
// The move of the work ref holds the ref's lock in a reference-transaction
// hook until the lock is gone, so the git is still holding it when its time
// is up. Only that one fetch gets the hook.
const hooks = join(w.dir, "hooks");
mkdirSync(hooks);
const lock = join(w.repo, `${REF}.lock`);
writeFileSync(join(hooks, "reference-transaction"), `#!/bin/bash\n[ "$1" = prepared ] || exit 0\nfor i in $(seq 200); do [ -e '${lock}' ] || exit 0; sleep 0.05; done\n`);
chmodSync(join(hooks, "reference-transaction"), 0o755);
const env = wrapGit(
w,
`self=0
for a in "$@"; do case "$a" in refs/mosaic/incoming/*:refs/heads/work/*) self=1;; esac; done
args=()
for a in "$@"; do
[ $self = 1 ] && [ "$a" = core.hooksPath=/dev/null ] && a='core.hooksPath=${hooks}'
args+=("$a")
done
exec "$REAL" "\${args[@]}"`,
);
commit(w.clone, "three");
const limits = { timeoutMs: 2000, graceMs: 5000 };
await assert.rejects(w.go(w.bundle(), { env, limits }), (e) => e instanceof WorkingError && e.code === "push-failed" && /killed by SIGTERM/.test(e.message));
assert.deepEqual(locks(w.repo), []);
assert.equal(w.refs(), `refs/heads/main ${w.first}\n${REF} ${c1}`);
const c3 = commit(w.clone, "four");
const r = await lands(w, () => w.go(w.bundle()));
assert.deepEqual(r, { ref: "work/vikunja-1-2", commit: c3, previous: c1 });
});
test("a git that ignores SIGTERM gets SIGKILL once the grace period is over", { timeout: 20000 }, async (t) => {
const w = world(t);
git(w.clone, "checkout", "-q", "-b", "feat");
commit(w.clone, "two");
// The fetch of main from the root never ends and ignores SIGTERM.
const env = wrapGit(
w,
`for a in "$@"; do [ "$a" = +HEAD:refs/heads/main ] && { trap '' TERM; while :; do sleep 0.05; done; }; done
exec "$REAL" "$@"`,
);
await assert.rejects(w.go(w.bundle(), { env, limits: { timeoutMs: 500, graceMs: 500 } }), (e) => e instanceof WorkingError && e.code === "root-unreadable" && /killed by SIGKILL/.test(e.message));
});
test("a working repository path that isn't a directory refuses", async (t) => {
const w = world(t);
git(w.clone, "checkout", "-q", "-b", "feat");