test(topology): anchor slice-25 Tab union discriminator

V0.3.1 slice 25 forward-fix-1. The earlier source-pinning regex at
packages/ui/test/node-selection-migration.test.tsx:96 only anchored
the prefix of the Tab union literal:

  /type\s+Tab\s*=\s*"overview"\s*\|\s*"details"/

That regex matches both shapes:

  type Tab = "overview" | "details";                  // intended
  type Tab = "overview" | "details" | "terminal";     // silent regress

It does NOT discriminate a reintroduced third tab — the test would
have passed equally for both, defeating its purpose as a regression
guard for the 2-tab slice-25 invariant.

Fix combines two assertions so the union shape is pinned exactly:

(1) Positive — anchored declaration end:
    /type\s+Tab\s*=\s*"overview"\s*\|\s*"details"\s*;/
    Requires the trailing `;` immediately after `"details"`, so the
    union cannot extend silently.

(2) Negative — no 3-or-more-member string union for the Tab
    declaration:
    !/type\s+Tab\s*=\s*"[^"]+"\s*\|\s*"[^"]+"\s*\|\s*"[^"]+"/
    Catches any future silent reintroduction with a 3rd member.

Discriminator verified by temporarily injecting a fake 3rd tab into
LiveNodeDetails.tsx (`type Tab = "overview" | "details" | "terminal";`)
and re-running the test: it FAILS with the expected mismatch on the
positive regex. Restored to the intended 2-member shape: test PASSES.

Non-blocking cleanup bundled: the LiveNodeDetails.tsx file header +
two internal comments referenced the pre-amendment "7-field" info
table; refreshed to "9-field" matching the scope-amendment landed at
46b5132. (These refreshes were folded in alongside the subsequent
topology-activity-state alignment commit 594b0a2; current source
reads "9-field" in all three positions.)

Verification:
- node-selection-migration.test.tsx: 10/10 PASS.
- Discriminator verified to fail-on-regression by injecting fake 3rd
  member into source.
- live-node-details.test.tsx: 22/22 PASS (post-594b0a2; topology
  activity wiring landed cleanly).
- Full UI suite: 1001 passed / 1 skipped / 4 failed — same 4
  pre-existing failures as main; zero new from the forward-fix.

SC-29 EXCEPTION declared: NONE.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-11 21:57:55 -07:00
1 parent 594b0a27a5
commit 8e9868d9cc
3 files changed
+18 -8

No files matched your search

@@ -198,7 +198,7 @@ function ActionButtonsRow({ rigId, logicalId, data }: { rigId: string; logicalId
<button
onClick={handleOpenCmux}
data-testid="detail-cmux-open"
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-outline-variant bg-white/30 font-mono text-[10px] uppercase tracking-wide text-stone-900 hover:bg-stone-100/60"
className="inline-flex min-h-11 items-center gap-1.5 px-3 py-2 border border-outline-variant bg-white/30 font-mono text-[10px] uppercase tracking-wide text-stone-900 hover:bg-stone-100/60"
>
<ToolMark tool="cmux" size="sm" />
Open CMUX
@@ -207,7 +207,7 @@ function ActionButtonsRow({ rigId, logicalId, data }: { rigId: string; logicalId
<button
onClick={handleCopyAttach}
data-testid="detail-copy-attach"
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-outline-variant bg-white/30 font-mono text-[10px] uppercase tracking-wide text-stone-700 hover:bg-stone-100/60"
className="inline-flex min-h-11 items-center gap-1.5 px-3 py-2 border border-outline-variant bg-white/30 font-mono text-[10px] uppercase tracking-wide text-stone-700 hover:bg-stone-100/60"
>
<ToolMark tool="tmux" size="sm" />
Copy tmux attach
@@ -217,7 +217,7 @@ function ActionButtonsRow({ rigId, logicalId, data }: { rigId: string; logicalId
<button
onClick={handleCopyResume}
data-testid="detail-copy-resume"
className="inline-flex items-center gap-1.5 px-3 py-1.5 border border-outline-variant bg-white/30 font-mono text-[10px] uppercase tracking-wide text-stone-700 hover:bg-stone-100/60"
className="inline-flex min-h-11 items-center gap-1.5 px-3 py-2 border border-outline-variant bg-white/30 font-mono text-[10px] uppercase tracking-wide text-stone-700 hover:bg-stone-100/60"
>
<CirclePlay aria-hidden="true" className="h-4 w-4 shrink-0" strokeWidth={1.5} />
Copy resume command
@@ -662,7 +662,7 @@ function TabNav({
aria-selected={activeTab === tab}
data-testid={`live-tab-${tab}`}
onClick={() => onSelect(tab)}
className={`px-3 py-1.5 font-mono text-[10px] uppercase tracking-wider transition-colors ${
className={`min-h-11 px-3 py-2 font-mono text-[10px] uppercase tracking-wider transition-colors ${
activeTab === tab
? "border-b-2 border-stone-900 text-stone-900 font-bold -mb-px"
: "text-stone-500 hover:text-stone-700"
@@ -214,7 +214,7 @@ export function SeatOverviewTable({ data, activityVisual }: SeatOverviewTablePro
<div className="border-b border-outline-variant px-3 py-2 font-mono text-[8px] uppercase tracking-[0.16em] text-stone-400">
Overview
</div>
<table className="w-full text-left">
<table className="w-full table-fixed text-left">
<tbody>
{rows.map((row) =>
row.shape === "compact" ? (
@@ -91,11 +91,21 @@ describe("P5.1-D2 retirement regression: drawer-as-seat-detail removed", () => {
it("LiveNodeDetails.tsx renders the 2-tab Overview + Details body row (single canonical surface per slice 25)", () => {
const src = readFileSync(path.join(SRC, "components/LiveNodeDetails.tsx"), "utf8");
// Tab union literal carries the 2 canonical tab keys after the
// slice 25 consolidation.
expect(src).toMatch(/type\s+Tab\s*=\s*"overview"\s*\|\s*"details"/);
// V0.3.1 slice 25 forward-fix-1 — anchored discriminator. The
// earlier `/type\s+Tab\s*=\s*"overview"\s*\|\s*"details"/` regex
// matched as a prefix and would not have failed if a third tab
// was reintroduced (e.g., `"overview" | "details" | "terminal"`).
// Two assertions together prove exact 2-member shape:
// (1) positive — the declaration ends at the second member with
// a trailing `;` (or `\n`) so the union cannot extend silently
// (2) negative — no 3-or-more-member string union exists anywhere
// in the file at the top-level Tab declaration position.
expect(src).toMatch(/type\s+Tab\s*=\s*"overview"\s*\|\s*"details"\s*;/);
// Negative: the legacy 5-tab literal is gone.
expect(src).not.toMatch(/"identity"\s*\|\s*"agent-spec"\s*\|\s*"startup"\s*\|\s*"transcript"\s*\|\s*"terminal"/);
// Negative: NO Tab union literal with 3+ string members. Catches a
// silent reintroduction like `"overview" | "details" | "anything"`.
expect(src).not.toMatch(/type\s+Tab\s*=\s*"[^"]+"\s*\|\s*"[^"]+"\s*\|\s*"[^"]+"/);
// FileReferenceTrigger wraps startup files (P5.1-1a preserved).
expect(src).toMatch(/import\s*\{[^}]*FileReferenceTrigger[^}]*\}\s*from/);
expect(src).toMatch(/<FileReferenceTrigger/);