Files
vyndr/tests/unit/freshness.test.js
T
builtbykev 7c7ab24be5 M1b: mobile app-bar clock — Design's resting clock + STALE reaction (Hybrid)
Design's mobile app bar shows a wall clock, no SYNC/STALE readout. Building it
literally would drop the Ship-A staleness signal on the device most users are
on. Kev's ruling: Hybrid — the wall clock is the RESTING state (the stillness),
STALE/amber is the REACTION (the punctuation), driven by the SAME real signal
as desktop (refreshed_at vs expected_interval_s, thresholds 1.5×/3×). Never
silently stale on mobile. Design drew the happy path; we keep the failure state.

- web/src/lib/freshness.js — extracted the freshness tier as ONE shared source
  (CommonJS, unit-tested); desktop HeartbeatBar + mobile clock both key off it,
  so mobile can't silently disagree with desktop about staleness.
- LiveLayer: <768px hides SIGNAL LIVE + EKG + graded + the SYNC label and shows
  a single right-aligned clock — ticking wall clock when calm, amber SYNC / red
  STALE when the tier reacts. Resting dot is static (the clock is the pulse).
- Test-lock: freshness.test.js (tier thresholds, absent≠false-stale, no negative
  age) + vyndrParityQA (mobile clock wired, one shared freshness source).

Built to Design's mobile spec, VISUALLY UNVERIFIED at 390px. NEXT shell
increments (still M1b): merge the clock into the logo row (Design's single-row
app bar), the breadth strip (EDGES/AVG CLV/GAMES — replaces the graded count
mobile lost here), and relocate the wire to the bottom of the board.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:26:00 -04:00

47 lines
1.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// Shared pipeline-freshness tier (Ship A + design-train M1b). The desktop
// HeartbeatBar and the mobile app-bar clock BOTH key off this — one source, so
// mobile can never silently disagree with desktop about staleness.
const { freshnessTier, DEFAULT_EXPECTED_S } = require('../../web/src/lib/freshness');
const S = 1000;
const now = 1_000_000_000_000;
describe('freshnessTier', () => {
test('calm when the heartbeat is recent (< 1.5× expected)', () => {
const r = freshnessTier(now - 1000 * S, 1200, now); // 1000s elapsed, expected 1200 → 0.83×
expect(r.tier).toBe('normal');
expect(r.elapsedMs).toBe(1000 * S);
});
test('amber at ≥ 1.5× expected (the reaction begins)', () => {
const r = freshnessTier(now - 1800 * S, 1200, now); // 1800/1200 = 1.5×
expect(r.tier).toBe('amber');
});
test('stale (red) at ≥ 3× expected — never silently stale', () => {
const r = freshnessTier(now - 3600 * S, 1200, now); // 3600/1200 = 3×
expect(r.tier).toBe('stale');
});
test('absent/NaN heartbeat is calm, not a false STALE', () => {
expect(freshnessTier(NaN, 1200, now).tier).toBe('normal');
expect(freshnessTier(NaN, 1200, now).elapsedMs).toBeNull();
});
test('non-positive expected falls back to the default 5h gap', () => {
// 4h elapsed vs the 5h (18000s) default → still under 1.5× → calm
const r = freshnessTier(now - 4 * 3600 * S, 0, now);
expect(DEFAULT_EXPECTED_S).toBe(18000);
expect(r.tier).toBe('normal');
});
test('future/negative elapsed clamps to 0 (calm), never a negative age', () => {
const r = freshnessTier(now + 5000 * S, 1200, now);
expect(r.elapsedMs).toBe(0);
expect(r.tier).toBe('normal');
});
});