Skip to content

Commit ffc60ff

Browse files
committed
feat(showcase): surface fleet pool comm-errors (unreachable overlay) on the dashboard
1 parent ef76c6d commit ffc60ff

10 files changed

Lines changed: 1205 additions & 17 deletions

File tree

showcase/shell-dashboard/src/components/__tests__/depth-chip.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,56 @@ describe("DepthChip", () => {
199199
const chip = getByTestId("depth-chip");
200200
expect(chip.className).toContain("danger");
201201
});
202+
203+
// ── REQ-B: pool comm-error / unreachable treatment ──────────────────
204+
205+
it("renders a DISTINCT unreachable treatment when unreachable=true", () => {
206+
const { getByTestId } = render(
207+
<DepthChip
208+
depth={5}
209+
status="wired"
210+
chipColor="green"
211+
unreachable
212+
commTooltip="pool unreachable: worker-unreachable — worker w-7"
213+
/>,
214+
);
215+
const chip = getByTestId("depth-chip");
216+
expect(chip.getAttribute("data-status")).toBe("unreachable");
217+
expect(chip.getAttribute("data-surface-state")).toBe("unreachable");
218+
// Tooltip names the comm-error kind + worker.
219+
expect(chip.getAttribute("title")).toContain("worker-unreachable");
220+
expect(chip.getAttribute("title")).toContain("w-7");
221+
// Visually distinct from green/amber/red/gray — uses the indigo overlay,
222+
// NOT the emerald/danger/amber/text-muted probe-colour classes.
223+
expect(chip.className).toContain("indigo");
224+
expect(chip.className).not.toContain("emerald");
225+
expect(chip.className).not.toContain("danger");
226+
});
227+
228+
it("unreachable overrides chipColor='red' (comm error ≠ red test)", () => {
229+
const { getByTestId } = render(
230+
<DepthChip depth={3} status="wired" chipColor="red" unreachable />,
231+
);
232+
const chip = getByTestId("depth-chip");
233+
expect(chip.getAttribute("data-status")).toBe("unreachable");
234+
// A comm error must never be painted as a red test.
235+
expect(chip.className).not.toContain("danger");
236+
expect(chip.className).toContain("indigo");
237+
});
238+
239+
it("renders normally (no unreachable) when unreachable=false", () => {
240+
const { getByTestId } = render(
241+
<DepthChip
242+
depth={5}
243+
status="wired"
244+
chipColor="green"
245+
unreachable={false}
246+
/>,
247+
);
248+
const chip = getByTestId("depth-chip");
249+
expect(chip.getAttribute("data-status")).not.toBe("unreachable");
250+
expect(chip.className).toContain("emerald");
251+
});
202252
});
203253

