import { describe, expect, it } from 'vitest'; import { classifyLiveness, computeFleetLiveness } from '../liveness.js'; import type { HeartbeatObservation, LivenessPolicy } from '../types.js'; // Small, dev-scale policy so the arithmetic is obvious: // online window = interval * missTolerance = 1000 * 2 = 2000ms // dark threshold = 5000ms const policy: LivenessPolicy = { heartbeatIntervalMs: 1000, missTolerance: 2, darkThresholdMs: 5000, }; describe('classifyLiveness (deterministic, heartbeat-age based — RFC-001 §4.5)', () => { it('is online when age is within interval * missTolerance', () => { expect(classifyLiveness(0, policy)).toBe('online'); expect(classifyLiveness(1999, policy)).toBe('online'); expect(classifyLiveness(2000, policy)).toBe('online'); // inclusive boundary }); it('is away when past the online window but before dark threshold', () => { expect(classifyLiveness(2001, policy)).toBe('away'); expect(classifyLiveness(4999, policy)).toBe('away'); }); it('is offline/dark at or past the dark threshold', () => { expect(classifyLiveness(5000, policy)).toBe('offline'); expect(classifyLiveness(50_000, policy)).toBe('offline'); }); it('treats a never-seen agent (Infinity age) as offline', () => { expect(classifyLiveness(Number.POSITIVE_INFINITY, policy)).toBe('offline'); }); it('never returns online for a negative-but-huge misconfig (guards NaN)', () => { // A NaN age must fail safe to offline, not silently report online. expect(classifyLiveness(Number.NaN, policy)).toBe('offline'); }); }); describe('computeFleetLiveness (A2/A3 core)', () => { const now = 100_000; const obs = (slug: string, lastSeenTs: number, lastSeq = 1): HeartbeatObservation => ({ slug, mxid: `@agent-${slug}:matrix.localhost`, lastSeenTs, lastSeq, assertedStatus: 'online', }); it('classifies a live fleet: fresh=online, stale=away, dark=offline', () => { const result = computeFleetLiveness( [ obs('alpha', now - 500), // 500ms old -> online obs('bravo', now - 3000), // 3000ms old -> away obs('charlie', now - 8000), // 8000ms old -> offline ], now, policy, ); const byslug = Object.fromEntries(result.map((r) => [r.slug, r.status])); expect(byslug).toEqual({ alpha: 'online', bravo: 'away', charlie: 'offline' }); }); it('A3: a previously-online agent flips to offline once age crosses dark threshold', () => { const lastBeat = 100_000; // agent was hard-killed right after this beat // Just before the threshold it is still merely "away"... const justBefore = computeFleetLiveness([obs('victim', lastBeat, 7)], lastBeat + 4999, policy); expect(justBefore[0]?.status).toBe('away'); // ...and the instant age reaches darkThresholdMs it is deterministically offline, // with no dependence on native Matrix presence timeouts. const atThreshold = computeFleetLiveness([obs('victim', lastBeat, 7)], lastBeat + 5000, policy); expect(atThreshold[0]?.status).toBe('offline'); expect(atThreshold[0]?.ageMs).toBe(5000); expect(atThreshold[0]?.lastSeq).toBe(7); }); it('reports ageMs and preserves mxid/slug/seq for the human view', () => { const [row] = computeFleetLiveness([obs('alpha', now - 1200, 42)], now, policy); expect(row).toMatchObject({ slug: 'alpha', mxid: '@agent-alpha:matrix.localhost', ageMs: 1200, lastSeq: 42, status: 'online', }); }); });