diff --git a/examples/chat/aimock-e2e/fixtures/interrupt-approval.json b/examples/chat/aimock-e2e/fixtures/interrupt-approval.json new file mode 100644 index 000000000..91e9bd14d --- /dev/null +++ b/examples/chat/aimock-e2e/fixtures/interrupt-approval.json @@ -0,0 +1,19 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "I want to clean up old database backups older than 90 days. Walk me through what you would delete, and call request_approval before doing anything destructive so I can review your plan." + }, + "response": { + "toolCalls": [ + { + "name": "request_approval", + "arguments": { + "reason": "Requesting approval to delete database backups older than 90 days across configured storage locations. Planned actions: 1) inventory existing backups (S3/GCS/Azure/local/RDS/EBS/manual snapshots/backup DB table), 2) run dry-run listing of items matching backup patterns older than 90 days for your review, 3) after final confirmation, delete the identified objects/snapshots (or move to quarantine/archive) and record audit logs. Please approve or deny; describe any exclusions or storage locations to omit." + } + } + ] + } + } + ] +} diff --git a/examples/chat/aimock-e2e/interrupt-approval.spec.ts b/examples/chat/aimock-e2e/interrupt-approval.spec.ts new file mode 100644 index 000000000..75fa90ff5 --- /dev/null +++ b/examples/chat/aimock-e2e/interrupt-approval.spec.ts @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; + +const PROMPT = + 'I want to clean up old database backups older than 90 days. Walk me through ' + + 'what you would delete, and call request_approval before doing anything ' + + 'destructive so I can review your plan.'; + +test('interrupt approval: pause renders the interrupt panel with the captured reason', async ({ + page, +}) => { + await page.goto('/embed'); + + const input = page.getByRole('textbox', { name: /message|prompt/i }); + await input.fill(PROMPT); + await page.getByRole('button', { name: /send/i }).click(); + + // When the parent emits request_approval, the langgraph node calls + // interrupt({...}) and the graph pauses. The chat composition surfaces a + // with the captured 'reason' text. We don't wait on + // data-streaming="false" here because the agent stays in the paused state + // until the human responds — the interrupt panel is the durable signal. + const panel = page.locator('chat-interrupt-panel'); + await expect(panel).toBeAttached({ timeout: 45_000 }); + + // The panel title is "Agent paused" (per the smoke checklist) — verifies + // the panel actually rendered, not just the host element being attached. + await expect(panel).toContainText(/agent paused/i); + + // The captured reason mentions "approval" and "delete" — assert one is in + // the panel body so the reason text is plumbing through. + await expect.poll( + async () => (await panel.innerText()).toLowerCase(), + { timeout: 30_000 }, + ).toMatch(/approval|delete|backup/i); +});