1313import React from "react" ;
1414import { describe , it , expect , vi , beforeEach } from "vitest" ;
1515import { render } from "@testing-library/react" ;
16- import { UnifiedCell } from "../unified-cell" ;
16+ import { UnifiedCell , arePropsEqual } from "../unified-cell" ;
1717import type { UnifiedCellProps } from "../unified-cell" ;
1818import 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" ;
2020import 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} ) ;
0 commit comments