docs(plans): add Full Suite engine/core watchdog-budget fix plan

This commit is contained in:
gsxdsm
2026-06-20 21:54:10 -07:00
parent fdccca9d27
commit 9af9bf4fd9

View File

@@ -0,0 +1,256 @@
---
title: "fix: Resolve Full Suite hang in @fusion/engine and @fusion/core test groups"
type: fix
status: active
date: 2026-06-21
plan_depth: standard
---
# fix: Resolve Full Suite hang in @fusion/engine and @fusion/core test groups
## Summary
The **Full Suite (non-blocking)** workflow on `main` has been red for 30+ consecutive
runs. The failure is **not** a test assertion — every test that reports a result
passes. Instead, three sharded vitest groups never exit and are SIGKILLed by the
CI wall-clock watchdog when they hit their per-group budget:
| Shard | Group | Watchdog budget | Outcome |
|-------|-------|-----------------|---------|
| 1/4 | `@fusion/engine [1/2]` | 405s | killed at budget → shard fails |
| 2/4 | `@fusion/engine [2/2]` | 405s | killed at budget → shard fails |
| 4/4 | `@fusion/core [2/2]` | 338s | killed at budget → shard fails |
| 3/4 | `@fusion/core [1/2]` | 338s | **passes** (32 heartbeats, then exits) |
The `[1/2]` halves and shard 3 finish well under budget, so this is a **genuine
hang in specific tests** — almost certainly leaked open handles (unterminated git
child processes, intervals/timers, or undrained pools) that prevent vitest from
exiting after the test bodies complete — not uniform slowness. The engine tests
that perform real git worktree/branch operations (`Preparing worktree (new branch
'fusion/fn-001')`, repeated `Switched to branch 'main'`) are the leading suspects.
This plan reproduces the hang locally with hanging-process detection, isolates the
leaking test(s), repairs the leak at its source (teardown / process termination /
timer cleanup), and verifies each group exits cleanly within budget.
---
## Problem Frame
- **Authoritative signal:** CI, not a local run. Reference run `27893078004`
(`gh run view 27893078004`), jobs: `Test shard 1/4`, `2/4`, `4/4` failed; `3/4`,
`Engine slow tier`, `Dashboard curated-gate guard` passed.
- **Symptom:** vitest process for a group keeps emitting work (git worktree ops)
and the watchdog `still running` heartbeat, never reaching its `Test Files …`
summary line, until SIGTERM/SIGKILL at budget. A killed group exits non-zero →
the shard fails → the Full Suite workflow fails.
- **Why it matters:** Full Suite is the broadest regression net. While marked
"non-blocking," a permanently-red suite means real regressions in engine/core
can land unnoticed. The two known pre-existing
`shared-branch-group-entry-points.test.ts` failures (see memory) may be related
but are a separate, smaller issue — this plan targets the *hang*.
- **Not in scope:** the `pi-claude-cli`/`droid-cli` "Failed to parse NDJSON line:
{bad" and `packages/cli` "AI metadata generation failed; using fallback" lines —
those are deliberate negative-path test fixtures on passing groups, not failures.
---
## Scope Boundaries
**In scope**
- Diagnose and fix whatever prevents `@fusion/engine` (both splits) and
`@fusion/core [2/2]` from exiting within their watchdog budgets.
- Restore the Full Suite workflow to green on `main`.
**Out of scope / non-goals**
- Rewriting the CI sharding or watchdog-budget derivation logic
(`scripts/ci-test-shard.mjs`, `scripts/lib/run-vitest-watchdog.mjs`). Only touch
budget as a deliberate, justified last resort (see U3 / KTD-2).
- Converting Full Suite to a blocking gate.
### Deferred to Follow-Up Work
- The 2 known `shared-branch-group-entry-points.test.ts` per-task-derivation
failures, unless reproduction shows they are the hang source.
- Broad test-suite speedups beyond what's needed to clear the budget.
---
## Key Technical Decisions
**KTD-1 — Fix the leak, do not raise the budget by default.**
The `[1/2]`/shard-3 halves prove the work fits comfortably under budget, so the
`[2/2]`/engine halves are hanging, not merely slow. Raising the watchdog budget
would mask a real open-handle leak and slow every CI run. Default posture:
identify the leaking test and repair its teardown so vitest exits cleanly.
**KTD-2 — Budget change only with evidence.**
If reproduction proves a group is *legitimately* slow (all tests complete, vitest
exits, but wall-clock genuinely exceeds budget), then and only then adjust the
group's budget/split via `scripts/ci-test-shard.mjs` timings, with the measured
numbers recorded in the commit. This is the documented exception to "don't touch
sharding."
**KTD-3 — Reproduce with the real CI invocation.**
Run the exact per-group vitest command the shard script issues (same
`--project`, same env) under a wall-clock `timeout`, plus vitest's
hanging-process reporter, so local results match CI rather than the cached
`pnpm test` changed-file path.
---
## High-Level Technical Design
```
CI shard → pnpm --filter @fusion/engine exec vitest run --project=…
│
├── all test bodies pass ✅ (Test Files summary never printed)
│
└── process does NOT exit ❌
│ leaked handle keeps event loop alive:
│ • spawned git/worktree child process not awaited/killed
│ • setInterval / heartbeat timer not cleared in teardown
│ • worktree-pool / db handle not closed
▼
watchdog budget reached → SIGTERM/SIGKILL → exit≠0 → shard FAIL
```
Fix target = close the leaked handle in `afterEach`/`afterAll` (or in the
production code path the test exercises) so the process exits naturally.
---
## Implementation Units
### U1. Reproduce the hang locally and isolate the leaking test(s)
**Goal:** Turn the CI-only failure into a deterministic local repro and name the
exact test file(s) and handle that keep the process alive.
**Dependencies:** none.
**Files (investigation, no production edits yet):**
- `packages/engine/vitest.config.ts` (projects: `engine-default`,
`engine-reliability`, `engine-core`, `engine-slow`)
- `scripts/ci-test-shard.mjs`, `scripts/lib/run-vitest-watchdog.mjs` (read-only:
confirm the exact per-group command + budget)
- Suspect real-git/worktree tests under `packages/engine/src/__tests__/`:
`worktree-acquisition.test.ts`, `worktree-pool-liveness.test.ts`,
`merger-integration-worktree.test.ts`,
`merger-finalize-unproven.real-git.test.ts`,
`self-healing-orphan-only-scope.real-git.test.ts`,
`self-healing-ghost-branch-recovery.test.ts`, `executor-worktree.test.ts`,
`restart.integration.test.ts`, `run-audit.integration.test.ts`
**Approach:**
1. Derive the exact group command from `ci-test-shard.mjs` for `engine [1/2]`,
`engine [2/2]`, and `core [2/2]`.
2. Run each under a hard wall-clock `timeout` (e.g. 420s) with hanging-process
detection — vitest `--reporter=hanging-process` (or `--reporter=verbose
--no-file-parallelism` plus `why-is-node-running`-style logging) to print the
handles still open after the run finishes.
3. Confirm the signature: "Test Files … passed" never prints (or prints but
process doesn't exit) and the reporter names the dangling handle/test file.
4. Record the offending file(s) + handle type for U2.
**Execution note:** Characterization-first — establish the failing repro and
capture the open-handle report *before* changing any production or test code.
Respect existing guards: do not kill the live dashboard port (port-4040 guards,
`FUSION_RESERVED_PORTS`) and do not kill the running dev instance.
**Test scenarios:** Test expectation: none — this unit is diagnostic; it produces
a repro recipe and a named culprit, not new tests.
**Verification:** A documented command that reliably hangs/leaks locally, and a
hanging-process report naming the test file(s) and handle keeping the loop alive.
---
### U2. Fix the leaked handle so the group exits cleanly
**Goal:** Eliminate the dangling handle so vitest exits on its own for all three
groups, with zero behavior change to the code under test.
**Dependencies:** U1.
**Files:** the test file(s) and/or production module(s) identified in U1.
Likely candidates (confirm in U1, do not assume): worktree-pool / git child
process spawn sites and their `afterEach`/`afterAll` teardown; any
`setInterval`/heartbeat timer (e.g. liveness/heartbeat code) not cleared on
teardown; db/sqlite handles left open.
**Approach (apply whichever U1 proves):**
- Await and/or terminate spawned git/worktree child processes in teardown; ensure
no detached process outlives the test.
- Clear timers/intervals registered by the code under test (use fake timers or an
explicit `clearInterval` in teardown).
- Close pools/db handles opened by the test.
- Prefer fixing the leak at the production source if the same handle could leak in
real runtime; otherwise fix the test's teardown.
**Patterns to follow:** Mirror teardown patterns in the engine `[1/2]`/core
`[1/2]` tests that already exit cleanly. Reuse existing temp-dir/worktree cleanup
helpers in `packages/engine/src/__tests__/`.
**Test scenarios:**
- Happy path: the previously-hanging test still asserts its original behavior and
passes.
- Resource cleanup: after the test, the hanging-process reporter shows **no**
dangling handle for the fixed file (regression guard against re-introduction).
- Edge: if a child process is killed in teardown, a test where the process already
exited does not throw on double-kill.
**Verification:** Each group runs to its `Test Files … passed` summary AND the
process exits 0 without watchdog intervention.
---
### U3. Verify all three groups finish within budget and Full Suite goes green
**Goal:** Confirm the fix across `engine [1/2]`, `engine [2/2]`, and
`core [2/2]`, and that the workflow passes end-to-end.
**Dependencies:** U2.
**Files:** none (verification). Only touch `scripts/ci-test-shard.mjs` timings if
KTD-2's slow-not-hung condition is proven in U1/U2.
**Approach:**
1. Re-run each group's exact command under the same wall-clock `timeout`; confirm
each exits 0 comfortably under its watchdog budget (405s / 405s / 338s).
2. Push the branch (or trigger the Full Suite via `workflow_dispatch`) and watch
the four shards go green via `gh pr checks --watch` / `gh run watch`.
3. If a group is proven slow-not-hung, apply the KTD-2 budget/split adjustment
with measured numbers in the commit message.
**Test scenarios:** Test expectation: none — verification unit.
**Verification:** Full Suite (non-blocking) run on the branch reports all shards
`success`; no group is SIGKILLed at budget.
---
## Risks & Dependencies
- **Repro may be environment-sensitive.** If the hang is CI-specific (e.g. git
identity, worktree path layout, missing TTY), local repro in U1 may not trigger
it. Mitigation: replicate CI env vars from the shard script; if still not
reproducible, drive the diagnosis from a `workflow_dispatch` run with the
hanging-process reporter enabled and artifacts uploaded.
- **vitest auto-kill history (memory):** older fn TUI builds SIGKILLed `vitest`
processes every 30s. Ensure the TUI is not running the broken build during
local repro, or run the repro outside the TUI, so a clean exit isn't mistaken
for a kill.
- **Multiple independent leaks.** engine `[1/2]` and `[2/2]` both failing may mean
more than one leaking test; treat U1/U2 as iterative until all three groups are
green.
## Sources & Research
- CI run `27893078004` (jobs + full log) — failure signature, watchdog budgets,
per-group heartbeat counts.
- `gh run list --branch main --workflow "Full Suite (non-blocking)"` — 30+
consecutive failures (chronic, not a fresh regression).
- `packages/engine/vitest.config.ts` — 30s testTimeout, 45s hookTimeout, project
splits.
- Memory: "Branch-group known failures", "vitest auto-kill incident",
"Port 4040 kill guards", "Engine src has no tsc emit".