Skip to content

fix: org and cluster card menu links no longer resolve one segment too deep - #1798

Draft
dawsontoth wants to merge 1 commit into
stagefrom
claude/1711-absolute-card-menu-targets
Draft

dawsontoth wants to merge 1 commit into
stagefrom
claude/1711-absolute-card-menu-targets

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

⊙ Problem

The org and cluster cards built their menu targets as bare relative segments (${cluster.id}/sign-in, ${organizationId}/roles, and the org card's whole-card to={organizationId}). TanStack resolves a relative to against the location the Link renders under, so a card still mounted while the router moves into a sibling entity re-resolves its hrefs one segment too deep: /org-a/clu-b/clu-c/sign-in, or /org-b/org-a, which the production route tree matches as the cluster home with clusterId: "org-a". Same class as #1710 (instance rows). RUM shows no occurrences of this variant, so this closes the pattern rather than a live incident.

❓ Your call: is a fix with zero RUM occurrences warranted? Chosen: yes, because the issue asked for the type tightening and it makes the class a compile error for all three menu producers; the alternative is closing #1711 as won't-fix and keeping the prose rule. Fully reversible, no behavior change for a correctly placed card.

Closes #1711

💡 Solution

All twelve targets now come from buildAbsoluteLinkToPage, built from the card's own entity ids rather than the current location: five menu items plus the whole-card link in the org card, six menu items in the cluster card. EntityMenuItem.to is narrowed from string to the helper's new AbsolutePath (`/${string}`) type, and the helper earns that type with a template literal rather than a cast, so the next relative menu target fails tsc at its call site. useInstanceMenuItems already passes the helper's output and compiles unchanged.

⚖️ Alternatives

❓ Your call: the type narrowing makes AbsolutePath a new export of buildAbsoluteLinkToPage.ts that components/ui now imports from lib/urls. Chosen: keep it beside the only function that produces it; the alternative is a shared lib/urls/types.ts. Trivially movable later.

🔧 Changes

Swept src for other rendered relative targets: none. The remaining relative ones (<Navigate to="../sign-in"> in FinishSetup/ClusterInstanceSignIn, navigate({ to: parts.join('/') }) in the org/instance list pages) fire once from the route component that owns the location, which the routing note already scopes as safe.

✅ Verification

Route: router/component integration tests (not a browser-level pending-navigation replay).

  • orgCardLinkLocationIndependence.test.tsx and clusterCardLinkLocationIndependence.test.tsx mount each card under a real TanStack router at its own list page and at sibling/unrelated locations, open the dropdown, and assert every menu href (and the org card's overlay link) equals the absolute path for the card's own entity; they click one link per card and assert the router lands there, and match every asserted href against the production rootRouteTree with exact route ids and params.
  • Fails on base: with the base card sources these fail 9 of 23 with the bug signature (/org-a/clu-b/clu-c/sign-in, /org-b/org-a/roles, /org-z/clu-z/instances/clu-c/edit); the own-list-page cases pass on base, as expected.
  • buildAbsoluteLinkToPage.test.ts pins the helper's output, expectTypeOf its AbsolutePath return, and a @ts-expect-error relative menu target. Mutation-checked: loosening EntityMenuItem.to back to string makes tsc -b fail with Unused '@ts-expect-error' directive; restoring the base card sources with the narrowed type makes tsc -b fail in both cards.
  • Gate (Node 24.21.0): npx vitest run 398 files / 3692 passed, 11 skipped (exit 0); npx tsc -b exit 0; npx oxlint --format stylish . exit 0; npx dprint check exit 0; husky pre-commit (full vitest, oxlint, dprint, commitlint) passed.
  • Cross-model: one full round, codex LGTM; gemini's findings rejected on evidence — the untyped react type imports in entityMenu.tsx are pre-existing and elided under isolatedModules, unencoded ids and the helper's array join are unchanged from base, and the production route tree's loaders need auth, which is why the shape check uses matchRoutes (the [RUM] Instance row links resolve relatively, producing /instance/instance/<id> and a dead-end sign-in for an instance that does not exist #1710 pattern). Its comment nit was taken. The domain adjudicator failed on an expired Claude OAuth session and the Cursor leg on the 1Password SSH agent, so the outside findings were not adjudicated.

🤖 Generated with Claude Code

Related PRs: #1712 overlaps (merged predecessor; this extends its routing note and tightens the EntityMenuItem.to contract it documented)
Complexity: easy

Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=1; full=1 @ 6ba1b9c

Review-Attention: read ~3m (raised: degraded review) @ 6ba1b9c

…ment too deep

OrgCard and ClusterCard built their menu targets as bare relative segments (`${cluster.id}/sign-in`,
`${organizationId}/roles`, and OrgCard's whole-card `to={organizationId}`). TanStack resolves a
relative `to` against the location the Link renders under, so a card still mounted while the router
moves into a sibling entity re-resolved to /org-a/clu-b/clu-c/sign-in or /org-b/org-a — the latter
matching the cluster home with clusterId bound to an org id. Same class as #1710's instance rows.

Build all twelve targets with buildAbsoluteLinkToPage from the card's own entity ids, and narrow
EntityMenuItem.to and the helper's return to a new AbsolutePath (`/${string}`) type, so the next
relative menu target is a compile error rather than a latent bug. useInstanceMenuItems already
passes the helper's output and compiles unchanged.

Fixes #1711

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enforces type safety and location independence for entity menu links by introducing a new AbsolutePath type and updating EntityMenuItem to require it. The ClusterCard and OrgCard components have been refactored to construct their menu links using buildAbsoluteLinkToPage instead of relative paths, and new unit tests have been added to verify correct link resolution. There are no review comments, so I have no feedback to provide.

@github-actions

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 67.56% 10106 / 14957
🔵 Statements 67.72% 10784 / 15924
🔵 Functions 60.82% 2601 / 4276
🔵 Branches 62.68% 7703 / 12289
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/ui/entityMenu.tsx 100% 100% 100% 100%
src/features/clusters/components/ClusterCard.tsx 54.65% 83.87% 25.92% 55.42% 106-118, 122-123, 126, 129-160, 264-293, 379, 454-493
src/features/organizations/components/OrgCard.tsx 77.77% 95.83% 20% 77.77% 48, 86, 121-163
src/lib/urls/buildAbsoluteLinkToPage.ts 100% 100% 100% 100%
Generated in workflow #2065 for commit 6ba1b9c by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Org and cluster card menu targets are relative, so they can re-resolve one segment too deep (same class as #1710)

1 participant