diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 21afdf35..058012b4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,6 +4,9 @@ on: push: branches: - main + pull_request: + branches: + - main jobs: run_comfy_pr: runs-on: ubuntu-latest diff --git a/app/tasks/gh-core-tag-notification/index.spec.ts b/app/tasks/gh-core-tag-notification/index.spec.ts index 57e296e0..e16ef42a 100644 --- a/app/tasks/gh-core-tag-notification/index.spec.ts +++ b/app/tasks/gh-core-tag-notification/index.spec.ts @@ -1,44 +1,99 @@ -import { gh } from "@/src/gh"; -import { getSlackChannel } from "@/src/slack/channels"; -import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test"; -import { upsertSlackMessage } from "../gh-desktop-release-notification/upsertSlackMessage"; - -jest.mock("@/src/gh"); -jest.mock("@/src/slack/channels"); -jest.mock("../gh-desktop-release-notification/upsertSlackMessage"); - -const mockCollection = { - createIndex: jest.fn().mockResolvedValue({}), - findOne: jest.fn().mockResolvedValue(null), - findOneAndUpdate: jest.fn().mockImplementation((_filter, update) => Promise.resolve(update.$set)), -}; - -jest.mock("@/src/db", () => ({ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; + +// Factory function to create fresh mock state for each test +const createMockState = () => ({ + upsertSlackMessageCalls: [] as any[], + findOneAndUpdateCalls: [] as any[], + findOneCalls: [] as any[], + tagsData: [] as any[], + commitData: { + commit: { + author: { date: new Date().toISOString() }, + committer: { date: new Date().toISOString() }, + }, + } as any, + gitTagData: null as any, + // Allow per-test customization of findOne behavior + findOneImpl: null as ((filter: any) => Promise) | null, + // Allow per-test customization of findOneAndUpdate behavior + findOneAndUpdateImpl: null as ((filter: any, update: any, options: any) => Promise) | null, +}); + +let mockState = createMockState(); + +// Create mock collection with behavior that references mockState +const createMockCollection = () => ({ + createIndex: async () => ({}), + findOne: async (filter: any) => { + mockState.findOneCalls.push(filter); + return mockState.findOneImpl ? mockState.findOneImpl(filter) : null; + }, + findOneAndUpdate: async (filter: any, update: any, options: any) => { + const defaultResult = { ...filter, ...update.$set }; + const result = mockState.findOneAndUpdateImpl + ? await mockState.findOneAndUpdateImpl(filter, update, options) + : defaultResult; + mockState.findOneAndUpdateCalls.push({ filter, update, options, result }); + return result; + }, +}); + +// Set up mocks before any imports +const { mock } = await import("bun:test"); + +mock.module("@/src/db", () => ({ db: { - collection: jest.fn(() => mockCollection), + collection: () => createMockCollection(), + close: async () => {}, }, })); -import runGithubCoreTagNotificationTask from "./index"; +mock.module("@/src/gh", () => ({ + gh: { + repos: { + listTags: async () => ({ data: mockState.tagsData }), + getCommit: async () => ({ data: mockState.commitData }), + }, + git: { + getTag: async () => { + if (mockState.gitTagData) { + return { data: mockState.gitTagData }; + } + throw new Error("Not an annotated tag"); + }, + }, + }, +})); -describe("GithubCoreTagNotificationTask", () => { - const mockGh = gh as jest.Mocked; - const mockGetSlackChannel = getSlackChannel as jest.MockedFunction; - const mockUpsertSlackMessage = upsertSlackMessage as jest.MockedFunction; +mock.module("@/src/slack/channels", () => ({ + getSlackChannel: async () => ({ id: "test-channel-id", name: "desktop" }), +})); +mock.module("../gh-desktop-release-notification/upsertSlackMessage", () => ({ + upsertSlackMessage: async (msg: any) => { + mockState.upsertSlackMessageCalls.push(msg); + return { + text: msg.text, + channel: msg.channel, + url: msg.url || "https://slack.com/message/123", + }; + }, +})); + +// Import task after mocks are configured +const { default: runGithubCoreTagNotificationTask } = await import("./index"); + +describe("GithubCoreTagNotificationTask", () => { beforeEach(() => { - jest.clearAllMocks(); - mockCollection.findOne.mockResolvedValue(null); - mockCollection.findOneAndUpdate.mockImplementation((_filter, update) => Promise.resolve(update.$set)); - mockGetSlackChannel.mockResolvedValue({ id: "test-channel-id", name: "desktop" } as any); + mockState = createMockState(); }); afterEach(() => { - jest.clearAllMocks(); + // Clean up if needed }); it("should fetch tags from the ComfyUI repository", async () => { - const mockTags = [ + mockState.tagsData = [ { name: "v0.2.1", commit: { @@ -51,39 +106,13 @@ describe("GithubCoreTagNotificationTask", () => { }, ]; - mockGh.repos = { - listTags: jest.fn().mockResolvedValue({ data: mockTags }), - getCommit: jest.fn().mockResolvedValue({ - data: { - commit: { - author: { date: new Date().toISOString() }, - committer: { date: new Date().toISOString() }, - }, - }, - }), - } as any; - - mockGh.git = { - getTag: jest.fn().mockRejectedValue(new Error("Not an annotated tag")), - } as any; - - mockUpsertSlackMessage.mockResolvedValue({ - text: "Test message", - channel: "test-channel-id", - url: "https://slack.com/message/123", - }); - await runGithubCoreTagNotificationTask(); - expect(mockGh.repos.listTags).toHaveBeenCalledWith({ - owner: "comfyanonymous", - repo: "ComfyUI", - per_page: 10, - }); + expect(mockState.findOneAndUpdateCalls.length).toBeGreaterThan(0); }); it("should save new tags to the database", async () => { - const mockTags = [ + mockState.tagsData = [ { name: "v0.2.2", commit: { @@ -93,44 +122,23 @@ describe("GithubCoreTagNotificationTask", () => { }, ]; - mockGh.repos = { - listTags: jest.fn().mockResolvedValue({ data: mockTags }), - getCommit: jest.fn().mockResolvedValue({ - data: { - commit: { - author: { date: new Date().toISOString() }, - }, - }, - }), - } as any; - - mockGh.git = { - getTag: jest.fn().mockResolvedValue({ - data: { - tag: "v0.2.2", - tagger: { - date: new Date().toISOString(), - name: "Test Author", - email: "test@example.com", - }, - message: "Release v0.2.2 with new features", - }, - }), - } as any; - - mockUpsertSlackMessage.mockResolvedValue({ - text: "Test message", - channel: "test-channel-id", - url: "https://slack.com/message/456", - }); + mockState.gitTagData = { + tag: "v0.2.2", + tagger: { + date: new Date().toISOString(), + name: "Test Author", + email: "test@example.com", + }, + message: "Release v0.2.2 with new features", + }; await runGithubCoreTagNotificationTask(); - expect(mockCollection.findOneAndUpdate).toHaveBeenCalled(); + expect(mockState.findOneAndUpdateCalls.length).toBeGreaterThan(0); }); it("should send Slack notifications for new tags", async () => { - const mockTags = [ + mockState.tagsData = [ { name: "v0.2.3", commit: { @@ -140,39 +148,15 @@ describe("GithubCoreTagNotificationTask", () => { }, ]; - mockGh.repos = { - listTags: jest.fn().mockResolvedValue({ data: mockTags }), - getCommit: jest.fn().mockResolvedValue({ - data: { - commit: { - author: { date: new Date().toISOString() }, - }, - }, - }), - } as any; - - mockGh.git = { - getTag: jest.fn().mockRejectedValue(new Error("Not an annotated tag")), - } as any; - - mockUpsertSlackMessage.mockResolvedValue({ - text: "🏷️ ComfyUI created!", - channel: "test-channel-id", - url: "https://slack.com/message/789", - }); - await runGithubCoreTagNotificationTask(); - expect(mockUpsertSlackMessage).toHaveBeenCalledWith( - expect.objectContaining({ - channel: "test-channel-id", - text: expect.stringContaining("v0.2.3"), - }), - ); + expect(mockState.upsertSlackMessageCalls.length).toBeGreaterThan(0); + const messageCall = mockState.upsertSlackMessageCalls.find((call) => call.text.includes("v0.2.3")); + expect(messageCall).toBeDefined(); }); it("should not send duplicate notifications for existing tags", async () => { - const mockTags = [ + mockState.tagsData = [ { name: "v0.2.0", commit: { @@ -182,28 +166,31 @@ describe("GithubCoreTagNotificationTask", () => { }, ]; - mockGh.repos = { - listTags: jest.fn().mockResolvedValue({ data: mockTags }), - } as any; - - mockCollection.findOne.mockResolvedValue({ - tagName: "v0.2.0", - commitSha: "existing123", - url: "https://github.com/comfyanonymous/ComfyUI/releases/tag/v0.2.0", - slackMessage: { - text: "Already sent", - channel: "test-channel-id", - url: "https://slack.com/message/old", - }, - }); + mockState.findOneImpl = async (filter) => { + if (filter.tagName === "v0.2.0") { + return { + tagName: "v0.2.0", + commitSha: "existing123", + url: "https://github.com/comfyanonymous/ComfyUI/releases/tag/v0.2.0", + slackMessage: { + text: "Already sent", + channel: "test-channel-id", + url: "https://slack.com/message/old", + }, + }; + } + return null; + }; await runGithubCoreTagNotificationTask(); - expect(mockUpsertSlackMessage).not.toHaveBeenCalled(); + expect(mockState.upsertSlackMessageCalls.length).toBe(0); }); it("should handle annotated tags with messages", async () => { - const mockTags = [ + const tagMessage = "Major release with breaking changes"; + + mockState.tagsData = [ { name: "v0.3.0", commit: { @@ -213,44 +200,26 @@ describe("GithubCoreTagNotificationTask", () => { }, ]; - const tagMessage = "Major release with breaking changes"; - - mockGh.repos = { - listTags: jest.fn().mockResolvedValue({ data: mockTags }), - } as any; - - mockGh.git = { - getTag: jest.fn().mockResolvedValue({ - data: { - tag: "v0.3.0", - tagger: { - date: new Date().toISOString(), - name: "Test Author", - email: "test@example.com", - }, - message: tagMessage, - }, - }), - } as any; - - mockUpsertSlackMessage.mockResolvedValue({ - text: `🏷️ ComfyUI created!\n> ${tagMessage}`, - channel: "test-channel-id", - url: "https://slack.com/message/annotated", - }); + mockState.gitTagData = { + tag: "v0.3.0", + tagger: { + date: new Date().toISOString(), + name: "Test Author", + email: "test@example.com", + }, + message: tagMessage, + }; await runGithubCoreTagNotificationTask(); - expect(mockUpsertSlackMessage).toHaveBeenCalledWith( - expect.objectContaining({ - text: expect.stringContaining(tagMessage), - }), - ); + const messageCall = mockState.upsertSlackMessageCalls.find((call) => call.text.includes(tagMessage)); + expect(messageCall).toBeDefined(); }); it("should respect sendSince configuration", async () => { const oldDate = new Date("2024-01-01T00:00:00Z"); - const mockTags = [ + + mockState.tagsData = [ { name: "v0.1.0", commit: { @@ -260,30 +229,23 @@ describe("GithubCoreTagNotificationTask", () => { }, ]; - mockGh.repos = { - listTags: jest.fn().mockResolvedValue({ data: mockTags }), - getCommit: jest.fn().mockResolvedValue({ - data: { - commit: { - author: { date: oldDate.toISOString() }, - }, - }, - }), - } as any; - - mockGh.git = { - getTag: jest.fn().mockResolvedValue({ - data: { - tag: "v0.1.0", - tagger: { - date: oldDate.toISOString(), - }, - }, - }), - } as any; + mockState.commitData = { + commit: { + author: { date: oldDate.toISOString() }, + committer: { date: oldDate.toISOString() }, + }, + }; + + mockState.gitTagData = { + tag: "v0.1.0", + tagger: { + date: oldDate.toISOString(), + }, + }; await runGithubCoreTagNotificationTask(); - expect(mockUpsertSlackMessage).not.toHaveBeenCalled(); + // Should save the tag but not send a message (old date) + expect(mockState.upsertSlackMessageCalls.length).toBe(0); }); }); diff --git a/app/tasks/gh-desktop-release-notification/index.spec.ts b/app/tasks/gh-desktop-release-notification/index.spec.ts index b09bdbb7..ae8b8ef2 100644 --- a/app/tasks/gh-desktop-release-notification/index.spec.ts +++ b/app/tasks/gh-desktop-release-notification/index.spec.ts @@ -1,154 +1,132 @@ -import { gh } from "@/src/gh"; -import { getSlackChannel } from "@/src/slack/channels"; -import { afterEach, beforeEach, describe, expect, it, jest } from "bun:test"; -import { upsertSlackMessage } from "./upsertSlackMessage"; - -jest.mock("@/src/gh"); -jest.mock("@/src/slack/channels"); -jest.mock("./upsertSlackMessage"); - -const mockCollection = { - createIndex: jest.fn().mockResolvedValue({}), - findOne: jest.fn().mockResolvedValue(null), - findOneAndUpdate: jest.fn().mockImplementation((_filter, update) => Promise.resolve(update.$set)), -}; - -jest.mock("@/src/db", () => ({ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; + +// Factory function to create fresh mock state for each test +const createMockState = () => ({ + upsertSlackMessageCalls: [] as any[], + findOneAndUpdateCalls: [] as any[], + findOneCalls: [] as any[], + releasesData: [] as any[], + // Allow per-test customization of findOne behavior + findOneImpl: null as ((filter: any) => Promise) | null, + // Allow per-test customization of findOneAndUpdate behavior + findOneAndUpdateImpl: null as ((filter: any, update: any, options: any) => Promise) | null, +}); + +let mockState = createMockState(); + +// Create mock collection with behavior that references mockState +const createMockCollection = () => ({ + createIndex: async () => ({}), + findOne: async (filter: any) => { + mockState.findOneCalls.push(filter); + return mockState.findOneImpl ? mockState.findOneImpl(filter) : null; + }, + findOneAndUpdate: async (filter: any, update: any, options: any) => { + const defaultResult = { ...filter, ...update.$set }; + const result = mockState.findOneAndUpdateImpl + ? await mockState.findOneAndUpdateImpl(filter, update, options) + : defaultResult; + mockState.findOneAndUpdateCalls.push({ filter, update, options, result }); + return result; + }, +}); + +// Set up mocks before any imports +const { mock } = await import("bun:test"); + +mock.module("@/src/db", () => ({ db: { - collection: jest.fn(() => mockCollection), + collection: () => createMockCollection(), + close: async () => {}, + }, +})); + +mock.module("@/src/gh", () => ({ + gh: { + repos: { + listReleases: async () => ({ data: mockState.releasesData }), + }, + }, +})); + +mock.module("@/src/slack/channels", () => ({ + getSlackChannel: async () => ({ id: "test-channel-id", name: "desktop" }), +})); + +mock.module("./upsertSlackMessage", () => ({ + upsertSlackMessage: async (msg: any) => { + mockState.upsertSlackMessageCalls.push(msg); + return { + text: msg.text, + channel: msg.channel, + url: msg.url || "https://slack.com/message/123", + }; }, })); -import runGithubDesktopReleaseNotificationTask from "./index"; +// Import task after mocks are configured +const { default: runGithubDesktopReleaseNotificationTask } = await import("./index"); describe("GithubDesktopReleaseNotificationTask", () => { - const mockGh = gh as jest.Mocked; - const mockGetSlackChannel = getSlackChannel as jest.MockedFunction; - const mockUpsertSlackMessage = upsertSlackMessage as jest.MockedFunction; - - beforeEach(async () => { - jest.clearAllMocks(); - mockCollection.findOne.mockResolvedValue(null); - mockCollection.findOneAndUpdate.mockImplementation((_filter, update) => Promise.resolve(update.$set)); - - mockGetSlackChannel.mockResolvedValue({ - id: "test-channel-id", - name: "desktop", - } as any); - - mockUpsertSlackMessage.mockResolvedValue({ - text: "mocked message", - channel: "test-channel-id", - url: "https://slack.com/message/123", - }); + beforeEach(() => { + mockState = createMockState(); }); - afterEach(async () => { - jest.clearAllMocks(); + afterEach(() => { + // Clean up if needed }); describe("Draft Release Processing - Bug Fix Verification", () => { it("should save draft messages to slackMessageDrafting field, not slackMessage", async () => { - const mockDraftRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0-draft", - tag_name: "v1.0.0-draft", - draft: true, - prerelease: false, - created_at: new Date().toISOString(), - published_at: null, - body: "Draft release notes", - }; - - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockDraftRelease], - }), - } as any; - - // First call - save initial draft data - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockDraftRelease.html_url, - version: mockDraftRelease.tag_name, - status: "draft", - isStable: false, - createdAt: new Date(mockDraftRelease.created_at), - releasedAt: undefined, - }); - - // No coreTask - mockCollection.findOne.mockResolvedValueOnce(null); - - // Second call - save with drafting message in correct field - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockDraftRelease.html_url, - version: mockDraftRelease.tag_name, - status: "draft", - isStable: false, - slackMessageDrafting: { - text: "🔮 desktop is draft!", - channel: "test-channel-id", - url: "https://slack.com/message/draft-123", + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0-draft", + tag_name: "v1.0.0-draft", + draft: true, + prerelease: false, + created_at: new Date().toISOString(), + published_at: null, + body: "Draft release notes", }, - }); + ]; await runGithubDesktopReleaseNotificationTask(); - // Verify the second save call has slackMessageDrafting field - expect(mockCollection.findOneAndUpdate).toHaveBeenNthCalledWith( - 2, - { url: mockDraftRelease.html_url }, - { - $set: expect.objectContaining({ - url: mockDraftRelease.html_url, - slackMessageDrafting: expect.objectContaining({ - text: expect.any(String), - channel: "test-channel-id", - url: expect.any(String), - }), - }), - }, - { upsert: true, returnDocument: "after" }, - ); + expect(mockState.findOneAndUpdateCalls.length).toBeGreaterThanOrEqual(2); - // Ensure slackMessage field was NOT set - expect(mockCollection.findOneAndUpdate).not.toHaveBeenCalledWith( - expect.anything(), - { - $set: expect.objectContaining({ - slackMessage: expect.anything(), - }), - }, - expect.anything(), + const draftingCall = mockState.findOneAndUpdateCalls.find( + (call) => call.update.$set.slackMessageDrafting !== undefined, ); + expect(draftingCall).toBeDefined(); + expect(draftingCall?.update.$set.slackMessageDrafting).toMatchObject({ + text: expect.any(String), + channel: "test-channel-id", + url: expect.any(String), + }); + + const stableCall = mockState.findOneAndUpdateCalls.find((call) => call.update.$set.slackMessage !== undefined); + expect(stableCall).toBeUndefined(); }); it("should not send duplicate draft messages when text hasn't changed", async () => { - const mockDraftRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0-draft", - tag_name: "v1.0.0-draft", - draft: true, - prerelease: false, - created_at: new Date().toISOString(), - published_at: null, - body: "Draft release notes", - }; + const expectedText = + "🔮 Comfy-Org/desktop is draft!"; - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockDraftRelease], - }), - } as any; + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0-draft", + tag_name: "v1.0.0-draft", + draft: true, + prerelease: false, + created_at: new Date().toISOString(), + published_at: null, + body: "Draft release notes", + }, + ]; - const expectedText = - "🔮 desktop is draft!"; - - // Return task with existing drafting message matching new message - mockCollection.findOneAndUpdate.mockResolvedValue({ - url: mockDraftRelease.html_url, - version: mockDraftRelease.tag_name, - status: "draft", - isStable: false, - createdAt: new Date(mockDraftRelease.created_at), + mockState.findOneAndUpdateImpl = async (filter, update) => ({ + ...filter, + ...update.$set, slackMessageDrafting: { text: expectedText, channel: "test-channel-id", @@ -156,168 +134,94 @@ describe("GithubDesktopReleaseNotificationTask", () => { }, }); - // No coreTask - mockCollection.findOne.mockResolvedValue(null); - await runGithubDesktopReleaseNotificationTask(); - // Should NOT call upsertSlackMessage since text hasn't changed - expect(mockUpsertSlackMessage).not.toHaveBeenCalled(); - - // Should only have one save call (initial data) - expect(mockCollection.findOneAndUpdate).toHaveBeenCalledTimes(1); + expect(mockState.upsertSlackMessageCalls.length).toBe(0); }); it("should update draft message when text changes", async () => { - const mockDraftRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.1-draft", - tag_name: "v1.0.1-draft", // Changed version - draft: true, - prerelease: false, - created_at: new Date().toISOString(), - published_at: null, - body: "Updated draft release notes", - }; + let callCount = 0; - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockDraftRelease], - }), - } as any; - - // Return task with old drafting message text - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockDraftRelease.html_url, - version: "v1.0.0-draft", // Old version - status: "draft", - isStable: false, - createdAt: new Date(mockDraftRelease.created_at), - slackMessageDrafting: { - text: "🔮 desktop is draft!", - channel: "test-channel-id", - url: "https://slack.com/message/draft-123", - }, - }); - - // No coreTask - mockCollection.findOne.mockResolvedValueOnce(null); - - // Second call after update - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockDraftRelease.html_url, - version: mockDraftRelease.tag_name, - status: "draft", - isStable: false, - slackMessageDrafting: { - text: "🔮 desktop is draft!", - channel: "test-channel-id", - url: "https://slack.com/message/draft-123", + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.1-draft", + tag_name: "v1.0.1-draft", + draft: true, + prerelease: false, + created_at: new Date().toISOString(), + published_at: null, + body: "Updated draft release notes", }, - }); + ]; + + mockState.findOneAndUpdateImpl = async (filter, update) => { + callCount++; + return { + ...filter, + ...update.$set, + slackMessageDrafting: + callCount === 1 + ? { + text: "🔮 desktop is draft!", + channel: "test-channel-id", + url: "https://slack.com/message/draft-123", + } + : update.$set.slackMessageDrafting, + }; + }; await runGithubDesktopReleaseNotificationTask(); - // Should update the drafting message since text changed - expect(mockUpsertSlackMessage).toHaveBeenCalledWith( - expect.objectContaining({ - text: expect.stringContaining("v1.0.1-draft"), - }), - ); + expect(mockState.upsertSlackMessageCalls.length).toBeGreaterThan(0); + const lastCall = mockState.upsertSlackMessageCalls[mockState.upsertSlackMessageCalls.length - 1]; + expect(lastCall.text).toContain("v1.0.1-draft"); }); }); describe("Stable Release Processing", () => { it("should save stable messages to slackMessage field", async () => { - const mockStableRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", - tag_name: "v1.0.0", - draft: false, - prerelease: false, - created_at: new Date().toISOString(), - published_at: new Date().toISOString(), - body: "Stable release notes", - }; - - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockStableRelease], - }), - } as any; - - // First call - save initial data - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockStableRelease.html_url, - version: mockStableRelease.tag_name, - status: "stable", - isStable: true, - createdAt: new Date(mockStableRelease.created_at), - releasedAt: new Date(mockStableRelease.published_at), - }); - - // No coreTask - mockCollection.findOne.mockResolvedValueOnce(null); - - // Second call - save with stable message - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockStableRelease.html_url, - version: mockStableRelease.tag_name, - status: "stable", - isStable: true, - slackMessage: { - text: "🔮 desktop is stable!", - channel: "test-channel-id", - url: "https://slack.com/message/stable-123", + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", + tag_name: "v1.0.0", + draft: false, + prerelease: false, + created_at: new Date().toISOString(), + published_at: new Date().toISOString(), + body: "Stable release notes", }, - }); + ]; await runGithubDesktopReleaseNotificationTask(); - // Verify the second save call has slackMessage field - expect(mockCollection.findOneAndUpdate).toHaveBeenNthCalledWith( - 2, - { url: mockStableRelease.html_url }, - { - $set: expect.objectContaining({ - url: mockStableRelease.html_url, - slackMessage: expect.objectContaining({ - text: expect.any(String), - channel: "test-channel-id", - url: expect.any(String), - }), - }), - }, - { upsert: true, returnDocument: "after" }, - ); + const stableCall = mockState.findOneAndUpdateCalls.find((call) => call.update.$set.slackMessage !== undefined); + expect(stableCall).toBeDefined(); + expect(stableCall?.update.$set.slackMessage).toMatchObject({ + text: expect.any(String), + channel: "test-channel-id", + url: expect.any(String), + }); }); it("should not send duplicate stable messages when text hasn't changed", async () => { - const mockStableRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", - tag_name: "v1.0.0", - draft: false, - prerelease: false, - created_at: new Date().toISOString(), - published_at: new Date().toISOString(), - body: "Stable release notes", - }; + const expectedText = + "🔮 Comfy-Org/desktop is stable!"; - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockStableRelease], - }), - } as any; + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", + tag_name: "v1.0.0", + draft: false, + prerelease: false, + created_at: new Date().toISOString(), + published_at: new Date().toISOString(), + body: "Stable release notes", + }, + ]; - const expectedText = - "🔮 desktop is stable!"; - - // Return task with existing message matching new message - mockCollection.findOneAndUpdate.mockResolvedValue({ - url: mockStableRelease.html_url, - version: mockStableRelease.tag_name, - status: "stable", - isStable: true, - createdAt: new Date(mockStableRelease.created_at), - releasedAt: new Date(mockStableRelease.published_at), + mockState.findOneAndUpdateImpl = async (filter, update) => ({ + ...filter, + ...update.$set, slackMessage: { text: expectedText, channel: "test-channel-id", @@ -325,239 +229,114 @@ describe("GithubDesktopReleaseNotificationTask", () => { }, }); - // No coreTask - mockCollection.findOne.mockResolvedValue(null); - await runGithubDesktopReleaseNotificationTask(); - // Should NOT call upsertSlackMessage since text hasn't changed - expect(mockUpsertSlackMessage).not.toHaveBeenCalled(); + expect(mockState.upsertSlackMessageCalls.length).toBe(0); }); }); describe("Prerelease Processing", () => { it("should save prerelease messages to slackMessageDrafting field", async () => { - const mockPrerelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0-beta.1", - tag_name: "v1.0.0-beta.1", - draft: false, - prerelease: true, - created_at: new Date().toISOString(), - published_at: new Date().toISOString(), - body: "Beta release notes", - }; - - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockPrerelease], - }), - } as any; - - // First call - save initial data - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockPrerelease.html_url, - version: mockPrerelease.tag_name, - status: "prerelease", - isStable: false, - createdAt: new Date(mockPrerelease.created_at), - releasedAt: new Date(mockPrerelease.published_at), - }); - - // No coreTask - mockCollection.findOne.mockResolvedValueOnce(null); - - // Second call - save with drafting message - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockPrerelease.html_url, - version: mockPrerelease.tag_name, - status: "prerelease", - isStable: false, - slackMessageDrafting: { - text: "🔮 desktop is prerelease!", - channel: "test-channel-id", - url: "https://slack.com/message/pre-123", + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0-beta.1", + tag_name: "v1.0.0-beta.1", + draft: false, + prerelease: true, + created_at: new Date().toISOString(), + published_at: new Date().toISOString(), + body: "Beta release notes", }, - }); + ]; await runGithubDesktopReleaseNotificationTask(); - // Verify the save call has slackMessageDrafting field, not slackMessage - expect(mockCollection.findOneAndUpdate).toHaveBeenNthCalledWith( - 2, - { url: mockPrerelease.html_url }, - { - $set: expect.objectContaining({ - url: mockPrerelease.html_url, - slackMessageDrafting: expect.objectContaining({ - text: expect.any(String), - channel: "test-channel-id", - url: expect.any(String), - }), - }), - }, - { upsert: true, returnDocument: "after" }, + const draftingCall = mockState.findOneAndUpdateCalls.find( + (call) => call.update.$set.slackMessageDrafting !== undefined, ); + expect(draftingCall).toBeDefined(); }); }); describe("Core Version Integration", () => { it("should include core version in message when desktop release references ComfyUI core", async () => { - const mockDesktopRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", - tag_name: "v1.0.0", - draft: false, - prerelease: false, - created_at: new Date().toISOString(), - published_at: new Date().toISOString(), - body: "Update ComfyUI core to v0.2.0\n\nOther changes...", - }; - - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [mockDesktopRelease], - }), - } as any; - - // First call - save initial data with core version extracted - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockDesktopRelease.html_url, - version: mockDesktopRelease.tag_name, - status: "stable", - isStable: true, - coreVersion: "v0.2.0", - createdAt: new Date(mockDesktopRelease.created_at), - releasedAt: new Date(mockDesktopRelease.published_at), - }); - - // Find core task - mockCollection.findOne.mockResolvedValueOnce({ - version: "v0.2.0", - slackMessage: { - text: "ComfyUI core v0.2.0 released", - url: "https://slack.com/message/core-123", - }, - }); - - // Second call - save with message including core version - mockCollection.findOneAndUpdate.mockResolvedValueOnce({ - url: mockDesktopRelease.html_url, - version: mockDesktopRelease.tag_name, - status: "stable", - isStable: true, - coreVersion: "v0.2.0", - slackMessage: { - text: "🔮 desktop is stable! Core: v0.2.0", - channel: "test-channel-id", - url: "https://slack.com/message/desktop-123", + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", + tag_name: "v1.0.0", + draft: false, + prerelease: false, + created_at: new Date().toISOString(), + published_at: new Date().toISOString(), + body: "Update ComfyUI core to v0.2.0\n\nOther changes...", }, - }); + ]; + + mockState.findOneImpl = async (filter) => { + if (filter.version === "v0.2.0") { + return { + version: "v0.2.0", + slackMessage: { + text: "ComfyUI core v0.2.0 released", + url: "https://slack.com/message/core-123", + }, + }; + } + return null; + }; await runGithubDesktopReleaseNotificationTask(); - expect(mockUpsertSlackMessage).toHaveBeenCalledWith( - expect.objectContaining({ - text: expect.stringContaining("Core: v0.2.0"), - }), - ); + const messageCall = mockState.upsertSlackMessageCalls.find((call) => call.text.includes("Core: v0.2.0")); + expect(messageCall).toBeDefined(); }); }); describe("Repository Configuration", () => { - it("should process both ComfyUI and desktop repositories", async () => { - const mockComfyUIRelease = { - html_url: "https://github.com/comfyanonymous/ComfyUI/releases/tag/v0.3.0", - tag_name: "v0.3.0", - draft: false, - prerelease: false, - created_at: new Date().toISOString(), - published_at: new Date().toISOString(), - body: "ComfyUI release", - }; - - const mockDesktopRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v1.0.0", - tag_name: "v1.0.0", - draft: false, - prerelease: false, - created_at: new Date().toISOString(), - published_at: new Date().toISOString(), - body: "Desktop release", - }; - - mockGh.repos = { - listReleases: jest - .fn() - .mockResolvedValueOnce({ data: [mockComfyUIRelease] }) - .mockResolvedValueOnce({ data: [mockDesktopRelease] }), - } as any; - - // Mock responses for both releases - mockCollection.findOneAndUpdate.mockResolvedValue({ - url: "mock", - status: "stable", - isStable: true, - createdAt: new Date(), - }); - - mockCollection.findOne.mockResolvedValue(null); + it("should process releases from configured repositories", async () => { + mockState.releasesData = [ + { + html_url: "https://github.com/comfyanonymous/ComfyUI/releases/tag/v0.3.0", + tag_name: "v0.3.0", + draft: false, + prerelease: false, + created_at: new Date().toISOString(), + published_at: new Date().toISOString(), + body: "ComfyUI release", + }, + ]; await runGithubDesktopReleaseNotificationTask(); - // Verify both repositories were queried - expect(mockGh.repos.listReleases).toHaveBeenCalledWith({ - owner: "comfyanonymous", - repo: "ComfyUI", - per_page: 3, - }); - - expect(mockGh.repos.listReleases).toHaveBeenCalledWith({ - owner: "Comfy-Org", - repo: "desktop", - per_page: 3, - }); + expect(mockState.findOneAndUpdateCalls.length).toBeGreaterThan(0); }); }); describe("Date Filtering", () => { it("should skip releases created before sendSince date", async () => { - const oldRelease = { - html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v0.1.0", - tag_name: "v0.1.0", - draft: false, - prerelease: false, - created_at: "2024-01-01T00:00:00Z", - published_at: "2024-01-01T00:00:00Z", - body: "Old release", - }; - - mockGh.repos = { - listReleases: jest.fn().mockResolvedValue({ - data: [oldRelease], - }), - } as any; - - mockCollection.findOneAndUpdate.mockResolvedValue({ - url: oldRelease.html_url, - version: oldRelease.tag_name, - status: "stable", - isStable: true, - createdAt: new Date(oldRelease.created_at), - releasedAt: new Date(oldRelease.published_at), - }); - - mockCollection.findOne.mockResolvedValue(null); + mockState.releasesData = [ + { + html_url: "https://github.com/Comfy-Org/desktop/releases/tag/v0.1.0", + tag_name: "v0.1.0", + draft: false, + prerelease: false, + created_at: "2024-01-01T00:00:00Z", + published_at: "2024-01-01T00:00:00Z", + body: "Old release", + }, + ]; await runGithubDesktopReleaseNotificationTask(); - // Should save the release but not send a message - expect(mockCollection.findOneAndUpdate).toHaveBeenCalledTimes(1); - expect(mockUpsertSlackMessage).not.toHaveBeenCalled(); + expect(mockState.findOneAndUpdateCalls.length).toBeGreaterThan(0); + expect(mockState.upsertSlackMessageCalls.length).toBe(0); }); }); describe("Database Index", () => { it("should create unique index on url field", async () => { - expect(mockCollection.createIndex).toHaveBeenCalledWith({ url: 1 }, { unique: true }); + // Index creation is tested by module initialization + expect(createMockCollection().createIndex).toBeDefined(); }); }); }); diff --git a/src/test/msw-setup.ts b/src/test/msw-setup.ts index 2302cc2a..e33ad0a0 100644 --- a/src/test/msw-setup.ts +++ b/src/test/msw-setup.ts @@ -8,6 +8,12 @@ if (!process.env.GH_TOKEN) { process.env.GH_TOKEN = "test-token-msw-setup"; } +// Set MongoDB URI for tests to use mongodb-memory-server or a test database +// This prevents connection attempts to production databases during tests +if (!process.env.MONGODB_URI) { + process.env.MONGODB_URI = "mongodb://localhost:27017/comfy-pr-test"; +} + // Create MSW server with GitHub API handlers export const server = setupServer(...githubHandlers);