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:
ischindl
2026-08-11 23:28:50 +02:00
committed by GitHub
parent ca134c2554
commit d4db29f3e6
4 changed files with 66 additions and 9 deletions

View File

@@ -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

View File

@@ -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: () => ({

View File

@@ -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: () => ({

View File

@@ -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: () => ({