import { describe, it, expect } from "bun:test"; import { updateCommentBody } from "../src/github/operations/comment-logic"; describe("updateCommentBody", () => { const baseInput = { currentBody: "Initial comment body", actionFailed: false, executionDetails: null, jobUrl: "https://github.com/owner/repo/actions/runs/123", branchName: undefined, triggerUsername: undefined, }; describe("working message replacement", () => { it("includes success message header with duration", () => { const input = { ...baseInput, currentBody: "Claude Code is working…", executionDetails: { duration_ms: 74000 }, // 1m 14s triggerUsername: "trigger-user", }; const result = updateCommentBody(input); expect(result).toContain( "**Claude finished @trigger-user's task in 1m 14s**", ); expect(result).not.toContain("Claude Code is working"); }); it("includes error message header with duration", () => { const input = { ...baseInput, currentBody: "Claude Code is working...", actionFailed: true, executionDetails: { duration_ms: 45000 }, // 45s }; const result = updateCommentBody(input); expect(result).toContain("**Claude encountered an error after 45s**"); }); it("includes error details when provided", () => { const input = { ...baseInput, currentBody: "Claude Code is working...", actionFailed: true, executionDetails: { duration_ms: 45000 }, errorDetails: "Failed to fetch issue data", }; const result = updateCommentBody(input); expect(result).toContain("**Claude encountered an error after 45s**"); expect(result).toContain("[View job]"); expect(result).toContain("```\nFailed to fetch issue data\n```"); // Ensure error details come after the header/links const errorIndex = result.indexOf("```"); const headerIndex = result.indexOf("**Claude encountered an error"); expect(errorIndex).toBeGreaterThan(headerIndex); }); it("handles username extraction from content when not provided", () => { const input = { ...baseInput, currentBody: "Claude Code is working… \n\nI'll work on this task @testuser", }; const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task**"); }); }); describe("job link", () => { it("includes job link in header", () => { const input = { ...baseInput, currentBody: "Some comment", }; const result = updateCommentBody(input); expect(result).toContain(`—— [View job](${baseInput.jobUrl})`); }); it("always includes job link in header, even if present in body", () => { const input = { ...baseInput, currentBody: `Some comment with [View job run](${baseInput.jobUrl})`, triggerUsername: "testuser", }; const result = updateCommentBody(input); // Check it's in the header with the new format expect(result).toContain(`—— [View job](${baseInput.jobUrl})`); // The old link in body is removed expect(result).not.toContain("View job run"); }); }); describe("branch link", () => { it("adds branch name with link to header when provided", () => { const input = { ...baseInput, branchName: "claude/issue-123-20240101_120000", }; const result = updateCommentBody(input); expect(result).toContain( "• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)", ); }); it("extracts branch name from branchLink if branchName not provided", () => { const input = { ...baseInput, branchLink: "\n[View branch](https://github.com/owner/repo/tree/branch-name)", }; const result = updateCommentBody(input); expect(result).toContain( "• [`branch-name`](https://github.com/owner/repo/tree/branch-name)", ); }); it("removes old branch links from body", () => { const input = { ...baseInput, currentBody: "Some comment with [View branch](https://github.com/owner/repo/tree/branch-name)", branchName: "new-branch-name", }; const result = updateCommentBody(input); expect(result).toContain( "• [`new-branch-name`](https://github.com/owner/repo/tree/new-branch-name)", ); expect(result).not.toContain("View branch"); }); }); describe("PR link", () => { it("adds PR link to header when provided", () => { const input = { ...baseInput, prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)", }; const result = updateCommentBody(input); expect(result).toContain( "• [Create PR ➔](https://github.com/owner/repo/pr-url)", ); }); it("moves PR link from body to header", () => { const input = { ...baseInput, currentBody: "Some comment with [Create a PR](https://github.com/owner/repo/pr-url)", }; const result = updateCommentBody(input); expect(result).toContain( "• [Create PR ➔](https://github.com/owner/repo/pr-url)", ); // Original Create a PR link is removed from body expect(result).not.toContain("[Create a PR]"); }); it("handles both body and provided PR links", () => { const input = { ...baseInput, currentBody: "Some comment with [Create a PR](https://github.com/owner/repo/pr-url-from-body)", prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url-provided)", }; const result = updateCommentBody(input); // Prefers the link found in content over the provided one expect(result).toContain( "• [Create PR ➔](https://github.com/owner/repo/pr-url-from-body)", ); }); it("handles complex PR URLs with encoded characters", () => { const complexUrl = "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20important%20bug%20fix&body=Fixes%20%23123%0A%0A%23%23%20Description%0AThis%20PR%20fixes%20an%20important%20bug%20that%20was%20causing%20issues%20with%20the%20application.%0A%0AGenerated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)"; const input = { ...baseInput, currentBody: `Some comment with [Create a PR](${complexUrl})`, }; const result = updateCommentBody(input); expect(result).toContain(`• [Create PR ➔](${complexUrl})`); // Original link should be removed from body expect(result).not.toContain("[Create a PR]"); }); it("handles PR links with encoded URLs containing parentheses", () => { const complexUrl = "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20bug%20fix&body=Generated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)"; const input = { ...baseInput, currentBody: `This PR was created.\n\n[Create a PR](${complexUrl})`, }; const result = updateCommentBody(input); expect(result).toContain(`• [Create PR ➔](${complexUrl})`); // Original link should be removed from body completely expect(result).not.toContain("[Create a PR]"); // Body content shouldn't have stray closing parens expect(result).toContain("This PR was created."); // Body part should be clean with no stray parens const bodyAfterSeparator = result.split("---")[1]?.trim(); expect(bodyAfterSeparator).toBe("This PR was created."); }); it("handles PR links with unencoded spaces and special characters", () => { const unEncodedUrl = "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix: update welcome message&body=Generated with [Claude Code](https://claude.ai/code)"; const expectedEncodedUrl = "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A+update+welcome+message&body=Generated+with+%5BClaude+Code%5D%28https%3A%2F%2Fclaude.ai%2Fcode%29"; const input = { ...baseInput, currentBody: `This PR was created.\n\n[Create a PR](${unEncodedUrl})`, }; const result = updateCommentBody(input); expect(result).toContain(`• [Create PR ➔](${expectedEncodedUrl})`); // Original link should be removed from body completely expect(result).not.toContain("[Create a PR]"); // Body content should be preserved expect(result).toContain("This PR was created."); }); it("falls back to prLink parameter when PR link in content cannot be encoded", () => { const invalidUrl = "not-a-valid-url-at-all"; const fallbackPrUrl = "https://github.com/owner/repo/pull/123"; const input = { ...baseInput, currentBody: `This PR was created.\n\n[Create a PR](${invalidUrl})`, prLink: `\n[Create a PR](${fallbackPrUrl})`, }; const result = updateCommentBody(input); expect(result).toContain(`• [Create PR ➔](${fallbackPrUrl})`); // Original link with invalid URL should still be in body since encoding failed expect(result).toContain("[Create a PR](not-a-valid-url-at-all)"); expect(result).toContain("This PR was created."); }); }); describe("execution details", () => { it("includes duration in header for success", () => { const input = { ...baseInput, executionDetails: { cost_usd: 0.13382595, duration_ms: 31033, duration_api_ms: 31034, }, triggerUsername: "testuser", }; const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task in 31s**"); }); it("formats duration in minutes and seconds in header", () => { const input = { ...baseInput, executionDetails: { duration_ms: 75000, // 1 minute 15 seconds }, triggerUsername: "testuser", }; const result = updateCommentBody(input); expect(result).toContain( "**Claude finished @testuser's task in 1m 15s**", ); }); it("includes duration in error header", () => { const input = { ...baseInput, actionFailed: true, executionDetails: { duration_ms: 45000, // 45 seconds }, }; const result = updateCommentBody(input); expect(result).toContain("**Claude encountered an error after 45s**"); }); it("handles missing duration gracefully", () => { const input = { ...baseInput, executionDetails: { cost_usd: 0.25, }, triggerUsername: "testuser", }; const result = updateCommentBody(input); expect(result).toContain("**Claude finished @testuser's task**"); expect(result).not.toContain(" in "); }); }); describe("combined updates", () => { it("combines all updates in correct order", () => { const input = { ...baseInput, currentBody: "Claude Code is working…\n\n### Todo List:\n- [x] Read README.md\n- [x] Add disclaimer", actionFailed: false, branchName: "claude-branch-123", prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)", executionDetails: { cost_usd: 0.01, duration_ms: 65000, // 1 minute 5 seconds }, triggerUsername: "trigger-user", }; const result = updateCommentBody(input); // Check the header structure expect(result).toContain( "**Claude finished @trigger-user's task in 1m 5s**", ); expect(result).toContain("—— [View job]"); expect(result).toContain( "• [`claude-branch-123`](https://github.com/owner/repo/tree/claude-branch-123)", ); expect(result).toContain("• [Create PR ➔]"); // Check order - header comes before separator with blank line const headerIndex = result.indexOf("**Claude finished"); const blankLineAndSeparatorPattern = /\n\n---\n/; expect(result).toMatch(blankLineAndSeparatorPattern); const separatorIndex = result.indexOf("---"); const todoIndex = result.indexOf("### Todo List:"); expect(headerIndex).toBeLessThan(separatorIndex); expect(separatorIndex).toBeLessThan(todoIndex); // Check content is preserved expect(result).toContain("### Todo List:"); expect(result).toContain("- [x] Read README.md"); expect(result).toContain("- [x] Add disclaimer"); }); it("handles PR link extraction from content", () => { const input = { ...baseInput, currentBody: "Claude Code is working…\n\nI've made changes.\n[Create a PR](https://github.com/owner/repo/pr-url-in-content)\n\n@john-doe", branchName: "feature-branch", triggerUsername: "john-doe", }; const result = updateCommentBody(input); // PR link should be moved to header expect(result).toContain( "• [Create PR ➔](https://github.com/owner/repo/pr-url-in-content)", ); // Original link should be removed from body expect(result).not.toContain("[Create a PR]"); // Username should come from argument, not extraction expect(result).toContain("**Claude finished @john-doe's task**"); // Content should be preserved expect(result).toContain("I've made changes."); }); it("includes PR link for new branches (issues and closed PRs)", () => { const input = { ...baseInput, currentBody: "Claude Code is working… ", branchName: "claude/pr-456-20240101_120000", prLink: "\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)", triggerUsername: "jane-doe", }; const result = updateCommentBody(input); // Should include the PR link in the formatted style expect(result).toContain( "• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)", ); expect(result).toContain("**Claude finished @jane-doe's task**"); }); it("includes both branch link and PR link for new branches", () => { const input = { ...baseInput, currentBody: "Claude Code is working…", branchName: "claude/issue-123-20240101_120000", branchLink: "\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)", prLink: "\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)", }; const result = updateCommentBody(input); // Should include both links in formatted style expect(result).toContain( "• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)", ); expect(result).toContain( "• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)", ); }); }); });