FN-6173: fix CLI stale constructor mocks and plugin SDK shims
Tighten CLI retry behavior and restore constructor-safe test/runtime wiring. - move CLI merge retries back to todo before clearing retry metadata so resets persist - add a plugin SDK runtime shim for @fusion/core board actions and alias it in the CLI bundle - harden CLI daemon, serve, dashboard, research, and task tests against stale non-constructible mocks - add a patch changeset for the published CLI package Files changed: .changeset/fn-6173-cli-fixes.md | 5 + packages/cli/src/__tests__/task-retry.test.ts | 4 +- packages/cli/src/commands/__tests__/daemon.test.ts | 182 ++++++++++-------- .../cli/src/commands/__tests__/dashboard.test.ts | 69 ++++--- .../cli/src/commands/__tests__/research.test.ts | 27 ++- packages/cli/src/commands/__tests__/serve.test.ts | 204 ++++++++++++--------- packages/cli/src/commands/__tests__/task.test.ts | 61 +++--- packages/cli/src/commands/task.ts | 15 +- packages/cli/src/plugin-sdk-core-runtime-shim.ts | 33 ++++ packages/cli/tsup.config.ts | 7 + 10 files changed, 385 insertions(+), 222 deletions(-) Fusion-Task-Id: FN-6173 Fusion-Task-Lineage: 50bdbadd-39f7-4b43-bcf8-3891271917db
This commit is contained in:
@@ -42,12 +42,14 @@ describe("runTaskRetry", () => {
|
||||
error: "merge deadlock",
|
||||
paused: true,
|
||||
pausedReason: "in-review-stall-deadlock",
|
||||
steps: [{ name: "implemented", status: "done" }],
|
||||
mergeRetries: 4,
|
||||
});
|
||||
|
||||
await runTaskRetry(task.id);
|
||||
|
||||
const updated = await store.getTask(task.id);
|
||||
const verificationStore = await createStore();
|
||||
const updated = await verificationStore.getTask(task.id);
|
||||
expect(updated.column).toBe("todo");
|
||||
expect(updated.status).toBeUndefined();
|
||||
expect(updated.error).toBeUndefined();
|
||||
|
||||
@@ -9,10 +9,12 @@ const { mockSyncStartupModels, mockShouldUseHybridExecutor, mockHybridExecutorCt
|
||||
mockShouldUseHybridExecutor: vi.fn().mockResolvedValue({ enabled: false, reason: "single-project-local-only" }),
|
||||
mockHybridExecutorInitialize: vi.fn().mockResolvedValue(undefined),
|
||||
mockHybridExecutorShutdown: vi.fn().mockResolvedValue(undefined),
|
||||
mockHybridExecutorCtor: vi.fn().mockImplementation(() => ({
|
||||
initialize: mockHybridExecutorInitialize,
|
||||
shutdown: mockHybridExecutorShutdown,
|
||||
})),
|
||||
mockHybridExecutorCtor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
initialize: mockHybridExecutorInitialize,
|
||||
shutdown: mockHybridExecutorShutdown,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
vi.mock("../startup-model-sync.js", () => ({
|
||||
syncStartupModels: mockSyncStartupModels,
|
||||
@@ -109,13 +111,13 @@ const mocks = vi.hoisted(() => {
|
||||
});
|
||||
}
|
||||
|
||||
const taskStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const taskStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const store = createTaskStoreMock();
|
||||
taskStores.push(store);
|
||||
return store;
|
||||
});
|
||||
|
||||
const automationStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const automationStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const automationStore = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
@@ -123,7 +125,7 @@ const mocks = vi.hoisted(() => {
|
||||
return automationStore;
|
||||
});
|
||||
|
||||
const agentStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const agentStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const agentStore = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
@@ -131,7 +133,7 @@ const mocks = vi.hoisted(() => {
|
||||
return agentStore;
|
||||
});
|
||||
|
||||
const centralCoreCtor = vi.fn().mockImplementation(() => {
|
||||
const centralCoreCtor = vi.fn().mockImplementation(function () {
|
||||
const now = new Date().toISOString();
|
||||
const projects = [
|
||||
{ id: "project-1", name: "Test Project", path: "/repo", status: "active", isolationMode: "in-process", createdAt: now, updatedAt: now },
|
||||
@@ -203,7 +205,7 @@ const mocks = vi.hoisted(() => {
|
||||
}),
|
||||
}));
|
||||
|
||||
const triageCtor = vi.fn().mockImplementation(() => {
|
||||
const triageCtor = vi.fn().mockImplementation(function () {
|
||||
const triage = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -213,7 +215,7 @@ const mocks = vi.hoisted(() => {
|
||||
return triage;
|
||||
});
|
||||
|
||||
const executorCtor = vi.fn().mockImplementation(() => {
|
||||
const executorCtor = vi.fn().mockImplementation(function () {
|
||||
const executor = {
|
||||
resumeOrphaned: vi.fn().mockResolvedValue(undefined),
|
||||
markStuckAborted: vi.fn(),
|
||||
@@ -225,7 +227,7 @@ const mocks = vi.hoisted(() => {
|
||||
return executor;
|
||||
});
|
||||
|
||||
const schedulerCtor = vi.fn().mockImplementation(() => {
|
||||
const schedulerCtor = vi.fn().mockImplementation(function () {
|
||||
const scheduler = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -234,7 +236,7 @@ const mocks = vi.hoisted(() => {
|
||||
return scheduler;
|
||||
});
|
||||
|
||||
const stuckDetectorCtor = vi.fn().mockImplementation(() => {
|
||||
const stuckDetectorCtor = vi.fn().mockImplementation(function () {
|
||||
const detector = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -244,7 +246,7 @@ const mocks = vi.hoisted(() => {
|
||||
return detector;
|
||||
});
|
||||
|
||||
const selfHealingCtor = vi.fn().mockImplementation(() => {
|
||||
const selfHealingCtor = vi.fn().mockImplementation(function () {
|
||||
const manager = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -254,7 +256,7 @@ const mocks = vi.hoisted(() => {
|
||||
return manager;
|
||||
});
|
||||
|
||||
const cronRunnerCtor = vi.fn().mockImplementation(() => {
|
||||
const cronRunnerCtor = vi.fn().mockImplementation(function () {
|
||||
const cron = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -263,7 +265,7 @@ const mocks = vi.hoisted(() => {
|
||||
return cron;
|
||||
});
|
||||
|
||||
const missionAutopilotCtor = vi.fn().mockImplementation(() => {
|
||||
const missionAutopilotCtor = vi.fn().mockImplementation(function () {
|
||||
const autopilot = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -273,7 +275,7 @@ const mocks = vi.hoisted(() => {
|
||||
return autopilot;
|
||||
});
|
||||
|
||||
const missionExecutionLoopCtor = vi.fn().mockImplementation(() => {
|
||||
const missionExecutionLoopCtor = vi.fn().mockImplementation(function () {
|
||||
const loop = {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -284,7 +286,7 @@ const mocks = vi.hoisted(() => {
|
||||
return loop;
|
||||
});
|
||||
|
||||
const notifierCtor = vi.fn().mockImplementation(() => {
|
||||
const notifierCtor = vi.fn().mockImplementation(function () {
|
||||
const notifier = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -293,7 +295,7 @@ const mocks = vi.hoisted(() => {
|
||||
return notifier;
|
||||
});
|
||||
|
||||
const pluginStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const pluginStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const pluginStore = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
listPlugins: vi.fn().mockResolvedValue([]),
|
||||
@@ -309,7 +311,7 @@ const mocks = vi.hoisted(() => {
|
||||
return pluginStore;
|
||||
});
|
||||
|
||||
const pluginLoaderCtor = vi.fn().mockImplementation(() => {
|
||||
const pluginLoaderCtor = vi.fn().mockImplementation(function () {
|
||||
const pluginLoader = {
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
loadAllPlugins: vi.fn().mockResolvedValue({ loaded: 0, errors: 0 }),
|
||||
@@ -341,25 +343,31 @@ const mocks = vi.hoisted(() => {
|
||||
refresh: vi.fn(),
|
||||
};
|
||||
|
||||
const agentSemaphoreCtor = vi.fn().mockImplementation(() => ({
|
||||
_active: 0,
|
||||
run: (fn: () => Promise<unknown>) => fn(),
|
||||
}));
|
||||
const agentSemaphoreCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
_active: 0,
|
||||
run: (fn: () => Promise<unknown>) => fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const heartbeatMonitorCtor = vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
startRun: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
executeHeartbeat: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
stopRun: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
const heartbeatMonitorCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
startRun: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
executeHeartbeat: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
stopRun: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
const heartbeatTriggerSchedulerCtor = vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
registerAgent: vi.fn(),
|
||||
getRegisteredAgents: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
const heartbeatTriggerSchedulerCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
registerAgent: vi.fn(),
|
||||
getRegisteredAgents: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
const createAiPromptExecutorMock = vi.fn().mockResolvedValue(vi.fn().mockResolvedValue("ok"));
|
||||
const syncInsightExtractionAutomationMock = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -373,7 +381,7 @@ const mocks = vi.hoisted(() => {
|
||||
pruning: { applied: false },
|
||||
});
|
||||
|
||||
const projectEngineCtor = vi.fn().mockImplementation((runtimeConfig: { workingDirectory: string }, _centralCore: unknown, options: { onInsightRunProcessed?: unknown }) => {
|
||||
const projectEngineCtor = vi.fn().mockImplementation(function (runtimeConfig: { workingDirectory: string }, _centralCore: unknown, options: { onInsightRunProcessed?: unknown }) {
|
||||
const store = taskStoreCtor(runtimeConfig.workingDirectory);
|
||||
const automationStore = automationStoreCtor(runtimeConfig.workingDirectory);
|
||||
const agentStore = agentStoreCtor();
|
||||
@@ -520,17 +528,21 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
CentralCore: mocks.centralCoreCtor,
|
||||
PluginStore: mocks.pluginStoreCtor,
|
||||
PluginLoader: mocks.pluginLoaderCtor,
|
||||
GlobalSettingsStore: vi.fn().mockImplementation(() => mocks.globalSettingsStoreInstance),
|
||||
GlobalSettingsStore: vi.fn().mockImplementation(function () {
|
||||
return mocks.globalSettingsStoreInstance;
|
||||
}),
|
||||
resolveGlobalDir: vi.fn().mockReturnValue("/home/user/.fusion"),
|
||||
getEnabledPiExtensionPaths: vi.fn(() => []),
|
||||
DaemonTokenManager: vi.fn().mockImplementation(() => ({
|
||||
getToken: vi.fn().mockImplementation(() => Promise.resolve(mocks.globalSettingsData.daemonToken as string | undefined)),
|
||||
generateToken: vi.fn().mockImplementation(() => {
|
||||
const token = "fn_a1b2c3d4e5f6789012345678901234ab";
|
||||
mocks.globalSettingsData.daemonToken = token;
|
||||
return Promise.resolve(token);
|
||||
}),
|
||||
})),
|
||||
DaemonTokenManager: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
getToken: vi.fn().mockImplementation(() => Promise.resolve(mocks.globalSettingsData.daemonToken as string | undefined)),
|
||||
generateToken: vi.fn().mockImplementation(function () {
|
||||
const token = "fn_a1b2c3d4e5f6789012345678901234ab";
|
||||
mocks.globalSettingsData.daemonToken = token;
|
||||
return Promise.resolve(token);
|
||||
}),
|
||||
};
|
||||
}),
|
||||
getTaskMergeBlocker: vi.fn().mockReturnValue(null),
|
||||
syncInsightExtractionAutomation: mocks.syncInsightExtractionAutomationMock,
|
||||
INSIGHT_EXTRACTION_SCHEDULE_NAME: "Memory Insight Extraction",
|
||||
@@ -540,7 +552,9 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
|
||||
vi.mock("@fusion/dashboard", () => ({
|
||||
createServer: mocks.createServerMock,
|
||||
GitHubClient: vi.fn().mockImplementation(() => ({})),
|
||||
GitHubClient: vi.fn().mockImplementation(function () {
|
||||
return {};
|
||||
}),
|
||||
createSkillsAdapter: vi.fn().mockReturnValue(undefined),
|
||||
getProjectSettingsPath: vi.fn().mockReturnValue("/tmp/project/.fusion/settings.json"),
|
||||
loadTlsCredentialsFromEnv: vi.fn().mockReturnValue(undefined),
|
||||
@@ -550,7 +564,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
const { createCliEngineMock } = await import("../../test/mockCoreEngine");
|
||||
return createCliEngineMock(() => importOriginal<typeof import("@fusion/engine")>(), {
|
||||
ProjectEngine: mocks.projectEngineCtor,
|
||||
ProjectEngineManager: vi.fn().mockImplementation((centralCore: any, options: any) => {
|
||||
ProjectEngineManager: vi.fn().mockImplementation(function (centralCore: any, options: any) {
|
||||
const engines = new Map<string, any>();
|
||||
return {
|
||||
startAll: vi.fn(async () => {
|
||||
@@ -578,27 +592,35 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
startReconciliation: vi.fn(),
|
||||
};
|
||||
}),
|
||||
PeerExchangeService: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
})),
|
||||
PeerExchangeService: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
};
|
||||
}),
|
||||
TriageProcessor: mocks.triageCtor,
|
||||
TaskExecutor: mocks.executorCtor,
|
||||
Scheduler: mocks.schedulerCtor,
|
||||
AgentSemaphore: mocks.agentSemaphoreCtor,
|
||||
WorktreePool: vi.fn().mockImplementation(() => ({
|
||||
rehydrate: vi.fn(),
|
||||
})),
|
||||
WorktreePool: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
rehydrate: vi.fn(),
|
||||
};
|
||||
}),
|
||||
aiMergeTask: vi.fn().mockResolvedValue({ merged: true }),
|
||||
UsageLimitPauser: vi.fn().mockImplementation(() => ({})),
|
||||
UsageLimitPauser: vi.fn().mockImplementation(function () {
|
||||
return {};
|
||||
}),
|
||||
PRIORITY_MERGE: 100,
|
||||
scanIdleWorktrees: vi.fn().mockResolvedValue([]),
|
||||
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
|
||||
NtfyNotifier: mocks.notifierCtor,
|
||||
PrMonitor: vi.fn().mockImplementation(() => ({
|
||||
onNewComments: vi.fn(),
|
||||
})),
|
||||
PrMonitor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
onNewComments: vi.fn(),
|
||||
};
|
||||
}),
|
||||
PrCommentHandler: vi.fn().mockImplementation(() => ({
|
||||
handleNewComments: vi.fn(),
|
||||
createFollowUpTask: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -619,9 +641,11 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
||||
AuthStorage: {
|
||||
create: vi.fn(() => mocks.authStorage),
|
||||
},
|
||||
DefaultPackageManager: vi.fn().mockImplementation(() => ({
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
})),
|
||||
DefaultPackageManager: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
};
|
||||
}),
|
||||
ModelRegistry: {
|
||||
create: vi.fn(() => mocks.modelRegistry),
|
||||
inMemory: vi.fn(() => mocks.modelRegistry),
|
||||
@@ -642,6 +666,8 @@ vi.mock("../task-lifecycle.js", () => ({
|
||||
processPullRequestMergeTask: vi.fn().mockResolvedValue("waiting"),
|
||||
createGroupPrCallback: vi.fn(() => vi.fn()),
|
||||
syncGroupPrCallback: vi.fn(() => vi.fn()),
|
||||
createPrNodeGithubOps: vi.fn(() => ({})),
|
||||
createPrReconcileGithubOps: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
vi.mock("../project-context.js", () => ({
|
||||
@@ -679,9 +705,9 @@ describe("runDaemon", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(function () {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
@@ -741,15 +767,17 @@ describe("runDaemon", () => {
|
||||
|
||||
it("continues startup when plugin auto-load fails", async () => {
|
||||
const { PluginLoader } = await import("@fusion/core");
|
||||
(PluginLoader as unknown as ReturnType<typeof vi.fn>).mockImplementationOnce(() => ({
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
loadAllPlugins: vi.fn().mockRejectedValue(new Error("plugin load failed")),
|
||||
stopPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
reloadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||
getPlugin: vi.fn(),
|
||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
(PluginLoader as unknown as ReturnType<typeof vi.fn>).mockImplementationOnce(function () {
|
||||
return {
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
loadAllPlugins: vi.fn().mockRejectedValue(new Error("plugin load failed")),
|
||||
stopPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
reloadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||
getPlugin: vi.fn(),
|
||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
await expect(runDaemon({})).resolves.toBeUndefined();
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
@@ -937,8 +965,8 @@ describe("runDaemon --token-only mode", () => {
|
||||
vi.clearAllMocks();
|
||||
mocks.reset();
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processExitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
|
||||
@@ -4,15 +4,32 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
function makeConstructibleMock<T extends (...args: any[]) => 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<T>) {
|
||||
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" }),
|
||||
mockHybridExecutorInitialize: vi.fn().mockResolvedValue(undefined),
|
||||
mockHybridExecutorShutdown: vi.fn().mockResolvedValue(undefined),
|
||||
mockHybridExecutorCtor: vi.fn().mockImplementation(() => ({
|
||||
initialize: mockHybridExecutorInitialize,
|
||||
shutdown: mockHybridExecutorShutdown,
|
||||
})),
|
||||
mockHybridExecutorCtor: makeConstructibleMock(function () {
|
||||
return {
|
||||
initialize: mockHybridExecutorInitialize,
|
||||
shutdown: mockHybridExecutorShutdown,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
vi.mock("../startup-model-sync.js", () => ({
|
||||
syncStartupModels: mockSyncStartupModels,
|
||||
@@ -137,8 +154,8 @@ function makeMockStore() {
|
||||
vi.mock("@fusion/core", async (importOriginal) => {
|
||||
const { createCliCoreMock } = await import("../../test/mockCoreEngine");
|
||||
return createCliCoreMock(() => importOriginal<typeof import("@fusion/core")>(), {
|
||||
TaskStore: vi.fn().mockImplementation(() => makeMockStore()),
|
||||
CentralCore: vi.fn().mockImplementation(() => ({
|
||||
TaskStore: makeConstructibleMock(() => makeMockStore()),
|
||||
CentralCore: makeConstructibleMock(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
getProjectByPath: vi.fn().mockResolvedValue({ id: "project-1" }),
|
||||
@@ -149,7 +166,7 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
{ id: "project-1", name: "Test Project", path: process.cwd(), status: "active", isolationMode: "in-process", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
||||
]),
|
||||
})),
|
||||
AutomationStore: vi.fn().mockImplementation(() => ({
|
||||
AutomationStore: makeConstructibleMock(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
listSchedules: vi.fn().mockResolvedValue([]),
|
||||
getSchedule: vi.fn().mockResolvedValue(null),
|
||||
@@ -159,7 +176,7 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
recordRun: vi.fn().mockResolvedValue({}),
|
||||
getDueSchedules: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
AgentStore: vi.fn().mockImplementation(() => ({
|
||||
AgentStore: makeConstructibleMock(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
createAgent: vi.fn(),
|
||||
updateAgentState: vi.fn(),
|
||||
@@ -172,7 +189,7 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
getBudgetStatus: vi.fn().mockResolvedValue({ isOverBudget: false, isOverThreshold: false, usagePercent: 0 }),
|
||||
getRecentRuns: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
PluginStore: vi.fn().mockImplementation(() => {
|
||||
PluginStore: makeConstructibleMock(function () {
|
||||
const emitter = new EventEmitter();
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -193,7 +210,7 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
emit: emitter.emit.bind(emitter),
|
||||
};
|
||||
}),
|
||||
PluginLoader: vi.fn().mockImplementation(() => {
|
||||
PluginLoader: makeConstructibleMock(function () {
|
||||
const emitter = new EventEmitter();
|
||||
return {
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -214,12 +231,12 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
}),
|
||||
getEnabledPiExtensionPaths: vi.fn(() => []),
|
||||
resolveGlobalDir: mockResolveGlobalDir,
|
||||
GlobalSettingsStore: vi.fn().mockImplementation(() => ({
|
||||
GlobalSettingsStore: makeConstructibleMock(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
getSettings: mockGlobalSettingsGetSettings,
|
||||
updateSettings: mockGlobalSettingsUpdateSettings,
|
||||
})),
|
||||
DaemonTokenManager: vi.fn().mockImplementation(() => ({
|
||||
DaemonTokenManager: makeConstructibleMock(() => ({
|
||||
getOrCreateToken: mockDaemonTokenGetOrCreate,
|
||||
getToken: vi.fn().mockResolvedValue(undefined),
|
||||
generateToken: vi.fn().mockResolvedValue("fn_test_dashboard_token"),
|
||||
@@ -330,7 +347,7 @@ vi.mock("@fusion/dashboard", () => ({
|
||||
opts.onProjectFirstAccessed?.("project-1");
|
||||
return { listen: mockListen };
|
||||
}),
|
||||
GitHubClient: vi.fn().mockImplementation(() => ({
|
||||
GitHubClient: makeConstructibleMock(() => ({
|
||||
findPrForBranch: mockFindPrForBranch,
|
||||
createPr: mockCreatePr,
|
||||
getPrMergeStatus: mockGetPrMergeStatus,
|
||||
@@ -359,7 +376,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
const { createCliEngineMock } = await import("../../test/mockCoreEngine");
|
||||
const coreModule = await import("@fusion/core");
|
||||
const taskStoreMock = (coreModule.TaskStore as any);
|
||||
const TriageProcessor = vi.fn().mockImplementation(() => ({
|
||||
const TriageProcessor = makeConstructibleMock(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
}));
|
||||
@@ -369,7 +386,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
resumeOrphaned: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
const StuckTaskDetector = vi.fn().mockImplementation(() => ({
|
||||
const StuckTaskDetector = makeConstructibleMock(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
checkNow: mockStuckCheckNow,
|
||||
@@ -377,11 +394,11 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
untrackTask: vi.fn(),
|
||||
markTaskProgress: vi.fn(),
|
||||
}));
|
||||
const Scheduler = vi.fn().mockImplementation(() => ({
|
||||
const Scheduler = makeConstructibleMock(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
}));
|
||||
const PrMonitor = vi.fn().mockImplementation(() => ({
|
||||
const PrMonitor = makeConstructibleMock(() => ({
|
||||
onNewComments: vi.fn(),
|
||||
startMonitoring: vi.fn(),
|
||||
stopMonitoring: vi.fn(),
|
||||
@@ -390,12 +407,12 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
updatePrInfo: vi.fn(),
|
||||
drainComments: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
const PrCommentHandler = vi.fn().mockImplementation(() => ({
|
||||
const PrCommentHandler = makeConstructibleMock(() => ({
|
||||
handleNewComments: vi.fn().mockResolvedValue(undefined),
|
||||
createFollowUpTask: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
const aiMergeTask = vi.fn().mockImplementation(() => Promise.resolve({ merged: true }));
|
||||
const CronRunner = vi.fn().mockImplementation(() => ({
|
||||
const CronRunner = makeConstructibleMock(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
}));
|
||||
@@ -674,7 +691,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
AgentSemaphore: original.AgentSemaphore,
|
||||
// Stub heavy classes/functions
|
||||
ProjectEngine,
|
||||
ProjectEngineManager: vi.fn().mockImplementation((centralCore: any, options: any) => {
|
||||
ProjectEngineManager: makeConstructibleMock((centralCore: any, options: any) => {
|
||||
const engines = new Map<string, any>();
|
||||
const starting = new Map<string, Promise<any>>();
|
||||
// Keep the chosen HEAD startEngine/starting async shape from the conflict resolution.
|
||||
@@ -726,7 +743,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
startReconciliation: vi.fn(),
|
||||
};
|
||||
}),
|
||||
ProjectManager: vi.fn().mockImplementation(() => ({
|
||||
ProjectManager: makeConstructibleMock(() => ({
|
||||
getRuntime: vi.fn().mockReturnValue(undefined),
|
||||
addProject: vi.fn().mockResolvedValue(undefined),
|
||||
stopAll: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -741,12 +758,12 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
CronRunner,
|
||||
createAiPromptExecutor,
|
||||
SelfHealingManager,
|
||||
MissionAutopilot: vi.fn().mockImplementation(() => ({
|
||||
MissionAutopilot: makeConstructibleMock(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
setScheduler: vi.fn(),
|
||||
})),
|
||||
PluginLoader: vi.fn().mockImplementation(() => ({
|
||||
PluginLoader: makeConstructibleMock(() => ({
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
stopPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
reloadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -754,7 +771,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
getPlugin: vi.fn(),
|
||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||
})),
|
||||
PeerExchangeService: vi.fn().mockImplementation(() => ({
|
||||
PeerExchangeService: makeConstructibleMock(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
@@ -772,7 +789,7 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
||||
AuthStorage: {
|
||||
create: vi.fn(() => mockAuthStorage),
|
||||
},
|
||||
DefaultPackageManager: vi.fn().mockImplementation(() => ({
|
||||
DefaultPackageManager: makeConstructibleMock(() => ({
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
})),
|
||||
ModelRegistry: {
|
||||
@@ -3103,6 +3120,8 @@ describe("StreamedLogBuffer", () => {
|
||||
|
||||
describe("runDashboard — merge stream sink routing", () => {
|
||||
it("routes streamed merge deltas through log sink without raw stdout writes", async () => {
|
||||
vi.clearAllMocks();
|
||||
resetGitHubMocks();
|
||||
process.env.FUSION_DASHBOARD_TOKEN = "fn_test_dashboard_token";
|
||||
const { TaskStore, AutomationStore, AgentStore, PluginStore, PluginLoader, CentralCore } = await import("@fusion/core");
|
||||
const { aiMergeTask } = await import("@fusion/engine");
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runResearchCancel, runResearchCreate, runResearchExport, runResearchList, runResearchRetry, runResearchShow } from "../research.js";
|
||||
|
||||
function makeConstructibleMock<T extends (...args: any[]) => 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<T>) {
|
||||
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 mockRun = {
|
||||
id: "RR-001",
|
||||
query: "test query",
|
||||
@@ -35,12 +50,12 @@ const orchestratorMock = {
|
||||
|
||||
const { resolveResearchSettingsMock, providerRegistryMock, writeFileMock } = vi.hoisted(() => ({
|
||||
resolveResearchSettingsMock: vi.fn(() => ({ enabled: true, limits: { maxConcurrentRuns: 2, maxSourcesPerRun: 5, requestTimeoutMs: 1000, maxDurationMs: 5000 } })),
|
||||
providerRegistryMock: vi.fn(() => ({ getAvailableProviders: () => ["tavily"], getProvider: () => ({ type: "tavily" }) })),
|
||||
providerRegistryMock: makeConstructibleMock(function () { return { getAvailableProviders: () => ["tavily"], getProvider: () => ({ type: "tavily" }) }; }),
|
||||
writeFileMock: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", () => ({
|
||||
TaskStore: vi.fn(() => storeMock),
|
||||
TaskStore: makeConstructibleMock(() => storeMock),
|
||||
resolveResearchSettings: resolveResearchSettingsMock,
|
||||
RESEARCH_RUN_STATUSES: ["queued", "running", "cancelling", "retry_waiting", "completed", "failed", "cancelled", "timed_out", "retry_exhausted"],
|
||||
RESEARCH_EXPORT_FORMATS: ["json", "markdown", "pdf"],
|
||||
@@ -49,7 +64,7 @@ vi.mock("@fusion/core", () => ({
|
||||
vi.mock("@fusion/engine", () => ({
|
||||
ResearchProviderRegistry: providerRegistryMock,
|
||||
ResearchStepRunner: vi.fn(),
|
||||
ResearchOrchestrator: vi.fn(() => orchestratorMock),
|
||||
ResearchOrchestrator: makeConstructibleMock(() => orchestratorMock),
|
||||
}));
|
||||
|
||||
vi.mock("../../project-context.js", () => ({ resolveProject: vi.fn(async () => undefined) }));
|
||||
@@ -66,7 +81,7 @@ describe("research commands", () => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as typeof process.exit);
|
||||
resolveResearchSettingsMock.mockReturnValue({ enabled: true, limits: { maxConcurrentRuns: 2, maxSourcesPerRun: 5, requestTimeoutMs: 1000, maxDurationMs: 5000 } });
|
||||
providerRegistryMock.mockReturnValue({ getAvailableProviders: () => ["tavily"], getProvider: () => ({ type: "tavily" }) });
|
||||
providerRegistryMock.mockImplementation(function () { return { getAvailableProviders: () => ["tavily"], getProvider: () => ({ type: "tavily" }) }; });
|
||||
researchStoreMock.getRun.mockReturnValue(mockRun);
|
||||
researchStoreMock.listRuns.mockReturnValue([mockRun]);
|
||||
orchestratorMock.retryRun.mockReturnValue("RR-003");
|
||||
@@ -89,7 +104,7 @@ describe("research commands", () => {
|
||||
searchProvider: "builtin",
|
||||
limits: { maxConcurrentRuns: 2, maxSourcesPerRun: 5, requestTimeoutMs: 1000, maxDurationMs: 5000 },
|
||||
});
|
||||
providerRegistryMock.mockReturnValueOnce({ getAvailableProviders: () => ["web-search"], getProvider: () => ({ type: "web-search" }) });
|
||||
providerRegistryMock.mockImplementationOnce(function () { return { getAvailableProviders: () => ["web-search"], getProvider: () => ({ type: "web-search" }) }; });
|
||||
|
||||
await runResearchCreate({ query: "hello builtin" });
|
||||
|
||||
@@ -154,7 +169,7 @@ describe("research commands", () => {
|
||||
});
|
||||
|
||||
it("errors when providers are unavailable", async () => {
|
||||
providerRegistryMock.mockReturnValue({ getAvailableProviders: () => [], getProvider: () => undefined });
|
||||
providerRegistryMock.mockImplementation(function () { return { getAvailableProviders: () => [], getProvider: () => undefined }; });
|
||||
await expect(runResearchCreate({ query: "hello" })).rejects.toThrow("process.exit:1");
|
||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("provider-unavailable"));
|
||||
});
|
||||
|
||||
@@ -9,10 +9,12 @@ const { mockSyncStartupModels, mockShouldUseHybridExecutor, mockHybridExecutorCt
|
||||
mockShouldUseHybridExecutor: vi.fn().mockResolvedValue({ enabled: false, reason: "single-project-local-only" }),
|
||||
mockHybridExecutorInitialize: vi.fn().mockResolvedValue(undefined),
|
||||
mockHybridExecutorShutdown: vi.fn().mockResolvedValue(undefined),
|
||||
mockHybridExecutorCtor: vi.fn().mockImplementation(() => ({
|
||||
initialize: mockHybridExecutorInitialize,
|
||||
shutdown: mockHybridExecutorShutdown,
|
||||
})),
|
||||
mockHybridExecutorCtor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
initialize: mockHybridExecutorInitialize,
|
||||
shutdown: mockHybridExecutorShutdown,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
vi.mock("../startup-model-sync.js", () => ({
|
||||
syncStartupModels: mockSyncStartupModels,
|
||||
@@ -132,13 +134,13 @@ const mocks = vi.hoisted(() => {
|
||||
});
|
||||
}
|
||||
|
||||
const taskStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const taskStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const store = createTaskStoreMock();
|
||||
taskStores.push(store);
|
||||
return store;
|
||||
});
|
||||
|
||||
const automationStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const automationStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const automationStore = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
@@ -146,7 +148,7 @@ const mocks = vi.hoisted(() => {
|
||||
return automationStore;
|
||||
});
|
||||
|
||||
const agentStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const agentStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const agentStore = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
@@ -154,7 +156,7 @@ const mocks = vi.hoisted(() => {
|
||||
return agentStore;
|
||||
});
|
||||
|
||||
const centralCoreCtor = vi.fn().mockImplementation(() => {
|
||||
const centralCoreCtor = vi.fn().mockImplementation(function () {
|
||||
const now = new Date().toISOString();
|
||||
const projects = [
|
||||
{ ...PROJECT_FIXTURES.primary, createdAt: now, updatedAt: now },
|
||||
@@ -223,7 +225,7 @@ const mocks = vi.hoisted(() => {
|
||||
}),
|
||||
}));
|
||||
|
||||
const triageCtor = vi.fn().mockImplementation(() => {
|
||||
const triageCtor = vi.fn().mockImplementation(function () {
|
||||
const triage = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -233,7 +235,7 @@ const mocks = vi.hoisted(() => {
|
||||
return triage;
|
||||
});
|
||||
|
||||
const executorCtor = vi.fn().mockImplementation(() => {
|
||||
const executorCtor = vi.fn().mockImplementation(function () {
|
||||
const executor = {
|
||||
resumeOrphaned: vi.fn().mockResolvedValue(undefined),
|
||||
markStuckAborted: vi.fn(),
|
||||
@@ -245,7 +247,7 @@ const mocks = vi.hoisted(() => {
|
||||
return executor;
|
||||
});
|
||||
|
||||
const schedulerCtor = vi.fn().mockImplementation(() => {
|
||||
const schedulerCtor = vi.fn().mockImplementation(function () {
|
||||
const scheduler = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -254,7 +256,7 @@ const mocks = vi.hoisted(() => {
|
||||
return scheduler;
|
||||
});
|
||||
|
||||
const stuckDetectorCtor = vi.fn().mockImplementation(() => {
|
||||
const stuckDetectorCtor = vi.fn().mockImplementation(function () {
|
||||
const detector = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -264,7 +266,7 @@ const mocks = vi.hoisted(() => {
|
||||
return detector;
|
||||
});
|
||||
|
||||
const selfHealingCtor = vi.fn().mockImplementation(() => {
|
||||
const selfHealingCtor = vi.fn().mockImplementation(function () {
|
||||
const manager = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -274,7 +276,7 @@ const mocks = vi.hoisted(() => {
|
||||
return manager;
|
||||
});
|
||||
|
||||
const cronRunnerCtor = vi.fn().mockImplementation(() => {
|
||||
const cronRunnerCtor = vi.fn().mockImplementation(function () {
|
||||
const cron = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -283,7 +285,7 @@ const mocks = vi.hoisted(() => {
|
||||
return cron;
|
||||
});
|
||||
|
||||
const missionAutopilotCtor = vi.fn().mockImplementation(() => {
|
||||
const missionAutopilotCtor = vi.fn().mockImplementation(function () {
|
||||
const autopilot = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -293,7 +295,7 @@ const mocks = vi.hoisted(() => {
|
||||
return autopilot;
|
||||
});
|
||||
|
||||
const missionExecutionLoopCtor = vi.fn().mockImplementation(() => {
|
||||
const missionExecutionLoopCtor = vi.fn().mockImplementation(function () {
|
||||
const loop = {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -304,7 +306,7 @@ const mocks = vi.hoisted(() => {
|
||||
return loop;
|
||||
});
|
||||
|
||||
const notifierCtor = vi.fn().mockImplementation(() => {
|
||||
const notifierCtor = vi.fn().mockImplementation(function () {
|
||||
const notifier = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
@@ -313,7 +315,7 @@ const mocks = vi.hoisted(() => {
|
||||
return notifier;
|
||||
});
|
||||
|
||||
const pluginStoreCtor = vi.fn().mockImplementation(() => {
|
||||
const pluginStoreCtor = vi.fn().mockImplementation(function () {
|
||||
const pluginStore = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
listPlugins: vi.fn().mockResolvedValue([]),
|
||||
@@ -329,7 +331,7 @@ const mocks = vi.hoisted(() => {
|
||||
return pluginStore;
|
||||
});
|
||||
|
||||
const pluginLoaderCtor = vi.fn().mockImplementation(() => {
|
||||
const pluginLoaderCtor = vi.fn().mockImplementation(function () {
|
||||
const pluginLoader = {
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
loadAllPlugins: vi.fn().mockResolvedValue({ loaded: 0, errors: 0 }),
|
||||
@@ -361,25 +363,31 @@ const mocks = vi.hoisted(() => {
|
||||
refresh: vi.fn(),
|
||||
};
|
||||
|
||||
const agentSemaphoreCtor = vi.fn().mockImplementation(() => ({
|
||||
_active: 0,
|
||||
run: (fn: () => Promise<unknown>) => fn(),
|
||||
}));
|
||||
const agentSemaphoreCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
_active: 0,
|
||||
run: (fn: () => Promise<unknown>) => fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const heartbeatMonitorCtor = vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
startRun: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
executeHeartbeat: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
stopRun: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
const heartbeatMonitorCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
startRun: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
executeHeartbeat: vi.fn().mockResolvedValue({ id: "run-1" }),
|
||||
stopRun: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
|
||||
const heartbeatTriggerSchedulerCtor = vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
registerAgent: vi.fn(),
|
||||
getRegisteredAgents: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
const heartbeatTriggerSchedulerCtor = vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
registerAgent: vi.fn(),
|
||||
getRegisteredAgents: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
const createAiPromptExecutorMock = vi.fn().mockResolvedValue(vi.fn().mockResolvedValue("ok"));
|
||||
const syncInsightExtractionAutomationMock = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -393,7 +401,7 @@ const mocks = vi.hoisted(() => {
|
||||
pruning: { applied: false },
|
||||
});
|
||||
|
||||
const projectEngineCtor = vi.fn().mockImplementation((runtimeConfig: { workingDirectory: string }, _centralCore: unknown, options: { onInsightRunProcessed?: unknown }) => {
|
||||
const projectEngineCtor = vi.fn().mockImplementation(function (runtimeConfig: { workingDirectory: string }, _centralCore: unknown, options: { onInsightRunProcessed?: unknown }) {
|
||||
const store = taskStoreCtor(runtimeConfig.workingDirectory);
|
||||
const automationStore = automationStoreCtor(runtimeConfig.workingDirectory);
|
||||
const agentStore = agentStoreCtor();
|
||||
@@ -575,19 +583,25 @@ vi.mock("@fusion/core", async (importOriginal) => {
|
||||
syncInsightExtractionAutomation: mocks.syncInsightExtractionAutomationMock,
|
||||
INSIGHT_EXTRACTION_SCHEDULE_NAME: "Memory Insight Extraction",
|
||||
processAndAuditInsightExtraction: mocks.processAndAuditInsightExtractionMock,
|
||||
DaemonTokenManager: vi.fn().mockImplementation(() => ({
|
||||
getToken: vi.fn().mockResolvedValue(null),
|
||||
generateToken: vi.fn().mockResolvedValue("fn_generated1234567890"),
|
||||
storeToken: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
GlobalSettingsStore: vi.fn().mockImplementation(() => ({})),
|
||||
DaemonTokenManager: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
getToken: vi.fn().mockResolvedValue(null),
|
||||
generateToken: vi.fn().mockResolvedValue("fn_generated1234567890"),
|
||||
storeToken: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}),
|
||||
GlobalSettingsStore: vi.fn().mockImplementation(function () {
|
||||
return {};
|
||||
}),
|
||||
resolveGlobalDir: vi.fn().mockReturnValue("/mock/global"),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock("@fusion/dashboard", () => ({
|
||||
createServer: mocks.createServerMock,
|
||||
GitHubClient: vi.fn().mockImplementation(() => ({})),
|
||||
GitHubClient: vi.fn().mockImplementation(function () {
|
||||
return {};
|
||||
}),
|
||||
createSkillsAdapter: vi.fn().mockReturnValue(undefined),
|
||||
getProjectSettingsPath: vi.fn().mockReturnValue("/tmp/project/.fusion/settings.json"),
|
||||
loadTlsCredentialsFromEnv: vi.fn().mockReturnValue(undefined),
|
||||
@@ -597,7 +611,7 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
const { createCliEngineMock } = await import("../../test/mockCoreEngine");
|
||||
return createCliEngineMock(() => importOriginal<typeof import("@fusion/engine")>(), {
|
||||
ProjectEngine: mocks.projectEngineCtor,
|
||||
ProjectEngineManager: vi.fn().mockImplementation((centralCore: any, options: any) => {
|
||||
ProjectEngineManager: vi.fn().mockImplementation(function (centralCore: any, options: any) {
|
||||
const engines = new Map<string, any>();
|
||||
return {
|
||||
startAll: vi.fn(async () => {
|
||||
@@ -629,26 +643,34 @@ vi.mock("@fusion/engine", async (importOriginal) => {
|
||||
startReconciliation: vi.fn(),
|
||||
};
|
||||
}),
|
||||
PeerExchangeService: vi.fn().mockImplementation(() => ({
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
})),
|
||||
PeerExchangeService: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}),
|
||||
TriageProcessor: mocks.triageCtor,
|
||||
TaskExecutor: mocks.executorCtor,
|
||||
Scheduler: mocks.schedulerCtor,
|
||||
AgentSemaphore: mocks.agentSemaphoreCtor,
|
||||
WorktreePool: vi.fn().mockImplementation(() => ({
|
||||
rehydrate: vi.fn(),
|
||||
})),
|
||||
WorktreePool: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
rehydrate: vi.fn(),
|
||||
};
|
||||
}),
|
||||
aiMergeTask: vi.fn().mockResolvedValue({ merged: true }),
|
||||
UsageLimitPauser: vi.fn().mockImplementation(() => ({})),
|
||||
UsageLimitPauser: vi.fn().mockImplementation(function () {
|
||||
return {};
|
||||
}),
|
||||
PRIORITY_MERGE: 100,
|
||||
scanIdleWorktrees: vi.fn().mockResolvedValue([]),
|
||||
cleanupOrphanedWorktrees: vi.fn().mockResolvedValue(0),
|
||||
NtfyNotifier: mocks.notifierCtor,
|
||||
PrMonitor: vi.fn().mockImplementation(() => ({
|
||||
onNewComments: vi.fn(),
|
||||
})),
|
||||
PrMonitor: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
onNewComments: vi.fn(),
|
||||
};
|
||||
}),
|
||||
PrCommentHandler: vi.fn().mockImplementation(() => ({
|
||||
handleNewComments: vi.fn(),
|
||||
createFollowUpTask: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -669,9 +691,11 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
||||
AuthStorage: {
|
||||
create: vi.fn(() => mocks.authStorage),
|
||||
},
|
||||
DefaultPackageManager: vi.fn().mockImplementation(() => ({
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
})),
|
||||
DefaultPackageManager: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
resolve: vi.fn().mockResolvedValue({ extensions: [] }),
|
||||
};
|
||||
}),
|
||||
ModelRegistry: {
|
||||
create: vi.fn(() => mocks.modelRegistry),
|
||||
inMemory: vi.fn(() => mocks.modelRegistry),
|
||||
@@ -696,6 +720,8 @@ vi.mock("../task-lifecycle.js", () => ({
|
||||
processPullRequestMergeTask: vi.fn().mockResolvedValue("waiting"),
|
||||
createGroupPrCallback: vi.fn(() => vi.fn()),
|
||||
syncGroupPrCallback: vi.fn(() => vi.fn()),
|
||||
createPrNodeGithubOps: vi.fn(() => ({})),
|
||||
createPrReconcileGithubOps: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
vi.mock("../project-context.js", () => ({
|
||||
@@ -735,9 +761,9 @@ describe("runServe", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(function () {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
@@ -956,7 +982,7 @@ describe("runServe — Plugin wiring", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
if (event === "SIGINT" || event === "SIGTERM") {
|
||||
@@ -1041,16 +1067,18 @@ describe("runServe — Plugin wiring", () => {
|
||||
|
||||
it("continues startup when plugin auto-load fails", async () => {
|
||||
const { PluginLoader } = await import("@fusion/core");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
(PluginLoader as unknown as ReturnType<typeof vi.fn>).mockImplementationOnce(() => ({
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
loadAllPlugins: vi.fn().mockRejectedValue(new Error("plugin load failed")),
|
||||
stopPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
reloadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||
getPlugin: vi.fn(),
|
||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||
}));
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
(PluginLoader as unknown as ReturnType<typeof vi.fn>).mockImplementationOnce(function () {
|
||||
return {
|
||||
loadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
loadAllPlugins: vi.fn().mockRejectedValue(new Error("plugin load failed")),
|
||||
stopPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
reloadPlugin: vi.fn().mockResolvedValue(undefined),
|
||||
getPluginRoutes: vi.fn().mockReturnValue([]),
|
||||
getPlugin: vi.fn(),
|
||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
await expect(runServe(4040, {})).resolves.toBeUndefined();
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
@@ -1101,9 +1129,9 @@ describe("runServe — Memory Insight Automation wiring", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(function () {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
@@ -1139,7 +1167,9 @@ describe("runServe — Memory Insight Automation wiring", () => {
|
||||
stopDiscovery: vi.fn(),
|
||||
};
|
||||
mocks.centralInstances.push(instance);
|
||||
CentralCore.mockImplementation(() => instance);
|
||||
CentralCore.mockImplementation(function () {
|
||||
return instance;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1229,7 +1259,7 @@ describe("runServe — Memory Insight Automation wiring", () => {
|
||||
|
||||
it("handles syncInsightExtractionAutomation errors gracefully", async () => {
|
||||
const { syncInsightExtractionAutomation } = await import("@fusion/core");
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
syncInsightExtractionAutomation.mockRejectedValueOnce(new Error("Sync failed"));
|
||||
|
||||
await runServe(4040, {});
|
||||
@@ -1300,7 +1330,9 @@ describe("runServe — Semaphore boundary (task lanes only)", () => {
|
||||
stopDiscovery: vi.fn(),
|
||||
};
|
||||
mocks.centralInstances.push(instance);
|
||||
CentralCore.mockImplementation(() => instance);
|
||||
CentralCore.mockImplementation(function () {
|
||||
return instance;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1452,7 +1484,7 @@ describe("runServe — Peer exchange and discovery", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(function () {});
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
if (event === "SIGINT" || event === "SIGTERM") {
|
||||
@@ -1465,7 +1497,7 @@ describe("runServe — Peer exchange and discovery", () => {
|
||||
// Override CentralCore to use original implementation that pushes to centralInstances
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
// Reset to the original constructor that creates and pushes instances
|
||||
CentralCore.mockImplementation(() => {
|
||||
CentralCore.mockImplementation(function () {
|
||||
const instance = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -1625,7 +1657,7 @@ describe("runServe --daemon flag", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
if (event === "SIGINT" || event === "SIGTERM") {
|
||||
@@ -1637,7 +1669,7 @@ describe("runServe --daemon flag", () => {
|
||||
|
||||
// Override CentralCore to use original implementation that pushes to centralInstances
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
CentralCore.mockImplementation(() => {
|
||||
CentralCore.mockImplementation(function () {
|
||||
const instance = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -1817,7 +1849,7 @@ describe("runServe — multi-project cwd/default engine resolution", () => {
|
||||
|
||||
signalHandlers = { SIGINT: [], SIGTERM: [] };
|
||||
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(function () {});
|
||||
cwdSpy = vi.spyOn(process, "cwd").mockReturnValue("/repo");
|
||||
processOnSpy = vi.spyOn(process, "on").mockImplementation(((event: string, listener: () => void) => {
|
||||
if (event === "SIGINT" || event === "SIGTERM") {
|
||||
@@ -1829,7 +1861,7 @@ describe("runServe — multi-project cwd/default engine resolution", () => {
|
||||
|
||||
// Override CentralCore to use original implementation that pushes to centralInstances
|
||||
const { CentralCore } = await import("@fusion/core");
|
||||
CentralCore.mockImplementation(() => {
|
||||
CentralCore.mockImplementation(function () {
|
||||
const instance = {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -1999,7 +2031,7 @@ describe("runServe — multi-project cwd/default engine resolution", () => {
|
||||
it("--no-auto-register falls back to existing started engines", async () => {
|
||||
const freshCwd = mkdtempSync(join(tmpdir(), "serve-no-auto-register-"));
|
||||
cwdSpy.mockReturnValue(freshCwd);
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(function () {});
|
||||
const ensureSpy = vi.spyOn(ensureProjectRegisteredModule, "ensureCwdProjectRegistered")
|
||||
.mockResolvedValue(null);
|
||||
|
||||
|
||||
@@ -54,6 +54,23 @@ vi.mock("@fusion/core", async (importActual) => {
|
||||
return impl(...args);
|
||||
})) as typeof TaskStoreMock.mockImplementation;
|
||||
|
||||
const CentralCoreMock = vi.fn(function () {});
|
||||
const centralCoreMockImplementation = CentralCoreMock.mockImplementation.bind(CentralCoreMock);
|
||||
CentralCoreMock.mockImplementation = ((impl: (...args: any[]) => unknown) =>
|
||||
centralCoreMockImplementation(function (this: unknown, ...args: any[]) {
|
||||
return impl(...args);
|
||||
})) as typeof CentralCoreMock.mockImplementation;
|
||||
CentralCoreMock.mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
listProjects: vi.fn().mockResolvedValue([]),
|
||||
getProject: vi.fn().mockResolvedValue(undefined),
|
||||
getProjectByPath: vi.fn().mockResolvedValue(undefined),
|
||||
registerProject: vi.fn().mockResolvedValue({ id: "proj_test", name: "test", path: "/test" }),
|
||||
getNode: vi.fn().mockResolvedValue(undefined),
|
||||
getNodeByName: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
return {
|
||||
...actual,
|
||||
TaskStore: TaskStoreMock,
|
||||
@@ -74,18 +91,7 @@ vi.mock("@fusion/core", async (importActual) => {
|
||||
}
|
||||
return ids;
|
||||
}),
|
||||
CentralCore: vi.fn().mockImplementation(function() {
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
listProjects: vi.fn().mockResolvedValue([]),
|
||||
getProject: vi.fn().mockResolvedValue(undefined),
|
||||
getProjectByPath: vi.fn().mockResolvedValue(undefined),
|
||||
registerProject: vi.fn().mockResolvedValue({ id: "proj_test", name: "test", path: "/test" }),
|
||||
getNode: vi.fn().mockResolvedValue(undefined),
|
||||
getNodeByName: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
}),
|
||||
CentralCore: CentralCoreMock,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -94,9 +100,11 @@ vi.mock("@fusion/engine", () => ({ aiMergeTask: vi.fn() }));
|
||||
|
||||
// Mock @fusion/dashboard
|
||||
vi.mock("@fusion/dashboard", () => ({
|
||||
GitHubClient: vi.fn().mockImplementation(() => ({
|
||||
createPr: vi.fn(),
|
||||
})),
|
||||
GitHubClient: vi.fn().mockImplementation(function () {
|
||||
return {
|
||||
createPr: vi.fn(),
|
||||
};
|
||||
}),
|
||||
generatePrMetadata: vi.fn(),
|
||||
loadTlsCredentialsFromEnv: vi.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
@@ -167,6 +175,8 @@ function makeTask(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(resolveProject).mockRejectedValue(new Error("No project context"));
|
||||
vi.mocked(runDeterministicDuplicateGuard).mockResolvedValue({
|
||||
action: "proceed",
|
||||
fingerprint: null,
|
||||
@@ -973,12 +983,16 @@ describe("project-aware task command behavior", () => {
|
||||
vi.mocked(isGhAvailable).mockReturnValue(true);
|
||||
vi.mocked(isGhAuthenticated).mockReturnValue(true);
|
||||
vi.mocked(getCurrentRepo).mockReturnValue({ owner: "acme", repo: "demo" });
|
||||
vi.mocked(GitHubClient).mockImplementation(() => ({ createPr: mockCreatePr }) as never);
|
||||
vi.mocked(GitHubClient).mockImplementation(function () {
|
||||
return { createPr: mockCreatePr } as never;
|
||||
});
|
||||
|
||||
(TaskStore as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => ({
|
||||
init: vi.fn(),
|
||||
getTask: vi.fn().mockResolvedValue(makeTask({ id: "FN-001", column: "in-review", branchName: "fusion/fn-001" })),
|
||||
updatePrInfo: vi.fn().mockResolvedValue(undefined),
|
||||
ensurePrEntityForSource: vi.fn(() => ({ id: "pr-entity-1" })),
|
||||
updatePrEntity: vi.fn(),
|
||||
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
@@ -2607,10 +2621,10 @@ describe("runTaskRetry", () => {
|
||||
error: null,
|
||||
mergeRetries: 0,
|
||||
}));
|
||||
expect(mockMoveTask).not.toHaveBeenCalled();
|
||||
expect(mockMoveTask).toHaveBeenCalledWith("FN-001", "todo");
|
||||
expect(mockLogEntry).toHaveBeenCalledWith(
|
||||
"FN-001",
|
||||
"Retry requested from CLI (in-review merge retry, mergeRetries reset)",
|
||||
"Retry requested from CLI (merge retry → todo, mergeRetries reset)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3014,6 +3028,7 @@ describe("runTaskPrCreate", () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
stderrWriteSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
||||
@@ -3025,9 +3040,11 @@ describe("runTaskPrCreate", () => {
|
||||
|
||||
// Setup GitHubClient mock
|
||||
mockCreatePr = vi.fn();
|
||||
vi.mocked(GitHubClient).mockImplementation(() => ({
|
||||
createPr: mockCreatePr,
|
||||
} as unknown as GitHubClient));
|
||||
vi.mocked(GitHubClient).mockImplementation(function () {
|
||||
return {
|
||||
createPr: mockCreatePr,
|
||||
} as unknown as GitHubClient;
|
||||
});
|
||||
vi.mocked(generatePrMetadata).mockResolvedValue({ title: "AI Generated Title", body: "AI Generated Body", templateUsed: false });
|
||||
|
||||
// Setup gh-cli mocks
|
||||
@@ -3044,6 +3061,8 @@ describe("runTaskPrCreate", () => {
|
||||
init: vi.fn(),
|
||||
getTask: mockGetTask,
|
||||
updatePrInfo: mockUpdatePrInfo,
|
||||
ensurePrEntityForSource: vi.fn(() => ({ id: "pr-entity-1" })),
|
||||
updatePrEntity: vi.fn(),
|
||||
logEntry: mockLogEntry,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -1058,6 +1058,7 @@ export async function runTaskRetry(id: string, projectName?: string) {
|
||||
// and merge failures (all steps done).
|
||||
if (isInReviewRetry) {
|
||||
if (isExecutionFailureInReview) {
|
||||
await store.moveTask(id, "todo", { preserveProgress: true });
|
||||
await store.updateTask(id, {
|
||||
status: null,
|
||||
error: null,
|
||||
@@ -1070,7 +1071,6 @@ export async function runTaskRetry(id: string, projectName?: string) {
|
||||
? `Retry requested from CLI (stranded in-review execution retry → todo, preserving progress${retryLogSuffix})`
|
||||
: `Retry requested from CLI (execution failure in-review → todo, preserving progress${retryLogSuffix})`,
|
||||
);
|
||||
await store.moveTask(id, "todo", { preserveProgress: true });
|
||||
|
||||
console.log();
|
||||
console.log(` ✓ Retried ${id} → todo (execution failure, preserving step progress)`);
|
||||
@@ -1078,20 +1078,26 @@ export async function runTaskRetry(id: string, projectName?: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
await store.moveTask(id, "todo");
|
||||
await store.updateTask(id, {
|
||||
status: null,
|
||||
error: null,
|
||||
...autoPauseClearPatch,
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
await store.logEntry(id, `Retry requested from CLI (in-review merge retry, mergeRetries reset${retryLogSuffix})`);
|
||||
await store.logEntry(id, `Retry requested from CLI (merge retry → todo, mergeRetries reset${retryLogSuffix})`);
|
||||
|
||||
console.log();
|
||||
console.log(` ✓ Retried ${id} → in-review (merge retry state cleared)`);
|
||||
console.log(` ✓ Retried ${id} → todo (merge retry state cleared)`);
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to todo column before applying retry resets. `moveTask` reads from the
|
||||
// store's durable index and may overwrite task.json-only updates, so apply the
|
||||
// manual retry reset patch after the move to make the cleared counters stick.
|
||||
await store.moveTask(id, 'todo');
|
||||
|
||||
// Clear failure state and stale branch refs so retry can choose a fresh base.
|
||||
await store.updateTask(id, {
|
||||
status: null,
|
||||
@@ -1104,9 +1110,6 @@ export async function runTaskRetry(id: string, projectName?: string) {
|
||||
...buildManualRetryResetPatch({ resetMergeRetries: true }),
|
||||
});
|
||||
|
||||
// Move to todo column
|
||||
await store.moveTask(id, 'todo');
|
||||
|
||||
// Log the retry action
|
||||
await store.logEntry(
|
||||
id,
|
||||
|
||||
33
packages/cli/src/plugin-sdk-core-runtime-shim.ts
Normal file
33
packages/cli/src/plugin-sdk-core-runtime-shim.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { BoardActionTaskStore, ColumnId, Task } from "@fusion/core";
|
||||
|
||||
export const WORKFLOW_EXTENSION_SCHEMA_VERSION = 1 as const;
|
||||
|
||||
export function workflowExtensionRegistryId(pluginId: string, extensionId: string): string {
|
||||
return `plugin:${pluginId}:${extensionId}`;
|
||||
}
|
||||
|
||||
export interface MoveBoardTaskInput {
|
||||
taskId: string;
|
||||
column: ColumnId;
|
||||
preserveProgress?: boolean;
|
||||
source?: "user" | "engine" | "scheduler";
|
||||
}
|
||||
|
||||
export interface UpdateBoardTaskInput {
|
||||
taskId: string;
|
||||
updates: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function createBoardActionServices(store: BoardActionTaskStore) {
|
||||
return {
|
||||
moveTask(input: MoveBoardTaskInput): Promise<Task> {
|
||||
return store.moveTask(input.taskId, input.column, {
|
||||
preserveProgress: input.preserveProgress,
|
||||
moveSource: input.source ?? "user",
|
||||
});
|
||||
},
|
||||
updateTask(input: UpdateBoardTaskInput): Promise<Task> {
|
||||
return store.updateTask(input.taskId, input.updates);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user