Files
2026-07-13 12:20:01 +08:00

310 lines
11 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { spawnSync } from "node:child_process"
import {
mkdtempSync,
writeFileSync,
readFileSync,
mkdirSync,
} from "node:fs"
import { tmpdir } from "node:os"
import path from "node:path"
// The cache helper is byte-duplicated per consuming skill (parity-guarded in
// repo-profile-cache-parity.test.ts). Behavior is identical, so exercise the
// canonical ce-pov copy here.
const SCRIPT = path.join(
__dirname,
"../skills/ce-pov/scripts/repo-profile-cache.py",
)
function git(cwd: string, ...args: string[]): string {
const r = spawnSync("git", args, { cwd, encoding: "utf8" })
if (r.status !== 0) {
throw new Error(`git ${args.join(" ")} failed: ${r.stderr}`)
}
return r.stdout ?? ""
}
function run(
cwd: string,
...args: string[]
): { code: number; stdout: string; stderr: string } {
const r = spawnSync("python3", [SCRIPT, ...args], { cwd, encoding: "utf8" })
return {
code: r.status ?? -1,
stdout: r.stdout ?? "",
stderr: r.stderr ?? "",
}
}
/** Fresh git repo with a manifest + README, one commit. Unique root SHA. */
function makeRepo(): string {
const dir = mkdtempSync(path.join(tmpdir(), "repo-profile-"))
git(dir, "init", "-q")
git(dir, "config", "user.email", "test@example.com")
git(dir, "config", "user.name", "Test")
git(dir, "config", "commit.gpgsign", "false")
writeFileSync(
path.join(dir, "package.json"),
'{"name":"x","version":"1.0.0"}\n',
)
writeFileSync(path.join(dir, "README.md"), "# x\n")
git(dir, "add", "-A")
git(dir, "commit", "-q", "-m", "init")
return dir
}
/** A full, valid agnostic profile — `put` now requires every top-level key. */
const VALID_PROFILE = {
stack: { languages: ["bun"] },
dependencies: { top_level: [], project_license: "MIT" },
topology: { monorepo: false, module_layout: "src/" },
conventions: { testing: "bun test" },
vocabulary: { concepts_present: false, terms: [] },
}
/** Write a profile JSON file and `put` it; return the cache path. */
function putProfile(dir: string, profile: object = VALID_PROFILE): string {
const profileFile = path.join(dir, "profile.json")
writeFileSync(profileFile, JSON.stringify(profile))
const res = run(dir, "put", profileFile)
expect(res.code).toBe(0)
return res.stdout.trim()
}
function getHitProfile(stdout: string): unknown {
const nl = stdout.indexOf("\n")
return JSON.parse(stdout.slice(nl + 1).trim())
}
describe("repo-profile-cache helper", () => {
test("fresh repo with no entry → MISS + a cache path under /tmp", () => {
const dir = makeRepo()
const res = run(dir, "get")
expect(res.code).toBe(0)
expect(res.stdout.startsWith("MISS\n")).toBe(true)
const writePath = res.stdout.split("\n")[1]
expect(writePath).toContain("/compound-engineering/repo-profile/")
expect(writePath.endsWith(".json")).toBe(true)
})
test("put then get (clean tree) → HIT with the stored profile", () => {
const dir = makeRepo()
putProfile(dir)
const res = run(dir, "get")
expect(res.code).toBe(0)
expect(res.stdout.startsWith("HIT\n")).toBe(true)
expect(getHitProfile(res.stdout)).toEqual(VALID_PROFILE)
})
test("dirty NON-input file (untracked source) stays HIT", () => {
const dir = makeRepo()
putProfile(dir)
mkdirSync(path.join(dir, "src"))
writeFileSync(path.join(dir, "src", "app.js"), "console.log(1)\n")
const res = run(dir, "get")
expect(res.stdout.startsWith("HIT\n")).toBe(true)
})
test("modified manifest → MISS (cardinal-rule input guard)", () => {
const dir = makeRepo()
putProfile(dir)
writeFileSync(
path.join(dir, "package.json"),
'{"name":"x","version":"2.0.0"}\n',
)
const res = run(dir, "get")
expect(res.stdout.startsWith("MISS\n")).toBe(true)
})
test("new UNTRACKED manifest (??) → MISS (untracked-input guard)", () => {
const dir = makeRepo()
putProfile(dir)
mkdirSync(path.join(dir, "packages", "sub"), { recursive: true })
writeFileSync(
path.join(dir, "packages", "sub", "package.json"),
'{"name":"sub"}\n',
)
const res = run(dir, "get")
expect(res.stdout.startsWith("MISS\n")).toBe(true)
})
test("new untracked root AGENTS.md (??) → MISS", () => {
const dir = makeRepo()
putProfile(dir)
writeFileSync(path.join(dir, "AGENTS.md"), "# rules\n")
const res = run(dir, "get")
expect(res.stdout.startsWith("MISS\n")).toBe(true)
})
test("schema-version mismatch → MISS", () => {
const dir = makeRepo()
const cachePath = putProfile(dir)
const doc = JSON.parse(readFileSync(cachePath, "utf8"))
doc.profile_schema_version = "0"
writeFileSync(cachePath, JSON.stringify(doc))
const res = run(dir, "get")
expect(res.stdout.startsWith("MISS\n")).toBe(true)
})
test("malformed cache file → MISS (degrades, never raises)", () => {
const dir = makeRepo()
const cachePath = putProfile(dir)
writeFileSync(cachePath, "not json at all")
const res = run(dir, "get")
expect(res.code).toBe(0)
expect(res.stdout.startsWith("MISS\n")).toBe(true)
})
test("non-git directory → NO-CACHE", () => {
const dir = mkdtempSync(path.join(tmpdir(), "repo-profile-nogit-"))
const res = run(dir, "get")
expect(res.code).toBe(0)
expect(res.stdout.trim()).toBe("NO-CACHE")
})
test("multi-root history yields a deterministic single-root path", () => {
const dir = makeRepo()
const orig = git(dir, "rev-parse", "--abbrev-ref", "HEAD").trim()
git(dir, "checkout", "-q", "--orphan", "second")
writeFileSync(path.join(dir, "other.txt"), "x\n")
git(dir, "add", "-A")
git(dir, "commit", "-q", "-m", "second root")
git(dir, "checkout", "-q", orig)
git(dir, "merge", "-q", "--allow-unrelated-histories", "--no-edit", "second")
const res = run(dir, "get")
expect(res.code).toBe(0)
const writePath = res.stdout.split("\n")[1]
// The <root-sha> path component must be a single 40-hex SHA, not a
// newline-joined pair from multiple roots.
const rootComponent = writePath.split("/repo-profile/")[1].split("/")[0]
expect(rootComponent).toMatch(/^[0-9a-f]{40}$/)
})
test("put rejects a non-object / empty / partial profile → not cached", () => {
// empty, non-object, AND partial / wrapper objects missing required keys
// (a profiler failure that still returns JSON must not be cached + served)
const bad = ["{}", '"oops"', "[]", "42", "null", '{"stack":{}}', '{"oops":"x"}']
for (const garbage of bad) {
const dir = makeRepo()
const f = path.join(dir, "bad.json")
writeFileSync(f, garbage)
const put = run(dir, "put", f)
expect(put.code).toBe(0)
expect(put.stdout.trim()).toBe("NO-CACHE") // refused to persist
// and nothing was cached, so a subsequent get is a MISS, not a HIT
const get = run(dir, "get")
expect(get.stdout.startsWith("MISS\n")).toBe(true)
}
}, 30000)
test("put refuses a profile derived from a dirty tree (revert-staleness guard)", () => {
const dir = makeRepo()
// dirty a profile input, then derive + put while dirty
writeFileSync(path.join(dir, "package.json"), '{"name":"x","version":"DIRTY"}\n')
const f = path.join(dir, "profile.json")
writeFileSync(f, JSON.stringify(VALID_PROFILE))
expect(run(dir, "put", f).stdout.trim()).toBe("NO-CACHE") // not persisted — dirty
// revert → clean tree at the same HEAD; must still MISS, not serve the dirty profile
writeFileSync(path.join(dir, "package.json"), '{"name":"x","version":"1.0.0"}\n')
expect(run(dir, "get").stdout.startsWith("MISS\n")).toBe(true)
})
test("usage error on missing/garbage subcommand → exit 2", () => {
const dir = makeRepo()
expect(run(dir).code).toBe(2)
expect(run(dir, "frobnicate").code).toBe(2)
expect(run(dir, "put").code).toBe(2) // put with no file
})
})
describe("repo-profile-cache helper — review-driven invalidation cases", () => {
test("renaming a profile input AWAY → MISS (both rename endpoints count)", () => {
const dir = makeRepo()
putProfile(dir)
git(dir, "mv", "package.json", "pkg.json") // R package.json -> pkg.json
const res = run(dir, "get")
expect(res.stdout.startsWith("MISS\n")).toBe(true)
})
test("renaming a NON-input file → stays HIT", () => {
const dir = makeRepo()
mkdirSync(path.join(dir, "src"))
writeFileSync(path.join(dir, "src", "lib.js"), "export const x = 1\n")
git(dir, "add", "-A")
git(dir, "commit", "-q", "-m", "add lib")
putProfile(dir)
git(dir, "mv", "src/lib.js", "src/lib2.js")
expect(run(dir, "get").stdout.startsWith("HIT\n")).toBe(true)
})
test("nested (subdir) instruction file stays HIT; only root invalidates", () => {
const dir = makeRepo()
putProfile(dir)
mkdirSync(path.join(dir, "sub"))
writeFileSync(path.join(dir, "sub", "AGENTS.md"), "# nested\n")
expect(run(dir, "get").stdout.startsWith("HIT\n")).toBe(true)
})
test("CI/config prefixes invalidate; non-workflow .github file does not", () => {
// .cursor/ and .github/workflows/ are profile inputs (MISS)
for (const p of [".cursor/rules", ".github/workflows/ci.yml"]) {
const dir = makeRepo()
putProfile(dir)
mkdirSync(path.join(dir, path.dirname(p)), { recursive: true })
writeFileSync(path.join(dir, p), "x\n")
expect(run(dir, "get").stdout.startsWith("MISS\n")).toBe(true)
}
// a .github file NOT under workflows/ is not an input (HIT)
const dir = makeRepo()
putProfile(dir)
mkdirSync(path.join(dir, ".github"))
writeFileSync(path.join(dir, ".github", "ISSUE_TEMPLATE.md"), "x\n")
expect(run(dir, "get").stdout.startsWith("HIT\n")).toBe(true)
}, 30000)
test("non-JS ecosystem + monorepo + IaC + deploy inputs invalidate", () => {
for (const p of [
"App.csproj", // .NET project-file suffix
"Package.swift", // Swift/iOS
"go.work", // Go workspace
"nx.json", // monorepo orchestrator
"infra.tf", // Terraform (.tf suffix)
"terraform/main.tf", // Terraform dir prefix
"k8s/deployment.yaml", // Kubernetes dir prefix
"Pulumi.yaml", // Pulumi IaC
".nvmrc", // runtime version selector
".tool-versions", // asdf/mise version selector
".cursorrules", // legacy root Cursor rules
"vercel.json", // deploy descriptor
".gitlab-ci.yml", // non-GitHub CI
]) {
const dir = makeRepo()
putProfile(dir)
const fp = path.join(dir, p)
mkdirSync(path.dirname(fp), { recursive: true })
writeFileSync(fp, "x\n")
expect(run(dir, "get").stdout.startsWith("MISS\n")).toBe(true)
}
}, 30000)
test("revert of a dirtied input restores the HIT (recompute determinism)", () => {
const dir = makeRepo()
const orig = '{"name":"x","version":"1.0.0"}\n'
putProfile(dir)
writeFileSync(path.join(dir, "package.json"), '{"name":"x","version":"9"}\n')
expect(run(dir, "get").stdout.startsWith("MISS\n")).toBe(true)
writeFileSync(path.join(dir, "package.json"), orig) // revert → clean again
expect(run(dir, "get").stdout.startsWith("HIT\n")).toBe(true)
})
test("cached doc with a non-object profile → MISS (get-side shape guard)", () => {
const dir = makeRepo()
const cachePath = putProfile(dir)
const doc = JSON.parse(readFileSync(cachePath, "utf8"))
doc.profile = null // externally corrupted / poisoned
writeFileSync(cachePath, JSON.stringify(doc))
expect(run(dir, "get").stdout.startsWith("MISS\n")).toBe(true)
})
})