#!/usr/bin/env bun import { describe, test, expect, beforeEach, afterEach, spyOn, type Mock, } from "bun:test"; import { updateTrackingComment } from "../src/github/operations/comments/update-with-branch"; import { _resetPlatformCache } from "../src/platform/detector"; import { createMockContext } from "./mockContext"; import type { Octokits } from "../src/github/api/client"; function createMockOctokits(overrides: Record = {}): Octokits { // updateTrackingComment calls updateClaudeComment(octokit.rest, ...), // which accesses octokit.rest.issues.updateComment / octokit.rest.pulls.updateReviewComment return { rest: { rest: { issues: { updateComment: overrides.updateComment ?? (async () => ({ data: { id: 1, html_url: "", updated_at: "" } })), }, pulls: { updateReviewComment: overrides.updateReviewComment ?? (async () => ({ data: { id: 1, html_url: "", updated_at: "" } })), }, }, }, graphql: {} as any, } as unknown as Octokits; } describe("updateTrackingComment", () => { let savedPlatform: string | undefined; let savedServerUrl: string | undefined; let savedRunNumber: string | undefined; let savedRunId: string | undefined; let consoleLogSpy: Mock<(...args: any[]) => void>; let consoleErrorSpy: Mock<(...args: any[]) => void>; beforeEach(() => { _resetPlatformCache(); savedPlatform = process.env.PLATFORM; savedServerUrl = process.env.GITHUB_SERVER_URL; savedRunNumber = process.env.GITHUB_RUN_NUMBER; savedRunId = process.env.GITHUB_RUN_ID; // Default to GitHub platform delete process.env.PLATFORM; process.env.GITHUB_SERVER_URL = "https://github.com"; consoleLogSpy = spyOn(console, "log").mockImplementation(() => {}); consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { if (savedPlatform !== undefined) process.env.PLATFORM = savedPlatform; else delete process.env.PLATFORM; if (savedServerUrl !== undefined) process.env.GITHUB_SERVER_URL = savedServerUrl; else delete process.env.GITHUB_SERVER_URL; if (savedRunNumber !== undefined) process.env.GITHUB_RUN_NUMBER = savedRunNumber; else delete process.env.GITHUB_RUN_NUMBER; if (savedRunId !== undefined) process.env.GITHUB_RUN_ID = savedRunId; else delete process.env.GITHUB_RUN_ID; consoleLogSpy.mockRestore(); consoleErrorSpy.mockRestore(); _resetPlatformCache(); }); describe("GitHub platform — body construction", () => { test("includes GitHub job run link using context.runId", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ runId: "55555" }); await updateTrackingComment(mockOctokits, context, 1); expect(capturedBody).toContain("actions/runs/55555"); expect(capturedBody).toContain("https://github.com/test-owner/test-repo"); }); test("includes branch link for issue context", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ isPR: false }); await updateTrackingComment(mockOctokits, context, 1, "claude/fix-123"); expect(capturedBody).toContain("View branch"); expect(capturedBody).toContain("tree/claude/fix-123"); }); test("omits branch link for PR context even when branch provided", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ isPR: true }); await updateTrackingComment(mockOctokits, context, 1, "claude/fix-123"); expect(capturedBody).not.toContain("View branch"); expect(capturedBody).not.toContain("tree/"); }); test("omits branch link when branch is undefined", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ isPR: false }); await updateTrackingComment(mockOctokits, context, 1); expect(capturedBody).not.toContain("View branch"); }); }); describe("Forgejo platform — body construction", () => { beforeEach(() => { _resetPlatformCache(); process.env.PLATFORM = "forgejo"; process.env.GITHUB_SERVER_URL = "https://git.example.com"; process.env.GITHUB_RUN_NUMBER = "77"; }); test("uses Forgejo job run link with GITHUB_RUN_NUMBER", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext(); await updateTrackingComment(mockOctokits, context, 1); expect(capturedBody).toContain( "https://git.example.com/test-owner/test-repo/actions/runs/77", ); }); test("uses Forgejo branch link for issues", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ isPR: false }); await updateTrackingComment(mockOctokits, context, 1, "claude/fix-42"); expect(capturedBody).toContain( "https://git.example.com/test-owner/test-repo/tree/claude/fix-42", ); }); test("uses Forgejo comment body with Unicode spinner", async () => { let capturedBody = ""; const mockOctokits = createMockOctokits({ updateComment: async (args: any) => { capturedBody = args.body; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext(); await updateTrackingComment(mockOctokits, context, 1); expect(capturedBody).toContain("⏳"); expect(capturedBody).not.toContain("user-attachments/assets"); }); }); describe("comment type routing", () => { test("uses issue comment update for non-review events", async () => { let issueUpdateCalled = false; let reviewUpdateCalled = false; const mockOctokits = createMockOctokits({ updateComment: async () => { issueUpdateCalled = true; return { data: { id: 1, html_url: "", updated_at: "" } }; }, updateReviewComment: async () => { reviewUpdateCalled = true; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ eventName: "issue_comment" }); await updateTrackingComment(mockOctokits, context, 1); expect(issueUpdateCalled).toBe(true); expect(reviewUpdateCalled).toBe(false); }); test("uses PR review comment update for review comment events", async () => { let issueUpdateCalled = false; let reviewUpdateCalled = false; const mockOctokits = createMockOctokits({ updateComment: async () => { issueUpdateCalled = true; return { data: { id: 1, html_url: "", updated_at: "" } }; }, updateReviewComment: async () => { reviewUpdateCalled = true; return { data: { id: 1, html_url: "", updated_at: "" } }; }, }); const context = createMockContext({ eventName: "pull_request_review_comment", isPR: true, }); await updateTrackingComment(mockOctokits, context, 1); expect(reviewUpdateCalled).toBe(true); expect(issueUpdateCalled).toBe(false); }); }); describe("error handling", () => { test("throws and logs error when update fails", async () => { const mockOctokits = createMockOctokits({ updateComment: async () => { throw new Error("Update failed"); }, }); const context = createMockContext(); await expect( updateTrackingComment(mockOctokits, context, 1), ).rejects.toThrow("Update failed"); expect(consoleErrorSpy).toHaveBeenCalled(); }); test("logs correct comment type label on success", async () => { const mockOctokits = createMockOctokits(); const issueContext = createMockContext({ eventName: "issue_comment" }); await updateTrackingComment(mockOctokits, issueContext, 42); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining("issue"), ); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining("42")); }); }); });