import { describe, expect, it } from 'vitest'; import type { Agent } from '../types'; import { applyStatsUpdates } from './applyStatsUpdate'; const baseAgent = (): Agent => ({ id: 'a1', name: 'node', wallet: '', ip: '10.0.0.1', version: '1', status: 'online', cpu_cores: 4, memory_gb: 8, last_seen: new Date().toISOString(), created_at: new Date().toISOString(), hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, shares_total: 0, shares_good: 0, shares_bad: 0, cpu_usage_pct: 10, memory_usage_pct: 20, uptime_seconds: 60, }); describe('applyStatsUpdates', () => { it('applies batch updates in one pass', () => { const agents = [baseAgent(), { ...baseAgent(), id: 'a2', hashrate_15m: 50 }]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 200, hashrate_1m: 200, hashrate_15m: 200, cpu_usage_pct: 15 }, { agent_id: 'a2', hashrate_15s: 80, hashrate_1m: 80, hashrate_15m: 80, cpu_usage_pct: 5 }, ]); expect(next[0].hashrate_15m).toBe(200); expect(next[1].hashrate_15m).toBe(80); }); it('returns same reference when nothing changed', () => { const agents = [baseAgent()]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, cpu_usage_pct: 10 }, ]); expect(next).toBe(agents); }); it('merges mining cascade fields from stats_batch updates', () => { const agents = [baseAgent()]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, cpu_usage_pct: 10, active_method: 'inprocess', stratum_overlay: true, chain_exhausted: false, chain_order: ['container', 'inprocess', 'stratum_direct'], failed_methods: [{ method: 'container', reason: 'blocked', at: '2026-06-06T12:00:00Z' }], last_error: 'container start blocked', }, ]); expect(next[0].active_method).toBe('inprocess'); expect(next[0].stratum_overlay).toBe(true); expect(next[0].chain_order).toEqual(['container', 'inprocess', 'stratum_direct']); expect(next[0].failed_methods).toHaveLength(1); expect(next[0].last_error).toBe('container start blocked'); }); it('applies batch mining updates for multiple agents', () => { const agents = [baseAgent(), { ...baseAgent(), id: 'a2', name: 'node-b' }]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, cpu_usage_pct: 10, active_method: 'container' }, { agent_id: 'a2', hashrate_15s: 50, hashrate_1m: 50, hashrate_15m: 50, cpu_usage_pct: 5, chain_exhausted: true }, ]); expect(next[0].active_method).toBe('container'); expect(next[1].chain_exhausted).toBe(true); }); it('merges vuln_findings and vuln_risk_score from stats_batch', () => { const agents = [{ ...baseAgent(), id: 'v1', name: 'Vuln Node' }]; const next = applyStatsUpdates(agents, [ { agent_id: 'v1', hashrate_15s: 0, hashrate_1m: 0, hashrate_15m: 0, cpu_usage_pct: 0, vuln_risk_score: 42, vuln_findings: [{ cve_id: 'CVE-2021-26855', severity: 'critical', patched: false }], }, ]); expect(next[0].vuln_risk_score).toBe(42); expect(next[0].vuln_findings?.[0].cve_id).toBe('CVE-2021-26855'); }); it('merges mining_hashrate and lotl_tier from stats_batch', () => { const agents = [baseAgent()]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, cpu_usage_pct: 10, mining_hashrate: 850, lotl_tier: 'tier-1', }, ]); expect(next[0].mining_hashrate).toBe(850); expect(next[0].lotl_tier).toBe('tier-1'); }); it('merges lotl_attempts from stats_batch', () => { const agents = [baseAgent()]; const attempts = [ { tier: 'container', ok: false, error: 'docker missing', duration_ms: 400 }, { tier: 'cpu_inprocess', ok: true, duration_ms: 900 }, ]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, cpu_usage_pct: 10, lotl_tier: 'cpu_inprocess', lotl_attempts: attempts, }, ]); expect(next[0].lotl_attempts).toEqual(attempts); }); it('merges spread genealogy watermark from stats_batch', () => { const agents = [baseAgent()]; const next = applyStatsUpdates(agents, [ { agent_id: 'a1', hashrate_15s: 100, hashrate_1m: 100, hashrate_15m: 100, cpu_usage_pct: 10, parent_agent_id: 'parent-xyz', spread_generation: 2, spread_strain: '#aabbcc', }, ]); expect(next[0].parent_agent_id).toBe('parent-xyz'); expect(next[0].spread_generation).toBe(2); expect(next[0].spread_strain).toBe('#aabbcc'); }); });