305 lines · 11.0 KB · TypeScript
RawDownload
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)");  });   it("includes policy instructions", () => {    const result = buildCommitMessagePrompt({      branch: "main",      stagedSummary: "M a.ts",      stagedPatch: "diff",      includeBranch: false,      policy: {        kind: "custom",        commitInstructions: "Use a terse repository-specific subject.",        inferRepositoryConventions: false,      },    });     expect(result.prompt).toContain("Additional instructions:");    expect(result.prompt).toContain("Use a terse repository-specific subject.");  });}); 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()");    expect(result.prompt).toContain("include headings '## Summary' and '## Testing'");  });   it("follows a repository PR template instead of the default body headings", () => {    const result = buildPrContentPrompt({      baseBranch: "main",      headBranch: "feature/auth",      commitSummary: "feat: add login page",      diffSummary: "3 files changed",      diffPatch: "diff",      changeRequestTemplate: "<!-- remove me -->\n## What changed\n\n## Verification",      policy: {        kind: "custom",        changeRequestInstructions: "Keep the title in sentence case.",        inferRepositoryConventions: false,      },    });     expect(result.prompt).toContain("Keep the title in sentence case.");    expect(result.prompt).toContain("follow the repository change request template structure");    expect(result.prompt).toContain("drop HTML comments from the template");    expect(result.prompt).toContain("Repository change request template:");    expect(result.prompt).toContain("<!-- remove me -->\n## What changed\n\n## Verification");    expect(result.prompt).not.toContain("include headings '## Summary' and '## Testing'");  });}); 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:");    expect(result.prompt).toContain(      "Generate a title that will help the user recognize this T3 Code thread weeks later.",    );    expect(result.prompt).toContain(      "Title the subject and outcome. Discard incidental instructions.",    );    expect(result.prompt).toContain(      "Name the product change, not the mock, plan, report, branch, or PR used to produce it.",    );    expect(result.prompt).not.toContain(      "Title should summarize the user's request, not restate it verbatim.",    );  });   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");  });   it("regenerates from recent thread contents and identifies the previous title", () => {    const result = buildThreadTitlePrompt({      message: `USER:\nInvestigate reconnect regressions\n\nASSISTANT:\nThe remaining issue is stale session state`,      previousTitle: "Investigate reconnect regressions",    });     expect(result.prompt).toContain(      "Regenerate the title for an existing T3 Code thread so the user can recognize it weeks later.",    );    expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".');    expect(result.prompt).toContain(      "Read the USER messages first. Identify the latest explicit durable goal.",    );    expect(result.prompt).toContain(      "Do not promote one assistant finding into the thread subject unless the user adopts it as a new goal.",    );    expect(result.prompt).toContain(      'A subagent-monitoring review that finds a Codex roster bug remains "Review Subagent Monitoring Risks,"',    );    expect(result.prompt).toContain("Thread contents:");    expect(result.prompt).toContain("The remaining issue is stale session state");  });   it("keeps the latest thread contents when regeneration context is truncated", () => {    const result = buildThreadTitlePrompt({      message: `${"old context ".repeat(1_000)}\n\nASSISTANT:\nCurrent thread state`,      previousTitle: "Old title",    });     expect(result.prompt).toContain("[Earlier content truncated]");    expect(result.prompt).toContain("Current thread state");    expect(result.prompt).not.toContain("[truncated]");  });   it("does not truncate an already-marked regeneration context twice", () => {    const retainedContext = "x".repeat(7_998);    const result = buildThreadTitlePrompt({      message: `[Earlier content truncated]\n\n${retainedContext}`,      previousTitle: "Old title",    });     expect(result.prompt).toContain(      `Thread contents:\n[Earlier content truncated]\n\n${retainedContext}`,    );    expect(result.prompt.match(/\[Earlier content truncated\]/g)).toHaveLength(1);  });}); 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");  });});