Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Single-host branch runner complements fallback chain under Fleet AI Control: persona ghost forks, onion_miner_log hops, server exhaustion splice from graft mining genome, Crucible depth badge, and hospice retire after max cycles.
373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
/**
|
|
* @vitest-environment happy-dom
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { MemoryRouter } from 'react-router-dom';
|
|
import { mockAgent } from '../test/fixtures';
|
|
import { useWebSocket } from '../hooks/useWebSocket';
|
|
import { api } from '../api/client';
|
|
import CruciblePage, {
|
|
agentColor,
|
|
patchLabel,
|
|
pendingBadge,
|
|
portsBadge,
|
|
postureBadge,
|
|
postureTooltip,
|
|
contingencyDepthBadge,
|
|
sshBadge,
|
|
thermalBadge,
|
|
} from './CruciblePage';
|
|
|
|
// ── Mocks ─────────────────────────────────────────────────────────────────
|
|
|
|
vi.mock('../hooks/useWebSocket', () => ({ useWebSocket: vi.fn() }));
|
|
|
|
vi.mock('../context/MatrixRainContext', () => ({
|
|
useMatrixRain: () => ({ setCrucibleFocus: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock('../hooks/useFleetGroups', () => ({
|
|
useFleetGroups: () => ({ groups: [], addGroup: vi.fn(), removeGroup: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock('../api/client', () => ({
|
|
api: {
|
|
sendAgentCommand: vi.fn().mockResolvedValue({ success: true }),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/CrucibleExpandedOps', () => ({
|
|
default: () => <div data-testid="expanded-ops" />,
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/FleetHeatMiniMap', () => ({
|
|
default: () => <div data-testid="heat-map" />,
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/LatencyBadge', () => ({
|
|
default: () => null,
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/CreateGroupModal', () => ({
|
|
default: () => null,
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/FleetGroupsStrip', () => ({
|
|
default: () => null,
|
|
}));
|
|
|
|
vi.mock('../hooks/useFleetBulkActions', () => ({
|
|
useFleetBulkActions: () => ({ bulkBusy: false, handleBulkAction: vi.fn() }),
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/CrucibleAgentMeta', () => ({
|
|
default: () => null,
|
|
}));
|
|
|
|
vi.mock('../components/Presence/AlsoHere', () => ({
|
|
default: () => null,
|
|
}));
|
|
|
|
vi.mock('../components/Fleet/FullSysCheckPanel', () => ({
|
|
default: () => null,
|
|
}));
|
|
|
|
vi.mock('../components/HelpTip', () => ({
|
|
HelpTip: () => null,
|
|
}));
|
|
|
|
vi.mock('../components/NeonCard/NeonCard', () => ({
|
|
default: ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
<div className={className}>{children}</div>
|
|
),
|
|
}));
|
|
|
|
const useWebSocketMock = vi.mocked(useWebSocket);
|
|
|
|
function makeWsValue(overrides: Partial<ReturnType<typeof useWebSocket>>) {
|
|
return {
|
|
isConnected: true,
|
|
agents: [],
|
|
recentShares: [],
|
|
fleetAlerts: [],
|
|
poolStatus: [],
|
|
aiActivity: [],
|
|
agentLogs: {},
|
|
commandResults: [],
|
|
policyAcks: [],
|
|
latestMessage: null,
|
|
sendDashboardMessage: vi.fn(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function renderCrucible(wsValue: ReturnType<typeof makeWsValue>) {
|
|
useWebSocketMock.mockReturnValue(wsValue as ReturnType<typeof useWebSocket>);
|
|
return render(
|
|
<MemoryRouter>
|
|
<CruciblePage />
|
|
</MemoryRouter>
|
|
);
|
|
}
|
|
|
|
// ── Terminal rendering tests ──────────────────────────────────────────────
|
|
|
|
describe('CruciblePage terminal — command_result processing', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
});
|
|
|
|
it('displays exec result in terminal when agent is selected', async () => {
|
|
const agent = mockAgent({ id: 'agent-aaa-001', name: 'TestNode', status: 'online' });
|
|
const commandResults = [
|
|
{ agent_id: 'agent-aaa-001', action: 'exec', success: true, message: 'hello world', _seq: 1 },
|
|
];
|
|
|
|
// Render with the agent already "selected" by providing a pre-selected state.
|
|
// The component reads commandResults from context and renders terminal lines.
|
|
renderCrucible(makeWsValue({ agents: [agent], commandResults }));
|
|
|
|
// The terminal should display the result message from the command.
|
|
await waitFor(() => {
|
|
expect(screen.getByText('hello world')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays multiple exec result lines split on newline', async () => {
|
|
const agent = mockAgent({ id: 'agent-bbb-002', name: 'MultiNode', status: 'online' });
|
|
const commandResults = [
|
|
{
|
|
agent_id: 'agent-bbb-002',
|
|
action: 'exec',
|
|
success: true,
|
|
message: 'line one\nline two\nline three',
|
|
_seq: 1,
|
|
},
|
|
];
|
|
|
|
renderCrucible(makeWsValue({ agents: [agent], commandResults }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('line one')).toBeInTheDocument();
|
|
expect(screen.getByText('line two')).toBeInTheDocument();
|
|
expect(screen.getByText('line three')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('skips command_result entries with missing agent_id', async () => {
|
|
const commandResults = [
|
|
{ agent_id: undefined, action: 'exec', success: true, message: 'ghost output', _seq: 1 },
|
|
];
|
|
|
|
renderCrucible(makeWsValue({ commandResults }));
|
|
|
|
// "ghost output" should not appear — entry has no agent_id so it's skipped.
|
|
await new Promise((r) => setTimeout(r, 50));
|
|
expect(screen.queryByText('ghost output')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('skips already-seen entries when new commandResults arrive', async () => {
|
|
const agent = mockAgent({ id: 'agent-ccc-003', name: 'SeqNode', status: 'online' });
|
|
|
|
// Initial render: seq=1
|
|
const ws1 = makeWsValue({
|
|
agents: [agent],
|
|
commandResults: [{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'first', _seq: 1 }],
|
|
});
|
|
const { rerender } = renderCrucible(ws1);
|
|
|
|
await waitFor(() => expect(screen.getByText('first')).toBeInTheDocument());
|
|
|
|
// Update: add seq=2, keep seq=1 — only 'second' should be added (not 'first' again)
|
|
const ws2 = makeWsValue({
|
|
agents: [agent],
|
|
commandResults: [
|
|
{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'first', _seq: 1 },
|
|
{ agent_id: 'agent-ccc-003', action: 'exec', success: true, message: 'second', _seq: 2 },
|
|
],
|
|
});
|
|
useWebSocketMock.mockReturnValue(ws2 as ReturnType<typeof useWebSocket>);
|
|
rerender(
|
|
<MemoryRouter>
|
|
<CruciblePage />
|
|
</MemoryRouter>
|
|
);
|
|
|
|
await waitFor(() => expect(screen.getByText('second')).toBeInTheDocument());
|
|
// 'first' should appear exactly once (not twice from a replay)
|
|
expect(screen.getAllByText('first')).toHaveLength(1);
|
|
});
|
|
|
|
it('renders fleet filter toolbar when agents are registered', async () => {
|
|
const agent = mockAgent({ name: 'FilterNode', status: 'online' });
|
|
renderCrucible(makeWsValue({ agents: [agent] }));
|
|
await waitFor(() => {
|
|
expect(screen.getByPlaceholderText('Search name, IP, notes, tags…')).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByText('FilterNode')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows filter empty hint when no nodes match', async () => {
|
|
const agent = mockAgent({ name: 'HiddenNode', tags: ['prod'] });
|
|
renderCrucible(makeWsValue({ agents: [agent] }));
|
|
await waitFor(() => expect(screen.getByPlaceholderText('Search name, IP, notes, tags…')).toBeInTheDocument());
|
|
await userEvent.setup().type(screen.getByPlaceholderText('Search name, IP, notes, tags…'), 'nomatchxyz');
|
|
expect(screen.getByText('No nodes match filters.')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows fallback line when command result message is empty', async () => {
|
|
const agent = mockAgent({ id: 'agent-empty-001', name: 'EmptyNode', status: 'online' });
|
|
renderCrucible(
|
|
makeWsValue({
|
|
agents: [agent],
|
|
commandResults: [
|
|
{ agent_id: 'agent-empty-001', action: 'pause', success: true, message: '', _seq: 1 },
|
|
],
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('[pause] OK')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('renders mining diagnostics JSON in terminal', async () => {
|
|
const agent = mockAgent({ id: 'agent-diag-001', name: 'DiagNode', status: 'online' });
|
|
const diagnostics = {
|
|
generated_at: '2026-06-06T12:00:00.000Z',
|
|
platform: 'windows',
|
|
configured_execution: 'auto',
|
|
execution_mode: 'inprocess',
|
|
active_method: 'inprocess',
|
|
lotl_tier: 'inprocess',
|
|
mining_hashrate: 512,
|
|
lotl_attempts: [
|
|
{ tier: 'container', ok: false, error: 'docker not found', duration_ms: 600 },
|
|
{ tier: 'inprocess', ok: true, duration_ms: 1800 },
|
|
],
|
|
likely_blockers: [
|
|
'mining paused by remote command or healthy container delegation',
|
|
'Windows Defender real-time protection is ON — use Calibrate exclusion script or allowlist install path',
|
|
],
|
|
cpu: { remote_paused: true, has_job: false, hashrate_hps: 0 },
|
|
gpu: { enabled: false, active: false, paused: false },
|
|
};
|
|
const commandResults = [
|
|
{
|
|
agent_id: 'agent-diag-001',
|
|
action: 'mining_diagnostics',
|
|
success: true,
|
|
message: JSON.stringify(diagnostics, null, 2),
|
|
_seq: 1,
|
|
},
|
|
];
|
|
|
|
renderCrucible(makeWsValue({ agents: [agent], commandResults }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('MINING DIAGNOSTICS')).toBeInTheDocument();
|
|
expect(screen.getByText('LIKELY BLOCKERS')).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText('mining paused by remote command or healthy container delegation'),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(
|
|
'Windows Defender real-time protection is ON — use Calibrate exclusion script or allowlist install path',
|
|
),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText('Execution')).toBeInTheDocument();
|
|
expect(screen.getAllByText('inprocess').length).toBeGreaterThanOrEqual(1);
|
|
expect(screen.getByText('LOTL TIER CHAIN')).toBeInTheDocument();
|
|
expect(screen.getByText('docker not found')).toBeInTheDocument();
|
|
expect(screen.getByText('600ms')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows LOTL tier badge on agent card when lotl_tier is set', async () => {
|
|
const agent = mockAgent({
|
|
id: 'agent-lotl-001',
|
|
name: 'LotlNode',
|
|
status: 'online',
|
|
lotl_tier: 'wsl',
|
|
});
|
|
renderCrucible(makeWsValue({ agents: [agent] }));
|
|
await waitFor(() => {
|
|
expect(screen.getByText('LOTL WSL')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ── Helper function tests ─────────────────────────────────────────────────
|
|
|
|
describe('CruciblePage helpers', () => {
|
|
it('agentColor cycles palette by agent order', () => {
|
|
const ids = ['a', 'b', 'c'];
|
|
expect(agentColor('a', ids)).toBe('#00e8f5');
|
|
expect(agentColor('b', ids)).toBe('#39ff14');
|
|
expect(agentColor('missing', ids)).toBe('#00e8f5');
|
|
});
|
|
|
|
it('sshBadge reflects ssh_available tri-state', () => {
|
|
expect(sshBadge(mockAgent({ ssh_available: true })).label).toBe('SSH ON');
|
|
expect(sshBadge(mockAgent({ ssh_available: false })).cls).toBe('ssh-off');
|
|
expect(sshBadge(mockAgent({ ssh_available: undefined })).label).toBe('SSH ?');
|
|
});
|
|
|
|
it('postureBadge buckets score thresholds', () => {
|
|
expect(postureBadge(undefined).cls).toBe('posture-unk');
|
|
expect(postureBadge(90).cls).toBe('posture-good');
|
|
expect(postureBadge(50).cls).toBe('posture-warn');
|
|
expect(postureBadge(10).cls).toBe('posture-bad');
|
|
});
|
|
|
|
it('patchLabel marks stale patches beyond 30 days', () => {
|
|
expect(patchLabel(10)?.cls).toBe('patch-ok');
|
|
expect(patchLabel(45)?.cls).toBe('patch-stale');
|
|
expect(patchLabel(undefined)).toBeNull();
|
|
});
|
|
|
|
it('portsBadge flags high listener counts', () => {
|
|
expect(portsBadge(5)?.cls).toBe('ports-ok');
|
|
expect(portsBadge(25)?.cls).toBe('ports-many');
|
|
});
|
|
|
|
it('pendingBadge encodes update counts', () => {
|
|
expect(pendingBadge(mockAgent({ pending_updates: 0 }))?.label).toBe('UP TO DATE');
|
|
expect(pendingBadge(mockAgent({ pending_updates: 3 }))?.cls).toBe('upd-warn');
|
|
expect(pendingBadge(mockAgent({ pending_updates: 12 }))?.cls).toBe('upd-bad');
|
|
expect(pendingBadge(mockAgent({ pending_updates: -1 }))?.label).toBe('UPD ?');
|
|
});
|
|
|
|
it('thermalBadge shows hot and warm thresholds', () => {
|
|
expect(thermalBadge(mockAgent({ cpu_temp_c: 60 }))).toBeNull();
|
|
expect(thermalBadge(mockAgent({ cpu_temp_c: 70 }))?.cls).toBe('therm-warm');
|
|
expect(thermalBadge(mockAgent({ gpu_temp_c: 85 }))?.cls).toBe('therm-hot');
|
|
});
|
|
|
|
it('contingencyDepthBadge shows ONION depth when present', () => {
|
|
expect(contingencyDepthBadge(mockAgent({ contingency_depth: 0 }))).toBeNull();
|
|
expect(contingencyDepthBadge(mockAgent({ contingency_depth: 3 }))?.label).toBe('ONION 3');
|
|
expect(contingencyDepthBadge(mockAgent({ contingency_depth: 10 }))?.cls).toBe('cn-contingency-deep');
|
|
});
|
|
|
|
it('postureTooltip includes defender, DNS drift, and services', () => {
|
|
const agent = mockAgent({
|
|
defender_enabled: true,
|
|
defender_rtp: false,
|
|
dns_servers: ['8.8.8.8'],
|
|
dns_drifted: true,
|
|
services: [{ name: 'sshd', display_name: 'OpenSSH', status: 'running', start_type: 'auto' }],
|
|
});
|
|
const tip = postureTooltip(agent);
|
|
expect(tip).toContain('Defender');
|
|
expect(tip).toContain('DNS changed since last heartbeat');
|
|
expect(tip).toContain('OpenSSH');
|
|
});
|
|
});
|