7c7ab24be5
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>
47 lines
1.7 KiB
JavaScript
47 lines
1.7 KiB
JavaScript
'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');
|
||
});
|
||
});
|