import { describe, expect, test } from "bun:test"; import { execFile } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { estimateCommitMinutes, getCommitsByDate, getGitCommits } from "./git"; const execFileAsync = promisify(execFile); const MAIN_AUTHOR = { name: "Ada Fixture", email: "ada@fixture.test" }; const OTHER_AUTHOR = { name: "Guy Other", email: "guy@other.test" }; interface FixtureCommit { message: string; file: string; contents: string; isoDate: string; author: { name: string; email: string }; } // Deterministic fixture history. Author dates are fixed so `since`/`until` // window filters are reproducible in CI. const FIXTURE_COMMITS: FixtureCommit[] = [ { message: "feat: add alpha", file: "alpha.txt", contents: "one\ntwo\nthree\n", isoDate: "2026-01-01T10:00:00Z", author: MAIN_AUTHOR, }, { message: "fix: patch beta", file: "beta.txt", contents: "left\nright\n", isoDate: "2026-01-02T10:00:00Z", author: MAIN_AUTHOR, }, { message: "docs: gamma notes", file: "gamma.txt", contents: "note\n", isoDate: "2026-02-01T10:00:00Z", author: OTHER_AUTHOR, }, ]; /** * Build a throwaway git repo on disk with a known, deterministic history so * ingestion can be asserted against expected counts + shape. This is the * ingestion correctness harness for the git source (OPL-2660): if `git log` * parsing silently regresses (e.g. "+0 events"), these assertions fail. */ async function createFixtureRepo(): Promise { const dir = await mkdtemp(join(tmpdir(), "scribe-git-fixture-")); const git = (args: string[], extraEnv: Record = {}) => execFileAsync("git", ["-C", dir, ...args], { env: { ...process.env, // Isolate from the host's global config / hooks / signing. GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null", HOME: dir, ...extraEnv, }, }); await git(["init", "-q", "-b", "main"]); await git(["config", "commit.gpgsign", "false"]); await git(["config", "core.hooksPath", "/dev/null"]); for (const commit of FIXTURE_COMMITS) { await writeFile(join(dir, commit.file), commit.contents); await git(["add", commit.file]); await git( [ "-c", `user.name=${commit.author.name}`, "-c", `user.email=${commit.author.email}`, "commit", "--no-verify", "--date", commit.isoDate, "-m", commit.message, ], { GIT_AUTHOR_DATE: commit.isoDate, GIT_COMMITTER_DATE: commit.isoDate, }, ); } return dir; } describe("git ingestion correctness harness", () => { test("ingests every fixture commit with correct count and shape", async () => { const dir = await createFixtureRepo(); try { const commits = await getGitCommits(dir); // Guards against the "+0 events" silent regression the harness exists for. expect(commits.length).toBe(FIXTURE_COMMITS.length); // git log default order is newest-first. const messages = commits.map((c) => c.message); expect(messages).toEqual([ "docs: gamma notes", "fix: patch beta", "feat: add alpha", ]); const alpha = commits.find((c) => c.message === "feat: add alpha"); expect(alpha).toBeDefined(); expect(alpha?.author).toBe(MAIN_AUTHOR.name); expect(alpha?.authorEmail).toBe(MAIN_AUTHOR.email); expect(alpha?.filesChanged).toEqual(["alpha.txt"]); expect(alpha?.additions).toBe(3); expect(alpha?.deletions).toBe(0); expect(alpha?.hash).toMatch(/^[0-9a-f]{40}$/); expect(alpha?.date.toISOString()).toBe("2026-01-01T10:00:00.000Z"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("authorEmail filter excludes commits from other authors", async () => { const dir = await createFixtureRepo(); try { const commits = await getGitCommits(dir, { authorEmail: MAIN_AUTHOR.email, }); expect(commits.length).toBe(2); expect(commits.every((c) => c.authorEmail === MAIN_AUTHOR.email)).toBe( true, ); } finally { await rm(dir, { recursive: true, force: true }); } }); test("since/until windows bound the ingested range (incremental cursor)", async () => { const dir = await createFixtureRepo(); try { const recent = await getGitCommits(dir, { since: new Date("2026-01-15T00:00:00Z"), }); expect(recent.map((c) => c.message)).toEqual(["docs: gamma notes"]); const early = await getGitCommits(dir, { until: new Date("2026-01-15T00:00:00Z"), }); expect(early.map((c) => c.message)).toEqual([ "fix: patch beta", "feat: add alpha", ]); } finally { await rm(dir, { recursive: true, force: true }); } }); test("getCommitsByDate groups commits by calendar day", async () => { const dir = await createFixtureRepo(); try { const byDate = await getCommitsByDate(dir); expect([...byDate.keys()].sort()).toEqual([ "2026-01-01", "2026-01-02", "2026-02-01", ]); expect(byDate.get("2026-01-01")).toHaveLength(1); } finally { await rm(dir, { recursive: true, force: true }); } }); test("returns empty (not throw) for a non-repo path", async () => { const dir = await mkdtemp(join(tmpdir(), "scribe-git-empty-")); try { const commits = await getGitCommits(dir); expect(commits).toEqual([]); } finally { await rm(dir, { recursive: true, force: true }); } }); test("estimateCommitMinutes is deterministic and bounded", () => { const minutes = estimateCommitMinutes({ hash: "deadbeef", date: new Date("2026-01-01T10:00:00Z"), message: "feat: add alpha", author: MAIN_AUTHOR.name, authorEmail: MAIN_AUTHOR.email, filesChanged: ["alpha.txt"], additions: 3, deletions: 0, }); // Base 15 min, scaled up slightly by the small change; never below base. expect(minutes).toBeGreaterThanOrEqual(15); expect(minutes).toBeLessThanOrEqual(240); }); });