Files
charleswiltgen__axiom/scripts/staleness.test.ts
T
Charles Wiltgen 876d863a29 build: gate the shipped Go binaries against their source
The binary in bin/ is what ships — the plugin, the MCP bundle and the
Codex and Cursor variants all carry it, never the Go source — and
nothing compared the two. A Go edit whose binary was never rebuilt
shipped the old tool with every check green: the args.go parity check
compares copies, the MCP coverage check confirms presence, and the Go
test step reads the source.

New static check, so it runs on `npm test` and on every commit through
the pre-commit hook. It leads with content — a compiled source changed
while the binary's bytes still match HEAD means the rebuild never
happened, which a `touch` on the binary cannot fake — and falls back to
the existing mtime-plus-dirtiness rule for a binary rebuilt once and
then left behind by a further edit. Source staged without the rebuilt
binary is its own failure, because the working tree can look right while
the commit ships the old binary. Deletions count too: a removed file
cannot appear in a directory walk.

It excludes what `go build` excludes — `_test.go`, `testdata/`,
underscore- and dot-prefixed files, other platforms' GOOS suffixes — so
a flagged binary is always one a rebuild fixes, and it exempts library
modules that ship no binary at all. The walk is guarded: a dangling
symlink used to throw out of Phase 1, skipping every later check and
reporting a crash in the same words as a finding.

`git status` now lists untracked files individually, so a brand-new
package directory is visible to this check and to the bundle and Codex
staleness checks, which a collapsed directory entry hid from all three.

Also ignore xclog's build artifacts, which the rebuild instruction left
untracked in the tree.
2026-09-19 15:50:51 -07:00

350 lines
12 KiB
TypeScript

