Skip to content

Commit 1a9ba33

Browse files
committed
fix: treat invalid or missing dashboard timestamps as stale
Date.parse() returns NaN for unparseable input, and every comparison against NaN (including >) evaluates to false in JavaScript. Without an explicit check, a malformed or missing fetchedAt would silently be reported as fresh instead of triggering the staleness warning. isStale() now returns true for missing/null/undefined timestamps and for strings that fail to parse, while leaving valid-timestamp freshness behavior (including future timestamps) unchanged.
1 parent 8b9d81b commit 1a9ba33

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"dev": "vite",
99
"build": "vite build",
1010
"preview": "vite preview",
11-
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx",
11+
"test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDashboardData.test.ts",
1212
"typecheck": "tsc --noEmit -p tsconfig.json"
1313
},
1414
"dependencies": {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import assert from "node:assert/strict";
2+
import { test } from "node:test";
3+
4+
import { isStale } from "./useDashboardData";
5+
6+
// Mirrors STALE_THRESHOLD_MS in useDashboardData.ts (5 minutes). Kept as a
7+
// local constant rather than imported so these tests pin the *contract*
8+
// (fresh below the threshold, stale above it) without depending on that
9+
// value being exported.
10+
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
11+
const NOW_MS = Date.parse("2026-06-15T12:00:00.000Z");
12+
13+
test("invalid or missing timestamps are treated as stale", () => {
14+
assert.equal(isStale("not-a-date", NOW_MS), true, "unparseable string");
15+
assert.equal(isStale("", NOW_MS), true, "empty string");
16+
assert.equal(isStale("abcdef", NOW_MS), true, "garbage string");
17+
assert.equal(isStale("2026-13-45T00:00:00Z", NOW_MS), true, "invalid calendar date");
18+
assert.equal(isStale(null, NOW_MS), true, "null");
19+
assert.equal(isStale(undefined, NOW_MS), true, "undefined");
20+
});
21+
22+
test("valid fresh timestamps are not stale", () => {
23+
const fetchedAt = new Date(NOW_MS - 30_000).toISOString(); // 30s ago
24+
assert.equal(isStale(fetchedAt, NOW_MS), false);
25+
});
26+
27+
test("valid old timestamps beyond the threshold are stale", () => {
28+
const fetchedAt = new Date(NOW_MS - STALE_THRESHOLD_MS - 60_000).toISOString(); // 6 min ago
29+
assert.equal(isStale(fetchedAt, NOW_MS), true);
30+
});
31+
32+
test("a timestamp equal to now is fresh", () => {
33+
const fetchedAt = new Date(NOW_MS).toISOString();
34+
assert.equal(isStale(fetchedAt, NOW_MS), false);
35+
});
36+
37+
test("a future timestamp is fresh (unchanged pre-existing behavior)", () => {
38+
const fetchedAt = new Date(NOW_MS + 60_000).toISOString(); // 1 min in the future
39+
assert.equal(isStale(fetchedAt, NOW_MS), false);
40+
});
41+
42+
test("boundary: exactly at the stale threshold is still fresh (strict greater-than)", () => {
43+
const fetchedAt = new Date(NOW_MS - STALE_THRESHOLD_MS).toISOString();
44+
assert.equal(isStale(fetchedAt, NOW_MS), false);
45+
});
46+
47+
test("boundary: one millisecond past the stale threshold is stale", () => {
48+
const fetchedAt = new Date(NOW_MS - STALE_THRESHOLD_MS - 1).toISOString();
49+
assert.equal(isStale(fetchedAt, NOW_MS), true);
50+
});
51+
52+
test("boundary: one millisecond inside the stale threshold is fresh", () => {
53+
const fetchedAt = new Date(NOW_MS - STALE_THRESHOLD_MS + 1).toISOString();
54+
assert.equal(isStale(fetchedAt, NOW_MS), false);
55+
});

apps/web/src/hooks/useDashboardData.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,27 @@ export interface UseDashboardDataResult {
1616
refetch: () => void;
1717
}
1818

19-
function isStale(fetchedAt: string, nowMs: number): boolean {
19+
/**
20+
* Determine whether dashboard data fetched at `fetchedAt` should be treated
21+
* as stale relative to `nowMs`.
22+
*
23+
* A missing or unparseable `fetchedAt` is treated as stale rather than
24+
* fresh: `Date.parse()` returns `NaN` for invalid input, and every
25+
* comparison against `NaN` (including `>`) evaluates to `false` in
26+
* JavaScript -- so without an explicit check, malformed timestamp data
27+
* would silently be reported as fresh instead of triggering the staleness
28+
* warning it's meant to guard against.
29+
*/
30+
export function isStale(fetchedAt: string | null | undefined, nowMs: number): boolean {
31+
if (!fetchedAt) {
32+
return true;
33+
}
34+
2035
const fetchedTime = Date.parse(fetchedAt);
36+
if (!Number.isFinite(fetchedTime)) {
37+
return true;
38+
}
39+
2140
return nowMs - fetchedTime > STALE_THRESHOLD_MS;
2241
}
2342

0 commit comments

Comments
 (0)