import { expect, test, describe } from "bun:test"; import { formatContext, formatBody, formatComments, formatReviewComments, formatChangedFiles, formatChangedFilesWithSHA, stripHtmlComments, } from "../src/github/data/formatter"; import type { GitHubPullRequest, GitHubIssue, GitHubComment, GitHubFile, } from "../src/github/types"; import type { GitHubFileWithSHA } from "../src/github/data/fetcher"; describe("formatContext", () => { test("formats PR context correctly", () => { const prData: GitHubPullRequest = { title: "Test PR", body: "PR body", author: { login: "test-user" }, baseRefName: "main", headRefName: "feature/test", headRefOid: "abc123", createdAt: "2023-01-01T00:00:00Z", additions: 50, deletions: 30, state: "OPEN", commits: { totalCount: 3, nodes: [], }, files: { nodes: [{} as GitHubFile, {} as GitHubFile], }, comments: { nodes: [], }, reviews: { nodes: [], }, }; const result = formatContext(prData, true); expect(result).toBe( `PR Title: Test PR PR Author: test-user PR Branch: feature/test -> main PR State: OPEN PR Additions: 50 PR Deletions: 30 Total Commits: 3 Changed Files: 2 files`, ); }); test("formats Issue context correctly", () => { const issueData: GitHubIssue = { title: "Test Issue", body: "Issue body", author: { login: "test-user" }, createdAt: "2023-01-01T00:00:00Z", state: "OPEN", comments: { nodes: [], }, }; const result = formatContext(issueData, false); expect(result).toBe( `Issue Title: Test Issue Issue Author: test-user Issue State: OPEN`, ); }); }); describe("formatBody", () => { test("replaces image URLs with local paths", () => { const body = `Here is some text with an image: ![screenshot](https://github.com/user-attachments/assets/test-image.png) And another one: ![another](https://github.com/user-attachments/assets/another-image.jpg) Some more text.`; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/test-image.png", "/tmp/github-images/image-1234-0.png", ], [ "https://github.com/user-attachments/assets/another-image.jpg", "/tmp/github-images/image-1234-1.jpg", ], ]); const result = formatBody(body, imageUrlMap); expect(result) .toBe(`Here is some text with an image: ![screenshot](/tmp/github-images/image-1234-0.png) And another one: ![another](/tmp/github-images/image-1234-1.jpg) Some more text.`); }); test("handles empty image map", () => { const body = "No images here"; const imageUrlMap = new Map(); const result = formatBody(body, imageUrlMap); expect(result).toBe("No images here"); }); test("preserves body when no images match", () => { const body = "![image](https://example.com/image.png)"; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/different.png", "/tmp/github-images/image-1234-0.png", ], ]); const result = formatBody(body, imageUrlMap); expect(result).toBe("![image](https://example.com/image.png)"); }); test("handles multiple occurrences of same image", () => { const body = `First: ![img](https://github.com/user-attachments/assets/test.png) Second: ![img](https://github.com/user-attachments/assets/test.png)`; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/test.png", "/tmp/github-images/image-1234-0.png", ], ]); const result = formatBody(body, imageUrlMap); expect(result).toBe(`First: ![img](/tmp/github-images/image-1234-0.png) Second: ![img](/tmp/github-images/image-1234-0.png)`); }); }); describe("formatComments", () => { test("formats comments correctly", () => { const comments: GitHubComment[] = [ { id: "1", databaseId: "100001", body: "First comment", author: { login: "user1" }, createdAt: "2023-01-01T00:00:00Z", }, { id: "2", databaseId: "100002", body: "Second comment", author: { login: "user2" }, createdAt: "2023-01-02T00:00:00Z", }, ]; const result = formatComments(comments); expect(result).toBe( `[user1 at 2023-01-01T00:00:00Z]: First comment\n\n[user2 at 2023-01-02T00:00:00Z]: Second comment`, ); }); test("returns empty string for empty comments array", () => { const result = formatComments([]); expect(result).toBe(""); }); test("replaces image URLs in comments", () => { const comments: GitHubComment[] = [ { id: "1", databaseId: "100001", body: "Check out this screenshot: ![screenshot](https://github.com/user-attachments/assets/screenshot.png)", author: { login: "user1" }, createdAt: "2023-01-01T00:00:00Z", }, { id: "2", databaseId: "100002", body: "Here's another image: ![bug](https://github.com/user-attachments/assets/bug-report.jpg)", author: { login: "user2" }, createdAt: "2023-01-02T00:00:00Z", }, ]; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/screenshot.png", "/tmp/github-images/image-1234-0.png", ], [ "https://github.com/user-attachments/assets/bug-report.jpg", "/tmp/github-images/image-1234-1.jpg", ], ]); const result = formatComments(comments, imageUrlMap); expect(result).toBe( `[user1 at 2023-01-01T00:00:00Z]: Check out this screenshot: ![screenshot](/tmp/github-images/image-1234-0.png)\n\n[user2 at 2023-01-02T00:00:00Z]: Here's another image: ![bug](/tmp/github-images/image-1234-1.jpg)`, ); }); test("handles comments with multiple images", () => { const comments: GitHubComment[] = [ { id: "1", databaseId: "100001", body: "Two images: ![first](https://github.com/user-attachments/assets/first.png) and ![second](https://github.com/user-attachments/assets/second.png)", author: { login: "user1" }, createdAt: "2023-01-01T00:00:00Z", }, ]; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/first.png", "/tmp/github-images/image-1234-0.png", ], [ "https://github.com/user-attachments/assets/second.png", "/tmp/github-images/image-1234-1.png", ], ]); const result = formatComments(comments, imageUrlMap); expect(result).toBe( `[user1 at 2023-01-01T00:00:00Z]: Two images: ![first](/tmp/github-images/image-1234-0.png) and ![second](/tmp/github-images/image-1234-1.png)`, ); }); test("preserves comments when imageUrlMap is undefined", () => { const comments: GitHubComment[] = [ { id: "1", databaseId: "100001", body: "Image: ![test](https://github.com/user-attachments/assets/test.png)", author: { login: "user1" }, createdAt: "2023-01-01T00:00:00Z", }, ]; const result = formatComments(comments); expect(result).toBe( `[user1 at 2023-01-01T00:00:00Z]: Image: ![test](https://github.com/user-attachments/assets/test.png)`, ); }); }); describe("formatReviewComments", () => { test("formats review with body and comments correctly", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300001", author: { login: "reviewer1" }, body: "This is a great PR! LGTM.", state: "APPROVED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [ { id: "comment1", databaseId: "200001", body: "Nice implementation", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/index.ts", line: 42, }, { id: "comment2", databaseId: "200002", body: "Consider adding error handling", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/utils.ts", line: null, }, ], }, }, ], }; const result = formatReviewComments(reviewData); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Nice implementation\n [Comment on src/utils.ts:?]: Consider adding error handling`, ); }); test("formats review with only body (no comments) correctly", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300002", author: { login: "reviewer1" }, body: "Looks good to me!", state: "APPROVED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [], }, }, ], }; const result = formatReviewComments(reviewData); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED`, ); }); test("formats review without body correctly", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300003", author: { login: "reviewer1" }, body: "", state: "COMMENTED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [ { id: "comment1", databaseId: "200003", body: "Small suggestion here", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/main.ts", line: 15, }, ], }, }, ], }; const result = formatReviewComments(reviewData); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: COMMENTED\n [Comment on src/main.ts:15]: Small suggestion here`, ); }); test("formats multiple reviews correctly", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300004", author: { login: "reviewer1" }, body: "Needs changes", state: "CHANGES_REQUESTED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [], }, }, { id: "review2", databaseId: "300005", author: { login: "reviewer2" }, body: "LGTM", state: "APPROVED", submittedAt: "2023-01-02T00:00:00Z", comments: { nodes: [], }, }, ], }; const result = formatReviewComments(reviewData); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: CHANGES_REQUESTED\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: APPROVED`, ); }); test("returns empty string for null reviewData", () => { const result = formatReviewComments(null); expect(result).toBe(""); }); test("returns empty string for empty reviewData", () => { const result = formatReviewComments({ nodes: [] }); expect(result).toBe(""); }); test("replaces image URLs in review comments", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300001", author: { login: "reviewer1" }, body: "Review with image: ![review-img](https://github.com/user-attachments/assets/review.png)", state: "APPROVED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [ { id: "comment1", databaseId: "200001", body: "Comment with image: ![comment-img](https://github.com/user-attachments/assets/comment.png)", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/index.ts", line: 42, }, ], }, }, ], }; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/review.png", "/tmp/github-images/image-1234-0.png", ], [ "https://github.com/user-attachments/assets/comment.png", "/tmp/github-images/image-1234-1.png", ], ]); const result = formatReviewComments(reviewData, imageUrlMap); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Comment with image: ![comment-img](/tmp/github-images/image-1234-1.png)`, ); }); test("handles multiple images in review comments", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300001", author: { login: "reviewer1" }, body: "Good work", state: "APPROVED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [ { id: "comment1", databaseId: "200001", body: "Two issues: ![issue1](https://github.com/user-attachments/assets/issue1.png) and ![issue2](https://github.com/user-attachments/assets/issue2.png)", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/main.ts", line: 15, }, ], }, }, ], }; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/issue1.png", "/tmp/github-images/image-1234-0.png", ], [ "https://github.com/user-attachments/assets/issue2.png", "/tmp/github-images/image-1234-1.png", ], ]); const result = formatReviewComments(reviewData, imageUrlMap); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/main.ts:15]: Two issues: ![issue1](/tmp/github-images/image-1234-0.png) and ![issue2](/tmp/github-images/image-1234-1.png)`, ); }); test("preserves review comments when imageUrlMap is undefined", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300001", author: { login: "reviewer1" }, body: "Review body", state: "APPROVED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [ { id: "comment1", databaseId: "200001", body: "Image: ![test](https://github.com/user-attachments/assets/test.png)", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/index.ts", line: 42, }, ], }, }, ], }; const result = formatReviewComments(reviewData); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Image: ![test](https://github.com/user-attachments/assets/test.png)`, ); }); }); describe("formatChangedFiles", () => { test("formats changed files correctly", () => { const files: GitHubFile[] = [ { path: "src/index.ts", additions: 10, deletions: 5, changeType: "MODIFIED", }, { path: "src/utils.ts", additions: 20, deletions: 0, changeType: "ADDED", }, ]; const result = formatChangedFiles(files); expect(result).toBe( `- src/index.ts (MODIFIED) +10/-5\n- src/utils.ts (ADDED) +20/-0`, ); }); test("returns empty string for empty files array", () => { const result = formatChangedFiles([]); expect(result).toBe(""); }); }); describe("formatChangedFilesWithSHA", () => { test("formats changed files with SHA correctly", () => { const files: GitHubFileWithSHA[] = [ { path: "src/index.ts", additions: 10, deletions: 5, changeType: "MODIFIED", sha: "abc123", }, { path: "src/utils.ts", additions: 20, deletions: 0, changeType: "ADDED", sha: "def456", }, ]; const result = formatChangedFilesWithSHA(files); expect(result).toBe( `- src/index.ts (MODIFIED) +10/-5 SHA: abc123\n- src/utils.ts (ADDED) +20/-0 SHA: def456`, ); }); test("returns empty string for empty files array", () => { const result = formatChangedFilesWithSHA([]); expect(result).toBe(""); }); }); describe("stripHtmlComments", () => { test("strips simple HTML comments", () => { const text = "Hello world"; expect(stripHtmlComments(text)).toBe("Hello world"); }); test("strips multiple HTML comments", () => { const text = "Start middle end"; expect(stripHtmlComments(text)).toBe("Start middle end"); }); test("strips multi-line HTML comments", () => { const text = `Line 1 Line 2`; expect(stripHtmlComments(text)).toBe(`Line 1 Line 2`); }); test("strips nested comment-like content", () => { const text = "Text still in comment --> after"; // HTML doesn't support true nested comments - the first --> ends the comment expect(stripHtmlComments(text)).toBe("Text still in comment --> after"); }); test("handles empty string", () => { expect(stripHtmlComments("")).toBe(""); }); test("handles text without comments", () => { const text = "No comments here!"; expect(stripHtmlComments(text)).toBe("No comments here!"); }); test("strips complex hidden content with XML tags", () => { const text = `Normal request More normal text`; expect(stripHtmlComments(text)).toBe(`Normal request More normal text`); }); test("handles malformed comments - no closing", () => { const text = "Text is not stripped expect(stripHtmlComments(text)).toBe("Text comment"; // Just --> without opening comment"); }); test("preserves legitimate HTML-like content outside comments", () => { const text = "Use the
tag and
closing tag"; expect(stripHtmlComments(text)).toBe( "Use the
tag and
closing tag", ); }); }); describe("formatBody with HTML comment stripping", () => { test("strips HTML comments from body", () => { const body = "Issue description visible text"; const imageUrlMap = new Map(); const result = formatBody(body, imageUrlMap); expect(result).toBe("Issue description visible text"); }); test("strips HTML comments and replaces images", () => { const body = `Check this ![img](https://github.com/user-attachments/assets/test.png)`; const imageUrlMap = new Map([ [ "https://github.com/user-attachments/assets/test.png", "/tmp/github-images/image-1234-0.png", ], ]); const result = formatBody(body, imageUrlMap); expect(result).toBe( "Check this ![img](/tmp/github-images/image-1234-0.png)", ); }); }); describe("formatComments with HTML comment stripping", () => { test("strips HTML comments from comment bodies", () => { const comments: GitHubComment[] = [ { id: "1", databaseId: "100001", body: "Good work on this PR", author: { login: "user1" }, createdAt: "2023-01-01T00:00:00Z", }, ]; const result = formatComments(comments); expect(result).toBe( "[user1 at 2023-01-01T00:00:00Z]: Good work on this PR", ); }); }); describe("formatReviewComments with HTML comment stripping", () => { test("strips HTML comments from review comment bodies", () => { const reviewData = { nodes: [ { id: "review1", databaseId: "300001", author: { login: "reviewer1" }, body: "LGTM", state: "APPROVED", submittedAt: "2023-01-01T00:00:00Z", comments: { nodes: [ { id: "comment1", databaseId: "200001", body: "Nice work here", author: { login: "reviewer1" }, createdAt: "2023-01-01T00:00:00Z", path: "src/index.ts", line: 42, }, ], }, }, ], }; const result = formatReviewComments(reviewData); expect(result).toBe( `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Nice work here`, ); }); });