204254
describe("depthColorClass (direct)", () => {

showcase/shell-dashboard/src/components/__tests__/unified-cell.test.tsx

Lines changed: 122 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
import React from "react";
1414
import { describe, it, expect, vi, beforeEach } from "vitest";
1515
import { render } from "@testing-library/react";
16-
import { UnifiedCell } from "../unified-cell";
16+
import { UnifiedCell, arePropsEqual } from "../unified-cell";
1717
import type { UnifiedCellProps } from "../unified-cell";
1818
import type { CellContext } from "@/components/feature-grid";
19-
import type { CellModel, TestLevel, ChipColor } from "@/lib/cell-model";
19+
import type { CellModel, TestLevel } from "@/lib/cell-model";
2020
import type { Overlay } from "@/lib/overlay-types";
21-
import type { LiveStatusMap } from "@/lib/live-status";
21+
import type { LiveStatusMap, StatusRow } from "@/lib/live-status";
22+
import { keyFor } from "@/lib/live-status";
2223

2324
// ---------------------------------------------------------------------------
2425
// Mocks -- isolate UnifiedCell's rendering logic from child components
@@ -29,12 +30,21 @@ vi.mock("@/components/depth-chip", () => ({
2930
({
3031
depth,
3132
chipColor,
33+
unreachable,
34+
commTooltip,
3235
}: {
3336
depth: number;
3437
status: string;
3538
chipColor?: string;
39+
unreachable?: boolean;
40+
commTooltip?: string;
3641
}) => (
37-
<span data-testid="mock-depth-chip" data-chip-color={chipColor ?? ""}>
42+
<span
43+
data-testid="mock-depth-chip"
44+
data-chip-color={chipColor ?? ""}
45+
data-unreachable={unreachable ? "1" : "0"}
46+
data-comm-tooltip={commTooltip ?? ""}
47+
>
3848
D{depth}
3949
</span>
4050
),
@@ -176,6 +186,7 @@ function makeModel(overrides?: Partial<CellModel>): CellModel {
176186
ceilingDepth: 5,
177187
chipColor: "green",
178188
isRegression: false,
189+
surfaceState: "green",
179190
...overrides,
180191
};
181192
}
@@ -198,7 +209,7 @@ describe("UnifiedCell", () => {
198209
it("renders only the ban icon with no badges and no depth chip", () => {
199210
const ctx = makeCtx();
200211
const model = makeModel({ supported: false });
201-
const { getByTestId, queryByTestId, queryAllByTestId } = render(
212+
const { getByTestId, queryByTestId } = render(
202213
<UnifiedCell
203214
ctx={ctx}
204215
model={model}
@@ -551,4 +562,110 @@ describe("UnifiedCell", () => {
551562
expect(queryByTestId("mock-badge-API")).not.toBeInTheDocument();
552563
});
553564
});
565+
566+
// ── REQ-B: pool comm-error → unreachable depth chip ─────────────────
567+
describe("pool comm-error overlay (REQ-B)", () => {
568+
it("renders the depth chip in the unreachable treatment when surfaceState is unreachable", () => {
569+
const ctx = makeCtx();
570+
const model = makeModel({
571+
surfaceState: "unreachable",
572+
// chipColor is preserved underneath — the overlay does not recolour it.
573+
chipColor: "green",
574+
commError: {
575+
kind: "worker-unreachable",
576+
message: "connect ECONNREFUSED",
577+
workerId: "worker-7",
578+
observedAt: new Date().toISOString(),
579+
},
580+
});
581+
const { getByTestId } = render(
582+
<UnifiedCell ctx={ctx} model={model} overlays={overlaySet("depth")} />,
583+
);
584+
const chip = getByTestId("mock-depth-chip");
585+
expect(chip.getAttribute("data-unreachable")).toBe("1");
586+
// Tooltip names the comm-error kind AND the worker for triage.
587+
const tooltip = chip.getAttribute("data-comm-tooltip") ?? "";
588+
expect(tooltip).toContain("worker-unreachable");
589+
expect(tooltip).toContain("worker-7");
590+
});
591+
592+
it("a normal red cell renders WITHOUT the unreachable treatment", () => {
593+
const ctx = makeCtx();
594+
const model = makeModel({ chipColor: "red", surfaceState: "red" });
595+
const { getByTestId } = render(
596+
<UnifiedCell ctx={ctx} model={model} overlays={overlaySet("depth")} />,
597+
);
598+
const chip = getByTestId("mock-depth-chip");
599+
expect(chip.getAttribute("data-unreachable")).toBe("0");
600+
expect(chip.getAttribute("data-chip-color")).toBe("red");
601+
});
602+
});
603+
604+
// ── REQ-B: arePropsEqual watches the d6 AGGREGATE row (worker-death) ──
605+
describe("arePropsEqual aggregate comm-error watch (Fix 2)", () => {
606+
function commRow(key: string): StatusRow {
607+
return {
608+
id: `id-${key}`,
609+
key,
610+
dimension: "d6",
611+
state: "green",
612+
signal: {
613+
__fleetCommError: {
614+
kind: "worker-crashed-mid-job",
615+
message: "lease expired with no terminal report",
616+
workerId: "fleet-worker-3",
617+
observedAt: new Date().toISOString(),
618+
},
619+
},
620+
observed_at: new Date().toISOString(),
621+
transitioned_at: new Date().toISOString(),
622+
fail_count: 0,
623+
first_failure_at: null,
624+
};
625+
}
626+
627+
it("forces a re-render when a comm error lands solely on the aggregate d6:<slug> row", () => {
628+
const slug = "next";
629+
// SAME model reference both renders — isolates the directKeys watch from
630+
// the modelsEqual backstop. Without keyFor("d6", slug) in directKeys this
631+
// returns true (skips the repaint) and the unreachable overlay is missed.
632+
const model = makeModel();
633+
const overlays = overlaySet("depth");
634+
635+
const prevLive: LiveStatusMap = new Map();
636+
const nextLive: LiveStatusMap = new Map();
637+
// Only the aggregate row differs — it now carries the worker-death signal.
638+
nextLive.set(keyFor("d6", slug), commRow(keyFor("d6", slug)));
639+
640+
// Share ALL ctx fields except liveStatus (arePropsEqual compares the
641+
// integration/feature/demo objects by reference, so a fresh makeCtx per
642+
// side would short-circuit on those before reaching the directKeys watch).
643+
const baseCtx = makeCtx({ liveStatus: prevLive });
644+
const prev: UnifiedCellProps = { ctx: baseCtx, model, overlays };
645+
const next: UnifiedCellProps = {
646+
ctx: { ...baseCtx, liveStatus: nextLive },
647+
model,
648+
overlays,
649+
};
650+
651+
// false === "props differ, re-render". The aggregate-key watch must catch
652+
// the change to the aggregate row even with an identical model reference.
653+
expect(arePropsEqual(prev, next)).toBe(false);
654+
});
655+
656+
it("still skips the re-render when nothing the cell reads changed (no false repaint)", () => {
657+
const model = makeModel();
658+
const overlays = overlaySet("depth");
659+
// Two distinct (empty) map identities but no watched key differs. Share
660+
// ctx fields so the comparison reaches (and passes) the directKeys loop.
661+
const baseCtx = makeCtx({ liveStatus: new Map() });
662+
const prev: UnifiedCellProps = { ctx: baseCtx, model, overlays };
663+
const next: UnifiedCellProps = {
664+
ctx: { ...baseCtx, liveStatus: new Map() },
665+
model,
666+
overlays,
667+
};
668+
expect(arePropsEqual(prev, next)).toBe(true);
669+
});
670+
});
554671
});

showcase/shell-dashboard/src/components/depth-chip.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ export interface DepthChipProps {
3333
* already determined the correct color (e.g. green when achieved == ceiling).
3434
*/
3535
chipColor?: "green" | "amber" | "red" | "gray";
36+
/**
37+
* Pool COMMUNICATION error (REQ-B). When set, the chip renders a DISTINCT
38+
* "couldn't reach the pool" treatment — a purple/indigo fill with a "⚡"
39+
* glyph — that is visually unlike green/amber/red/gray, so an operator can
40+
* tell "the pool was unreachable" apart from "the test went red". The
41+
* underlying depth/colour is intentionally suppressed in favour of the
42+
* comm-error overlay; the descriptive tooltip is supplied via `commTooltip`.
43+
*/
44+
unreachable?: boolean;
45+
/** Tooltip text for the unreachable treatment (names the comm-error kind). */
46+
commTooltip?: string;
3647
}
3748

3849
/**
@@ -91,7 +102,27 @@ export function DepthChip({
91102
regression,
92103
maxDepth,
93104
chipColor,
105+
unreachable,
106+
commTooltip,
94107
}: DepthChipProps) {
108+
// Pool comm-error overlay (REQ-B) takes precedence over every probe colour:
109+
// a "couldn't reach the pool" state must never be mistaken for a red test.
110+
// A distinct indigo fill + ⚡ glyph, resolved BEFORE the unshipped/unsupported
111+
// branches so an unreachable cell is always loud.
112+
if (unreachable) {
113+
return (
114+
<span
115+
data-testid="depth-chip"
116+
data-status="unreachable"
117+
data-surface-state="unreachable"
118+
className="inline-flex items-center justify-center min-w-[32px] h-5 px-1.5 rounded text-[10px] font-semibold tabular-nums border border-indigo-400/60 bg-indigo-500/20 text-indigo-300"
119+
title={commTooltip ?? "pool unreachable — comm error"}
120+
>
121+
122+
</span>
123+
);
124+
}
125+
95126
if (status === "unshipped") {
96127
return (
97128
<span

showcase/shell-dashboard/src/components/unified-cell.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import { memo, useState, useEffect, useRef, useCallback } from "react";
1515
import type { CellContext } from "@/components/feature-grid";
1616
import type { CellModel, TestLevel } from "@/lib/cell-model";
17+
import type { PoolCommError } from "@/lib/live-status";
1718
import { DepthChip } from "@/components/depth-chip";
1819
import { Badge, FlashOnChange } from "@/components/badges";
1920
import type { BadgeTone } from "@/lib/live-status";
@@ -97,6 +98,21 @@ function TestBadge({
9798
);
9899
}
99100

101+
// ---------------------------------------------------------------------------
102+
// Pool comm-error tooltip (REQ-B)
103+
// ---------------------------------------------------------------------------
104+
105+
/**
106+
* Format the "couldn't reach the pool" tooltip for the unreachable chip,
107+
* NAMING the `PoolCommErrorKind` and (when known) the worker so an operator
108+
* can triage which pool member dropped. Falls back to the raw message when no
109+
* structured detail is available.
110+
*/
111+
export function commErrorTooltip(err: PoolCommError): string {
112+
const worker = err.workerId ? ` — worker ${err.workerId}` : "";
113+
return `pool unreachable: ${err.kind}${worker}${err.message}`;
114+
}
115+
100116
// ---------------------------------------------------------------------------
101117
// Layer renderers
102118
// ---------------------------------------------------------------------------
@@ -173,6 +189,10 @@ function DepthLayer({ ctx, model }: { ctx: CellContext; model: CellModel }) {
173189
chipColor={model.chipColor}
174190
depth={model.achievedDepth}
175191
status="wired"
192+
unreachable={model.surfaceState === "unreachable"}
193+
commTooltip={
194+
model.commError ? commErrorTooltip(model.commError) : undefined
195+
}
176196
/>
177197
</button>
178198
{drilldownOpen && (
@@ -361,6 +381,10 @@ function modelsEqual(a: CellModel, b: CellModel): boolean {
361381
a.achievedDepth === b.achievedDepth &&
362382
a.ceilingDepth === b.ceilingDepth &&
363383
a.chipColor === b.chipColor &&
384+
a.surfaceState === b.surfaceState &&
385+
a.commError?.kind === b.commError?.kind &&
386+
a.commError?.workerId === b.commError?.workerId &&
387+
a.commError?.message === b.commError?.message &&
364388
a.d6Effective === b.d6Effective &&
365389
a.isRegression === b.isRegression &&
366390
testLevelsEqual(a.d3, b.d3) &&
@@ -370,7 +394,13 @@ function modelsEqual(a: CellModel, b: CellModel): boolean {
370394
);
371395
}
372396

373-
function arePropsEqual(
397+
/**
398+
* Memo equality used by `UnifiedCell`. Exported for direct unit testing of the
399+
* `directKeys` watch (a comm error landing solely on the aggregate `d6:<slug>`
400+
* row must force a re-render even when the precomputed `model` reference is
401+
* reused). Returns `true` to SKIP the re-render, `false` to re-render.
402+
*/
403+
export function arePropsEqual(
374404
prev: UnifiedCellProps,
375405
next: UnifiedCellProps,
376406
): boolean {
@@ -398,6 +428,17 @@ function arePropsEqual(
398428
keyFor("e2e", slug, featureId),
399429
keyFor("chat", slug),
400430
keyFor("tools", slug),
431+
// health:<slug> is a pool comm-error carrier (REQ-B) even though it does
432+
// not feed the depth ladder — watch it so an incoming comm-error signal
433+
// repaints the cell's unreachable overlay.
434+
keyFor("health", slug),
435+
// d6:<slug> is the integration-level AGGREGATE row. It does not feed the
436+
// per-cell depth ladder (that reads `d6:<slug>/<featureType>`), but it IS
437+
// a pool comm-error carrier (REQ-B): a worker-death comm error lands solely
438+
// on the aggregate row, and `decodeCellCommError` in buildCellModel reads it
439+
// to light the cell's "unreachable" overlay. Watch it here so that signal
440+
// actually triggers a re-render — keep in sync with buildCellModel.
441+
keyFor("d6", slug),
401442
];
402443

403444
// Add D5 + D6 sub-keys from CATALOG_TO_D5_KEY. D6 is per-cell (not the

0 commit comments

Comments
 (0)