Closes the Phase 5 P2 checkbox (second half — first half: pino logging
in 1e64d75). Phase 5 is now fully green.
Two changes:
1. `web/e2e/hermes.spec.ts` now intercepts `/api/hermes/ops` with a
fixture snapshot. The backend's hermes-ops endpoint shells out to
`systemctl` / `git` / `ps` / `du` on the live VM and is therefore
neither available nor deterministic in CI. Mocking it lets the
suite run against the web stack alone (no backend, no live VM).
Fixture shape mirrors the Zod schema in
`backend/src/modules/hermes-ops/types.ts`.
2. `.gitea/workflows/ci.yml` re-enables the previously-commented-out
E2E step. Adds a preceding `playwright install --with-deps
chromium` step so the runner pulls the browser fresh per run.
The web suite starts its own Next dev server via Playwright's
`webServer` config (`pnpm exec next dev -p 3200`), so we do NOT
start the backend in CI — every backend route used by the suite
is mocked via `page.route` (auth, csrf, services, deployments,
health/cache, seed, hermes-ops).
Verified locally: `pnpm exec playwright test` → 6 passed in 19.5s
(2 hermes specs + 4 dashboard/login specs across desktop + mobile).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
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 <HermesOpsPanel>, 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),
|
|
});
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|