import { expect, test } from '@playwright/test' import { mockNexusApi, PROPOSAL_ID } from './support/nexusApi' test.describe('agent proposal lifecycle', () => { test('an owner creates, reviews, and approves a proposal with mutation guards', async ({ page }) => { const api = await mockNexusApi(page, { role: 'owner' }) await page.goto('/agents/new') await expect(page.getByRole('heading', { name: 'Agent vorschlagen' })).toBeVisible() await page.getByRole('textbox', { name: 'Name Pflichtfeld', exact: true }).fill('Release Sentinel') await page.getByLabel('Rolle').fill('Release Operations') await page.getByLabel('Zweck und Verantwortungsbereich').fill( 'Verifies release readiness and reports blockers.', ) await page.getByLabel('OpenClaw-Modell').selectOption('openai/gpt-5.4') await page.getByLabel('Datei').selectOption('SOUL.md') await page.getByLabel('SOUL.md Inhalt').fill('# Release Sentinel\nVerify before reporting.') await page.getByRole('button', { name: 'Vorschlag anlegen' }).click() await expect(page).toHaveURL(new RegExp(`/agents/proposals/${PROPOSAL_ID}$`)) await expect(page.getByText('Freigabe offen', { exact: true })).toBeVisible() const createRequest = api.proposalRequests.find(request => request.path === '/api/v1/openclaw/agent-proposals' ) expect(createRequest).toBeDefined() expect(createRequest?.headers['idempotency-key']).toBeTruthy() expect(createRequest?.headers['x-correlation-id']).toBeTruthy() expect(createRequest?.body).toMatchObject({ name: 'Release Sentinel', role: 'Release Operations', model: 'openai/gpt-5.4', files: { 'SOUL.md': '# Release Sentinel\nVerify before reporting.', }, }) await page.getByRole('button', { name: 'Freigeben' }).click() const dialog = page.getByRole('dialog', { name: 'Agent-Provisionierung freigeben' }) await expect(dialog).toBeVisible() await dialog.getByLabel('Notiz (optional)').fill('Reviewed in the owner E2E flow.') await dialog.getByRole('button', { name: 'Bestätigen' }).click() await expect(page.getByText('Bereit', { exact: true })).toBeVisible() await expect(page.getByText('Agent ist bereit.', { exact: true })).toBeVisible() const approveRequest = api.proposalRequests.find(request => request.path.endsWith('/approve') ) expect(approveRequest).toBeDefined() expect(approveRequest?.headers['idempotency-key']).toBeTruthy() expect(approveRequest?.headers['x-correlation-id']).toBeTruthy() expect(approveRequest?.body).toMatchObject({ expectedRevision: 1, reason: 'Reviewed in the owner E2E flow.', }) }) test('a non-owner sees the permission boundary without owner API traffic', async ({ page }) => { const api = await mockNexusApi(page, { role: 'member' }) await page.goto('/agents') await expect(page.getByText( 'Agent-Vorschläge und OpenClaw-Provisionierung sind ausschließlich für Owner sichtbar.', )).toBeVisible() await expect(page.getByRole('link', { name: 'Agent vorschlagen' })).toHaveCount(0) await page.goto('/agents/new') await expect(page.getByRole('alert')).toContainText('Owner-Berechtigung erforderlich') await expect(page.getByRole('button', { name: 'Vorschlag anlegen' })).toHaveCount(0) await page.goto(`/agents/proposals/${PROPOSAL_ID}`) await expect(page.getByRole('alert')).toContainText('Owner-Berechtigung erforderlich') await expect(page.getByRole('button', { name: 'Freigeben' })).toHaveCount(0) expect(api.proposalRequests).toEqual([]) }) test('an Iris proposal event becomes a structured approval card and deep link', async ({ page }) => { const api = await mockNexusApi(page, { role: 'owner' }) await page.goto('/dashboard') await page.getByRole('button', { name: 'Iris Chat öffnen' }).click() const chat = page.getByRole('dialog', { name: 'Iris Chat' }) await expect(chat).toBeVisible() await chat.getByRole('textbox', { name: 'Nachricht an Iris' }).fill( 'Schlage einen Release Sentinel Agenten vor, aber provisioniere ihn nicht.', ) await chat.getByRole('button', { name: 'Nachricht senden' }).click() await expect(chat.getByText('OpenClaw accepted the Iris request.')).toBeVisible() expect(api.chatRequests).toHaveLength(1) expect(api.chatRequests[0]?.body).toMatchObject({ agentId: 'iris', message: 'Schlage einen Release Sentinel Agenten vor, aber provisioniere ihn nicht.', }) expect(api.proposalRequests).toEqual([]) await page.evaluate(({ proposalId, occurredAt }) => { window.dispatchEvent(new CustomEvent('nexus:domain-event', { detail: { sequence: 73, eventType: 'agent.proposal.created', entity: { type: 'agent-proposal', id: proposalId, label: 'Release Sentinel', }, entityRevision: 1, occurredAt, payload: { state: 'awaiting_approval' }, }, })) }, { proposalId: PROPOSAL_ID, occurredAt: '2026-07-31T10:00:00.000Z' }) await expect(chat.getByText( 'Iris hat einen Agent-Vorschlag zur Owner-Freigabe angelegt.', )).toBeVisible() const proposalLink = chat.getByRole('link', { name: /Agent-Vorschlag Release Sentinel/, }) await expect(proposalLink).toHaveAttribute( 'href', `/agents/proposals/${PROPOSAL_ID}`, ) await proposalLink.click() await expect(page).toHaveURL(`/agents/proposals/${PROPOSAL_ID}`) await expect(page.getByRole('heading', { level: 1, name: 'Release Sentinel', })).toBeVisible() await expect(chat).toHaveCount(0) }) })