Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions examples/chat/aimock-e2e/fixtures/interrupt-approval.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
]
}
}
]
}
36 changes: 36 additions & 0 deletions examples/chat/aimock-e2e/interrupt-approval.spec.ts
Original file line number Diff line number Diff line change
@@ -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
// <chat-interrupt-panel> 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);
});
Loading