diff --git a/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts b/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts index a4bc515a89..dffdf1352c 100644 --- a/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts +++ b/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card-model.ts @@ -45,13 +45,18 @@ export function trendHasAnySignal(weeks: SlopDuplicateTrendWeek[]): boolean { return seriesHasSignal(weeks, "slop") || seriesHasSignal(weeks, "duplicate"); } +/** Latest week bearing a signal for ONE series. The two rates are independently nullable, so each + * legend must resolve its own latest week — a single shared "any signal" lookup would let one + * series' null in the newest signal-bearing week hide an older real value for the other (#8667). */ export function latestWeekWithSignal( weeks: SlopDuplicateTrendWeek[], + series: "slop" | "duplicate", ): SlopDuplicateTrendWeek | null { for (let index = weeks.length - 1; index >= 0; index -= 1) { const week = weeks[index]; if (!week) continue; - if (week.slopFlagRatePct !== null || week.duplicateFlagRatePct !== null) return week; + const value = series === "slop" ? week.slopFlagRatePct : week.duplicateFlagRatePct; + if (value !== null) return week; } return null; } diff --git a/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx b/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx index 0b1417a0f2..50c1daf246 100644 --- a/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.test.tsx @@ -2,7 +2,11 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { SlopDuplicateTrendCard } from "@/components/site/app-panels/slop-duplicate-trend-card"; -import type { MaintainerSlopDuplicateTrend } from "@/components/site/app-panels/slop-duplicate-trend-card-model"; +import { + latestWeekWithSignal, + type MaintainerSlopDuplicateTrend, + type SlopDuplicateTrendWeek, +} from "@/components/site/app-panels/slop-duplicate-trend-card-model"; function trend( overrides: Partial = {}, @@ -86,4 +90,106 @@ describe("SlopDuplicateTrendCard", () => { render(); expect(screen.getByText(/stale snapshot/i)).toBeTruthy(); }); + + // The two series are independently nullable (#8667): each legend must resolve its OWN latest + // signal-bearing week, not share the single most-recent week that has *any* signal. + it("keeps the slop legend on its own latest signal week when the newest signal week only has duplicate data", () => { + render( + , + ); + // Slop legend reflects the EARLIER week's real band — not "latest: —" from the newest week's null. + expect(screen.getByText(/latest band: elevated/i)).toBeTruthy(); + expect(screen.queryByText("latest: —")).toBeNull(); + // Duplicate legend independently reflects the newest week's own value. + expect(screen.getByText(/latest: 12\.5%/i)).toBeTruthy(); + }); + + it("keeps the duplicate legend on its own latest signal week when the newest signal week only has slop data", () => { + render( + , + ); + // Duplicate legend reflects the EARLIER week's real value — not the newest week's null. + expect(screen.getByText(/latest: 25%/i)).toBeTruthy(); + expect(screen.queryByText("latest: —")).toBeNull(); + // Slop legend independently reflects the newest week's own band. + expect(screen.getByText(/latest band: low/i)).toBeTruthy(); + }); +}); + +describe("latestWeekWithSignal", () => { + const week = ( + weekStart: string, + slopFlagRatePct: number | null, + duplicateFlagRatePct: number | null, + ): SlopDuplicateTrendWeek => ({ + weekStart, + slopFlagRatePct, + slopBandLabel: slopFlagRatePct === null ? null : "low", + duplicateFlagRatePct, + }); + + it("resolves a different latest week per series when the two series' nulls diverge", () => { + const weeks = [week("2026-06-02", 40, null), week("2026-06-09", null, 12.5)]; + expect(latestWeekWithSignal(weeks, "slop")?.weekStart).toBe("2026-06-02"); + expect(latestWeekWithSignal(weeks, "duplicate")?.weekStart).toBe("2026-06-09"); + }); + + it("returns the same (newest) week for both series when null-ness stays in lockstep", () => { + const weeks = [ + week("2026-06-02", 30, 20), + week("2026-06-09", null, null), + week("2026-06-16", 10, 5), + ]; + expect(latestWeekWithSignal(weeks, "slop")?.weekStart).toBe("2026-06-16"); + expect(latestWeekWithSignal(weeks, "duplicate")?.weekStart).toBe("2026-06-16"); + }); + + it("returns null for a series with no signal in any week, independently of the other series", () => { + const weeks = [week("2026-06-02", 40, null), week("2026-06-09", 10, null)]; + expect(latestWeekWithSignal(weeks, "slop")?.weekStart).toBe("2026-06-09"); + expect(latestWeekWithSignal(weeks, "duplicate")).toBeNull(); + expect(latestWeekWithSignal([], "slop")).toBeNull(); + }); + + it("skips sparse holes while scanning backwards", () => { + const weeks: SlopDuplicateTrendWeek[] = []; + weeks[0] = week("2026-06-02", 40, 25); + weeks.length = 2; // trailing hole — scanned first, must be skipped, not crashed on + expect(latestWeekWithSignal(weeks, "slop")?.weekStart).toBe("2026-06-02"); + expect(latestWeekWithSignal(weeks, "duplicate")?.weekStart).toBe("2026-06-02"); + }); }); diff --git a/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx b/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx index 7e01136f94..f16975bd30 100644 --- a/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/slop-duplicate-trend-card.tsx @@ -26,7 +26,10 @@ export function SlopDuplicateTrendCard({ trend }: { trend: MaintainerSlopDuplica const hasSignal = trendHasAnySignal(trend.weeks); const hasSlop = seriesHasSignal(trend.weeks, "slop"); const hasDuplicate = seriesHasSignal(trend.weeks, "duplicate"); - const latest = latestWeekWithSignal(trend.weeks); + // Each legend resolves its own latest signal-bearing week — the two series are independently + // nullable, so a shared lookup could surface one series' null over the other's real data (#8667). + const latestSlop = latestWeekWithSignal(trend.weeks, "slop"); + const latestDuplicate = latestWeekWithSignal(trend.weeks, "duplicate"); return (