diff --git a/packages/cli/src/commands/__tests__/dashboard.test.ts b/packages/cli/src/commands/__tests__/dashboard.test.ts index 7b9df34a10..6967624988 100644 --- a/packages/cli/src/commands/__tests__/dashboard.test.ts +++ b/packages/cli/src/commands/__tests__/dashboard.test.ts @@ -412,6 +412,8 @@ vi.mock("@fusion/engine", async (importOriginal) => { createFollowUpTask: vi.fn().mockResolvedValue(undefined), })); const aiMergeTask = vi.fn().mockImplementation(() => Promise.resolve({ merged: true })); + const runAiMerge = vi.fn().mockImplementation(() => Promise.resolve({ merged: true })); + const landWorkspaceTask = vi.fn().mockImplementation(() => Promise.resolve({ allLanded: true, repos: [] })); const CronRunner = makeConstructibleMock(() => ({ start: vi.fn(), stop: vi.fn(), @@ -756,7 +758,8 @@ vi.mock("@fusion/engine", async (importOriginal) => { PrMonitor, PrCommentHandler, aiMergeTask, - runAiMerge: aiMergeTask, + runAiMerge, + landWorkspaceTask, CronRunner, createAiPromptExecutor, SelfHealingManager, @@ -3159,7 +3162,7 @@ describe("runDashboard — merge stream sink routing", () => { resetGitHubMocks(); process.env.FUSION_DASHBOARD_TOKEN = "fn_test_dashboard_token"; const { TaskStore, AutomationStore, AgentStore, PluginStore, PluginLoader, CentralCore } = await import("@fusion/core"); - const { aiMergeTask, createFusionAuthStorage } = await import("@fusion/engine"); + const { runAiMerge, createFusionAuthStorage } = await import("@fusion/engine"); const { createServer } = await import("@fusion/dashboard"); const { DefaultPackageManager, ModelRegistry, discoverAndLoadExtensions, createExtensionRuntime } = await import("@earendil-works/pi-coding-agent"); @@ -3211,7 +3214,8 @@ describe("runDashboard — merge stream sink routing", () => { const stdoutWriteSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}); - (aiMergeTask as ReturnType).mockImplementationOnce( + // FNXC:MergerUnification 2026-06-24-23:04: UI-only dashboard manual merges stream through runAiMerge's onAgentText callback; the test must assert line-buffered merge logs instead of deprecated aiMergeTask or raw stdout writes. + (runAiMerge as ReturnType).mockImplementationOnce( async (_store: unknown, _cwd: string, _taskId: string, opts: { onAgentText?: (delta: string) => void }) => { opts.onAgentText?.("Hel"); opts.onAgentText?.("lo"); @@ -3221,24 +3225,29 @@ describe("runDashboard — merge stream sink routing", () => { }, ); - await runDashboard(0, { open: false, noEngine: true }); - consoleLogSpy.mockClear(); - stdoutWriteSpy.mockClear(); + try { + await runDashboard(0, { open: false, noEngine: true }); + consoleLogSpy.mockClear(); + stdoutWriteSpy.mockClear(); - const createServerCall = (createServer as ReturnType).mock.calls[0]; - const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise }; + const createServerCall = (createServer as ReturnType).mock.calls[0]; + const serverOpts = createServerCall[1] as { onMerge: (taskId: string) => Promise }; - await serverOpts.onMerge("FN-TEST"); + await serverOpts.onMerge("FN-TEST"); - expect(stdoutWriteSpy).not.toHaveBeenCalled(); - expect(consoleLogSpy).toHaveBeenCalledWith("[merge] Hello"); - expect(consoleLogSpy).toHaveBeenCalledWith("[merge] World!"); - expect(consoleLogSpy).toHaveBeenCalledWith("[merge] Tail"); - expect(consoleLogSpy).not.toHaveBeenCalledWith("[merge] H"); - - stdoutWriteSpy.mockRestore(); - consoleLogSpy.mockRestore(); - delete process.env.FUSION_DASHBOARD_TOKEN; + expect(runAiMerge).toHaveBeenCalledWith(expect.anything(), expect.any(String), "FN-TEST", expect.objectContaining({ + onAgentText: expect.any(Function), + })); + expect(stdoutWriteSpy).not.toHaveBeenCalled(); + expect(consoleLogSpy).toHaveBeenCalledWith("[merge] Hello"); + expect(consoleLogSpy).toHaveBeenCalledWith("[merge] World!"); + expect(consoleLogSpy).toHaveBeenCalledWith("[merge] Tail"); + expect(consoleLogSpy).not.toHaveBeenCalledWith("[merge] H"); + } finally { + stdoutWriteSpy.mockRestore(); + consoleLogSpy.mockRestore(); + delete process.env.FUSION_DASHBOARD_TOKEN; + } }); }); diff --git a/packages/cli/src/commands/__tests__/task.test.ts b/packages/cli/src/commands/__tests__/task.test.ts index 1f2648f95d..bbaa34bfa6 100644 --- a/packages/cli/src/commands/__tests__/task.test.ts +++ b/packages/cli/src/commands/__tests__/task.test.ts @@ -96,8 +96,11 @@ vi.mock("@fusion/core", async (importActual) => { }); // Mock @fusion/engine -const { runAiMergeMock } = vi.hoisted(() => ({ runAiMergeMock: vi.fn() })); -vi.mock("@fusion/engine", () => ({ runAiMerge: runAiMergeMock, aiMergeTask: runAiMergeMock })); +vi.mock("@fusion/engine", () => ({ + aiMergeTask: vi.fn(), + runAiMerge: vi.fn(), + landWorkspaceTask: vi.fn(), +})); // Mock @fusion/dashboard vi.mock("@fusion/dashboard", () => ({ @@ -156,7 +159,7 @@ import { import { GitHubClient, generatePrMetadata } from "@fusion/dashboard"; import { createSession, submitResponse } from "@fusion/dashboard/planning"; import { resolveProject } from "../../project-context.js"; -import { runAiMerge } from "@fusion/engine"; +import { aiMergeTask, runAiMerge, landWorkspaceTask } from "@fusion/engine"; const mockedExec = vi.mocked(exec); @@ -1229,6 +1232,7 @@ describe("project-aware task command behavior", () => { isRegistered: true, store: resolvedStore, }); + // FNXC:MergerUnification 2026-06-24-22:58: CLI `fn task merge` normal-task tests must exercise the unified runAiMerge entrypoint; aiMergeTask remains deprecated and should not mask stale mocks. vi.mocked(runAiMerge).mockResolvedValue({ merged: true, task: makeTask({ id: "FN-123" }), @@ -1236,16 +1240,26 @@ describe("project-aware task command behavior", () => { worktreeRemoved: true, branchDeleted: true, } as never); + const exitSpy = vi.spyOn(process, "exit").mockImplementation((((code?: number) => { + throw new Error(`process.exit:${code}`); + }) as unknown) as (code?: string | number | null | undefined) => never); - await runTaskUpdate("FN-123", "0", "done", "demo-project"); - await runTaskLog("FN-123", "hello", undefined, "demo-project"); - await runTaskMerge("FN-123", "demo-project"); - await runTaskDuplicate("FN-123", "demo-project"); - await runTaskRefine("FN-123", "more tests", "demo-project"); + try { + await runTaskUpdate("FN-123", "0", "done", "demo-project"); + await runTaskLog("FN-123", "hello", undefined, "demo-project"); + await runTaskMerge("FN-123", "demo-project"); + await runTaskDuplicate("FN-123", "demo-project"); + await runTaskRefine("FN-123", "more tests", "demo-project"); + } finally { + exitSpy.mockRestore(); + } expect(updateStep).toHaveBeenCalled(); expect(logEntry).toHaveBeenCalled(); expect(runAiMerge).toHaveBeenCalledWith(resolvedStore, "/test", "FN-123", expect.any(Object)); + expect(landWorkspaceTask).not.toHaveBeenCalled(); + expect(aiMergeTask).not.toHaveBeenCalled(); + expect(exitSpy).not.toHaveBeenCalled(); expect(duplicateTask).toHaveBeenCalledWith("FN-123"); expect(refineTask).toHaveBeenCalledWith("FN-123", "more tests"); });