import { expect, test, type Page, type Route } from '@playwright/test' const observedAt = '2026-07-18T00:00:00Z' const result: MockResult = { kind: 'manifest', manifest_id: 'manifest_001', schema_version: 1, manifest: { store: 'target-main', bucket: 'fixture-bucket', key: 'manifest.json', checksum: `sha256:${'a'.repeat(64)}`, size: 12, }, member_count: 1, total_bytes: 12, } test.beforeEach(async ({ page }) => { await page.routeWebSocket(/\/v1\/events/, () => {}) }) test('completes credential, submission, result, history, and lifecycle workflows', async ({ page, }) => { const state = await mockApi(page) await authenticatedSession(page) await expect .poll(() => page.evaluate(() => sessionStorage.getItem( `oidc.user:${window.location.origin}/oidc:remora-web`, ), ), ) .not.toBeNull() await expect(page.getByRole('heading', { name: 'Remora' })).toBeVisible() await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible() await expect(page.getByLabel('Synchronization status')).toHaveText('live') await expect.poll(() => state.paths).toContain('jobs') await expect(page.getByText('Existing archive')).toBeVisible() await page.keyboard.press('Tab') await expect(page.getByRole('button', { name: 'Sign out' })).toBeFocused() await page.getByRole('button', { name: 'Credentials' }).click() await page.getByLabel('Label').fill('Archive token') const newToken = page.getByRole('textbox', { name: 'New token', exact: true, }) await newToken.fill('secret-token') await page.getByRole('button', { name: 'Create credential' }).click() await expect(newToken).toHaveValue('') await page.getByLabel('Replacement token').fill('replacement-token') await page.getByRole('button', { name: 'Rotate credential' }).click() await expect(page.getByLabel('Replacement token')).toHaveValue('') await page.getByRole('button', { name: 'Submit job' }).click() await page.getByLabel('Label').fill('Imported archive') await page .getByLabel('URL') .fill('https://example.invalid/archive?token=sensitive') await page.getByLabel('Result path').fill('archive.bin') await page.getByLabel('Source credential').selectOption('credential_001') await page.getByRole('button', { name: 'Submit job now' }).click() await expect(page.getByText('Job accepted')).toBeVisible() await expect.poll(() => state.submission?.label).toBe('Imported archive') await expect( page.getByRole('heading', { name: 'Imported archive' }), ).toBeVisible() expect(state.submission?.source.credential_id).toBe('credential_001') await page.getByRole('button', { name: 'Load attempt history' }).click() await expect(page.getByText('AttemptCommitted')).toBeVisible() await page.getByRole('button', { name: 'Browse result' }).click() await expect(page.getByText('archive.bin')).toBeVisible() const softDelete = page.getByRole('button', { name: 'Soft delete' }) await softDelete.click() await expect(page.getByRole('alertdialog')).toContainText( 'may still be restored', ) await expect(page.getByRole('button', { name: 'Keep job' })).toBeFocused() await page.keyboard.press('Escape') await expect(page.getByRole('alertdialog')).toBeHidden() await expect(softDelete).toBeFocused() await softDelete.click() await page.getByRole('button', { name: 'Confirm' }).click() await page.reload() await expect(page.getByLabel('Synchronization status')).toHaveText('live') await page.getByRole('button', { name: /Imported archive/ }).click() expect(state.includeDeletedRequests).toBeGreaterThan(1) await expect(page.getByRole('button', { name: 'Browse result' })).toBeHidden() await page.getByRole('button', { name: 'Restore' }).click() await page.getByRole('button', { name: 'Confirm' }).click() await page.getByRole('button', { name: 'Revoke execution grant' }).click() await page.getByRole('button', { name: 'Confirm' }).click() await page.getByRole('button', { name: 'Hard delete' }).click() await expect(page.getByRole('alertdialog')).toContainText('irreversible') await page.getByRole('button', { name: 'Confirm' }).click() await expect( page.getByRole('list', { name: 'Deletion operations' }).getByText('queued'), ).toBeVisible() await page.getByRole('button', { name: 'Credentials' }).click() await page.getByRole('button', { name: /Archive token/ }).click() await page.getByRole('button', { name: 'Revoke credential' }).click() await expect(page.getByRole('alertdialog')).toContainText('future execution') await page.getByRole('button', { name: 'Confirm' }).click() await expect( page.getByText('Credential revoked for future grants'), ).toBeVisible() expect(state.mutations).toEqual([ 'soft-delete', 'restore', 'revoke', 'hard-delete', ]) expect(new Set(state.idempotencyKeys).size).toBe(state.idempotencyKeys.length) }) async function authenticatedSession(page: Page): Promise { await page.goto('/') await page.evaluate(() => { const authority = `${window.location.origin}/oidc` sessionStorage.setItem( `oidc.user:${authority}:remora-web`, JSON.stringify({ id_token: 'id-token', access_token: 'access-token', token_type: 'Bearer', scope: 'openid profile', profile: { iss: authority, sub: 'subject-0001' }, expires_at: Math.floor(Date.now() / 1000) + 3600, }), ) }) await page.reload() } async function mockApi(page: Page): Promise<{ submission: MockSubmission | null mutations: string[] idempotencyKeys: string[] paths: string[] includeDeletedRequests: number }> { const state: { submission: MockSubmission | null mutations: string[] idempotencyKeys: string[] paths: string[] includeDeletedRequests: number } = { submission: null, mutations: [], idempotencyKeys: [], paths: [], includeDeletedRequests: 0, } let credential = sourceCredential('active') let currentJob = job('Existing archive') await page.route(/\/v1\//, async (route) => { const request = route.request() const url = new URL(request.url()) const path = url.pathname.replace('/v1/', '') state.paths.push(path) if ( path === 'jobs' && request.method() === 'GET' && url.searchParams.get('include_deleted') === 'true' ) state.includeDeletedRequests += 1 if (request.method() === 'POST') { const key = request.headers()['idempotency-key'] if (path !== 'event-tickets' && key !== undefined) state.idempotencyKeys.push(key) } if (path === 'jobs' && request.method() === 'GET') return json(route, { items: [currentJob], next_cursor: null, event_cursor: 'cursor-snapshot-0001', management_capabilities: { revoke_execution_grants: true }, }) if (path === 'event-tickets') return json( route, { ticket: 'ticket_00001', expires_at: '2026-07-18T00:01:00Z', }, 201, ) if (path === 'source-credentials' && request.method() === 'GET') return json(route, { items: [credential], next_cursor: null }) if (path === 'source-credentials' && request.method() === 'POST') { const body = object(request.postDataJSON()) credential = { ...sourceCredential('active'), label: String(body.label), } return json(route, credential, 201) } if (path.endsWith('/versions')) { credential = { ...credential, active_version: { id: 'version_0002', created_at: observedAt }, } return json(route, credential, 201) } if (path.endsWith('/revoke') && path.startsWith('source-credentials/')) { credential = { ...credential, status: 'revoked', active_version: null } return json(route, credential) } if (path === 'jobs' && request.method() === 'POST') { const body = object(request.postDataJSON()) const source = object(body.source) state.submission = { ...(typeof body.label === 'string' ? { label: body.label } : {}), source: { ...(typeof source.credential_id === 'string' ? { credential_id: source.credential_id } : {}), }, } currentJob = { ...job(state.submission.label ?? 'Submitted job'), source: { kind: 'http', display_url: 'https://example.invalid/archive', result_path: 'archive.bin', ...(state.submission.source.credential_id === undefined ? {} : { credential_id: state.submission.source.credential_id }), }, } return json(route, currentJob, 202) } if (path.endsWith('/attempts')) return json(route, { items: [ { id: 'attempt_0001', fence: 1, worker_incarnation: 'worker_00001', state: 'AttemptCommitted', progress: null, result, failure: null, created_at: observedAt, updated_at: observedAt, }, ], next_cursor: null, }) if (path.endsWith('/result')) return json(route, result) if (path.endsWith('/result/members')) return json(route, { items: [ { id: 'member_00001', path: 'archive.bin', size: 12, object: result.manifest, }, ], next_cursor: null, }) for (const operation of ['soft-delete', 'restore', 'revoke']) { if (path.endsWith(`/${operation}`)) { state.mutations.push(operation) currentJob = { ...currentJob, revision: currentJob.revision + 1, lifecycle: operation === 'soft-delete' ? 'soft_deleted' : operation === 'restore' ? 'visible' : currentJob.lifecycle, } return json(route, currentJob, operation === 'revoke' ? 202 : 200) } } if (path.endsWith('/hard-delete')) { state.mutations.push('hard-delete') return json( route, { id: 'deletion_001', job_id: currentJob.id, state: 'queued', problem: null, created_at: observedAt, updated_at: observedAt, }, 202, ) } return route.fulfill({ status: 404 }) }) return state } function job(label: string): MockJob { return { resource_kind: 'job', id: 'job_00000001', owner: 'owner_000001', label, revision: 1, phase: 'Succeeded', lifecycle: 'visible', source: { kind: 'http', display_url: 'https://example.invalid/archive', result_path: 'archive.bin', }, target: { store: 'target-main', bucket: 'fixture-bucket', prefix: 'results', }, authoritative_attempt: null, progress: null, checkpoint: null, result, failure: null, created_at: observedAt, updated_at: observedAt, event_cursor: 'cursor-snapshot-0001', } } function sourceCredential(status: MockCredential['status']): MockCredential { return { id: 'credential_001', owner: 'owner_000001', label: 'Fixture credential', kind: 'bearer_token', status, active_version: status === 'active' ? { id: 'version_0001', created_at: observedAt } : null, created_at: observedAt, updated_at: observedAt, } } function object(value: unknown): Record { if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected object request') return Object.fromEntries(Object.entries(value)) } interface MockSubmission { label?: string source: { credential_id?: string } } interface MockResult { kind: 'manifest' manifest_id: string schema_version: number manifest: { store: string bucket: string key: string checksum: string size: number } member_count: number total_bytes: number } interface MockJob { resource_kind: 'job' id: string owner: string label: string revision: number phase: 'Succeeded' lifecycle: 'visible' | 'soft_deleted' source: { kind: 'http' display_url: string result_path: string credential_id?: string } target: { store: string; bucket: string; prefix: string } authoritative_attempt: null progress: null checkpoint: null result: MockResult failure: null created_at: string updated_at: string event_cursor: string } interface MockCredential { id: string owner: string label: string kind: 'bearer_token' status: 'active' | 'revoked' active_version: { id: string; created_at: string } | null created_at: string updated_at: string } function json(route: Route, body: unknown, status = 200): Promise { return route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body), }) }