mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* test(e2e): fix approval waiting regression coverage * test(e2e): address Copilot review notes
189 lines
6.5 KiB
Python
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}"
|
|
)
|