diff --git a/docs/architecture.md b/docs/architecture.md index aad9b3f8bb..229790e576 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -626,6 +626,7 @@ See [Memory Plugin Contract](./memory-plugin-contract.md) for the full plan. ### Agent roles - **Planning**: the planning processor generates task plans (`PROMPT.md`) and selects eligible planning tasks by priority first, then FIFO (`createdAt` ascending) within each priority tier. Each attempt captures the authoritative artifact baseline and owns its fallback callback provenance. Only a settled, fallback-free attempt that changed that exact baseline and passes deterministic validation may hand off to workflow Plan Review. Empty, unchanged, or fallback-engaged attempts use the shared bounded `recoveryRetryCount`/`nextRecoveryAt` backoff; exhaustion persists an actionable planning error and never signals successful handoff. After a prompt settles, triage awaits the originating runtime's finite `settleFallbackDispatch` lifecycle signal, then awaits every observer callback admitted by that signal before deciding. A configured runtime that cannot supply this signal fails closed through the same bounded planning recovery rather than handing a potentially fallback-authored plan to review. This deliberately never inspects arbitrary Node timers: clean planner housekeeping can schedule unrelated one-shot or recurring timers without delaying admission. A callback from an obsolete attempt remains scoped to that attempt. Explicit duplicate-marker closure runs only after this same clean-attempt admission. If the stuck-task detector kills a not-yet-approved planning session after a non-empty `PROMPT.md` draft exists, the retry is requeued as `needs-replan` and seeds the next prompt in revision mode from that draft instead of cold-starting. A newly added dependency in a hold lane follows the same durable `needs-replan` path; it never clears status, so a planner interrupted after prompt persistence remains claimable and cannot silently bypass the approval/release handoff. When `PROMPT.md` is absent, a non-empty `plan` task document written through `fn_task_document_write` is the fallback seed; missing or whitespace-only drafts still cold-start. - **Executor**: `TaskExecutor` (`executor.ts`) implements tasks in worktrees + - **Task-pinned orphan recovery:** task-ID-pinned acquisition holds one path reservation across classification, preservation, quarantine reconciliation, and recreation. Inactive incomplete or unregistered directories are atomically moved to `/.fusion/recovery/worktrees`, or to `/.fusion-recovery/worktrees` after an `EXDEV` cross-filesystem refusal. Each actual recovery root retains the newest 10 recognized Fusion-generated entries; pruning is fail-soft and preserves unknown, symlinked, unreadable, or active paths. Worktree pool and self-healing scans exclude both `.ai-merge` and `.fusion-recovery` as internal container boundaries. - **Execution-only reused-base refresh (FN-8693):** planning creates isolated worktrees but does not refresh them; immediately before a graph `code` node, normal executor dispatch, or durable-agent heartbeat session, refresh-enabled reuse resolves the current integration target C1 and compares it with durable `task.baseCommitSha`. A clean no-own-commit checkout resets to C1; a clean own-commit checkout rebases and retains its resulting C2 `HEAD`, while storing C1—not C2—as the baseline. A durable C0/C1 mismatch is rechecked from git and durable metadata on every acquisition, so restart reconciliation needs no in-memory marker. Dirty, unresolved, unsupported worktrunk, git, conflict, persistence, and unprovable-reconciliation cases are typed non-execution outcomes that park before session start. If baseline persistence fails after git moves `HEAD`, the engine compensates to the original clean checkout and emits `worktree:base-refresh-persistence-failed-compensated`; otherwise it requires later proof-based reconciliation. Audit events are `worktree:base-refreshed`, `worktree:base-refresh-blocked`, `worktree:base-refresh-conflict`, `worktree:base-refresh-persistence-failed-compensated`, and `worktree:base-refresh-reconciled`. Plan/review/gate acquisition and merger acquisition remain excluded; production merger behavior is the unified clean-room `runAiMerge` path. - **Reviewer**: `reviewStep()` (`reviewer.ts`) performs plan/code/spec reviews diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx index b84a003fbc..66061a97d6 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx @@ -79,9 +79,28 @@ const mockUseTasks = vi.fn(() => ({ archiveAllDone: vi.fn(), refreshTasks: vi.fn(), })); -vi.mock("../../hooks/useTasks", () => ({ - useTasks: (_options?: any) => mockUseTasks(), -})); +/* +FNXC:DashboardTests 2026-08-09-08:02: +Commit 132026545 (FN-8796 'stabilize task-detail lifecycle snapshots') added `mergeTaskSnapshot` +to hooks/useTasks.ts, imported by components this test renders (``). A curated vi.mock +decorator for "../../hooks/useTasks" must surface every export those components import (`useTasks` + +`mergeTaskSnapshot`) or the fixture throws `No "mergeTaskSnapshot" export is defined on the useTasks +mock` at import time. + +FNXC:DashboardTests 2026-08-09-08:25: +This directly mirrors the App.test.tsx / navigation-history fixtures: the file renders `` and +drives the SSE merge path at App.tsx:2179 (`liveTask.id` where `liveTask = mergeTaskSnapshot(snapshot, +boardTask)` for retained task detail popups), so `mergeTaskSnapshot: vi.fn()` returning `undefined` +makes `liveTask.id` throw. Surface the real implementation via a partial `importOriginal` mock while +`useTasks` stays mocked. +*/ +vi.mock("../../hooks/useTasks", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useTasks: (_options?: any) => mockUseTasks(), + }; +}); vi.mock("../../hooks/useInsights", () => ({ useInsights: () => ({ diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx index 825ba4cc88..d81f761a54 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx @@ -83,9 +83,28 @@ const mockUseTasks = vi.fn(() => ({ archiveAllDone: vi.fn(), refreshTasks: vi.fn(), })); -vi.mock("../../hooks/useTasks", () => ({ - useTasks: (_options?: any) => mockUseTasks(), -})); +/* +FNXC:DashboardTests 2026-08-09-08:02: +Commit 132026545 (FN-8796 'stabilize task-detail lifecycle snapshots') added `mergeTaskSnapshot` +to hooks/useTasks.ts, imported by components this test renders (``). A curated vi.mock +decorator for "../../hooks/useTasks" must surface every export those components import (`useTasks` + +`mergeTaskSnapshot`) or the fixture throws `No "mergeTaskSnapshot" export is defined on the useTasks +mock` at import time. + +FNXC:DashboardTests 2026-08-09-08:25: +This directly mirrors the App.test.tsx / navigation-history fixtures: the file renders `` and +drives the SSE merge path at App.tsx:2179 (`liveTask.id` where `liveTask = mergeTaskSnapshot(snapshot, +boardTask)` for retained task detail popups), so `mergeTaskSnapshot: vi.fn()` returning `undefined` +makes `liveTask.id` throw. Surface the real implementation via a partial `importOriginal` mock while +`useTasks` stays mocked. +*/ +vi.mock("../../hooks/useTasks", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useTasks: (_options?: any) => mockUseTasks(), + }; +}); vi.mock("../../hooks/useInsights", () => ({ useInsights: () => ({ diff --git a/packages/dashboard/app/components/__tests__/navigation-history.test.tsx b/packages/dashboard/app/components/__tests__/navigation-history.test.tsx index ded3b3dd61..687ac56d4a 100644 --- a/packages/dashboard/app/components/__tests__/navigation-history.test.tsx +++ b/packages/dashboard/app/components/__tests__/navigation-history.test.tsx @@ -99,9 +99,27 @@ const mockUseTasks = vi.fn(() => ({ refreshTasks: vi.fn(), })); -vi.mock("../../hooks/useTasks", () => ({ - useTasks: (_options?: any) => mockUseTasks(), -})); +/* +FNXC:DashboardTests 2026-08-09-08:02: +Commit 132026545 (FN-8796 'stabilize task-detail lifecycle snapshots') added `mergeTaskSnapshot` +to hooks/useTasks.ts and imports it from App.tsx/MainContent.tsx. Curated tests that vi.mock +"../../hooks/useTasks" must surface every export the rendered components import from that module +(`useTasks` + `mergeTaskSnapshot`), or the fixture throws `No "mergeTaskSnapshot" export is defined +on the useTasks mock` at import time. + +FNXC:DashboardTests 2026-08-09-08:25: +Like App.test.tsx, this file renders App and drives the SSE merge path at App.tsx:2179 +(`liveTask.id` where `liveTask = mergeTaskSnapshot(snapshot, boardTask)` for retained task popups). +A `mergeTaskSnapshot: vi.fn()` returning `undefined` makes `liveTask.id` throw, so surface the real +implementation via a partial `importOriginal` mock while `useTasks` stays mocked. +*/ +vi.mock("../../hooks/useTasks", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useTasks: (_options?: any) => mockUseTasks(), + }; +}); vi.mock("../../hooks/useInsights", () => ({ useInsights: () => ({