import { test, expect, type Page, type Route } from '@playwright/test'; /** * E2E tests for the Fleet (Agent Gigafactory) control-plane dashboard. * * Deterministic: every backend call is mocked at the Next.js proxy boundary * (`/api/fleet/**`, `/api/auth/**`). No live platform-service is required. The * mock dispatcher is method- and URL-aware and holds a little mutable state so * operator actions (requeue) and budget pause/resume reflect in follow-up GETs. */ const PRODUCT = 'tracker-e2e'; const ISO = '2025-01-01T00:00:00.000Z'; type Job = { id: string; productId: string; stage: string; idempotencyKey: string; bodyMd: string; priority: string; priorityOrder: number; capabilities: string[]; kind: string; attempts: number; leaseEpoch: number; createdAt: string; updatedAt: string; }; function makeJob(partial: Partial & { id: string; idempotencyKey: string }): Job { return { productId: PRODUCT, stage: 'building', bodyMd: '# task', priority: 'high', priorityOrder: 1, capabilities: ['build'], kind: 'task', attempts: 1, leaseEpoch: 2, createdAt: ISO, updatedAt: ISO, ...partial, }; } const FACTORY = { id: 'f1', productId: PRODUCT, factoryId: 'factory-alpha', capabilities: ['build', 'test'], health: 'ok' as const, load: 1, seatLimit: 4, lastHeartbeatAt: ISO, }; /** Authenticate the dashboard: seed the token + selected product, mock /me. */ async function authenticate(page: Page): Promise { await page.addInitScript( ([product]) => { localStorage.setItem('tracker_token', 'fake-e2e-token'); localStorage.setItem('tracker_selected_product', product); }, [PRODUCT] ); await page.route('**/api/auth/me', (route: Route) => route.fulfill({ json: { id: 'u1', email: 'admin@example.com', role: 'admin', displayName: 'Admin' }, }) ); } /** * Install the fleet API mock. Returns the mutable state so a test can inspect or * assert on the latest values the dispatcher served. */ async function mockFleet( page: Page, opts?: { jobStage?: string; budgetStatus?: 'active' | 'paused' } ): Promise<{ state: { jobStage: string; budgetStatus: 'active' | 'paused' } }> { const state = { jobStage: opts?.jobStage ?? 'building', budgetStatus: opts?.budgetStatus ?? ('active' as 'active' | 'paused'), }; const budget = () => ({ id: PRODUCT, productId: PRODUCT, ceilingUsd: 100, window: 'monthly', spentUsd: 25, status: state.budgetStatus, updatedAt: ISO, }); await page.route('**/api/fleet/**', (route: Route) => { const req = route.request(); const url = new URL(req.url()); const path = url.pathname; // e.g. /api/fleet/jobs/job-1/events const method = req.method(); // ── Live event stream (SSE) ── if (path.includes('/events/stream')) { const evt = { id: 'job-1:evt:0', jobId: 'job-1', seq: 0, type: 'submitted', at: ISO, data: {}, }; return route.fulfill({ status: 200, headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' }, body: `retry: 3000\n\nid: 0\nevent: fleet-event\ndata: ${JSON.stringify(evt)}\n\n`, }); } if (path.endsWith('/events')) { return route.fulfill({ json: { events: [ { id: 'job-1:evt:0', jobId: 'job-1', seq: 0, type: 'submitted', at: ISO, data: {} }, ], }, }); } if (path.endsWith('/runs')) return route.fulfill({ json: { runs: [] } }); if (path.endsWith('/artifacts')) return route.fulfill({ json: { artifacts: [] } }); // DAG + explain: degrade to 404 so the page renders without those panels. // Body omits `error` so the client throws "HTTP 404" (apiFetchOptional → null). if (path.endsWith('/dag') || path.endsWith('/explain')) { return route.fulfill({ status: 404, json: {} }); } // ── Operator actions (requeue / cancel / reject) ── const actionMatch = path.match(/\/jobs\/[^/]+\/actions\/(\w+)$/); if (actionMatch && method === 'POST') { const action = actionMatch[1]; state.jobStage = action === 'requeue' ? 'queued' : action === 'reject' ? 'dead_letter' : 'failed'; return route.fulfill({ json: makeJob({ id: 'job-1', idempotencyKey: 'feat-x', stage: state.jobStage }), }); } // ── Budgets ── if (path.endsWith('/burndown')) return route.fulfill({ status: 404, json: {} }); if (path.endsWith('/pause') && method === 'POST') { state.budgetStatus = 'paused'; return route.fulfill({ json: budget() }); } if (path.endsWith('/resume') && method === 'POST') { state.budgetStatus = 'active'; return route.fulfill({ json: budget() }); } if (path.match(/\/budgets\/[^/]+$/)) return route.fulfill({ json: budget() }); // ── Jobs ── if (path.match(/\/jobs\/[^/]+$/) && method === 'GET') { return route.fulfill({ json: makeJob({ id: 'job-1', idempotencyKey: 'feat-x', stage: state.jobStage }), }); } if (path.endsWith('/jobs') && method === 'GET') { return route.fulfill({ json: { jobs: [makeJob({ id: 'job-1', idempotencyKey: 'feat-x', stage: state.jobStage })] }, }); } if (path.endsWith('/factories')) return route.fulfill({ json: { factories: [FACTORY] } }); // ── Fleet metrics ── if (path.endsWith('/metrics')) { return route.fulfill({ json: { productId: 'lysnrai', generatedAt: ISO, jobs: { total: 1, byStage: { queued: 1 }, queueDepth: 1, blocked: 0, active: 0, oldestQueuedAgeMs: 1000, }, factories: { total: 1, live: 1, stale: 0, byHealth: { ok: 1, degraded: 0, down: 0 }, seatsUsed: 1, seatsTotal: 4, utilizationPct: 25, }, alerts: [ { level: 'warning', code: 'queue_starvation', message: 'A job has waited too long.' }, ], }, }); } return route.fulfill({ json: {} }); }); return { state }; } test.describe('Fleet — Overview', () => { test('renders factory cards and the recent-jobs table', async ({ page }) => { await authenticate(page); await mockFleet(page); await page.goto('/dashboard/fleet'); await expect(page.getByRole('heading', { name: 'Fleet Control Plane' })).toBeVisible(); await expect(page.getByTestId('fleet-metrics')).toBeVisible(); await expect(page.getByTestId('fleet-alerts')).toBeVisible(); await expect(page.getByLabel('Factory factory-alpha')).toBeVisible(); await expect(page.getByRole('table', { name: 'Recent fleet jobs' })).toBeVisible(); await expect(page.getByRole('link', { name: 'feat-x' })).toBeVisible(); }); }); test.describe('Fleet — Jobs table', () => { test('lists jobs and exposes the stage filter', async ({ page }) => { await authenticate(page); await mockFleet(page); await page.goto('/dashboard/fleet/jobs'); await expect(page.getByRole('heading', { name: 'Fleet Jobs' })).toBeVisible(); await expect(page.getByRole('table', { name: 'Fleet jobs table' })).toBeVisible(); await expect(page.getByLabel('Filter by stage')).toBeVisible(); await expect(page.getByRole('link', { name: 'View job feat-x' })).toBeVisible(); }); }); test.describe('Fleet — Job detail', () => { test('requeues a job and reflects the new stage; shows the live badge', async ({ page }) => { await authenticate(page); await mockFleet(page, { jobStage: 'building' }); await page.goto('/dashboard/fleet/jobs/job-1'); await expect(page.getByRole('heading', { name: 'feat-x' })).toBeVisible(); // SSE snapshot delivers an event → the Live indicator should appear. await expect(page.getByTestId('live-indicator')).toBeVisible(); // Stage card starts at 'building'. await expect(page.getByText('building', { exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Requeue this job' }).click(); // After requeue the coordinator returns stage 'queued', mirrored on refresh. await expect(page.getByText('queued', { exact: true }).first()).toBeVisible(); }); }); test.describe('Fleet — Budget', () => { test('pauses and resumes the budget', async ({ page }) => { await authenticate(page); await mockFleet(page, { budgetStatus: 'active' }); await page.goto('/dashboard/fleet/budget'); await expect(page.getByRole('heading', { name: 'Fleet Budget' })).toBeVisible(); await expect(page.getByText('active', { exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Pause budget' }).click(); await expect(page.getByText('paused', { exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Resume budget' }).click(); await expect(page.getByText('active', { exact: true })).toBeVisible(); }); });