Multi-agent review addressed:
Code:
- Gate routeHash watcher behind blockNavDepth so router.replace() during
redirectToRoot() can't re-enter navigateToHash and double-fire setGraph.
- Rename blockHashUpdateDepth -> blockNavDepth (now gates both watchers).
- ensureCanvasOnRoot(): null-guard rootGraph and canvas so teardown/HMR
paths can't throw inside a finally and swallow the original error.
- Catch router.replace rejections in redirectToRoot and updateHash; treat
isNavigationFailure(duplicated/cancelled) as benign.
- Wrap router.push/replace in updateHash() with safeRouterCall to stop
bare-await rejections from becoming unhandled promise rejections.
- Rename isValidSubgraphId -> isUuidShapedSubgraphId so the predicate's
scope ('eligible for root.subgraphs.get') is on the tin.
Tests:
- Add regression test asserting the routeHash watcher does NOT re-enter
navigateToHash when router.replace() rewrites the hash during recovery.
- Strengthen empty-hash test: transition from non-empty -> empty, assert
no redirect AND no canvas reset.
- Strengthen stale-canvas test: assert setGraph(rootGraph) in addition to
router.replace.
- Add 'non-UUID slug that doesn't match root' test to lock in the
validation path (the slug-equals-root test alone passes without any
validator).
- Replace flushHashWatcher double-calls with vi.waitFor for determinism.
- Drop unused findSubgraphPathById mock (not consulted in any nav path).
E2E:
- Wait for the store's initial hash/route sync before mutating
location.hash to avoid racing the initialLoad branch.
- Add an explicit timeout to the URL-hash poll so a missed redirect
fails fast on loaded CI.
Schema:
- Trim docstring to the only non-obvious bit (untrusted-input boundary).
- Dedupe canonical UUID literal in the test.