diff --git a/packages/cli/src/__tests__/experiment-finalize.test.ts b/packages/cli/src/__tests__/experiment-finalize.test.ts index 836720c6af..8371b5ceac 100644 --- a/packages/cli/src/__tests__/experiment-finalize.test.ts +++ b/packages/cli/src/__tests__/experiment-finalize.test.ts @@ -3,6 +3,21 @@ import { writeFile, mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const previewPlan = vi.fn(); const finalize = vi.fn(); const init = vi.fn(); @@ -18,12 +33,12 @@ const mockErrors = vi.hoisted(() => ({ })); vi.mock("@fusion/core", () => ({ - TaskStore: vi.fn(() => ({ init, getExperimentSessionStore })), + TaskStore: makeConstructibleMock(() => ({ init, getExperimentSessionStore })), })); vi.mock("@fusion/engine", () => ({ defaultGitOps: vi.fn(() => ({})), - ExperimentFinalizeService: vi.fn(() => ({ previewPlan, finalize })), + ExperimentFinalizeService: makeConstructibleMock(() => ({ previewPlan, finalize })), ExperimentFinalizeStateError: class extends Error { code = "state_error" as const; }, ExperimentFinalizeNoKeptRunsError: class extends Error { code = "no_kept_runs" as const; }, ExperimentFinalizePlanError: class extends Error { code = "plan_error" as const; }, diff --git a/packages/cli/src/__tests__/extension-experiment-finalize.test.ts b/packages/cli/src/__tests__/extension-experiment-finalize.test.ts index 5def4116bc..6571678896 100644 --- a/packages/cli/src/__tests__/extension-experiment-finalize.test.ts +++ b/packages/cli/src/__tests__/extension-experiment-finalize.test.ts @@ -1,5 +1,20 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const previewPlanMock = vi.hoisted(() => vi.fn()); const finalizeMock = vi.hoisted(() => vi.fn()); @@ -18,7 +33,7 @@ const mockErrors = vi.hoisted(() => ({ })); vi.mock("@fusion/core", () => ({ - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: vi.fn().mockResolvedValue(undefined), getExperimentSessionStore: vi.fn(() => ({})), })), @@ -43,7 +58,7 @@ vi.mock("@fusion/engine", () => ({ createFnAgent: vi.fn(), fetchWebContent: vi.fn(), defaultGitOps: vi.fn(() => ({})), - ExperimentFinalizeService: vi.fn(() => ({ previewPlan: previewPlanMock, finalize: finalizeMock })), + ExperimentFinalizeService: makeConstructibleMock(() => ({ previewPlan: previewPlanMock, finalize: finalizeMock })), ExperimentFinalizeStateError: mockErrors.StateError, ExperimentFinalizeNoKeptRunsError: mockErrors.NoKeptError, ExperimentFinalizePlanError: mockErrors.PlanError, diff --git a/packages/cli/src/__tests__/plugin-dev.test.ts b/packages/cli/src/__tests__/plugin-dev.test.ts index 4478110a17..54fafba270 100644 --- a/packages/cli/src/__tests__/plugin-dev.test.ts +++ b/packages/cli/src/__tests__/plugin-dev.test.ts @@ -3,6 +3,21 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const pluginCommandMocks = vi.hoisted(() => { const store = { registerPlugin: vi.fn(async () => ({ id: "fusion-plugin-dev-test", enabled: true })), @@ -16,8 +31,8 @@ const pluginCommandMocks = vi.hoisted(() => { return { store, loader, - createPluginStore: vi.fn(async () => store), - createPluginLoader: vi.fn(async () => ({ store, loader })), + createPluginStore: makeConstructibleMock(async () => store), + createPluginLoader: makeConstructibleMock(async () => ({ store, loader })), resolvePluginEntryFile: vi.fn(async (dir: string) => join(dir, "dist", "index.js")), loadManifestFromPath: vi.fn(async () => ({ manifest: { diff --git a/packages/cli/src/__tests__/project-resolver.test.ts b/packages/cli/src/__tests__/project-resolver.test.ts index e9ade98c36..4240c2e6bc 100644 --- a/packages/cli/src/__tests__/project-resolver.test.ts +++ b/packages/cli/src/__tests__/project-resolver.test.ts @@ -2,6 +2,21 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { existsSync, statSync } from "node:fs"; import { TaskStore } from "@fusion/core"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const { mockIsValidSqliteDatabaseFile } = vi.hoisted(() => ({ mockIsValidSqliteDatabaseFile: vi.fn(), })); @@ -41,7 +56,7 @@ vi.mock("@fusion/core", async () => { }, isValidSqliteDatabaseFile: (...args: Parameters) => mockIsValidSqliteDatabaseFile(...args), - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: vi.fn().mockResolvedValue(undefined), listTasks: vi.fn().mockResolvedValue([]), })), diff --git a/packages/cli/src/__tests__/task-plan.test.ts b/packages/cli/src/__tests__/task-plan.test.ts index a8ae3ebd3b..1a6f224bb9 100644 --- a/packages/cli/src/__tests__/task-plan.test.ts +++ b/packages/cli/src/__tests__/task-plan.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + // Mock node:readline/promises before importing vi.mock("node:readline/promises", () => ({ createInterface: vi.fn(), @@ -8,7 +23,7 @@ vi.mock("node:readline/promises", () => ({ // Mock @fusion/core before importing vi.mock("@fusion/core", async (importOriginal) => ({ ...(await importOriginal()), - TaskStore: vi.fn(), + TaskStore: makeConstructibleMock(), COLUMNS: ["triage", "todo", "in-progress", "in-review", "done", "archived"], COLUMN_LABELS: { triage: "Triage", diff --git a/packages/cli/src/__tests__/task-steer.test.ts b/packages/cli/src/__tests__/task-steer.test.ts index bec5070b4d..4bc8becb4c 100644 --- a/packages/cli/src/__tests__/task-steer.test.ts +++ b/packages/cli/src/__tests__/task-steer.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + // Mock node:readline/promises before importing vi.mock("node:readline/promises", () => ({ createInterface: vi.fn(), @@ -8,7 +23,7 @@ vi.mock("node:readline/promises", () => ({ // Mock @fusion/core before importing vi.mock("@fusion/core", async (importOriginal) => ({ ...(await importOriginal()), - TaskStore: vi.fn(), + TaskStore: makeConstructibleMock(), COLUMNS: ["triage", "todo", "in-progress", "in-review", "done", "archived"], COLUMN_LABELS: { triage: "Triage", diff --git a/packages/cli/src/__tests__/update-cache.test.ts b/packages/cli/src/__tests__/update-cache.test.ts index a3b3f7747c..802b38ddf3 100644 --- a/packages/cli/src/__tests__/update-cache.test.ts +++ b/packages/cli/src/__tests__/update-cache.test.ts @@ -2,6 +2,21 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { readFileSync } from "node:fs"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const CLI_PACKAGE_VERSION = ( JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8")) as { version: string } ).version; @@ -16,7 +31,7 @@ const { cacheDir, mockResolveGlobalDir } = vi.hoisted(() => { vi.mock("@fusion/core", () => ({ resolveGlobalDir: mockResolveGlobalDir, - GlobalSettingsStore: vi.fn(), + GlobalSettingsStore: makeConstructibleMock(), })); const { getCachedUpdateStatus } = await import("../update-cache.js"); diff --git a/packages/cli/src/commands/__tests__/agent.test.ts b/packages/cli/src/commands/__tests__/agent.test.ts index 735c52ef93..df2793908f 100644 --- a/packages/cli/src/commands/__tests__/agent.test.ts +++ b/packages/cli/src/commands/__tests__/agent.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + // ── Mock AgentStore ────────────────────────────────────────────────── const mockGetAgent = vi.fn(); @@ -9,7 +24,7 @@ const mockInit = vi.fn().mockResolvedValue(undefined); // AgentStore mock — vi.fn() with mockImplementation works with `new` in vitest. // We return a plain object from the constructor which becomes the instance. vi.mock("@fusion/core", () => ({ - AgentStore: vi.fn().mockImplementation(() => ({ + AgentStore: makeConstructibleMock(() => ({ init: mockInit, getAgent: mockGetAgent, updateAgentState: mockUpdateAgentState, diff --git a/packages/cli/src/commands/__tests__/backup.test.ts b/packages/cli/src/commands/__tests__/backup.test.ts index ceca9eed1c..acfe564b3b 100644 --- a/packages/cli/src/commands/__tests__/backup.test.ts +++ b/packages/cli/src/commands/__tests__/backup.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const { mockListBackups, mockListBackupPairs, @@ -20,7 +35,7 @@ const { vi.mock("@fusion/core", () => ({ BackupManager: vi.fn(), - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: vi.fn().mockResolvedValue(undefined), getSettings: mockGetSettings, fusionDir: "/cwd/.fusion", diff --git a/packages/cli/src/commands/__tests__/db.test.ts b/packages/cli/src/commands/__tests__/db.test.ts index b209e3f5ec..61f3368971 100644 --- a/packages/cli/src/commands/__tests__/db.test.ts +++ b/packages/cli/src/commands/__tests__/db.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + // Hoist mocks so they are evaluated before module imports const { mockGetDatabase, mockVacuum, mockResolveProject } = vi.hoisted(() => ({ mockGetDatabase: vi.fn(), @@ -8,7 +23,7 @@ const { mockGetDatabase, mockVacuum, mockResolveProject } = vi.hoisted(() => ({ })); vi.mock("@fusion/core", () => ({ - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: vi.fn(), getDatabase: mockGetDatabase, })), diff --git a/packages/cli/src/commands/__tests__/desktop.test.ts b/packages/cli/src/commands/__tests__/desktop.test.ts index f6cb0a18b0..6e662d41c9 100644 --- a/packages/cli/src/commands/__tests__/desktop.test.ts +++ b/packages/cli/src/commands/__tests__/desktop.test.ts @@ -122,7 +122,9 @@ const mocks = vi.hoisted(() => { server, app, spawn, - taskStoreCtor: vi.fn(() => store), + taskStoreCtor: vi.fn(function () { + return store; + }), createServer: vi.fn(() => app), }; }); diff --git a/packages/cli/src/commands/__tests__/init.test.ts b/packages/cli/src/commands/__tests__/init.test.ts index a6cbc68477..1313ed238a 100644 --- a/packages/cli/src/commands/__tests__/init.test.ts +++ b/packages/cli/src/commands/__tests__/init.test.ts @@ -11,6 +11,21 @@ import { exec } from "node:child_process"; import { promisify } from "node:util"; import { GitRepositoryInitializationError } from "@fusion/core"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const execAsync = promisify(exec); const mockCentralInit = vi.fn(); @@ -27,7 +42,7 @@ vi.mock("@fusion/core", async () => { const actual = await vi.importActual("@fusion/core"); return { ...actual, - CentralCore: vi.fn().mockImplementation(() => ({ + CentralCore: makeConstructibleMock(() => ({ init: mockCentralInit, close: mockCentralClose, getProjectByPath: mockGetProjectByPath, diff --git a/packages/cli/src/commands/__tests__/memory-backup.test.ts b/packages/cli/src/commands/__tests__/memory-backup.test.ts index 8813a33c16..b174371c6f 100644 --- a/packages/cli/src/commands/__tests__/memory-backup.test.ts +++ b/packages/cli/src/commands/__tests__/memory-backup.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const { mockListBackups, mockRestoreBackup, @@ -15,7 +30,7 @@ const { })); vi.mock("@fusion/core", () => ({ - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: vi.fn().mockResolvedValue(undefined), getSettings: mockGetSettings, fusionDir: "/cwd/.fusion", diff --git a/packages/cli/src/commands/__tests__/message.test.ts b/packages/cli/src/commands/__tests__/message.test.ts index 1ebc147915..74bb36914c 100644 --- a/packages/cli/src/commands/__tests__/message.test.ts +++ b/packages/cli/src/commands/__tests__/message.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + // ── Mock MessageStore ──────────────────────────────────────────────── const mockGetInbox = vi.fn(); @@ -17,7 +32,7 @@ vi.mock("@fusion/core", () => { }; return { createDatabase: vi.fn().mockReturnValue(mockDb), - MessageStore: vi.fn().mockImplementation(() => ({ + MessageStore: makeConstructibleMock(() => ({ getInbox: mockGetInbox, getOutbox: mockGetOutbox, getMailbox: mockGetMailbox, diff --git a/packages/cli/src/commands/__tests__/node.test.ts b/packages/cli/src/commands/__tests__/node.test.ts index 9fc22510d8..2b4800b9db 100644 --- a/packages/cli/src/commands/__tests__/node.test.ts +++ b/packages/cli/src/commands/__tests__/node.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const mockInit = vi.fn().mockResolvedValue(undefined); const mockClose = vi.fn().mockResolvedValue(undefined); const mockListNodes = vi.fn(); @@ -13,7 +28,7 @@ const mockQuestion = vi.fn(); const mockRlClose = vi.fn(); vi.mock("@fusion/core", () => ({ - CentralCore: vi.fn().mockImplementation(() => ({ + CentralCore: makeConstructibleMock(() => ({ init: mockInit, close: mockClose, listNodes: mockListNodes, diff --git a/packages/cli/src/commands/__tests__/plugin.test.ts b/packages/cli/src/commands/__tests__/plugin.test.ts index 3a0b8e771f..d70109d9f1 100644 --- a/packages/cli/src/commands/__tests__/plugin.test.ts +++ b/packages/cli/src/commands/__tests__/plugin.test.ts @@ -3,6 +3,21 @@ import { dirname, join, resolve } from "node:path"; import { tmpdir } from "node:os"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const mocks = vi.hoisted(() => { const pluginStoreInstances: Array<{ init: ReturnType; @@ -15,9 +30,9 @@ const mocks = vi.hoisted(() => { let loaderTaskStore: { getRootDir?: () => string } | undefined; let loaderRootDir: string | undefined; - const PluginStore = vi.fn(); + const PluginStore = makeConstructibleMock(); - const PluginLoader = vi.fn(); + const PluginLoader = makeConstructibleMock(); const setupDefaults = () => { PluginStore.mockImplementation(() => { diff --git a/packages/cli/src/commands/__tests__/project.test.ts b/packages/cli/src/commands/__tests__/project.test.ts index 5fc892ef39..7d813a7d10 100644 --- a/packages/cli/src/commands/__tests__/project.test.ts +++ b/packages/cli/src/commands/__tests__/project.test.ts @@ -3,6 +3,21 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const mockListProjects = vi.fn(); const mockRegisterProject = vi.fn(); const mockEnsureProjectForPath = vi.fn(async (...args: unknown[]) => ({ @@ -29,7 +44,7 @@ const mockEnsureMemoryFileWithBackend = vi.fn(); // Mock @fusion/core vi.mock("@fusion/core", () => ({ - CentralCore: vi.fn().mockImplementation(() => ({ + CentralCore: makeConstructibleMock(() => ({ init: mockInit.mockResolvedValue(undefined), close: mockClose.mockResolvedValue(undefined), listProjects: mockListProjects, @@ -41,11 +56,11 @@ vi.mock("@fusion/core", () => ({ getProjectByPath: mockGetProjectByPath, getProjectHealth: mockGetProjectHealth, })), - GlobalSettingsStore: vi.fn().mockImplementation(() => ({ + GlobalSettingsStore: makeConstructibleMock(() => ({ init: mockGlobalInit.mockResolvedValue(undefined), getSettings: mockGetSettings, })), - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: mockTaskStoreInit, listTasks: mockTaskStoreListTasks, })), diff --git a/packages/cli/src/commands/__tests__/serve.test.ts b/packages/cli/src/commands/__tests__/serve.test.ts index 994c732186..d1c7a9bf7d 100644 --- a/packages/cli/src/commands/__tests__/serve.test.ts +++ b/packages/cli/src/commands/__tests__/serve.test.ts @@ -4,6 +4,21 @@ import { mkdtempSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const { mockSyncStartupModels, mockShouldUseHybridExecutor, mockHybridExecutorCtor, mockHybridExecutorInitialize, mockHybridExecutorShutdown } = vi.hoisted(() => ({ mockSyncStartupModels: vi.fn().mockResolvedValue(undefined), mockShouldUseHybridExecutor: vi.fn().mockResolvedValue({ enabled: false, reason: "single-project-local-only" }), @@ -590,7 +605,7 @@ vi.mock("@fusion/core", async (importOriginal) => { storeToken: vi.fn().mockResolvedValue(undefined), }; }), - GlobalSettingsStore: vi.fn().mockImplementation(function () { + GlobalSettingsStore: makeConstructibleMock(function () { return {}; }), resolveGlobalDir: vi.fn().mockReturnValue("/mock/global"), diff --git a/packages/cli/src/commands/__tests__/settings-export.test.ts b/packages/cli/src/commands/__tests__/settings-export.test.ts index 626131f94e..56ec19ed0f 100644 --- a/packages/cli/src/commands/__tests__/settings-export.test.ts +++ b/packages/cli/src/commands/__tests__/settings-export.test.ts @@ -4,6 +4,21 @@ import { join, resolve } from "node:path"; import { TaskStore, exportSettings, generateExportFilename } from "@fusion/core"; import { resolveProject } from "../../project-context.js"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const mockStoreInit = vi.fn().mockResolvedValue(undefined); vi.mock("node:fs/promises", () => ({ @@ -11,7 +26,7 @@ vi.mock("node:fs/promises", () => ({ })); vi.mock("@fusion/core", () => ({ - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: mockStoreInit, })), exportSettings: vi.fn(), diff --git a/packages/cli/src/commands/__tests__/settings-import.test.ts b/packages/cli/src/commands/__tests__/settings-import.test.ts index 1dc563b4d3..5b33dff5be 100644 --- a/packages/cli/src/commands/__tests__/settings-import.test.ts +++ b/packages/cli/src/commands/__tests__/settings-import.test.ts @@ -3,6 +3,21 @@ import { existsSync } from "node:fs"; import { TaskStore, importSettings, readExportFile, validateImportData } from "@fusion/core"; import { resolveProject } from "../../project-context.js"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + const mockStoreInit = vi.fn().mockResolvedValue(undefined); vi.mock("node:fs", () => ({ @@ -10,7 +25,7 @@ vi.mock("node:fs", () => ({ })); vi.mock("@fusion/core", () => ({ - TaskStore: vi.fn().mockImplementation(() => ({ + TaskStore: makeConstructibleMock(() => ({ init: mockStoreInit, })), importSettings: vi.fn(), diff --git a/packages/cli/src/commands/__tests__/settings.test.ts b/packages/cli/src/commands/__tests__/settings.test.ts index de9772e093..364a10aa58 100644 --- a/packages/cli/src/commands/__tests__/settings.test.ts +++ b/packages/cli/src/commands/__tests__/settings.test.ts @@ -1,5 +1,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +function makeConstructibleMock unknown>(impl?: T) { + const mock = vi.fn(function () {}); + const originalMockImplementation = mock.mockImplementation.bind(mock); + const originalMockImplementationOnce = mock.mockImplementationOnce.bind(mock); + const wrap = (nextImpl: T) => function (this: unknown, ...args: Parameters) { + return nextImpl(...args); + }; + mock.mockImplementation = ((nextImpl: T) => originalMockImplementation(wrap(nextImpl))) as typeof mock.mockImplementation; + mock.mockImplementationOnce = ((nextImpl: T) => originalMockImplementationOnce(wrap(nextImpl))) as typeof mock.mockImplementationOnce; + if (impl) { + mock.mockImplementation(impl); + } + return mock; +} + vi.mock("@fusion/core", () => { const DEFAULT_SETTINGS = { maxConcurrent: 2, @@ -23,7 +38,7 @@ vi.mock("@fusion/core", () => { }; return { - GlobalSettingsStore: vi.fn(), + GlobalSettingsStore: makeConstructibleMock(), DEFAULT_SETTINGS, SUPPORTED_LOCALES: ["en", "zh-CN", "zh-TW", "fr", "es", "ko"], resolveWorktrunkSettings: (globalValue: any, projectValue: any) => ({ diff --git a/packages/cli/src/commands/__tests__/task.test.ts b/packages/cli/src/commands/__tests__/task.test.ts index 0363711814..6b4e569ef7 100644 --- a/packages/cli/src/commands/__tests__/task.test.ts +++ b/packages/cli/src/commands/__tests__/task.test.ts @@ -2460,6 +2460,7 @@ describe("runTaskRetry", () => { reviewerContextRetryCount: 0, reviewerFallbackRetryCount: 0, completionHandoffLimboRecoveryCount: 0, + graphResumeRetryCount: 0, mergeAuditBounceCount: 0, mergeRetries: 0, resumeLimboCount: 0, @@ -2534,6 +2535,7 @@ describe("runTaskRetry", () => { reviewerContextRetryCount: 0, reviewerFallbackRetryCount: 0, completionHandoffLimboRecoveryCount: 0, + graphResumeRetryCount: 0, mergeAuditBounceCount: 0, mergeRetries: 0, resumeLimboCount: 0,