fix(dashboard): surface mergeTaskSnapshot on useTasks mocks to fix task-detail tests (#3409)
## Problem
`app/components/__tests__/navigation-history.test.tsx`,
`TaskDetail.swipe-back.test.tsx`, and
`TaskDetail.mobile-transition.board-panel.test.tsx` fail to load.
FN-8796 (`132026545`, "stabilize task-detail lifecycle snapshots") added
`mergeTaskSnapshot` to `hooks/useTasks.ts`, which
`App.tsx`/`MainContent.tsx` now import. The curated test suites that
`vi.mock("../../hooks/useTasks")` still only expose `useTasks`, so the
fixture throws at import time:
```
No "mergeTaskSnapshot" export is defined on the useTasks mock
```
Confirmed on `origin/main`: `mergeTaskSnapshot` is present in
`useTasks.ts` and imported by `App.tsx`, while these tests' `vi.mock`
shims don't surface it.
## Fix
Update the mocks to surface every export the rendered components import,
using a partial `importOriginal` mock so `mergeTaskSnapshot` keeps its
real implementation (a `vi.fn()` returning `undefined` would make
`App.tsx:2179` `liveTask.id` throw):
```ts
vi.mock("../../hooks/useTasks", async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, useTasks: (_options?: any) => mockUseTasks() };
});
```
## Verification
- `FUSION_DASHBOARD_DEEP=1 vitest run navigation-history swipe-back
board-panel` → **14 files, 116 tests passed**
- Pre-existing on `origin/main`, fixes a regression introduced by
FN-8796.
## Scope
Tests + a one-line docs note only. No production behavior change.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Documentation**
- Documented improved worktree recovery behavior, including safer
handling of incomplete, inactive, and unregistered directories.
- Added guidance for recovery across different storage locations and
bounded cleanup of recognized entries.
- Clarified protections that prevent internal recovery containers from
being treated as worktrees.
- **Tests**
- Improved dashboard test coverage and reliability for task detail
rendering, mobile transitions, swipe-back navigation, and navigation
history.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Fusion <noreply@runfusion.ai>
Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
@@ -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 `<project>/.fusion/recovery/worktrees`, or to `<worktreesDir>/.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.
|
||||
<!-- FNXC:MergerUnification 2026-08-09-12:04: Master-plan U0 made clean-room `runAiMerge` the sole production merge path. The legacy `aiMergeTask` auto-prerebase policy is retained but inert, so executor reused-base refresh must not describe it as live merger behavior. -->
|
||||
- **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
|
||||
|
||||
@@ -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 (`<App />`). 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 `<App />` 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<typeof import("../../hooks/useTasks")>();
|
||||
return {
|
||||
...actual,
|
||||
useTasks: (_options?: any) => mockUseTasks(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../hooks/useInsights", () => ({
|
||||
useInsights: () => ({
|
||||
|
||||
@@ -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 (`<App />`). 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 `<App />` 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<typeof import("../../hooks/useTasks")>();
|
||||
return {
|
||||
...actual,
|
||||
useTasks: (_options?: any) => mockUseTasks(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../hooks/useInsights", () => ({
|
||||
useInsights: () => ({
|
||||
|
||||
@@ -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<typeof import("../../hooks/useTasks")>();
|
||||
return {
|
||||
...actual,
|
||||
useTasks: (_options?: any) => mockUseTasks(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../../hooks/useInsights", () => ({
|
||||
useInsights: () => ({
|
||||
|
||||
Reference in New Issue
Block a user