/** * E2E tests for the L1 Workspace surface (Phase 1). * * Covers: * 1. L1 user lands on /l1 after login and can start an ad-hoc walk, take * notes (autosave), and resolve the session. * 2. L1 user cannot access /pilot, /trees/new, or /escalations — route * guards bounce them back to /. * 3. Engineer with can_cover_l1=true sees the "L1 Workspace" nav entry and * the "You're covering L1" banner. * 4. escalate-without-walk API endpoint returns an escalated adhoc session * when called from an authenticated L1 user. * * Seed users (added by seed_test_users.py): * l1@resolutionflow.example.com — account_role=l1_tech * engineer-coverage@resolutionflow.example.com — engineer + can_cover_l1 */ import { test, expect, type Page } from '@playwright/test' // These tests always log in fresh — no shared storageState from auth.setup.ts. test.use({ storageState: { cookies: [], origins: [] } }) const L1_EMAIL = 'l1@resolutionflow.example.com' const COVERAGE_EMAIL = 'engineer-coverage@resolutionflow.example.com' const PASSWORD = 'TestPass123!' const apiOrigin = process.env.PLAYWRIGHT_API_ORIGIN || 'http://127.0.0.1:8000' /** * Log in via the login form using exact test-IDs / labels that LoginPage uses. * Uses data-testid="login-form", getByLabel('Email address'), getByLabel('Password'), * and data-testid="login-submit" — matching the actual LoginPage.tsx markup. */ async function login(page: Page, email: string): Promise { await page.goto('/login') await expect(page.getByTestId('login-form')).toBeVisible() await page.getByLabel('Email address').fill(email) await page.getByLabel('Password').fill(PASSWORD) await page.getByTestId('login-submit').click() } /** * Obtain a bearer token for the given email via the JSON login endpoint. * Used for direct API assertions without going through the browser. */ async function getToken( page: Page, email: string, ): Promise { const response = await page.request.post(`${apiOrigin}/api/v1/auth/login/json`, { data: { email, password: PASSWORD }, }) expect(response.ok()).toBeTruthy() const body = (await response.json()) as { access_token: string } return body.access_token } test.describe('L1 Workspace', () => { // ------------------------------------------------------------------------- // Test 1: Happy path — login → /l1 → start walk → notes → resolve // ------------------------------------------------------------------------- test('L1 user lands on /l1 after login and can intake, take notes, and resolve', async ({ page }) => { await login(page, L1_EMAIL) // ProtectedRoute redirects l1_tech from / → /l1 await expect(page).toHaveURL(/\/l1$/, { timeout: 10_000 }) // Greeting heading: "Good morning|afternoon|evening, ." await expect( page.getByRole('heading', { name: /Good (morning|afternoon|evening)/i }), ).toBeVisible() // Fill in problem statement textarea. The problem must NOT keyword-match // any DEFAULT_L1_CATEGORIES token (Phase 2A routes in-category problems to // an AI-build walk, not ad-hoc) — a custom LOB app is out of scope. const problemTextarea = page.getByPlaceholder("What's the user calling about?") await expect(problemTextarea).toBeVisible() await problemTextarea.fill('Custom LOB billing app crashes on launch for one user') // Click "Start walk →" button await page.getByRole('button', { name: /Start walk/i }).click() // Out-of-scope prompt offers the free-form fallback — take it await page.getByRole('button', { name: /Walk it ad-hoc/i }).click() // Should navigate to /l1/walk/ await expect(page).toHaveURL(/\/l1\/walk\//, { timeout: 10_000 }) // The header badge shows "Ad-hoc walk" await expect(page.getByText('Ad-hoc walk')).toBeVisible() // Take notes in the walk textarea const notesTextarea = page.getByPlaceholder( 'What did the customer say? What did you check? What did you try?', ) await expect(notesTextarea).toBeVisible() await notesTextarea.fill('Walked customer through closing and reopening Outlook — issue resolved') // Autosave fires after 300ms debounce; wait up to 5s for the "Saved Xs ago" indicator await expect( page.getByText(/Saved \d+s ago|Saving…/i), ).toBeVisible({ timeout: 5_000 }) // Open the Resolve modal await page.getByRole('button', { name: /Resolve/i }).click() // Modal heading: "Did this resolve it?" await expect( page.getByRole('heading', { name: 'Did this resolve it?' }), ).toBeVisible() // Click "Yes" await page.getByRole('button', { name: 'Yes' }).click() // Fill resolution notes await page.getByPlaceholder('Resolution notes…').fill('Fixed via restarting Outlook') // Confirm await page.getByRole('button', { name: 'Confirm' }).click() // After resolution, onDone() navigates back to /l1 await expect(page).toHaveURL(/\/l1$/, { timeout: 10_000 }) }) // ------------------------------------------------------------------------- // Test 2: Route guard — L1 user cannot access engineer-only routes // ------------------------------------------------------------------------- test('L1 user cannot access /pilot, /trees/new, or /escalations', async ({ page }) => { await login(page, L1_EMAIL) await expect(page).toHaveURL(/\/l1$/, { timeout: 10_000 }) // /pilot — ProtectedRoute requires at least engineer rank; l1_tech gets bounced await page.goto('/pilot') await expect(page).not.toHaveURL(/\/pilot/, { timeout: 5_000 }) // /trees/new — same guard await page.goto('/trees/new') await expect(page).not.toHaveURL(/\/trees\/new/, { timeout: 5_000 }) // /escalations — if this route exists with a role guard it should bounce too await page.goto('/escalations') await expect(page).not.toHaveURL(/\/escalations/, { timeout: 5_000 }) }) // ------------------------------------------------------------------------- // Test 3: Coverage engineer sees the L1 nav link and the coverage banner // ------------------------------------------------------------------------- test('Engineer with can_cover_l1 sees the L1 Workspace nav and coverage banner', async ({ page }) => { await login(page, COVERAGE_EMAIL) // Coverage engineer is not l1_tech — they land on the normal workspace root await expect(page.getByTestId('app-shell')).toBeVisible({ timeout: 10_000 }) // Sidebar should show "L1 Workspace" link const l1NavLink = page.getByRole('link', { name: /L1 Workspace/i }) await expect(l1NavLink).toBeVisible({ timeout: 10_000 }) // Navigate to /l1 await l1NavLink.click() await expect(page).toHaveURL(/\/l1/, { timeout: 10_000 }) // L1CoverageBanner renders: "You're covering L1. Actions logged as coverage." await expect( page.getByText(/You're covering L1/i), ).toBeVisible({ timeout: 5_000 }) }) // ------------------------------------------------------------------------- // Test 4: escalate-without-walk endpoint — direct API assertion // ------------------------------------------------------------------------- test('escalate-without-walk returns an escalated adhoc session', async ({ page }) => { const token = await getToken(page, L1_EMAIL) const response = await page.request.post( `${apiOrigin}/api/v1/l1/escalate-without-walk`, { data: { problem_statement: 'Customer issue with no KB content available', reason_category: 'No KB available', }, headers: { Authorization: `Bearer ${token}` }, }, ) expect(response.status()).toBe(200) const body = (await response.json()) as { status: string session_kind: string } expect(body.status).toBe('escalated') expect(body.session_kind).toBe('adhoc') }) })