Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Direct-navigate Calibrate, harden stub agent timing, and add Emberwake/Seer/Oath smokes so the full 32-spec E2E sweep passes reliably.
169 lines
6.6 KiB
TypeScript
169 lines
6.6 KiB
TypeScript
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({ timeout: 15_000 });
|
|
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<string, unknown>;
|
|
const server = (body.server as Record<string, unknown> | 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<string, unknown>;
|
|
const server = (body.server as Record<string, unknown> | 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: 25_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();
|
|
});
|
|
});
|