Files
optimclaw/tests/e2e/scenarios/test_tool_approval.py
T
Henry ParkandGitHub d0cb5f0ac5 test(e2e): fix approval waiting regression coverage (#1270)
* test(e2e): fix approval waiting regression coverage

* test(e2e): address Copilot review notes
2026-03-16 20:06:15 -07:00

189 lines
6.5 KiB
Python

"""Scenario 6: Tool approval overlay UI behavior."""
import pytest
from helpers import SEL
INJECT_APPROVAL_JS = """
(data) => {
// Simulate an approval_needed SSE event by calling showApproval directly
showApproval(data);
}
"""
async def test_approval_card_appears(page):
"""Injecting an approval event should show the approval card."""
# Inject a fake approval_needed event
await page.evaluate("""
showApproval({
request_id: 'test-req-001',
thread_id: currentThreadId,
tool_name: 'shell',
description: 'Execute: echo hello world',
parameters: '{"command": "echo hello world"}'
})
""")
# Verify the approval card appeared
card = page.locator(SEL["approval_card"])
await card.wait_for(state="visible", timeout=5000)
# Check card contents
header = card.locator(SEL["approval_header"].replace(".approval-card ", ""))
assert await header.text_content() == "Tool requires approval"
tool_name = card.locator(".approval-tool-name")
assert await tool_name.text_content() == "shell"
desc = card.locator(".approval-description")
assert "echo hello world" in await desc.text_content()
# Verify all three buttons exist
assert await card.locator("button.approve").count() == 1
assert await card.locator("button.always").count() == 1
assert await card.locator("button.deny").count() == 1
async def test_approval_approve_disables_buttons(page):
"""Clicking Approve should disable all buttons and show status."""
# Inject approval card
await page.evaluate("""
showApproval({
request_id: 'test-req-002',
thread_id: currentThreadId,
tool_name: 'http',
description: 'GET https://example.com',
})
""")
card = page.locator('.approval-card[data-request-id="test-req-002"]')
await card.wait_for(state="visible", timeout=5000)
# Click Approve
await card.locator("button.approve").click()
# Buttons should be disabled
await page.wait_for_timeout(500)
buttons = card.locator(".approval-actions button")
count = await buttons.count()
for i in range(count):
is_disabled = await buttons.nth(i).is_disabled()
assert is_disabled, f"Button {i} should be disabled after approval"
# Resolved status should show
resolved = card.locator(".approval-resolved")
assert await resolved.text_content() == "Approved"
async def test_approval_deny_shows_denied(page):
"""Clicking Deny should show 'Denied' status."""
await page.evaluate("""
showApproval({
request_id: 'test-req-003',
thread_id: currentThreadId,
tool_name: 'write_file',
description: 'Write to /tmp/test.txt',
})
""")
card = page.locator('.approval-card[data-request-id="test-req-003"]')
await card.wait_for(state="visible", timeout=5000)
# Click Deny
await card.locator("button.deny").click()
await page.wait_for_timeout(500)
resolved = card.locator(".approval-resolved")
assert await resolved.text_content() == "Denied"
async def test_approval_params_toggle(page):
"""Parameters toggle should show/hide the parameter details."""
await page.evaluate("""
showApproval({
request_id: 'test-req-004',
thread_id: currentThreadId,
tool_name: 'shell',
description: 'Run command',
parameters: '{"command": "ls -la /tmp"}'
})
""")
card = page.locator('.approval-card[data-request-id="test-req-004"]')
await card.wait_for(state="visible", timeout=5000)
# Parameters should be hidden initially
params = card.locator(".approval-params")
assert await params.is_hidden(), "Parameters should be hidden initially"
# Click toggle to show
toggle = card.locator(".approval-params-toggle")
await toggle.click()
await page.wait_for_timeout(300)
assert await params.is_visible(), "Parameters should be visible after toggle"
text = await params.text_content()
assert "ls -la /tmp" in text
# Click toggle again to hide
await toggle.click()
await page.wait_for_timeout(300)
assert await params.is_hidden(), "Parameters should be hidden after second toggle"
async def test_waiting_for_approval_message_no_error_prefix(page):
"""Verify that input submitted while awaiting approval shows non-error status with tool context.
Trigger a real approval-needed tool call, then attempt to send another message while
approval is pending. The backend should reject the second input with a non-error
status that includes the pending tool context.
"""
assistant_messages = page.locator(SEL["message_assistant"])
chat_input = page.locator(SEL["chat_input"])
await chat_input.wait_for(state="visible", timeout=5000)
# Trigger a real HTTP tool call that pauses for approval in the default E2E harness.
await chat_input.fill("make approval post approval-required")
await chat_input.press("Enter")
card = page.locator(SEL["approval_card"]).last
await card.wait_for(state="visible", timeout=10000)
tool_name = await card.locator(".approval-tool-name").text_content()
desc_text = await card.locator(".approval-description").text_content()
assert tool_name == "http"
assert desc_text is not None and "HTTP requests to external APIs" in desc_text
# With the thread now genuinely awaiting approval, the next message should be rejected
# as a non-error pending status.
initial_count = await assistant_messages.count()
await chat_input.fill("send another message now")
await chat_input.press("Enter")
await page.wait_for_function(
f"() => document.querySelectorAll('{SEL['message_assistant']}').length > {initial_count}",
timeout=10000,
)
last_msg = assistant_messages.last.locator(".message-content")
msg_text = await last_msg.inner_text()
# Verify no "Error:" prefix
assert not msg_text.lower().startswith("error:"), (
f"Approval rejection must NOT have 'Error:' prefix. Got: {msg_text!r}"
)
# Verify it contains "waiting for approval"
assert "waiting for approval" in msg_text.lower(), (
f"Expected 'Waiting for approval' text. Got: {msg_text!r}"
)
# Verify it contains the tool name and description
assert "http" in msg_text.lower(), (
f"Expected tool name 'http' in message. Got: {msg_text!r}"
)
assert "HTTP requests to external APIs" in msg_text, (
f"Expected tool description in message. Got: {msg_text!r}"
)