153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
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');
|
|
});
|
|
});
|