|
| 1 | +/** |
| 2 | + * Co-change reach tier (#143) — end-to-end on a real git repo. |
| 3 | + * Proves the cross-directory win path-reach structurally can't reach: |
| 4 | + * a src/lib file co-changing with a prisma/ schema in different directories. |
| 5 | + */ |
| 6 | +import { describe, it, expect, beforeEach, afterEach } from "vitest"; |
| 7 | +import { init, relatedFilesFor, getStore } from "../src/core.js"; |
| 8 | +import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs"; |
| 9 | +import { execFileSync } from "node:child_process"; |
| 10 | +import { join } from "node:path"; |
| 11 | +import { tmpdir } from "node:os"; |
| 12 | + |
| 13 | +function git(cwd: string, ...args: string[]): void { |
| 14 | + execFileSync("git", args, { cwd, stdio: "pipe" }); |
| 15 | +} |
| 16 | + |
| 17 | +describe("co-change reach tier (#143)", () => { |
| 18 | + let root: string; |
| 19 | + |
| 20 | + beforeEach(async () => { |
| 21 | + root = mkdtempSync(join(tmpdir(), "engram-cc-")); |
| 22 | + git(root, "init", "-q"); |
| 23 | + git(root, "config", "user.email", "t@t"); |
| 24 | + git(root, "config", "user.name", "t"); |
| 25 | + mkdirSync(join(root, "src", "lib"), { recursive: true }); |
| 26 | + mkdirSync(join(root, "prisma"), { recursive: true }); |
| 27 | + // 3 commits that change a src/lib file AND a prisma schema together — a |
| 28 | + // cross-directory co-change pair (count 3 ≥ CO_CHANGE_MIN_COUNT=2). |
| 29 | + for (let i = 0; i < 3; i++) { |
| 30 | + writeFileSync(join(root, "src", "lib", "db.ts"), `export const q${i} = ${i};\n`); |
| 31 | + writeFileSync(join(root, "prisma", "schema.prisma"), `model M${i} {}\n`); |
| 32 | + git(root, "add", "-A"); |
| 33 | + git(root, "commit", "-q", "-m", `change ${i}`); |
| 34 | + } |
| 35 | + await init(root); |
| 36 | + }); |
| 37 | + |
| 38 | + afterEach(() => rmSync(root, { recursive: true, force: true })); |
| 39 | + |
| 40 | + it("getCoChangeNeighbors surfaces the cross-dir co-changed file (either query direction)", async () => { |
| 41 | + const store = await getStore(root); |
| 42 | + try { |
| 43 | + expect(store.getCoChangeNeighbors("src/lib/db.ts", 10)).toContain("prisma/schema.prisma"); |
| 44 | + expect(store.getCoChangeNeighbors("prisma/schema.prisma", 10)).toContain("src/lib/db.ts"); |
| 45 | + } finally { |
| 46 | + store.close(); |
| 47 | + } |
| 48 | + }); |
| 49 | + |
| 50 | + it("relatedFilesFor reaches the co-changed schema (a non-code, cross-dir file with no graph node)", async () => { |
| 51 | + const rel = await relatedFilesFor(root, "src/lib/db.ts", 5); |
| 52 | + expect(rel).toContain("prisma/schema.prisma"); |
| 53 | + }); |
| 54 | + |
| 55 | + it("co-change is rebuilt as a full replace on re-init (idempotent, no duplicates)", async () => { |
| 56 | + await init(root); // second mine |
| 57 | + const store = await getStore(root); |
| 58 | + try { |
| 59 | + const n = store.getCoChangeNeighbors("src/lib/db.ts", 10); |
| 60 | + expect(n.filter((f) => f === "prisma/schema.prisma")).toHaveLength(1); |
| 61 | + } finally { |
| 62 | + store.close(); |
| 63 | + } |
| 64 | + }); |
| 65 | + |
| 66 | + it("getCoChangeNeighbors: bidirectional lookup + count ordering + tie-break (direct)", async () => { |
| 67 | + const store = await getStore(root); |
| 68 | + try { |
| 69 | + // Hand-seed known pairs (stored once under the sorted key, a < b). |
| 70 | + store.replaceCoChange([ |
| 71 | + { a: "a.ts", b: "b.ts", count: 5 }, // query side = file_a |
| 72 | + { a: "b.ts", b: "c.ts", count: 9 }, // query "b.ts" must hit the file_a side |
| 73 | + { a: "b.ts", b: "d.ts", count: 9 }, // tie with c.ts at count 9 → path ASC |
| 74 | + ]); |
| 75 | + // From b.ts: neighbours are c.ts(9), d.ts(9), a.ts(5) — count DESC, tie by path ASC. |
| 76 | + expect(store.getCoChangeNeighbors("b.ts", 10)).toEqual(["c.ts", "d.ts", "a.ts"]); |
| 77 | + // From a.ts (only on the file_a side of one pair) → b.ts. |
| 78 | + expect(store.getCoChangeNeighbors("a.ts", 10)).toEqual(["b.ts"]); |
| 79 | + // From c.ts (only on the file_b side) → b.ts. Never returns itself. |
| 80 | + expect(store.getCoChangeNeighbors("c.ts", 10)).toEqual(["b.ts"]); |
| 81 | + // Limit is respected. |
| 82 | + expect(store.getCoChangeNeighbors("b.ts", 1)).toEqual(["c.ts"]); |
| 83 | + } finally { |
| 84 | + store.close(); |
| 85 | + } |
| 86 | + }); |
| 87 | +}); |
0 commit comments