/**
* Tests for scripts/staleness.ts.
*
* Run via `node --test scripts/staleness.test.ts` (Node 24 native, no extra
* deps). Wired into npm `test:unit` via the scripts/*.test.ts glob.
*
* All three exported functions are pure — the caller (pre-deploy.ts) does the fs
* walk and the single `git status` call, then passes strings/arrays in.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { parsePorcelain, resolveGoBinaryStaleness, resolveStaleness } from "./staleness.ts";
describe("parsePorcelain", () => {
it("returns an empty set for clean output", () => {
assert.deepEqual(parsePorcelain(""), new Set());
});
it("extracts a modified path", () => {
assert.deepEqual(
parsePorcelain(" M scripts/pre-deploy.ts"),
new Set(["scripts/pre-deploy.ts"]),
);
});
it("extracts an untracked path", () => {
assert.deepEqual(
parsePorcelain("?? scripts/new.ts"),
new Set(["scripts/new.ts"]),
);
});
it("takes the destination path of a rename", () => {
assert.deepEqual(
parsePorcelain("R old/a.md -> new/b.md"),
new Set(["new/b.md"]),
);
});
it("unquotes paths git quotes for special chars", () => {
assert.deepEqual(
parsePorcelain('?? "weird name.md"'),
new Set(["weird name.md"]),
);
});
it("keeps a literal non-ASCII path (caller forces core.quotepath=false)", () => {
assert.deepEqual(parsePorcelain(" M café.md"), new Set(["café.md"]));
});
it("parses multiple lines and ignores blanks", () => {
const out = " M a.md\n?? b.md\n\nMM c.md\n";
assert.deepEqual(parsePorcelain(out), new Set(["a.md", "b.md", "c.md"]));
});
});
describe("resolveStaleness", () => {
it("is not stale when nothing is newer than the artifact", () => {
const v = resolveStaleness({
newerFiles: [],
dirtyFiles: [],
gitAvailable: true,
});
assert.equal(v.stale, false);
});
it("is not stale when newer files are all git-clean (mtime skew)", () => {
const v = resolveStaleness({
newerFiles: ["skills/a/SKILL.md", "skills/b/SKILL.md"],
dirtyFiles: [],
gitAvailable: true,
});
assert.equal(v.stale, false);
assert.match(v.reason, /content matches HEAD|git-clean|mtime/i);
});
it("is stale when at least one newer file is dirty/untracked", () => {
const v = resolveStaleness({
newerFiles: ["skills/a/SKILL.md", "skills/b/SKILL.md"],
dirtyFiles: ["skills/b/SKILL.md"],
gitAvailable: true,
});
assert.equal(v.stale, true);
assert.match(v.reason, /1 source file/);
});
it("falls back to stale (conservative) when git is unavailable", () => {
const v = resolveStaleness({
newerFiles: ["skills/a/SKILL.md"],
dirtyFiles: [],
gitAvailable: false,
});
assert.equal(v.stale, true);
assert.match(v.reason, /git unavailable/i);
});
});
describe("resolveGoBinaryStaleness", () => {
const dirty = (...p: string[]) => new Set(p);
it("reports a tool stale when a changed .go source is newer than its binary", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 100,
binaryDirty: true, // rebuilt once, then left behind by a further edit
sources: [
{ path: "tools/xcui/sim.go", mtimeMs: 200 },
{ path: "tools/xcui/main.go", mtimeMs: 50 },
],
}],
dirty("tools/xcui/sim.go"),
true,
);
assert.equal(verdicts.length, 1);
assert.equal(verdicts[0].state, "stale");
assert.match(verdicts[0].reason, /1 source file\(s\) changed/);
});
it("ignores _test.go files — go build does not compile them into the binary", () => {
// A test-only edit leaves the shipped binary correct. Flagging it would train
// the reader to rebuild (and re-commit a 6 MB binary) for nothing.
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 100,
binaryDirty: false,
sources: [{ path: "tools/xcui/sim_test.go", mtimeMs: 900 }],
}],
dirty("tools/xcui/sim_test.go"),
true,
);
assert.equal(verdicts[0].state, "ok");
});
it("treats go.mod and go.sum as compiled inputs — a dependency change rebuilds the binary", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcsym",
binaryMtimeMs: 100,
binaryDirty: true,
sources: [{ path: "tools/xcsym/go.mod", mtimeMs: 900 }, { path: "tools/xcsym/go.sum", mtimeMs: 900 }],
}],
dirty("tools/xcsym/go.mod"),
true,
);
assert.equal(verdicts[0].state, "stale");
});
it("is not stale when mtimes are newer but every file matches HEAD (fresh clone)", () => {
// A clone gives every file the checkout time, so mtime alone would fail a
// tree nobody has edited.
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 100,
binaryDirty: false,
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 900 }],
}],
new Set<string>(),
true,
);
assert.equal(verdicts[0].state, "ok");
assert.match(verdicts[0].reason, /mtime skew/);
});
it("is stale without git, where content cannot be confirmed", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 100,
binaryDirty: false,
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 900 }],
}],
new Set<string>(),
false,
);
assert.equal(verdicts[0].state, "stale");
});
it("reports a module with no shipped binary — the tool cannot reach users", () => {
const verdicts = resolveGoBinaryStaleness(
[{ tool: "xcnew", binaryMtimeMs: null, binaryDirty: false, sources: [{ path: "tools/xcnew/main.go", mtimeMs: 10 }] }],
new Set<string>(),
true,
);
assert.equal(verdicts[0].state, "missing-binary");
});
it("is stale when source is dirty but the committed binary is not — the `touch` bypass", () => {
// mtime alone cannot see this: `touch bin/xcui` makes the binary the newest
// file, so the mtime clause reports ok on a binary nobody rebuilt. git content
// can: a rebuilt binary has different bytes, so it would be dirty too.
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000, // binary "newer" than every source
binaryDirty: false, // …but its bytes never changed
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 100 }],
}],
dirty("tools/xcui/sim.go"),
true,
);
assert.equal(verdicts[0].state, "stale");
assert.match(verdicts[0].reason, /not rebuilt/);
});
it("is ok when source and binary are both dirty — the binary was rebuilt", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: true,
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 100 }],
}],
dirty("tools/xcui/sim.go", ".claude-plugin/plugins/axiom/bin/xcui"),
true,
);
assert.equal(verdicts[0].state, "ok");
});
it("ignores a dirty _test.go for the rebuild check too", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: false,
sources: [{ path: "tools/xcui/sim_test.go", mtimeMs: 100 }],
}],
dirty("tools/xcui/sim_test.go"),
true,
);
assert.equal(verdicts[0].state, "ok");
});
it("is stale when an already-rebuilt binary is left behind by a further edit (clause 2's own job)", () => {
// Pins the mtime clause on the case clause 1 cannot see: the binary is dirty
// (rebuilt once), so only the mtime comparison can catch the later edit. A
// mutation making binaryDirty short-circuit to ok passed every other test.
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 500,
binaryDirty: true,
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 900 }],
}],
dirty("tools/xcui/sim.go", ".claude-plugin/plugins/axiom/bin/xcui"),
true,
);
assert.equal(verdicts[0].state, "stale");
});
it("is stale when a compiled source was DELETED — it cannot appear in the fs walk", () => {
// git reports " D tools/xcui/helper.go", but the walker can never list a file
// that is gone, so the deletion has to arrive separately or it is invisible.
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: false,
sources: [{ path: "tools/xcui/main.go", mtimeMs: 100 }],
deletedSources: ["tools/xcui/helper.go"],
}],
dirty("tools/xcui/helper.go"),
true,
);
assert.equal(verdicts[0].state, "stale");
assert.match(verdicts[0].reason, /not rebuilt/);
});
it("ignores a deleted _test.go — go build never compiled it", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: false,
sources: [{ path: "tools/xcui/main.go", mtimeMs: 100 }],
deletedSources: ["tools/xcui/helper_test.go"],
}],
dirty("tools/xcui/helper_test.go"),
true,
);
assert.equal(verdicts[0].state, "ok");
});
it("flags source staged for commit without the rebuilt binary — HEAD would ship the old tool", () => {
// The gate reads the WORKING TREE, but what ships is HEAD. Staging sim.go alone
// (binary rebuilt but left unstaged) lands a commit whose source and binary
// disagree, and every worktree-based clause reads green.
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: true,
binaryStaged: false,
stagedSources: ["tools/xcui/sim.go"],
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 100 }],
}],
dirty("tools/xcui/sim.go", ".claude-plugin/plugins/axiom/bin/xcui"),
true,
);
assert.equal(verdicts[0].state, "binary-not-staged");
});
it("is ok when source and binary are staged together", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: true,
binaryStaged: true,
stagedSources: ["tools/xcui/sim.go"],
sources: [{ path: "tools/xcui/sim.go", mtimeMs: 100 }],
}],
dirty("tools/xcui/sim.go", ".claude-plugin/plugins/axiom/bin/xcui"),
true,
);
assert.equal(verdicts[0].state, "ok");
});
it("ignores a staged _test.go — it changes no binary, so it needs no binary staged", () => {
const verdicts = resolveGoBinaryStaleness(
[{
tool: "xcui",
binaryMtimeMs: 9_000,
binaryDirty: false,
binaryStaged: false,
stagedSources: ["tools/xcui/sim_test.go"],
sources: [{ path: "tools/xcui/main.go", mtimeMs: 100 }],
}],
new Set<string>(),
true,
);
assert.equal(verdicts[0].state, "ok");
});
it("returns one verdict per tool, in the order given", () => {
const verdicts = resolveGoBinaryStaleness(
[
{ tool: "xclog", binaryMtimeMs: 100, binaryDirty: false, sources: [{ path: "tools/xclog/main.go", mtimeMs: 10 }] },
{ tool: "xcui", binaryMtimeMs: 100, binaryDirty: true, sources: [{ path: "tools/xcui/main.go", mtimeMs: 900 }] },
],
dirty("tools/xcui/main.go"),
true,
);
assert.deepEqual(verdicts.map((v) => [v.tool, v.state]), [["xclog", "ok"], ["xcui", "stale"]]);
});
});