Files
ironclaw/tests/e2e/scenarios/test_tool_execution.py
Illia Polosukhin c4927ba6e1 fix(ci): unblock staging Docker Build and echo tool E2E test (#2661)
Two independent staging CI regressions:

1. Docker Build was failing because `cargo install wasm-tools@1.246.1`
   re-resolved to the newest compatible `constant_time_eq@0.4.3`, which
   requires rustc >= 1.95, while the chef stage is pinned to rust:1.92.
   Add `--locked` so cargo uses the Cargo.lock shipped with each crate.

2. `test_builtin_echo_tool` started failing after PR #2555 intentionally
   aligned the in-memory history path with DB semantics: tool previews
   now surface in `result` with `result_preview` left empty. The test
   only inspected `result_preview`, so it timed out. Accept the preview
   from either field in `_wait_for_turn`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:44:01 +09:00

117 lines
3.9 KiB
Python

"""Tool execution E2E tests via the chat history API."""
import asyncio
from helpers import api_get, api_post
async def _create_thread(base_url: str) -> str:
response = await api_post(base_url, "/api/chat/thread/new", timeout=15)
assert response.status_code == 200, response.text
return response.json()["id"]
async def _send_chat_message(base_url: str, thread_id: str, content: str) -> None:
response = await api_post(
base_url,
"/api/chat/send",
json={"content": content, "thread_id": thread_id},
timeout=30,
)
assert response.status_code in (200, 202), response.text
async def _wait_for_turn(
base_url: str,
thread_id: str,
*,
response_fragment: str | None = None,
tool_name: str | None = None,
result_fragment: str | None = None,
timeout: float = 30.0,
) -> dict:
deadline = asyncio.get_running_loop().time() + timeout
while asyncio.get_running_loop().time() < deadline:
response = await api_get(
base_url,
f"/api/chat/history?thread_id={thread_id}",
timeout=10,
)
assert response.status_code == 200, response.text
history = response.json()
turns = history.get("turns", [])
if turns:
turn = turns[-1]
response_ok = response_fragment is None or (
turn.get("response") and response_fragment.lower() in turn["response"].lower()
)
tool_ok = tool_name is None
if tool_name is not None:
for tool_call in turn.get("tool_calls", []):
if tool_call.get("name") != tool_name:
continue
# In-memory turns populate `result`; DB-hydrated turns
# populate `result_preview`. Accept either — see PR #2555.
preview = (
tool_call.get("result_preview")
or tool_call.get("result")
or ""
).lower()
tool_ok = tool_call.get("has_result") is True and (
result_fragment is None or result_fragment.lower() in preview
)
if tool_ok:
break
if response_ok and tool_ok:
return turn
await asyncio.sleep(0.25)
raise AssertionError(
f"Timed out waiting for turn: response_fragment={response_fragment!r}, "
f"tool_name={tool_name!r}, result_fragment={result_fragment!r}"
)
async def test_builtin_echo_tool(ironclaw_server):
"""A chat request can execute the built-in echo tool and persist its result."""
thread_id = await _create_thread(ironclaw_server)
await _send_chat_message(ironclaw_server, thread_id, "echo hello world")
turn = await _wait_for_turn(
ironclaw_server,
thread_id,
tool_name="echo",
result_fragment="hello world",
)
assert any(tc.get("name") == "echo" for tc in turn.get("tool_calls", [])), turn
async def test_builtin_time_tool(ironclaw_server):
"""A chat request can execute the built-in time tool and persist its result."""
thread_id = await _create_thread(ironclaw_server)
await _send_chat_message(ironclaw_server, thread_id, "what time is it")
turn = await _wait_for_turn(
ironclaw_server,
thread_id,
tool_name="time",
)
assert any(tc.get("name") == "time" for tc in turn.get("tool_calls", [])), turn
async def test_non_tool_message_still_works(ironclaw_server):
"""Messages that do not trigger tools still get a normal assistant response."""
thread_id = await _create_thread(ironclaw_server)
await _send_chat_message(ironclaw_server, thread_id, "What is 2+2?")
turn = await _wait_for_turn(
ironclaw_server,
thread_id,
response_fragment="4",
timeout=15.0,
)
assert "4" in (turn.get("response") or ""), turn