- content-hash: createRepoContentSnapshot — 2 repo-wide git spawns shared across all hash computations (was ~2 spawns x N packages x 2 passes, ~0.6-1.6s per cache-miss run); snapshot-equivalence test pins zero-spawn path - test-changed: one hash memo + snapshot shared between applyCacheToPlan and recordCachePass (record pass now re-hashes nothing) - ci-test-shard: listPackageTestFiles single source of truth for the test-file glob (was triplicated) - check-test-inventory: curatedProjects defaults to projects (removes duplicated 11-entry list in spec) - ensure-test-artifacts: drop detectMissingArtifacts passthrough alias - drop dead statSync re-export; cross-reference comments on the two shared-input path lists Skipped deliberately: --cold-start-probe/--check-timings-staleness removal (plan artifacts for U8 re-eval + scheduled refresh), best-fit dedup + threshold² (behavior-preserving refactor of verified shard math — follow-up), worker-budget duplication (pre-existing on main)
204 lines
8.2 KiB
JavaScript
204 lines
8.2 KiB
JavaScript
/**
|
|
* Unit tests for scripts/lib/content-hash.mjs (U3).
|
|
*
|
|
* Runner: node --test scripts/__tests__/content-hash.test.mjs
|
|
*
|
|
* These tests use injectable gitFn/readFn stubs so they never touch the real
|
|
* repo or shell out to git.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { computeContentHash, createRepoContentSnapshot } from "../lib/content-hash.mjs";
|
|
|
|
/**
|
|
* Build a fake git runner from a description of the tree.
|
|
*
|
|
* @param {object} tree
|
|
* @param {Record<string,string>} tree.tracked path -> blob sha (clean tracked files)
|
|
* @param {string[]} [tree.dirty] tracked paths that are modified in worktree
|
|
* @param {string[]} [tree.untracked] untracked-not-ignored paths
|
|
*/
|
|
function fakeGit(tree) {
|
|
const { tracked = {}, dirty = [], untracked = [] } = tree;
|
|
// Honor the `-- <paths>` filter the way real git does (exact file or dir
|
|
// prefix); commands without a path filter return the whole tree.
|
|
const selected = (args, file) => {
|
|
const dashIdx = args.indexOf("--");
|
|
if (dashIdx === -1) return true;
|
|
const inputs = args.slice(dashIdx + 1);
|
|
return inputs.some((input) => file === input || file.startsWith(`${input}/`));
|
|
};
|
|
return (args) => {
|
|
if (args[0] === "ls-files") {
|
|
return Object.entries(tracked)
|
|
.filter(([file]) => selected(args, file))
|
|
.map(([file, sha]) => `100644 ${sha} 0\t${file}`)
|
|
.join("\n");
|
|
}
|
|
if (args[0] === "status") {
|
|
const lines = [];
|
|
for (const file of dirty) if (selected(args, file)) lines.push(` M ${file}`);
|
|
for (const file of untracked) if (selected(args, file)) lines.push(`?? ${file}`);
|
|
return lines.join("\n");
|
|
}
|
|
return null;
|
|
};
|
|
}
|
|
|
|
const readBytes = (contentByPath) => (absPath) => {
|
|
// absPath is rootDir + "/" + relPath; match on suffix.
|
|
for (const [rel, content] of Object.entries(contentByPath)) {
|
|
if (absPath.endsWith(rel)) return Buffer.from(content);
|
|
}
|
|
throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
|
|
};
|
|
|
|
const base = { rootDir: "/repo", inputPaths: ["packages/core/src"] };
|
|
|
|
test("computeContentHash is stable for identical tracked content", () => {
|
|
const git = fakeGit({ tracked: { "packages/core/src/a.ts": "aaa", "packages/core/src/b.ts": "bbb" } });
|
|
const h1 = computeContentHash({ ...base, gitFn: git, readFn: readBytes({}) });
|
|
const h2 = computeContentHash({ ...base, gitFn: git, readFn: readBytes({}) });
|
|
assert.equal(h1, h2);
|
|
assert.equal(h1.length, 64);
|
|
});
|
|
|
|
test("computeContentHash busts when a tracked blob sha changes (real source change)", () => {
|
|
const before = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" } }),
|
|
readFn: readBytes({}),
|
|
});
|
|
const after = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "zzz" } }),
|
|
readFn: readBytes({}),
|
|
});
|
|
assert.notEqual(before, after);
|
|
});
|
|
|
|
test("branch-switch with identical content yields the same hash (mtime-independent)", () => {
|
|
// Two 'branches' with the same tracked blob shas → identical hash, even though
|
|
// a real checkout would rewrite mtimes. The hash never reads mtime.
|
|
const git = fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" } });
|
|
const branchA = computeContentHash({ ...base, gitFn: git, readFn: readBytes({}) });
|
|
const branchB = computeContentHash({ ...base, gitFn: git, readFn: readBytes({}) });
|
|
assert.equal(branchA, branchB);
|
|
});
|
|
|
|
test("dirty tracked file is hashed by working-tree bytes, not the stale index sha", () => {
|
|
// Same index blob sha, but the worktree content differs → different hashes.
|
|
const clean = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" } }),
|
|
readFn: readBytes({ "packages/core/src/a.ts": "ON-DISK-V1" }),
|
|
});
|
|
const dirty = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" }, dirty: ["packages/core/src/a.ts"] }),
|
|
readFn: readBytes({ "packages/core/src/a.ts": "ON-DISK-V2" }),
|
|
});
|
|
assert.notEqual(clean, dirty);
|
|
});
|
|
|
|
test("two different working-tree contents of a dirty file produce different hashes", () => {
|
|
const v1 = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" }, dirty: ["packages/core/src/a.ts"] }),
|
|
readFn: readBytes({ "packages/core/src/a.ts": "V1" }),
|
|
});
|
|
const v2 = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" }, dirty: ["packages/core/src/a.ts"] }),
|
|
readFn: readBytes({ "packages/core/src/a.ts": "V2" }),
|
|
});
|
|
assert.notEqual(v1, v2);
|
|
});
|
|
|
|
test("untracked file is folded into the hash via its bytes", () => {
|
|
const without = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" } }),
|
|
readFn: readBytes({}),
|
|
});
|
|
const withUntracked = computeContentHash({
|
|
...base,
|
|
gitFn: fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" }, untracked: ["packages/core/src/new.ts"] }),
|
|
readFn: readBytes({ "packages/core/src/new.ts": "brand new" }),
|
|
});
|
|
assert.notEqual(without, withUntracked);
|
|
});
|
|
|
|
test("porcelain status parsing tolerates spacing variants (M path vs ' M path')", () => {
|
|
// git emits the worktree-modified code in either " M path" or "M path" form
|
|
// depending on staged/worktree state; both must be recognized as dirty.
|
|
const tracked = { "packages/core/src/a.ts": "aaa" };
|
|
const readFn = (absPath) => (absPath.endsWith("a.ts") ? Buffer.from("ON-DISK") : Buffer.from(""));
|
|
|
|
const variantGit = (statusLine) => (args) => {
|
|
if (args[0] === "ls-files") {
|
|
return Object.entries(tracked).map(([f, s]) => `100644 ${s} 0\t${f}`).join("\n");
|
|
}
|
|
if (args[0] === "status") return statusLine;
|
|
return null;
|
|
};
|
|
|
|
const clean = computeContentHash({ ...base, gitFn: variantGit(""), readFn: () => Buffer.from("") });
|
|
const variantA = computeContentHash({ ...base, gitFn: variantGit(" M packages/core/src/a.ts"), readFn });
|
|
const variantB = computeContentHash({ ...base, gitFn: variantGit("M packages/core/src/a.ts"), readFn });
|
|
|
|
assert.notEqual(clean, variantA, "leading-space variant must register as dirty");
|
|
assert.notEqual(clean, variantB, "trailing-space variant must register as dirty");
|
|
// Both variants describe the same dirty file/content → identical hash.
|
|
assert.equal(variantA, variantB);
|
|
});
|
|
|
|
test("versionPrefix busts the hash so a format bump invalidates all entries", () => {
|
|
const git = fakeGit({ tracked: { "packages/core/src/a.ts": "aaa" } });
|
|
const v1 = computeContentHash({ ...base, versionPrefix: "v1", gitFn: git, readFn: readBytes({}) });
|
|
const v2 = computeContentHash({ ...base, versionPrefix: "v2", gitFn: git, readFn: readBytes({}) });
|
|
assert.notEqual(v1, v2);
|
|
});
|
|
|
|
test("snapshot path produces identical hashes to the spawn path and spawns no git", () => {
|
|
const tree = {
|
|
tracked: {
|
|
"packages/core/src/a.ts": "aaa",
|
|
"packages/core/src/b.ts": "bbb",
|
|
"packages/engine/src/c.ts": "ccc",
|
|
"pnpm-lock.yaml": "lll",
|
|
},
|
|
dirty: ["packages/core/src/b.ts"],
|
|
untracked: ["packages/engine/src/new.ts"],
|
|
};
|
|
const readFn = readBytes({
|
|
"packages/core/src/b.ts": "B-ON-DISK",
|
|
"packages/engine/src/new.ts": "NEW-ON-DISK",
|
|
});
|
|
|
|
const snapshot = createRepoContentSnapshot({ rootDir: base.rootDir, gitFn: fakeGit(tree) });
|
|
|
|
for (const inputPaths of [["packages/core"], ["packages/engine"], ["pnpm-lock.yaml"], ["packages/core", "pnpm-lock.yaml"]]) {
|
|
const viaSpawn = computeContentHash({ ...base, inputPaths, gitFn: fakeGit(tree), readFn });
|
|
let spawnCalls = 0;
|
|
const viaSnapshot = computeContentHash({
|
|
...base,
|
|
inputPaths,
|
|
gitFn: () => {
|
|
spawnCalls += 1;
|
|
return null;
|
|
},
|
|
readFn,
|
|
snapshot,
|
|
});
|
|
assert.equal(viaSnapshot, viaSpawn, `hash mismatch for ${inputPaths.join(",")}`);
|
|
assert.equal(spawnCalls, 0, "snapshot path must not invoke git");
|
|
}
|
|
|
|
// Prefix selection must not match sibling dirs sharing a name prefix.
|
|
const coreOnly = computeContentHash({ ...base, inputPaths: ["packages/core"], readFn, snapshot });
|
|
const engineOnly = computeContentHash({ ...base, inputPaths: ["packages/engine"], readFn, snapshot });
|
|
assert.notEqual(coreOnly, engineOnly);
|
|
});
|