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: [], }; 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), }); }); // /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.getByText('Task table')).toBeVisible(); await page.goto('/hermes/tasks/task-1'); await expect(page.getByRole('heading', { name: 'Hermes learning' })).toBeVisible(); await expect(page.getByText('Timeline')).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' })).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(); }); });