Fix Fleet AI and LOTL test regressions after parallel merges.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Stub slow syscheck/listen-port probes in agent tests, fix ai_snapshot mutex deadlock, reorder fleet clearance vs connectivity checks, and add AI control precedence plus LotlTimeline vitest coverage.
This commit is contained in:
196
server/web/src/pages/LotlTimelinePage.test.tsx
Normal file
196
server/web/src/pages/LotlTimelinePage.test.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @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, Route, Routes } from 'react-router-dom';
|
||||
import LotlTimelinePage from './LotlTimelinePage';
|
||||
import { mockAgent, mockServerConfig } from '../test/fixtures';
|
||||
import { routerFuture } from '../routerFuture';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { api } from '../api/client';
|
||||
|
||||
vi.mock('../hooks/useWebSocket', () => ({
|
||||
useWebSocket: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../components/HelpTip', () => ({
|
||||
HelpTip: () => null,
|
||||
}));
|
||||
|
||||
const useWebSocketMock = vi.mocked(useWebSocket);
|
||||
|
||||
const agentA = mockAgent({
|
||||
id: 'agent-a',
|
||||
name: 'Alpha Node',
|
||||
status: 'online',
|
||||
lotl_tier: 'wsl',
|
||||
lotl_attempts: [
|
||||
{ tier: 'vuln_recon', ok: true, duration_ms: 400, phase: 'recon' },
|
||||
{ tier: 'docker', ok: false, error: 'no daemon', duration_ms: 900, phase: 'deploy' },
|
||||
],
|
||||
});
|
||||
|
||||
function renderPage(initial = '/lotl-timeline') {
|
||||
return render(
|
||||
<MemoryRouter future={routerFuture} initialEntries={[initial]}>
|
||||
<Routes>
|
||||
<Route path="/lotl-timeline" element={<LotlTimelinePage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('LotlTimelinePage', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(api, 'getConfig').mockResolvedValue(mockServerConfig());
|
||||
vi.spyOn(api, 'getAIDecisions').mockResolvedValue([]);
|
||||
vi.spyOn(api, 'getClearanceEvents').mockResolvedValue([]);
|
||||
useWebSocketMock.mockReturnValue({
|
||||
isConnected: true,
|
||||
agents: [agentA],
|
||||
recentShares: [],
|
||||
fleetAlerts: [],
|
||||
poolStatus: [],
|
||||
aiActivity: [],
|
||||
agentLogs: {},
|
||||
commandResults: [],
|
||||
latestMessage: null,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders LOTL Timeline heading and tier chain for online agent', async () => {
|
||||
renderPage();
|
||||
expect(await screen.findByRole('heading', { name: /LOTL Timeline/i })).toBeInTheDocument();
|
||||
expect(screen.getByText('ONION TIER CHAIN')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Alpha Node').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('Vuln Recon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when fleet has no agents', async () => {
|
||||
useWebSocketMock.mockReturnValue({
|
||||
isConnected: true,
|
||||
agents: [],
|
||||
recentShares: [],
|
||||
fleetAlerts: [],
|
||||
poolStatus: [],
|
||||
aiActivity: [],
|
||||
agentLogs: {},
|
||||
commandResults: [],
|
||||
latestMessage: null,
|
||||
});
|
||||
renderPage();
|
||||
expect(
|
||||
await screen.findByText(/No agents in fleet yet/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('loads AI decision panel when ai_control_enabled is true', async () => {
|
||||
vi.mocked(api.getConfig).mockResolvedValue(
|
||||
mockServerConfig({ server: { ai_control_enabled: true } }),
|
||||
);
|
||||
vi.mocked(api.getAIDecisions).mockResolvedValue([
|
||||
{
|
||||
id: 1,
|
||||
agent_id: 'agent-a',
|
||||
response: 'restart mining after docker failure',
|
||||
commands_executed: 'restart_mining:ok',
|
||||
ts: '2026-06-07T12:00:00Z',
|
||||
},
|
||||
]);
|
||||
renderPage();
|
||||
expect(await screen.findByText('LAST AI DECISION')).toBeInTheDocument();
|
||||
expect(screen.getByText('restart mining after docker failure')).toBeInTheDocument();
|
||||
expect(screen.getByText('restart_mining:ok')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('loads court session decision with prosecutor/defender/judge roles', async () => {
|
||||
vi.mocked(api.getConfig).mockResolvedValue(
|
||||
mockServerConfig({ server: { ai_control_enabled: true } }),
|
||||
);
|
||||
vi.mocked(api.getAIDecisions).mockResolvedValue([
|
||||
{
|
||||
id: 2,
|
||||
agent_id: 'agent-a',
|
||||
response: 'Verdict: restart mining after tier exhaustion.',
|
||||
commands_executed: 'restart_mining:ok',
|
||||
court_session: true,
|
||||
prosecutor_snippet: 'Failure atlas: docker 8/8 failed',
|
||||
defender_snippet: 'Fleet phenotype from worker-07',
|
||||
judge_verdict: 'restart mining after tier exhaustion.',
|
||||
ts: '2026-06-07T12:05:00Z',
|
||||
},
|
||||
]);
|
||||
renderPage();
|
||||
expect(await screen.findByText('SINGULAR MACHINE COURT')).toBeInTheDocument();
|
||||
expect(screen.getByText('Prosecutor')).toBeInTheDocument();
|
||||
expect(screen.getByText('Defender')).toBeInTheDocument();
|
||||
expect(screen.getByText('Judge')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Failure atlas: docker 8\/8 failed/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Fleet phenotype from worker-07/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/restart mining after tier exhaustion/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selects agent from ?agent= query param', async () => {
|
||||
const agentB = mockAgent({ id: 'agent-b', name: 'Beta Node', status: 'online' });
|
||||
useWebSocketMock.mockReturnValue({
|
||||
isConnected: true,
|
||||
agents: [agentA, agentB],
|
||||
recentShares: [],
|
||||
fleetAlerts: [],
|
||||
poolStatus: [],
|
||||
aiActivity: [],
|
||||
agentLogs: {},
|
||||
commandResults: [],
|
||||
latestMessage: null,
|
||||
});
|
||||
renderPage('/lotl-timeline?agent=agent-b');
|
||||
const betaNodes = await screen.findAllByText('Beta Node');
|
||||
expect(betaNodes.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('renders clearance history when events exist', async () => {
|
||||
vi.mocked(api.getClearanceEvents).mockResolvedValue([
|
||||
{
|
||||
id: 7,
|
||||
agent_id: 'agent-a',
|
||||
from_level: 1,
|
||||
to_level: 2,
|
||||
reason: 'spread lane needed',
|
||||
source: 'ai_scheduler',
|
||||
ts: '2026-06-07T11:00:00Z',
|
||||
},
|
||||
]);
|
||||
renderPage();
|
||||
expect(await screen.findByText('CLEARANCE HISTORY')).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/AI: L1 → L2/i).length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('switches selected agent from fleet overview', async () => {
|
||||
const agentB = mockAgent({ id: 'agent-b', name: 'Beta Node', status: 'online' });
|
||||
useWebSocketMock.mockReturnValue({
|
||||
isConnected: true,
|
||||
agents: [agentA, agentB],
|
||||
recentShares: [],
|
||||
fleetAlerts: [],
|
||||
poolStatus: [],
|
||||
aiActivity: [],
|
||||
agentLogs: {},
|
||||
commandResults: [],
|
||||
latestMessage: null,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderPage();
|
||||
await screen.findAllByText('Alpha Node');
|
||||
await user.click(screen.getByRole('button', { name: /Beta Node/i }));
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Beta Node').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user