import { expect, test } from '@playwright/test'; import { loginToDashboard } from './fixtures'; import { ensureLiveStubAgent, isLiveStubReady } from './live-stub'; import { E2E_STUB_AGENT_HOSTNAME, E2E_STUB_AGENT_ID } from './stub-agent'; /** Display labels for DEFAULT_LOTL_ONION_TIERS (14-tier spread chain). */ const LOTL_ONION_TIER_LABELS = [ 'Vuln Recon', 'Docker', 'WSL', 'PowerShell', 'dotnet', 'bits/curl', 'do_peer', 'wsus_cache_peer', 'dns_txt', 'webrtc_mesh', 'SMB', 'WinRM', 'Linux', 'GPO', ] as const; test.describe('LOTL Timeline E2E', () => { test.beforeAll(async ({ request }) => { await ensureLiveStubAgent(request); }); test.beforeEach(async ({ page }) => { test.skip( !isLiveStubReady(), 'Requires live E2E server (test-suite phase 8 on :18989 or AETHERFORGE_URL)', ); await loginToDashboard(page); }); test('renders 14-tier onion chain, fleet overview, and stub agent progression', async ({ page }) => { await page.goto(`/lotl-timeline?agent=${encodeURIComponent(E2E_STUB_AGENT_ID)}`); await expect(page.getByRole('heading', { name: 'LOTL Timeline' })).toBeVisible({ timeout: 10_000, }); await expect(page.getByText('FLEET ONION PROGRESS')).toBeVisible(); await expect( page.locator('.lotl-fleet-chip').filter({ hasText: E2E_STUB_AGENT_HOSTNAME }), ).toBeVisible({ timeout: 15_000 }); await expect(page.locator('.lotl-fleet-chip--selected').filter({ hasText: E2E_STUB_AGENT_HOSTNAME })).toBeVisible(); await expect(page.getByText('ONION TIER CHAIN')).toBeVisible(); await expect(page.locator('.lotl-tier-timeline')).toBeVisible(); const steps = page.locator('.lotl-tier-step'); await expect(steps).toHaveCount(14); for (const label of LOTL_ONION_TIER_LABELS) { await expect(page.locator('.lotl-tier-label', { hasText: label })).toBeVisible(); } // Stub lotl_attempts: container (docker alias) failed — spread onion timeline. await expect(page.locator('.lotl-tier-step--failed', { hasText: /Docker/i })).toBeVisible({ timeout: 15_000, }); }); test('navigates from sidebar Onion link to /lotl-timeline', async ({ page }) => { await page.getByRole('navigation').getByRole('link', { name: 'Onion', exact: true }).click(); await expect(page).toHaveURL(/\/lotl-timeline/); await expect(page.getByRole('heading', { name: 'LOTL Timeline' })).toBeVisible({ timeout: 10_000, }); }); test('shows AI decision panel when ai_control_enabled and decisions are mocked', async ({ page }) => { await page.route('**/api/v1/config', async (route) => { const res = await route.fetch(); const body = (await res.json()) as Record; const server = (body.server as Record | undefined) ?? {}; await route.fulfill({ json: { ...body, server: { ...server, ai_control_enabled: true } }, }); }); await page.route(`**/api/v1/ai/decisions?agent_id=${E2E_STUB_AGENT_ID}*`, async (route) => { await route.fulfill({ json: [ { id: 42, agent_id: E2E_STUB_AGENT_ID, response: 'restart mining after docker failure', commands_executed: 'restart_mining:ok', ts: '2026-06-07T12:00:00Z', }, ], }); }); await page.goto(`/lotl-timeline?agent=${encodeURIComponent(E2E_STUB_AGENT_ID)}`); await expect( page.locator('.lotl-fleet-chip').filter({ hasText: E2E_STUB_AGENT_HOSTNAME }), ).toBeVisible({ timeout: 15_000 }); await expect(page.getByText('LAST AI DECISION')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText('restart mining after docker failure')).toBeVisible(); await expect(page.getByText('restart_mining:ok')).toBeVisible(); }); test('shows Singular Machine Court panel when court session is mocked', async ({ page }) => { await page.route('**/api/v1/config', async (route) => { const res = await route.fetch(); const body = (await res.json()) as Record; const server = (body.server as Record | undefined) ?? {}; await route.fulfill({ json: { ...body, server: { ...server, ai_control_enabled: true } }, }); }); await page.route(`**/api/v1/ai/decisions?agent_id=${E2E_STUB_AGENT_ID}*`, async (route) => { await route.fulfill({ json: [ { id: 43, agent_id: E2E_STUB_AGENT_ID, 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', }, ], }); }); await page.goto(`/lotl-timeline?agent=${encodeURIComponent(E2E_STUB_AGENT_ID)}`); await expect( page.locator('.lotl-fleet-chip').filter({ hasText: E2E_STUB_AGENT_HOSTNAME }), ).toBeVisible({ timeout: 15_000 }); await expect(page.getByText('SINGULAR MACHINE COURT')).toBeVisible({ timeout: 10_000 }); await expect(page.getByText('Prosecutor')).toBeVisible(); await expect(page.getByText('Defender')).toBeVisible(); await expect(page.getByText('Judge')).toBeVisible(); await expect(page.getByText(/Failure atlas: docker 8\/8 failed/i)).toBeVisible(); await expect(page.getByText(/Fleet phenotype from worker-07/i)).toBeVisible(); }); test('shows clearance history and events panels when mocked', async ({ page }) => { await page.route(`**/api/v1/ai/clearance-events?agent_id=${E2E_STUB_AGENT_ID}*`, async (route) => { await route.fulfill({ json: [ { id: 7, agent_id: E2E_STUB_AGENT_ID, from_level: 1, to_level: 2, reason: 'spread lane needed', source: 'ai_scheduler', ts: '2026-06-07T11:00:00Z', }, ], }); }); await page.goto(`/lotl-timeline?agent=${encodeURIComponent(E2E_STUB_AGENT_ID)}`); await expect( page.locator('.lotl-fleet-chip').filter({ hasText: E2E_STUB_AGENT_HOSTNAME }), ).toBeVisible({ timeout: 15_000 }); await expect(page.getByText('CLEARANCE HISTORY')).toBeVisible({ timeout: 10_000 }); await expect(page.locator('.lotl-clearance-list').getByText(/AI: L1 → L2/i)).toBeVisible(); await expect(page.getByText('CLEARANCE EVENTS')).toBeVisible(); await expect(page.locator('.lotl-clearance-event-list').getByText(/spread lane needed/i)).toBeVisible(); }); });