import { describe, expect, it } from "vite-plus/test"; import { buildBranchNamePrompt, buildCommitMessagePrompt, buildPrContentPrompt, buildThreadTitlePrompt, } from "./TextGenerationPrompts.ts"; import { normalizeCliError, sanitizeThreadTitle } from "./TextGenerationUtils.ts"; import { TextGenerationError } from "@t3tools/contracts"; describe("buildCommitMessagePrompt", () => { it("includes staged patch and summary in the prompt", () => { const result = buildCommitMessagePrompt({ branch: "main", stagedSummary: "M README.md", stagedPatch: "diff --git a/README.md b/README.md\n+hello", includeBranch: false, }); expect(result.prompt).toContain("Staged files:"); expect(result.prompt).toContain("M README.md"); expect(result.prompt).toContain("Staged patch:"); expect(result.prompt).toContain("diff --git a/README.md b/README.md"); expect(result.prompt).toContain("Branch: main"); // Should NOT include the branch generation instruction expect(result.prompt).not.toContain("branch must be a short semantic git branch fragment"); }); it("includes branch generation instruction when includeBranch is true", () => { const result = buildCommitMessagePrompt({ branch: "feature/foo", stagedSummary: "M README.md", stagedPatch: "diff", includeBranch: true, }); expect(result.prompt).toContain("branch must be a short semantic git branch fragment"); expect(result.prompt).toContain("Return a JSON object with keys: subject, body, branch."); }); it("shows (detached) when branch is null", () => { const result = buildCommitMessagePrompt({ branch: null, stagedSummary: "M a.ts", stagedPatch: "diff", includeBranch: false, }); expect(result.prompt).toContain("Branch: (detached)"); }); }); describe("buildPrContentPrompt", () => { it("includes branch names, commits, and diff in the prompt", () => { const result = buildPrContentPrompt({ baseBranch: "main", headBranch: "feature/auth", commitSummary: "feat: add login page", diffSummary: "3 files changed", diffPatch: "diff --git a/auth.ts b/auth.ts\n+export function login()", }); expect(result.prompt).toContain("Base branch: main"); expect(result.prompt).toContain("Head branch: feature/auth"); expect(result.prompt).toContain("Commits:"); expect(result.prompt).toContain("feat: add login page"); expect(result.prompt).toContain("Diff stat:"); expect(result.prompt).toContain("3 files changed"); expect(result.prompt).toContain("Diff patch:"); expect(result.prompt).toContain("export function login()"); }); }); describe("buildBranchNamePrompt", () => { it("includes the user message in the prompt", () => { const result = buildBranchNamePrompt({ message: "Fix the login timeout bug", }); expect(result.prompt).toContain("User message:"); expect(result.prompt).toContain("Fix the login timeout bug"); expect(result.prompt).not.toContain("Attachment metadata:"); }); it("includes attachment metadata when attachments are provided", () => { const result = buildBranchNamePrompt({ message: "Fix the layout from screenshot", attachments: [ { type: "image" as const, id: "att-123", name: "screenshot.png", mimeType: "image/png", sizeBytes: 12345, }, ], }); expect(result.prompt).toContain("Attachment metadata:"); expect(result.prompt).toContain("screenshot.png"); expect(result.prompt).toContain("image/png"); expect(result.prompt).toContain("12345 bytes"); }); }); describe("buildThreadTitlePrompt", () => { it("includes the user message in the prompt", () => { const result = buildThreadTitlePrompt({ message: "Investigate reconnect regressions after session restore", }); expect(result.prompt).toContain("User message:"); expect(result.prompt).toContain("Investigate reconnect regressions after session restore"); expect(result.prompt).not.toContain("Attachment metadata:"); }); it("includes attachment metadata when attachments are provided", () => { const result = buildThreadTitlePrompt({ message: "Name this thread from the screenshot", attachments: [ { type: "image" as const, id: "att-456", name: "thread.png", mimeType: "image/png", sizeBytes: 67890, }, ], }); expect(result.prompt).toContain("Attachment metadata:"); expect(result.prompt).toContain("thread.png"); expect(result.prompt).toContain("image/png"); expect(result.prompt).toContain("67890 bytes"); }); }); describe("sanitizeThreadTitle", () => { it("truncates long titles with the shared sidebar-safe limit", () => { expect( sanitizeThreadTitle( ' "Reconnect failures after restart because the session state does not recover" ', ), ).toBe("Reconnect failures after restart because the se..."); }); }); describe("normalizeCliError", () => { it("detects 'Command not found' and includes CLI name in the message", () => { const error = normalizeCliError( "claude", "generateCommitMessage", new Error("Command not found: claude"), "Something went wrong", ); expect(error).toBeInstanceOf(TextGenerationError); expect(error.detail).toContain("Claude CLI"); expect(error.detail).toContain("not available on PATH"); }); it("uses the CLI name from the first argument for codex", () => { const error = normalizeCliError( "codex", "generateBranchName", new Error("Command not found: codex"), "Something went wrong", ); expect(error).toBeInstanceOf(TextGenerationError); expect(error.detail).toContain("Codex CLI"); expect(error.detail).toContain("not available on PATH"); }); it("returns the error as-is if it is already a TextGenerationError", () => { const existing = new TextGenerationError({ operation: "generatePrContent", detail: "Already wrapped", }); const result = normalizeCliError("claude", "generatePrContent", existing, "fallback"); expect(result).toBe(existing); }); it("wraps unknown non-Error values with the fallback message", () => { const result = normalizeCliError("codex", "generateCommitMessage", "string error", "fallback"); expect(result).toBeInstanceOf(TextGenerationError); expect(result.detail).toBe("fallback"); }); it("does not expose CLI failure details in the public error message", () => { const result = normalizeCliError( "codex", "generateCommitMessage", new Error("request failed with access_token=secret-token"), "Failed to generate a commit message", ); expect(result.detail).toBe("Failed to generate a commit message"); expect(result.message).not.toContain("secret-token"); }); });