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

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:
AetherForge
2026-06-07 02:38:27 -07:00
parent 4ce9826660
commit 4b94776432
18 changed files with 579 additions and 80 deletions

View 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);
});
});
});