From beaf52bb50682eb693d372468328ac24b3ed3f01 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 21 Jun 2026 20:40:59 -0700 Subject: [PATCH] FN-6898: stabilize ci-status plugin tests Rescue the ci-status plugin test suite by making CI polling deterministic and network-free. - Stub global fetch for the full suite so refresh handlers and interval polls cannot reach ci.example.com. - Share the mock task fixture across refresh and task-move tests. - Cover tracked-branch and empty-branch refresh paths plus interval polling behavior. Files changed: .../src/__tests__/index.test.ts | 145 ++++++++++++++++----- 1 file changed, 115 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-6898 Fusion-Task-Lineage: 3758d995-bda9-4ab9-add7-e6a3f710dc6f --- .../src/__tests__/index.test.ts | 145 ++++++++++++++---- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/plugins/examples/fusion-plugin-ci-status/src/__tests__/index.test.ts b/plugins/examples/fusion-plugin-ci-status/src/__tests__/index.test.ts index cda76c6d29..e61015ff25 100644 --- a/plugins/examples/fusion-plugin-ci-status/src/__tests__/index.test.ts +++ b/plugins/examples/fusion-plugin-ci-status/src/__tests__/index.test.ts @@ -63,15 +63,48 @@ function createMockResponse() { return { json, status }; } +const mockTask = { + id: "FN-001", + title: "Test Task", + description: "A test task", + column: "in-progress" as const, + dependencies: [], + steps: [], + currentStep: 0, + size: "M" as const, + reviewLevel: "full" as const, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", +}; + +function getFetchMock(): ReturnType { + return globalThis.fetch as unknown as ReturnType; +} + +async function drainDetachedPoll(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + // ── Test Suite ───────────────────────────────────────────────────────────────── describe("ci-status plugin", () => { beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); + // FNXC:PluginTesting 2026-06-21-00:00: ci-status suite must mock global.fetch — postRefreshHandler and the onLoad poll call pollCIStatus(), and an unmocked real fetch to ci.example.com caused a load-sensitive POST /refresh timeout (FN-6714/FN-6898). Keep zero real-network calls; assert the mock catches both the route and timer-driven polls. + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ statuses: [] }), + }), + ); }); - afterEach(() => { + afterEach(async () => { + await plugin.hooks.onUnload?.({} as any); + vi.unstubAllGlobals(); vi.restoreAllMocks(); vi.useRealTimers(); }); @@ -192,20 +225,6 @@ describe("ci-status plugin", () => { }); describe("hooks.onTaskMoved", () => { - const mockTask = { - id: "FN-001", - title: "Test Task", - description: "A test task", - column: "in-progress" as const, - dependencies: [], - steps: [], - currentStep: 0, - size: "M" as const, - reviewLevel: "full" as const, - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - }; - it("should track branch when task moves to in-progress", async () => { const ctx = createMockContext(); await plugin.hooks.onTaskMoved?.( @@ -292,19 +311,7 @@ describe("ci-status plugin", () => { // First add the branch via onTaskMoved await plugin.hooks.onTaskMoved?.( - { - id: "FN-001", - title: "Test Task", - description: "A test task", - column: "in-progress" as const, - dependencies: [], - steps: [], - currentStep: 0, - size: "M" as const, - reviewLevel: "full" as const, - createdAt: "2024-01-01T00:00:00.000Z", - updatedAt: "2024-01-01T00:00:00.000Z", - } as any, + mockTask as any, "todo", "in-progress", ctx as any, @@ -343,20 +350,98 @@ describe("ci-status plugin", () => { }); describe("POST /refresh", () => { - it("should trigger refresh and return branches", async () => { + it("should trigger refresh for tracked branches through the mocked fetch", async () => { const ctx = createMockContext(); const req = createMockRequest({ method: "POST" }); - const res = createMockResponse(); + const fetchMock = getFetchMock(); + + await plugin.hooks.onTaskMoved?.( + mockTask as any, + "todo", + "in-progress", + ctx as any, + ); + fetchMock.mockClear(); const route = plugin.routes!.find( (r) => r.method === "POST" && r.path === "/refresh", )!; const result = await route.handler(req as any, ctx as any) as { refreshed?: boolean; branches?: unknown[] }; + await drainDetachedPoll(); expect(result).toHaveProperty("refreshed", true); expect(result).toHaveProperty("branches"); expect(Array.isArray(result.branches)).toBe(true); + expect(vi.isMockFunction(globalThis.fetch)).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + "https://ci.example.com/api/status", + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ branches: ["fusion/fn-001"] }), + }), + ); + }); + + it("should return refreshed without fetch when no branches are tracked", async () => { + const ctx = createMockContext(); + const req = createMockRequest({ method: "POST" }); + const fetchMock = getFetchMock(); + + const route = plugin.routes!.find( + (r) => r.method === "POST" && r.path === "/refresh", + )!; + + const result = await route.handler(req as any, ctx as any) as { refreshed?: boolean; branches?: unknown[] }; + await drainDetachedPoll(); + + expect(result).toHaveProperty("refreshed", true); + expect(result).toHaveProperty("branches", []); + expect(vi.isMockFunction(globalThis.fetch)).toBe(true); + expect(fetchMock).not.toHaveBeenCalled(); + }); + }); + + describe("onLoad polling interval", () => { + it("should poll tracked branches through the mocked fetch when timers advance", async () => { + const ctx = createMockContext(); + const fetchMock = getFetchMock(); + + await plugin.hooks.onTaskMoved?.( + mockTask as any, + "todo", + "in-progress", + ctx as any, + ); + await plugin.hooks.onLoad?.(ctx as any); + fetchMock.mockClear(); + + await vi.advanceTimersByTimeAsync(60000); + + expect(vi.isMockFunction(globalThis.fetch)).toBe(true); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith( + "https://ci.example.com/api/status", + expect.objectContaining({ + method: "POST", + body: JSON.stringify({ branches: ["fusion/fn-001"] }), + }), + ); + }); + + it("should skip fetch on interval ticks when no branches are tracked", async () => { + const ctx = createMockContext(); + const fetchMock = getFetchMock(); + + await plugin.hooks.onLoad?.(ctx as any); + fetchMock.mockClear(); + + await vi.advanceTimersByTimeAsync(60000); + + expect(vi.isMockFunction(globalThis.fetch)).toBe(true); + expect(fetchMock).not.toHaveBeenCalled(); }); }); });