161 lines
4.9 KiB
Python
161 lines
4.9 KiB
Python
"""E2E test: Claude SDK executor auto-collects sub-agent results (mock LLM).
|
|
|
|
Verifies that when the Claude SDK executor spawns a sub-agent,
|
|
the workflow auto-collects the results before the parent task
|
|
completes. The user sends a single message and gets back the
|
|
sub-agent's output -- no second message or manual polling needed.
|
|
|
|
The mock LLM returns canned responses. The LLM judge is skipped
|
|
because it requires a real OpenAI key.
|
|
|
|
Usage::
|
|
|
|
pytest tests/e2e/test_claude_coder_auto_collect.py -v --timeout=180
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import uuid
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from tests.e2e.conftest import (
|
|
configure_mock_llm,
|
|
create_runner_bound_session,
|
|
poll_session_until_terminal,
|
|
register_inline_agent,
|
|
reset_mock_llm,
|
|
send_user_message_to_session,
|
|
)
|
|
|
|
|
|
def _extract_all_text(body: dict[str, Any]) -> str:
|
|
"""
|
|
Concatenate all output_text blocks from a response body.
|
|
|
|
:param body: The terminal response body.
|
|
:returns: All assistant text joined by newlines.
|
|
"""
|
|
parts: list[str] = []
|
|
for item in body.get("output", []):
|
|
if item.get("type") == "message":
|
|
for block in item.get("content", []):
|
|
text = block.get("text")
|
|
if text:
|
|
parts.append(text)
|
|
return "\n".join(parts)
|
|
|
|
|
|
def test_single_message_subagent_auto_collect(
|
|
http_client: httpx.Client,
|
|
live_runner_id: str,
|
|
mock_llm_server_url: str,
|
|
) -> None:
|
|
"""
|
|
One message triggers spawn + auto-collect. The response includes
|
|
the sub-agent's review output.
|
|
|
|
The mock LLM for the parent issues ``sys_session_send`` then
|
|
reports the collected result. The reviewer mock returns a canned
|
|
review. The LLM judge is skipped.
|
|
"""
|
|
reset_mock_llm(mock_llm_server_url)
|
|
|
|
parent_model = f"mock-autocollect-{uuid.uuid4().hex[:6]}"
|
|
reviewer_model = f"mock-reviewer-ac-{uuid.uuid4().hex[:6]}"
|
|
|
|
agent_name = register_inline_agent(
|
|
http_client,
|
|
name=f"autocollect-{uuid.uuid4().hex[:6]}",
|
|
harness="claude-sdk",
|
|
model=parent_model,
|
|
profile="",
|
|
prompt=(
|
|
"You are a coding assistant. You have a sub-agent called 'reviewer' for code reviews."
|
|
),
|
|
mock_llm_base_url=mock_llm_server_url,
|
|
extra_config={
|
|
"tools": {
|
|
"reviewer": {
|
|
"type": "agent",
|
|
"description": "Reviews code for bugs and quality.",
|
|
"executor": {
|
|
"harness": "claude-sdk",
|
|
"model": reviewer_model,
|
|
},
|
|
"prompt": "You are a code reviewer.",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
# Parent: issue sys_session_send, then report collected results.
|
|
configure_mock_llm(
|
|
mock_llm_server_url,
|
|
[
|
|
{
|
|
"tool_calls": [
|
|
{
|
|
"call_id": f"call_{uuid.uuid4().hex[:8]}",
|
|
"name": "sys_session_send",
|
|
"arguments": json.dumps(
|
|
{
|
|
"agent": "reviewer",
|
|
"message": "Review /etc/hosts for any issues.",
|
|
}
|
|
),
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"text": (
|
|
"I spawned the reviewer sub-agent to review /etc/hosts. "
|
|
"The sub-agent completed its review via check_sub_agents. "
|
|
"The reviewer found that /etc/hosts is a standard hosts "
|
|
"file with localhost entries. No critical issues found."
|
|
),
|
|
},
|
|
],
|
|
key=parent_model,
|
|
)
|
|
|
|
# Reviewer: return a review.
|
|
configure_mock_llm(
|
|
mock_llm_server_url,
|
|
[
|
|
{
|
|
"text": (
|
|
"## Review: /etc/hosts\n\n"
|
|
"Standard hosts file. localhost entries present.\n"
|
|
"No security concerns for a system hosts file."
|
|
),
|
|
},
|
|
],
|
|
key=reviewer_model,
|
|
)
|
|
|
|
session_id = create_runner_bound_session(
|
|
http_client, agent_name=agent_name, runner_id=live_runner_id
|
|
)
|
|
response_id = send_user_message_to_session(
|
|
http_client,
|
|
session_id=session_id,
|
|
content=(
|
|
"Use sys_session_send to spawn the 'reviewer' sub-agent "
|
|
"and ask it to review /etc/hosts."
|
|
),
|
|
)
|
|
|
|
body = poll_session_until_terminal(
|
|
http_client,
|
|
session_id=session_id,
|
|
response_id=response_id,
|
|
timeout=240,
|
|
)
|
|
assert body["status"] == "completed", f"Task failed: {body.get('error')}"
|
|
|
|
text = _extract_all_text(body)
|
|
assert len(text) > 20, f"Expected substantial response with sub-agent results, got: {text!r}"
|