Clears the **last red test in the dashboard lane** (measured on `main`
at `41af5e5dbd`: `1 failed | 11180 passed`) and restores a leak-class
invariant that has had no assertion since FN-8619.
## What was wrong
FN-8619 moved Planning out of `MainContent` (its branch returns `null`)
into `PlanningKeepAlive`, mounted by `App`.
`MainContent.planning-project-remount.test.tsx` kept asserting there, so
it could only fail — and nothing asserted the invariant at the new
location. **Deleting the project id from `App.tsx:1956`'s key, or
loosening the gate at `:1954`, failed no test.**
That invariant is not cosmetic. Before the project-keyed host, Planning
kept a running plan's stream, selected session and sidebar list from the
**previous** project, and persisted its session under the **new**
project's storage key.
## I withdrew this test once, and that was my mistake
I wrote it earlier, mutated each guard, saw it stay green both times,
concluded it was vacuous, and handed the problem back. **That reasoning
was wrong.**
App defends this invariant **twice, independently**:
1. the `planningEverOpenedProjectId === currentProject.id` gate, which
unmounts the host for a project that never opened Planning; and
2. the project id inside the host's `key`, which forces a remount
instead of reconciling A's live instance under B.
Either alone upholds the contract. So a single-guard mutation *should*
leave a correct test green — that is defence in depth working, not a
hole. The probe that settles it is breaking **both**:
| mutation | result |
|---|---|
| both guards intact | **pass** |
| key only (drop project id) | pass |
| gate only (`!== null`) | pass |
| **both** | **fail** |
I had mutated guards, not the invariant, and mistook redundancy for
vacuity. Worth recording because it is the opposite error from the one I
have been making all day: I have caught four genuinely vacuous guards by
mutation, and that success made "survived a mutation" read as "proves
nothing" when the honest reading was "the system has a second defence."
## The assertion, and why it is `gone OR different node`
An earlier draft asserted the subtree must be **absent** after the
switch. Wrong: `planningViewActive` stays true, so the latch re-arms for
the new project and a fresh host is expected. Both outcomes satisfy the
real contract — *project A's instance is not reused* — which is what
`subtreeForA.isConnected === false` pins.
## Scope
- Planning case moves to `App.test.tsx`, which owns both halves of the
guarantee.
- `MainContent.planning-project-remount.test.tsx` keeps its **Chat** and
**Missions** cases — those still render from MainContent — and gains a
note saying where Planning went, so it is not re-added there.
**Verified:** `App.test.tsx` 143/143,
`MainContent.planning-project-remount` 2/2, `tsc -p tsconfig.app.json` 0
errors, lint clean, FNXC gate exit 0. Test-only; no product code
touched, no changeset.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>