From 123c92de10bb55cbfda982ed95c219bf2419df92 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Thu, 11 Jun 2026 10:55:51 -0700 Subject: [PATCH] test(examples/ag-ui): interrupt-approval e2e over the AG-UI transport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports examples/chat's interrupt spec: request_approval pauses the run via on_interrupt, and the app shell's (#649) renders the captured reason + Accept/Edit/Respond/Ignore. The interrupt-approval.json aimock fixture already shipped with the harness — only the spec was missing. 10/10 suite green locally. Co-Authored-By: Claude Fable 5 --- .../angular/e2e/interrupt-approval.spec.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 examples/ag-ui/angular/e2e/interrupt-approval.spec.ts diff --git a/examples/ag-ui/angular/e2e/interrupt-approval.spec.ts b/examples/ag-ui/angular/e2e/interrupt-approval.spec.ts new file mode 100644 index 000000000..78580f29c --- /dev/null +++ b/examples/ag-ui/angular/e2e/interrupt-approval.spec.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +import { test, expect } from '@playwright/test'; +import { openDemo } from './test-helpers'; + +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.'; + +// Mirrors examples/chat's interrupt-approval spec over the AG-UI transport: +// the langgraph node calls interrupt({...}), ag-ui-langgraph emits the +// on_interrupt CUSTOM event, the adapter populates agent.interrupt(), and the +// app shell renders (added for parity in #649). +test('interrupt approval: pause renders the interrupt panel with the captured reason', async ({ + page, +}) => { + await openDemo(page); + + const input = page.getByRole('textbox', { name: /message|prompt/i }); + await input.fill(PROMPT); + await page.getByRole('button', { name: /send/i }).click(); + + // The run stays paused until a human responds — the interrupt panel is the + // durable signal, so don't wait on streaming-complete. + const panel = page.locator('chat-interrupt-panel'); + await expect(panel).toBeAttached({ timeout: 45_000 }); + + await expect(panel).toContainText(/agent paused/i); + + // The captured reason mentions the destructive plan — assert it plumbed + // through the on_interrupt payload to the panel body. + await expect.poll( + async () => (await panel.innerText()).toLowerCase(), + { timeout: 30_000 }, + ).toMatch(/approval|delete|backup/i); + + await expect(panel.getByRole('button', { name: /accept/i })).toBeVisible(); + await expect(panel.getByRole('button', { name: /edit|respond/i }).first()).toBeVisible(); + await expect(panel.getByRole('button', { name: /ignore/i })).toBeVisible(); + await expect(page.locator('chat-message').filter({ has: panel })).toHaveCount(0); +});