Skip to content

Commit e19159e

Browse files
committed
fix(showcase): downgrade stale-green D5/D6 dashboard rows like D3
Extend the e2e staleness downgrade to resolveD5/resolveD6 in cell-model and to depth-utils so a frozen-green pipeline no longer credits D5/D6 as healthy. Also correct misleading comments/test-doc nits flagged in review.
1 parent 81159ec commit e19159e

4 files changed

Lines changed: 228 additions & 12 deletions

File tree

showcase/shell-dashboard/src/components/__tests__/depth-utils.test.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
2-
* Unit tests for depth derivation utility.
3-
* Parameterized: all D0-D6 combos, short-circuit on red, unshipped returns D0.
2+
* Unit tests for the depth-derivation utility (deriveDepth).
3+
* Covers the D0-D6 ladder walk, short-circuit on non-green, unshipped/
4+
* unsupported handling, maxPossible/isRegression computation, multi-key D5
5+
* mappings, and D5/D6 staleness downgrades.
46
*/
57
import { describe, it, expect } from "vitest";
68
import { deriveDepth } from "../depth-utils";
@@ -370,6 +372,69 @@ describe("deriveDepth", () => {
370372
expect(result.achieved).toBe(6);
371373
});
372374

375+
// ── D5/D6 staleness mirrors cell-model.ts (both consumers agree) ──
376+
describe("D5/D6 staleness downgrade", () => {
377+
const NOW = Date.parse("2026-05-30T12:00:00Z");
378+
const STALE_AT = new Date(NOW - 7 * 60 * 60 * 1000).toISOString();
379+
const FRESH_AT = new Date(NOW - 60 * 1000).toISOString();
380+
381+
it("does not credit D5 when its green row is stale", () => {
382+
const c = cell("lgp", "agentic-chat");
383+
const live = mapOf([
384+
row("health:lgp", "health", "green", FRESH_AT),
385+
row("agent:lgp", "agent", "green", FRESH_AT),
386+
row("e2e:lgp/agentic-chat", "e2e", "green", FRESH_AT),
387+
row("chat:lgp", "chat", "green", FRESH_AT),
388+
row("d5:lgp/agentic-chat", "d5", "green", STALE_AT),
389+
]);
390+
const result = deriveDepth(c, live, NOW);
391+
// Stale green D5 must not advance past D4.
392+
expect(result.achieved).toBe(4);
393+
});
394+
395+
it("credits D5 when its green row is fresh", () => {
396+
const c = cell("lgp", "agentic-chat");
397+
const live = mapOf([
398+
row("health:lgp", "health", "green", FRESH_AT),
399+
row("agent:lgp", "agent", "green", FRESH_AT),
400+
row("e2e:lgp/agentic-chat", "e2e", "green", FRESH_AT),
401+
row("chat:lgp", "chat", "green", FRESH_AT),
402+
row("d5:lgp/agentic-chat", "d5", "green", FRESH_AT),
403+
]);
404+
const result = deriveDepth(c, live, NOW);
405+
expect(result.achieved).toBe(5);
406+
});
407+
408+
it("does not credit D6 when its green row is stale", () => {
409+
const c = cell("lgp", "agentic-chat");
410+
const live = mapOf([
411+
row("health:lgp", "health", "green", FRESH_AT),
412+
row("agent:lgp", "agent", "green", FRESH_AT),
413+
row("e2e:lgp/agentic-chat", "e2e", "green", FRESH_AT),
414+
row("chat:lgp", "chat", "green", FRESH_AT),
415+
row("d5:lgp/agentic-chat", "d5", "green", FRESH_AT),
416+
row("d6:lgp", "d6", "green", STALE_AT),
417+
]);
418+
const result = deriveDepth(c, live, NOW);
419+
// Stale green D6 must not advance past D5.
420+
expect(result.achieved).toBe(5);
421+
});
422+
423+
it("credits D6 when its green row is fresh", () => {
424+
const c = cell("lgp", "agentic-chat");
425+
const live = mapOf([
426+
row("health:lgp", "health", "green", FRESH_AT),
427+
row("agent:lgp", "agent", "green", FRESH_AT),
428+
row("e2e:lgp/agentic-chat", "e2e", "green", FRESH_AT),
429+
row("chat:lgp", "chat", "green", FRESH_AT),
430+
row("d5:lgp/agentic-chat", "d5", "green", FRESH_AT),
431+
row("d6:lgp", "d6", "green", FRESH_AT),
432+
]);
433+
const result = deriveDepth(c, live, NOW);
434+
expect(result.achieved).toBe(6);
435+
});
436+
});
437+
373438
it("returns D4 for feature with no D5 mapping", () => {
374439
const c = cell("lgp", "unknown-feature");
375440
const live = mapOf([

showcase/shell-dashboard/src/components/depth-utils.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ function isGreen(live: LiveStatusMap, key: string): boolean {
6464
return row?.state === "green";
6565
}
6666

67+
/**
68+
* A row counts as green only when it is green AND fresh. A frozen-green row
69+
* from a stalled driver must NOT credit its depth — the same false-green
70+
* staleness downgrade `cell-model.ts` applies to D3/D5/D6, mirrored here so
71+
* both consumers agree.
72+
*/
73+
function isGreenAndFresh(
74+
live: LiveStatusMap,
75+
key: string,
76+
now: number,
77+
): boolean {
78+
const row = live.get(key);
79+
if (row?.state !== "green") return false;
80+
return !isRowStale(row, now, E2E_STALE_AFTER_MS);
81+
}
82+
6783
/** Row is stale if `observed_at` is older than `maxAgeMs` relative to `now`. */
6884
function isRowStale(row: StatusRow, now: number, maxAgeMs: number): boolean {
6985
const observedMs = Date.parse(row.observed_at);
@@ -89,13 +105,16 @@ function isE2eGreenAndFresh(
89105
}
90106

91107
/**
92-
* Check whether all D5 PB rows for a given (slug, catalogFeatureId) are green.
93-
* Returns false if the feature has no D5 mapping or any mapped row is missing/non-green.
108+
* Check whether all D5 PB rows for a given (slug, catalogFeatureId) are green
109+
* AND fresh. Returns false if the feature has no D5 mapping or any mapped row
110+
* is missing/non-green/stale. The staleness gate mirrors `cell-model.ts` so a
111+
* frozen-green CV row from a stalled driver no longer credits D5.
94112
*/
95113
function isD5Green(
96114
live: LiveStatusMap,
97115
slug: string,
98116
featureId: string,
117+
now: number,
99118
): boolean {
100119
const d5Keys = CATALOG_TO_D5_KEY[featureId];
101120
// No D5 mapping = no CV test exists for this feature = cannot be D5.
@@ -104,7 +123,9 @@ function isD5Green(
104123
if (!d5Keys || d5Keys.length === 0) {
105124
return false;
106125
}
107-
return d5Keys.every((d5Key) => isGreen(live, keyFor("d5", slug, d5Key)));
126+
return d5Keys.every((d5Key) =>
127+
isGreenAndFresh(live, keyFor("d5", slug, d5Key), now),
128+
);
108129
}
109130

110131
/**
@@ -236,7 +257,7 @@ export function deriveDepth(
236257
achieved = 4;
237258

238259
// D5: d5:<slug>/<d5FeatureType> green (per-cell, mapped via CATALOG_TO_D5_KEY)
239-
if (!isD5Green(live, cell.integration, cell.feature)) {
260+
if (!isD5Green(live, cell.integration, cell.feature, now)) {
240261
return {
241262
achieved,
242263
maxPossible,
@@ -247,7 +268,7 @@ export function deriveDepth(
247268
achieved = 5;
248269

249270
// D6: d6:<slug> green (integration-scoped aggregate)
250-
if (isGreen(live, keyFor("d6", cell.integration))) {
271+
if (isGreenAndFresh(live, keyFor("d6", cell.integration), now)) {
251272
achieved = 6;
252273
}
253274

showcase/shell-dashboard/src/lib/__tests__/cell-model.test.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ describe("buildCellModel", () => {
170170
});
171171
});
172172

173-
// ── D3+D4 pass, D5 exists but fails → amber chip ───────────────────
173+
// ── D3+D4 pass, D5 exists but fails → red chip ─────────────────────
174174
describe("D3+D4 pass, D5 exists but fails", () => {
175175
it("returns red chip (D6-ceiling: D5 red, D6 absent)", () => {
176176
const live = mapOf([
@@ -623,4 +623,114 @@ describe("buildCellModel", () => {
623623
expect(model.d3?.status).toBe("red");
624624
});
625625
});
626+
627+
// ── D5/D6 staleness downgrade (same false-green mode, one dimension up) ──
628+
// A frozen-green D5/D6 row from a stalled driver must be downgraded the
629+
// same way D3 is, so it no longer credits the depth ladder / ceiling.
630+
describe("D5/D6 staleness downgrade", () => {
631+
const NOW = Date.parse("2026-05-30T12:00:00Z");
632+
633+
function rowAtAge(
634+
key: string,
635+
dimension: string,
636+
ageMs: number,
637+
state: State = "green",
638+
) {
639+
const observedAt = new Date(NOW - ageMs).toISOString();
640+
return row(key, dimension, state, {
641+
observed_at: observedAt,
642+
transitioned_at: observedAt,
643+
});
644+
}
645+
646+
const STALE = E2E_STALE_AFTER_MS + 60 * 60 * 1000;
647+
const FRESH = 60 * 1000;
648+
649+
it("downgrades a stale green D5 row to amber (no longer credits D5/ceiling)", () => {
650+
const live = mapOf([
651+
rowAtAge(keyFor("e2e", "agno", "agentic-chat"), "e2e", FRESH, "green"),
652+
rowAtAge(keyFor("chat", "agno"), "chat", FRESH, "green"),
653+
rowAtAge(keyFor("d5", "agno", "agentic-chat"), "d5", STALE, "green"),
654+
]);
655+
const model = buildCellModel(
656+
live,
657+
wiredInput("agno", "agentic-chat"),
658+
NOW,
659+
);
660+
// Stale green D5 must NOT present as a healthy D5.
661+
expect(model.d5?.status).toBe("amber");
662+
// Depth ladder must not credit D5 when the signal is stale.
663+
expect(model.achievedDepth).toBe(4);
664+
// Chip: D5 not green, D6 absent → red (no longer amber-on-green-D5).
665+
expect(model.chipColor).toBe("red");
666+
});
667+
668+
it("keeps a fresh green D5 row green", () => {
669+
const live = mapOf([
670+
rowAtAge(keyFor("e2e", "agno", "agentic-chat"), "e2e", FRESH, "green"),
671+
rowAtAge(keyFor("chat", "agno"), "chat", FRESH, "green"),
672+
rowAtAge(keyFor("d5", "agno", "agentic-chat"), "d5", FRESH, "green"),
673+
]);
674+
const model = buildCellModel(
675+
live,
676+
wiredInput("agno", "agentic-chat"),
677+
NOW,
678+
);
679+
expect(model.d5?.status).toBe("green");
680+
expect(model.achievedDepth).toBe(5);
681+
// D5 green but no D6 → amber.
682+
expect(model.chipColor).toBe("amber");
683+
});
684+
685+
it("downgrades a stale green D6 row to amber (no longer credits D6/green chip)", () => {
686+
const live = mapOf([
687+
rowAtAge(keyFor("e2e", "agno", "agentic-chat"), "e2e", FRESH, "green"),
688+
rowAtAge(keyFor("chat", "agno"), "chat", FRESH, "green"),
689+
rowAtAge(keyFor("d5", "agno", "agentic-chat"), "d5", FRESH, "green"),
690+
rowAtAge(keyFor("d6", "agno"), "d6", STALE, "green"),
691+
]);
692+
const model = buildCellModel(
693+
live,
694+
wiredInput("agno", "agentic-chat"),
695+
NOW,
696+
);
697+
// Stale green D6 must NOT present as a healthy D6.
698+
expect(model.d6?.status).toBe("amber");
699+
// Depth ladder must not credit D6 when the signal is stale.
700+
expect(model.achievedDepth).toBe(5);
701+
// D5 green but D6 not green → amber, not green.
702+
expect(model.chipColor).toBe("amber");
703+
});
704+
705+
it("keeps a fresh green D6 row green", () => {
706+
const live = mapOf([
707+
rowAtAge(keyFor("e2e", "agno", "agentic-chat"), "e2e", FRESH, "green"),
708+
rowAtAge(keyFor("chat", "agno"), "chat", FRESH, "green"),
709+
rowAtAge(keyFor("d5", "agno", "agentic-chat"), "d5", FRESH, "green"),
710+
rowAtAge(keyFor("d6", "agno"), "d6", FRESH, "green"),
711+
]);
712+
const model = buildCellModel(
713+
live,
714+
wiredInput("agno", "agentic-chat"),
715+
NOW,
716+
);
717+
expect(model.d6?.status).toBe("green");
718+
expect(model.achievedDepth).toBe(6);
719+
expect(model.chipColor).toBe("green");
720+
});
721+
722+
it("leaves a stale RED D5 row red (staleness only downgrades green)", () => {
723+
const live = mapOf([
724+
rowAtAge(keyFor("e2e", "agno", "agentic-chat"), "e2e", FRESH, "green"),
725+
rowAtAge(keyFor("chat", "agno"), "chat", FRESH, "green"),
726+
rowAtAge(keyFor("d5", "agno", "agentic-chat"), "d5", STALE, "red"),
727+
]);
728+
const model = buildCellModel(
729+
live,
730+
wiredInput("agno", "agentic-chat"),
731+
NOW,
732+
);
733+
expect(model.d5?.status).toBe("red");
734+
});
735+
});
626736
});

showcase/shell-dashboard/src/lib/cell-model.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,23 @@ function resolveD4(live: LiveStatusMap, slug: string): TestLevel {
123123
* Uses `CATALOG_TO_D5_KEY` to map catalog feature IDs to D5 PB row key
124124
* suffixes. When multiple sub-keys exist (e.g. `beautiful-chat` fans out
125125
* to 5 pills), worst-state wins.
126+
*
127+
* Staleness applies the same downgrade as `resolveD3`: a green D5 row whose
128+
* `observed_at` is older than `E2E_STALE_AFTER_MS` is downgraded to `amber`
129+
* (degraded). When the driver stops writing, a frozen-green row would
130+
* otherwise credit D5 forever — the same false-green mode one dimension up.
131+
* Only green is downgraded; a stale red/degraded row already signals a
132+
* problem and is left as-is.
126133
*/
127134
function resolveD5(
128135
live: LiveStatusMap,
129136
slug: string,
130137
featureId: string,
138+
now: number,
131139
): TestLevel {
132140
const d5Keys = CATALOG_TO_D5_KEY[featureId];
133141

134-
// No mapping and no fallback row → test doesn't exist for this feature.
142+
// No mapping → test doesn't exist for this feature.
135143
if (!d5Keys || d5Keys.length === 0) {
136144
return { exists: false, status: null, row: null };
137145
}
@@ -151,6 +159,10 @@ function resolveD5(
151159
return { exists: true, status: null, row: null };
152160
}
153161

162+
if (worst.state === "green" && isStale(worst, now, E2E_STALE_AFTER_MS)) {
163+
return { exists: true, status: "amber", row: worst };
164+
}
165+
154166
return {
155167
exists: true,
156168
status: stateToTestStatus(worst.state),
@@ -204,12 +216,20 @@ function resolveD3(
204216
* D6 is an aggregate integration-level signal (`d6:<slug>`), NOT per-cell.
205217
* The e2e-full driver emits a single row per integration that covers
206218
* the entire parity comparison against the reference implementation.
219+
*
220+
* Staleness applies the same downgrade as `resolveD3`: a green D6 row whose
221+
* `observed_at` is older than `E2E_STALE_AFTER_MS` is downgraded to `amber`
222+
* (degraded), so a frozen-green row from a stalled driver no longer credits
223+
* D6 forever. Only green is downgraded; stale red/degraded is left as-is.
207224
*/
208-
function resolveD6(live: LiveStatusMap, slug: string): TestLevel {
225+
function resolveD6(live: LiveStatusMap, slug: string, now: number): TestLevel {
209226
const row = live.get(keyFor("d6", slug)) ?? null;
210227
if (!row) {
211228
return { exists: false, status: null, row: null };
212229
}
230+
if (row.state === "green" && isStale(row, now, E2E_STALE_AFTER_MS)) {
231+
return { exists: true, status: "amber", row };
232+
}
213233
return {
214234
exists: true,
215235
status: stateToTestStatus(row.state),
@@ -272,8 +292,8 @@ export function buildCellModel(
272292
// ── Wired + supported: resolve each depth independently ───────────
273293
const d3 = resolveD3(live, slug, featureId, now);
274294
const d4 = resolveD4(live, slug);
275-
const d5 = resolveD5(live, slug, featureId);
276-
const d6 = resolveD6(live, slug);
295+
const d5 = resolveD5(live, slug, featureId, now);
296+
const d6 = resolveD6(live, slug, now);
277297

278298
// ceilingDepth: highest CONTIGUOUS depth where a test EXISTS.
279299
// D4 only counts if D3 exists; D5 only counts if D4 counts; D6 only

0 commit comments

Comments
 (0)