import {getOctokit} from "@actions/github" import * as core from "@actions/core" import { vi, describe, it, expect, beforeEach } from 'vitest' import { createComment, deleteComment, findPreviousComment, getBodyOf, updateComment, minimizeComment, commentsEqual } from "../src/comment" vi.mock("@actions/core", () => ({ warning: vi.fn() })) const repo = { owner: "marocchino", repo: "sticky-pull-request-comment" } it("findPreviousComment", async () => { const authenticatedBotUser = { login: "github-actions[bot]" } const authenticatedUser = { login: "github-actions" } const otherUser = { login: "some-user" } const comment = { id: "1", author: authenticatedUser, isMinimized: false, body: "previous message\n" } const commentWithCustomHeader = { id: "2", author: authenticatedUser, isMinimized: false, body: "previous message\n" } const headerFirstComment = { id: "3", author: authenticatedUser, isMinimized: false, body: "\nheader first message" } const otherUserComment = { id: "4", author: otherUser, isMinimized: false, body: "Fake previous message\n" } const otherComments = [ { id: "5", author: otherUser, isMinimized: false, body: "lgtm" }, { id: "6", author: authenticatedUser, isMinimized: false, body: "previous message\n" } ] const octokit = getOctokit("github-token") vi.spyOn(octokit, "graphql").mockResolvedValue({ viewer: authenticatedBotUser, repository: { pullRequest: { comments: { nodes: [ commentWithCustomHeader, otherUserComment, comment, headerFirstComment, ...otherComments ], pageInfo: { hasNextPage: false, endCursor: "6" } } } } } as any) expect(await findPreviousComment(octokit, repo, 123, "")).toBe(comment) expect(await findPreviousComment(octokit, repo, 123, "TypeA")).toBe(commentWithCustomHeader) expect(await findPreviousComment(octokit, repo, 123, "LegacyComment")).toBe(headerFirstComment) expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { after: null, number: 123, owner: "marocchino", repo: "sticky-pull-request-comment" }) }) describe("findPreviousComment edge cases", () => { const octokit = getOctokit("github-token") const authenticatedBotUser = { login: "github-actions[bot]" } beforeEach(() => { // Reset the spy/mock before each test in this describe block vi.spyOn(octokit, "graphql").mockReset(); }); it("should return undefined if pullRequest is null", async () => { vi.spyOn(octokit, "graphql").mockResolvedValue({ viewer: authenticatedBotUser, repository: { pullRequest: null } } as any) expect(await findPreviousComment(octokit, repo, 123, "")).toBeUndefined() }) it("should return undefined if comments.nodes is null or empty", async () => { vi.spyOn(octokit, "graphql").mockResolvedValueOnce({ viewer: authenticatedBotUser, repository: { pullRequest: { comments: { nodes: null, pageInfo: {hasNextPage: false, endCursor: null} } } } } as any) expect(await findPreviousComment(octokit, repo, 123, "")).toBeUndefined() vi.spyOn(octokit, "graphql").mockResolvedValueOnce({ viewer: authenticatedBotUser, repository: { pullRequest: { comments: { nodes: [], pageInfo: {hasNextPage: false, endCursor: null} } } } } as any) expect(await findPreviousComment(octokit, repo, 123, "")).toBeUndefined() }) it("should handle pagination correctly", async () => { const commentInPage2 = { id: "page2-comment", author: { login: "github-actions" }, isMinimized: false, body: "Comment from page 2\n" } const graphqlMockFn = vi.fn() .mockResolvedValueOnce({ viewer: authenticatedBotUser, repository: { pullRequest: { comments: { nodes: [{ id: "page1-comment", author: { login: "github-actions" } , isMinimized: false, body: "Page 1\n" }], pageInfo: { hasNextPage: true, endCursor: "cursor1" } } } } } as any) .mockResolvedValueOnce({ viewer: authenticatedBotUser, repository: { pullRequest: { comments: { nodes: [commentInPage2], pageInfo: { hasNextPage: false, endCursor: "cursor2" } } } } } as any) vi.spyOn(octokit, "graphql").mockImplementation(graphqlMockFn) const foundComment = await findPreviousComment(octokit, repo, 123, "Page2Test") expect(foundComment).toEqual(commentInPage2); expect(graphqlMockFn).toHaveBeenCalledTimes(2) expect(graphqlMockFn).toHaveBeenNthCalledWith(1, expect.any(String), expect.objectContaining({ after: null })) expect(graphqlMockFn).toHaveBeenNthCalledWith(2, expect.any(String), expect.objectContaining({ after: "cursor1" })) }) it("should find comment by non-bot author when viewer is bot", async () => { const userAuthor = { login: "real-user" }; const targetComment = { id: "user-comment-id", author: userAuthor, isMinimized: false, body: "A comment by a real user\n" }; vi.spyOn(octokit, "graphql").mockResolvedValue({ viewer: authenticatedBotUser, repository: { pullRequest: { comments: { nodes: [targetComment], pageInfo: { hasNextPage: false, endCursor: null } } } } } as any); // Corrected expectation: The function should NOT find a comment by a different author // if the viewer is the bot and the comment author is not the bot or the user equivalent of the bot. const result = await findPreviousComment(octokit, repo, 123, "UserAuthored"); expect(result).toBeUndefined(); }); }) describe("updateComment", () => { const octokit = getOctokit("github-token") beforeEach(() => { vi.spyOn(octokit, "graphql").mockReset().mockResolvedValue({ updateIssueComment: { issueComment: { id: "456" } } } as any); }) it("with new body and previous body (old content)", async () => { await updateComment(octokit, "456", "new content", "TestHeader", "old content") expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { input: { id: "456", body: "old content\nnew content\n" } }) }) it("with empty new body and previous body (old content)", async () => { await updateComment(octokit, "456", "", "TestHeader", "old content") expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { input: { id: "456", body: "old content\n\n" } }) }) it("with comment body (no previous body)", async () => { await updateComment(octokit, "456", "hello there", "") expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { input: { id: "456", body: "hello there\n" } }) await updateComment(octokit, "456", "hello there", "TypeA") expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { input: { id: "456", body: "hello there\n" } }) }) it("without comment body and without previous body (should warn)", async () => { await updateComment(octokit, "456", "", "", "") expect(octokit.graphql).not.toHaveBeenCalled() expect(core.warning).toHaveBeenCalledWith("Comment body cannot be blank") }) }) describe("createComment", () => { const octokit = getOctokit("github-token") beforeEach(() => { vi.spyOn(octokit.rest.issues, "createComment").mockReset().mockResolvedValue({ data: { id: 789, html_url: "created_url" } } as any) }) it("with new body and previous body (old content) - no header re-added", async () => { await createComment(octokit, repo, 456, "new message", "TestHeader", "previous message content") expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ issue_number: 456, owner: "marocchino", repo: "sticky-pull-request-comment", body: "previous message content\nnew message" }) }) it("with empty new body and previous body (old content) - no header re-added", async () => { await createComment(octokit, repo, 456, "", "TestHeader", "previous message content") expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ issue_number: 456, owner: "marocchino", repo: "sticky-pull-request-comment", body: "previous message content\n" }) }) it("with comment body only (no previousBody - header is added)", async () => { await createComment(octokit, repo, 456, "hello there", "") expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ issue_number: 456, owner: "marocchino", repo: "sticky-pull-request-comment", body: "hello there\n" }) await createComment(octokit, repo, 456, "hello there", "TypeA") expect(octokit.rest.issues.createComment).toHaveBeenCalledWith({ issue_number: 456, owner: "marocchino", repo: "sticky-pull-request-comment", body: "hello there\n" }) }) it("without comment body and without previousBody (should warn)", async () => { await createComment(octokit, repo, 456, "", "", "") expect(octokit.rest.issues.createComment).not.toHaveBeenCalled() expect(core.warning).toHaveBeenCalledWith("Comment body cannot be blank") }) }) it("deleteComment", async () => { const octokit = getOctokit("github-token") vi.spyOn(octokit, "graphql").mockReset().mockResolvedValue(undefined as any) await deleteComment(octokit, "456") expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { input: { id: "456" } }) }) it("minimizeComment", async () => { const octokit = getOctokit("github-token") vi.spyOn(octokit, "graphql").mockReset().mockResolvedValue(undefined as any) await minimizeComment(octokit, "456", "OUTDATED") expect(octokit.graphql).toHaveBeenCalledWith(expect.any(String), { input: { subjectId: "456", classifier: "OUTDATED" } }) }) describe("getBodyOf", () => { const nullPrevious = {} const simplePrevious = { body: "hello there\n" } const detailsPrevious = { body: `
title content
` } const replaced = `
title content
` it.each` append | hideDetails | previous | expected ${false} | ${false} | ${detailsPrevious} | ${undefined} ${true} | ${false} | ${nullPrevious} | ${undefined} ${true} | ${false} | ${detailsPrevious} | ${detailsPrevious.body} ${true} | ${true} | ${nullPrevious} | ${undefined} ${true} | ${true} | ${simplePrevious} | ${simplePrevious.body} ${true} | ${true} | ${detailsPrevious} | ${replaced} `( "receive $previous, $append, $hideDetails and returns $expected", ({append, hideDetails, previous, expected}) => { expect(getBodyOf(previous, append, hideDetails)).toEqual(expected) } ) }) describe("commentsEqual", () => { it.each([ { body: "body", previous: "body\n", header: "header", expected: true }, { body: "body", previous: "body\n", header: "", expected: true }, { body: "body", previous: "body\n", header: "header", expected: false }, {body: "body", previous: "body", header: "header", expected: false}, {body: "body", previous: "", header: "header", expected: false}, {body: "", previous: "body", header: "header", expected: false} ])("commentsEqual(%s, %s, %s)", ({body, previous, header, expected}) => { expect(commentsEqual(body, previous, header)).toEqual(expected) }) })