import { test, expect } from '@playwright/test'; const adminUser = { id: 'user-1', email: 'admin@example.test', role: 'admin', plan: 'internal', displayName: 'Dashboard Admin', emailVerified: true, currentProduct: 'bytelyst-devops', products: [{ productId: 'bytelyst-devops', plan: 'internal', role: 'admin' }], mfaEnabled: false, mfaMethods: [], }; // /hermes mounts , which calls api.getHermesOps() against the // backend's `/api/hermes/ops` endpoint. The backend shells out to systemctl / // git / ps / du on the live VM and is therefore neither available nor // deterministic in CI. We intercept the fetch with a fixture snapshot so the // E2E suite can run against the web stack alone. // Shape mirrors `HermesOpsSnapshot` in `web/src/lib/api.ts` (which mirrors the // backend Zod schema in `backend/src/modules/hermes-ops/types.ts`). Empty // `quickLinks`/`instances` arrays are deliberate — the panel is only required // to render without throwing in CI; the mission-control overview is what the // suite actually asserts on. const hermesOpsSnapshot = { generatedAt: '2026-01-01T00:00:00.000Z', tailscaleIp: '100.0.0.1', emergencyDriveUpload: { name: 'hermes-emergency-drive-upload.timer', active: false, nextRun: null, lastRun: null, }, activeSessions: { active: 0, updatedAt: '2026-01-01T00:00:00.000Z' }, cronJobs: [], recentAlerts: [], quickLinks: [], instances: [], warnings: [], }; const hermesTelemetrySnapshot = (instanceId: 'vijay' | 'bheem') => ({ generatedAt: '2026-01-01T00:00:00.000Z', cached: false, instanceId, sessions: { totalSessions: instanceId === 'vijay' ? 12 : 7, totalMessages: instanceId === 'vijay' ? 480 : 210, status: 'up' }, sessionList: { status: 'up', entries: [ { id: `${instanceId}-session-1`, sessionKey: `agent:main:telegram:dm:${instanceId}`, platform: 'telegram', chatType: 'dm', displayName: instanceId === 'vijay' ? 'S' : 'Uma', createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:06:00.000Z', suspended: false, resumePending: false, totalTokens: 100, estimatedCostUsd: 0, }, ], }, sessionEvents: { status: 'up', sourceCount: 1, entries: [ { id: `${instanceId}-events.jsonl:3`, sessionFile: `${instanceId}-events.jsonl`, timestamp: '2026-01-01T00:06:00.000Z', role: 'assistant', eventType: 'tool-call', summary: 'assistant tool call: exec_command', toolNames: ['exec_command'], itemTypes: [], status: 'tool_calls', }, ], }, cron: { status: 'up', entries: [ { id: `${instanceId}-digest`, name: `${instanceId} digest`, schedule: '0 * * * *', lastRun: '2026-01-01T00:00:00.000Z', nextRun: '2026-01-01T01:00:00.000Z', lastStatus: 'ok', active: true, }, ], }, memory: { status: 'up', items: [] }, skills: { status: 'up', items: [] }, watchdog: { source: `/tmp/${instanceId}-watchdog.log`, status: 'up', alerts: [ { timestamp: '2026-01-01T00:05:00.000Z', severity: 'info', message: `${instanceId} watchdog healthy`, }, ], }, backupHistory: { repoPath: `/tmp/${instanceId}-repo`, status: 'up', entries: [ { sha: `${instanceId}123456`, committedAt: '2026-01-01T00:03:00.000Z', subject: `${instanceId} backup`, }, ], }, warnings: [], }); test.describe('Hermes Mission Control', () => { test.beforeEach(async ({ page }) => { await page.addInitScript(() => { window.localStorage.setItem('access_token', 'e2e-access-token'); window.localStorage.setItem('refresh_token', 'e2e-refresh-token'); }); await page.route('**/auth/me', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(adminUser) }); }); await page.route('**/api/hermes/ops', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(hermesOpsSnapshot), }); }); await page.route('**/api/hermes/telemetry/vijay', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(hermesTelemetrySnapshot('vijay')), }); }); await page.route('**/api/hermes/telemetry/bheem', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(hermesTelemetrySnapshot('bheem')), }); }); // /hermes/products fetches the real service registry + health module // (Phase 3 slice 2). Backend isn't running in CI, so we satisfy those // routes the same way the dashboard spec does. await page.route('**/api/services', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]) }); }); await page.route('**/api/health', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([]) }); }); await page.route('**/api/health/cache', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ message: 'Cache cleared' }) }); }); }); test('renders the mission control overview and navigates to companion views', async ({ page }) => { await page.goto('/hermes'); await expect(page.getByRole('heading', { name: 'Hermes Mission Control' })).toBeVisible(); await expect(page.getByText('Active Missions')).toBeVisible(); await expect(page.getByText('Founder Attention Queue')).toBeVisible(); await expect(page.getByRole('heading', { name: 'Product Health Snapshot' })).toBeVisible(); await page.getByRole('link', { name: 'Task Ledger' }).click(); await expect(page.getByRole('heading', { name: 'Task Ledger' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Task table' })).toBeVisible(); await page.goto('/hermes/tasks/task-1'); await expect(page.getByRole('heading', { name: 'Hermes learning' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Timeline', exact: true })).toBeVisible(); await page.goto('/hermes/products'); await expect(page.getByRole('heading', { name: 'Product Portfolio' })).toBeVisible(); await page.goto('/hermes/history'); await expect(page.getByRole('heading', { name: 'Historical Activity' })).toBeVisible(); await page.goto('/hermes/agents'); await expect(page.getByRole('heading', { name: 'Agent & Tool Observability' })).toBeVisible(); await page.goto('/hermes/settings'); await expect(page.getByRole('heading', { name: 'Settings & Configuration' })).toBeVisible(); }); test('renders the mission control overview at mobile width', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/hermes'); await expect(page.getByRole('heading', { name: 'Hermes Mission Control' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Task Ledger' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Product Portfolio' })).toBeVisible(); await page.goto('/hermes/tasks/task-1'); await expect(page.getByRole('heading', { name: 'Hermes learning' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Timeline', exact: true })).toBeVisible(); }); test('exposes a global instance switcher with All / Vijay / Bheem', async ({ page }) => { await page.goto('/hermes'); const switcher = page.getByRole('radiogroup', { name: 'Hermes instance filter' }); await expect(switcher).toBeVisible(); // Default selection persists across navigation; "All" should be the // active option on first visit (no localStorage entry yet). await expect(switcher.getByRole('radio', { name: 'All' })).toHaveAttribute('aria-checked', 'true'); // Per-instance roll-up section should always be visible regardless of filter. await expect(page.getByRole('heading', { name: 'Per-instance roll-up' })).toBeVisible(); // Switching to Bheem should still keep the page rendering correctly. await switcher.getByRole('radio', { name: 'Bheem (uma)' }).click(); await expect(switcher.getByRole('radio', { name: 'Bheem (uma)' })).toHaveAttribute('aria-checked', 'true'); await expect(page.getByRole('heading', { name: 'Hermes Mission Control' })).toBeVisible(); }); });