import { describe, expect, test } from "bun:test"; import { describeMessages, diffExcerpt, isLockfile, mockSuggestion, parseSuggestion } from "./prompt"; import type { DescribeInput } from "./prompt"; function file(path: string, extra: Partial = {}) { return { path, oldPath: null, status: "modified" as const, oldSha: "a", newSha: "b", oldMode: "100644", newMode: "100644", additions: 1, deletions: 1, binary: false, truncated: false, ...extra, }; } function chunk(path: string, body = "@@ -1 +1 @@\n-old\n+new\n"): string { return `diff --git a/${path} b/${path}\nindex 1..2 100644\n--- a/${path}\n+++ b/${path}\n${body}`; } const input: DescribeInput = { base: "main", head: "feature/retry", commits: [ { subject: "feat: add retry to uploader", message: "feat: add retry to uploader\n\nThree attempts with backoff." }, { subject: "chore: bump deps", message: "chore: bump deps" }, ], diff: { patch: chunk("src/upload.ts") + chunk("bun.lock") + chunk("logo.png", "Binary files a/logo.png and b/logo.png differ\n"), files: [file("src/upload.ts"), file("bun.lock", { additions: 400 }), file("logo.png", { binary: true })], }, }; describe("describeMessages", () => { test("includes commits with bodies, file stats and only text non-lockfile diff chunks", () => { const [system, user] = describeMessages(input); expect(system.role).toBe("system"); expect(system.content).toContain('{"title": string, "body": string}'); expect(user.content).toContain('Merging branch "feature/retry" into "main"'); expect(user.content).toContain("- feat: add retry to uploader\n Three attempts with backoff."); expect(user.content).toContain("- modified src/upload.ts (+1 -1)"); expect(user.content).toContain("- modified logo.png (binary)"); expect(user.content).toContain("diff --git a/src/upload.ts"); expect(user.content).not.toContain("diff --git a/bun.lock"); expect(user.content).not.toContain("diff --git a/logo.png"); }); test("caps the commit list at 30 and says how many were omitted", () => { const commits = Array.from({ length: 45 }, (_, index) => ({ subject: `c${index}`, message: `c${index}` })); const [, user] = describeMessages({ ...input, commits }); expect(user.content).toContain("- c29"); expect(user.content).not.toContain("- c30\n"); expect(user.content).toContain("(15 more commits omitted)"); }); test("omits the diff section entirely when nothing is worth showing", () => { const [, user] = describeMessages({ ...input, diff: { patch: chunk("bun.lock"), files: [file("bun.lock")] } }); expect(user.content).not.toContain("Diff excerpt"); }); }); describe("diffExcerpt", () => { test("stays within the character limit and marks truncation", () => { const big = chunk("a.ts", `@@ -1 +1 @@\n${"+x".repeat(5000)}\n`) + chunk("b.ts", `@@ -1 +1 @@\n${"+y".repeat(5000)}\n`); const excerpt = diffExcerpt({ patch: big, files: [file("a.ts"), file("b.ts")] }, 2000); expect(excerpt.length).toBeLessThan(2200); expect(excerpt).toContain("[... truncated ...]"); expect(excerpt).toContain("diff --git a/b.ts"); }); test("recognises lockfiles anywhere in the tree", () => { expect(isLockfile("bun.lock")).toBe(true); expect(isLockfile("packages/web/pnpm-lock.yaml")).toBe(true); expect(isLockfile("Cargo.lock")).toBe(true); expect(isLockfile("src/lock.ts")).toBe(false); expect(isLockfile("docs/locks.md")).toBe(false); }); }); describe("parseSuggestion", () => { test("accepts plain JSON", () => { expect(parseSuggestion('{"title": "feat: retry uploads", "body": "## Changes\\n- retry"}')).toEqual({ title: "feat: retry uploads", body: "## Changes\n- retry", }); }); test("accepts fenced JSON wrapped in prose", () => { const text = 'Sure! Here you go:\n```json\n{"title":"fix: thing","body":"Body"}\n```\nHope that helps.'; expect(parseSuggestion(text)).toEqual({ title: "fix: thing", body: "Body" }); }); test("falls back to the outermost braces and tolerates a missing body", () => { expect(parseSuggestion('Result: {"title": " fix: a \\"quoted\\" word "} trailing')).toEqual({ title: 'fix: a "quoted" word', body: "", }); }); test("collapses whitespace, strips wrapping quotes and bounds length", () => { const parsed = parseSuggestion(JSON.stringify({ title: `"${"word ".repeat(40)}"`, body: "b".repeat(10_000) })); expect(parsed?.title.length).toBeLessThanOrEqual(100); expect(parsed?.title.startsWith("word word")).toBe(true); expect(parsed?.body.length).toBe(6000); }); test("recovers title and body when strings contain raw newlines", () => { const text = '{"title": "feat: retry",\n"body": "Summary line.\n\n## Changes\n- one\n- two"}'; expect(parseSuggestion(text)).toEqual({ title: "feat: retry", body: "Summary line.\n\n## Changes\n- one\n- two" }); }); test("returns null for junk, arrays and missing titles", () => { expect(parseSuggestion("no json here")).toBeNull(); expect(parseSuggestion('["a"]')).toBeNull(); expect(parseSuggestion('{"body": "only"}')).toBeNull(); expect(parseSuggestion('{"title": " "}')).toBeNull(); }); }); describe("mockSuggestion", () => { test("derives a deterministic title and bulleted body from the input", () => { const suggestion = mockSuggestion(input); expect(suggestion.title).toBe("chore: add retry to uploader"); expect(suggestion.body).toContain("Merges `feature/retry` into `main`."); expect(suggestion.body).toContain("- chore: bump deps"); expect(suggestion.body).toContain("- modified `src/upload.ts`"); }); });