- skill-sync check conditioned on content hash of its inputs (skips ~0.3s spawn) - ensure-test-artifacts: git-blob content-hash staleness; branch switches no longer trigger spurious ~2.6s tsc rebuilds (mtime fallback when dirty) - isolation guard: cheap --before-fast reusing prior post-run baseline (~2.1s -> ~0.07s); detection proven preserved via injected-leak failure test - vitest-setup: CI skips 4040-4045 discovery probe unless FUSION_RESERVED_PORTS set; kill-guard wrapper untouched, asymmetry pinned by port-probe-policy tests - cache-fresh fast path skips sync/artifacts/HOME-prune entirely (mode line: fast-path=cache-fresh)
154 lines
6.2 KiB
JavaScript
154 lines
6.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 } 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;
|
|
return (args) => {
|
|
if (args[0] === "ls-files") {
|
|
return Object.entries(tracked)
|
|
.map(([file, sha]) => `100644 ${sha} 0\t${file}`)
|
|
.join("\n");
|
|
}
|
|
if (args[0] === "status") {
|
|
const lines = [];
|
|
for (const file of dirty) lines.push(` M ${file}`);
|
|
for (const file of untracked) 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);
|
|
});
|