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