Fixes phantom-worktree context bleed where the engine refused to start a coding agent in a missing worktree for in-review/merge-active tasks even when scopeOverride=1, stranding them past the normal recovery paths and retry budget. - Add isMergeActiveMissingWorktreeSessionStartFailure/isInReviewMissingWorktreeSessionStartFailure classifiers and MERGE_ACTIVE_MISSING_WORKTREE_STATUSES (merging/merging-pr/merging-fix) in restart-recovery-coordinator.ts, exported from @fusion/engine. - Self-healing: reorder missing-worktree-review-failures sweep earlier, extend the in-review sweep to also match merge-active missing-worktree failures with a triple-proof-guarded, bounded (recoveryRetryCount) stale-metadata clear and fresh session-start retry budget reset. - Self-healing: extend scopeOverride worktree-metadata reconciliation to safely clear phantom worktree/branch/session metadata for in-review tasks stuck in a merge-active sub-status, narrowly scoped to avoid clobbering genuinely live in-progress/mid-step tasks (FN-5256 guard preserved). - CLI (task.ts), pi extension (extension.ts), and dashboard route (register-task-workflow-routes.ts) retry paths now bypass the merge-active status gate via a signature-only check, clearing worktree/branch/sessionFile and requeuing to todo while preserving progress. - Add regression coverage across self-healing.test.ts, restart-recovery-coordinator.test.ts, extension.test.ts, task-retry.test.ts, and routes-tasks-ops.test.ts; update mockCoreEngine.ts test scaffolding. - Update docs/architecture.md, docs/self-healing-backward-move-audit.md, docs/task-management.md, and AGENTS.md to describe the new merge-active missing-worktree recovery behavior. - Add changeset (patch) for @runfusion/fusion. Files changed: .changeset/fn-7802-phantom-worktree-merge-active-recovery.md | 7 + AGENTS.md | 1 + docs/architecture.md | 4 +- docs/self-healing-backward-move-audit.md | 5 +- docs/task-management.md | 2 +- packages/cli/src/__tests__/extension.test.ts | 64 +++++ packages/cli/src/__tests__/task-retry.test.ts | 49 ++++ packages/cli/src/commands/task.ts | 28 +- packages/cli/src/extension.ts | 26 +- packages/dashboard/src/__tests__/routes-tasks-ops.test.ts | 52 ++++ packages/dashboard/src/routes/register-task-workflow-routes.ts | 25 +- packages/dashboard/src/test/mockCoreEngine.ts | 11 + packages/engine/src/__tests__/restart-recovery-coordinator.test.ts | 20 ++ packages/engine/src/__tests__/self-healing.test.ts | 297 +++++++++++++++++++++ packages/engine/src/index.ts | 13 + packages/engine/src/restart-recovery-coordinator.ts | 19 +- packages/engine/src/self-healing.ts | 157 +++++++++-- 17 files changed, 744 insertions(+), 36 deletions(-) Fusion-Task-Id: FN-7802 Fusion-Task-Lineage: 5897105b-6b5c-49d5-a8e8-519902182861 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
/**
|
|
* Canonical @fusion/core and @fusion/engine mock helpers for dashboard server tests.
|
|
*
|
|
* If a route test starts failing with "No \"X\" export is defined", update this
|
|
* helper first instead of adding another full inline export map in the test file.
|
|
*/
|
|
import { vi, type Mock } from "vitest";
|
|
|
|
type AnyModule = Record<string, unknown>;
|
|
type AnyMock = Mock;
|
|
|
|
const fallbackFns = new Map<string, AnyMock>();
|
|
|
|
function getFallback(name: string): AnyMock {
|
|
if (!fallbackFns.has(name)) fallbackFns.set(name, vi.fn());
|
|
return fallbackFns.get(name)!;
|
|
}
|
|
|
|
function withFallbackFunctions(actual: AnyModule, moduleValue: AnyModule): AnyModule {
|
|
return new Proxy(moduleValue, {
|
|
get(target, prop, receiver) {
|
|
if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
|
|
if (Reflect.has(target, prop)) return Reflect.get(target, prop, receiver);
|
|
if (["then", "catch", "finally"].includes(prop)) return undefined;
|
|
|
|
const actualValue = actual[prop];
|
|
if (typeof actualValue === "function" || actualValue === undefined) {
|
|
const fn = getFallback(prop);
|
|
target[prop] = fn;
|
|
return fn;
|
|
}
|
|
return actualValue;
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function createCoreMock(
|
|
importActual: () => Promise<AnyModule>,
|
|
overrides: AnyModule = {},
|
|
): Promise<AnyModule> {
|
|
const actual = await importActual();
|
|
return withFallbackFunctions(actual, { ...actual, ...overrides });
|
|
}
|
|
|
|
export function createEngineMock(overrides: AnyModule = {}): AnyModule {
|
|
const actual: AnyModule = {};
|
|
return withFallbackFunctions(actual, {
|
|
createFnAgent: vi.fn(),
|
|
promptWithFallback: vi.fn(),
|
|
/*
|
|
FNXC:TestSkills 2026-06-17-19:33:
|
|
Dashboard route tests mock @fusion/engine wholesale, so skill-aware planning lanes need a shaped session-skill helper result instead of the fallback vi.fn() returning undefined.
|
|
*/
|
|
buildSessionSkillContextSync: vi.fn(() => ({
|
|
skillSelectionContext: undefined,
|
|
resolvedSkillNames: [],
|
|
skillSource: "none" as const,
|
|
})),
|
|
// Returns an iterable tool list; dashboard code spreads its result
|
|
// (`...createWorkflowAuthoringTools(...)`), so it must not be undefined.
|
|
createWorkflowAuthoringTools: vi.fn(() => []),
|
|
/*
|
|
FNXC:DashboardRouteTests 2026-06-18-09:07:
|
|
Planning and chat route files can share worker-level @fusion/engine mocks during broad dashboard API quality runs.
|
|
Keep chat task document tools iterable by default so rescuing chat-routes from quarantine does not poison planning route imports with a fallback vi.fn() result.
|
|
*/
|
|
createChatTaskDocumentTools: vi.fn(() => []),
|
|
createChatArtifactTools: vi.fn(() => []),
|
|
/*
|
|
FNXC:MissingWorktreeRetry 2026-07-10-18:45:
|
|
Dashboard route tests mock @fusion/engine wholesale; the retry route must still exercise the upstream #1992 classifier so merge-active unusable-worktree failures are admitted while unrelated merging rows remain rejected.
|
|
*/
|
|
isInReviewMissingWorktreeSessionStartFailure: vi.fn((task: { column?: string; error?: unknown }) => (
|
|
task.column === "in-review"
|
|
&& typeof task.error === "string"
|
|
&& (task.error.includes("Refusing to start coding agent in missing worktree:")
|
|
|| task.error.includes("Refusing to start coding agent in incomplete worktree:")
|
|
|| task.error.includes("Refusing to start coding agent in unregistered git worktree:"))
|
|
)),
|
|
// FNXC:McpConfig 2026-07-02-13:45: Planning/mission route tests share this engine mock; MCP resolution must return the full shaped empty result so readonly session creation can proceed without importing real engine stores.
|
|
resolveMcpServersForStore: vi.fn(async () => ({ servers: [], errors: [] })),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
export function resetDashboardServerMockState(): void {
|
|
for (const fn of fallbackFns.values()) fn.mockReset();
|
|
}
|