Files
wehub-resource-sync 0418dc5cf9
CI / Python tests (3.10) (push) Has been cancelled
CI / Python tests (3.11) (push) Has been cancelled
CI / Python quality (push) Has been cancelled
CI / Frontend typecheck (push) Has been cancelled
Autopilot Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:45:00 +08:00

245 lines
8.5 KiB
Python

from __future__ import annotations
import json
from typing import Any
import pytest
from openharness.api.client import ApiMessageRequest, ApiMessageCompleteEvent, ApiTextDeltaEvent
from openharness.api.codex_client import (
CodexApiClient,
_convert_messages_to_codex,
_format_codex_stream_error,
_resolve_codex_url,
)
from openharness.engine.messages import ConversationMessage, ImageBlock, TextBlock, ToolResultBlock, ToolUseBlock
class _FakeStreamResponse:
def __init__(self, *, status_code: int = 200, lines: list[str] | None = None, body: str = "") -> None:
self.status_code = status_code
self._lines = lines or []
self._body = body.encode("utf-8")
async def __aenter__(self) -> "_FakeStreamResponse":
return self
async def __aexit__(self, exc_type, exc, tb) -> None:
return None
async def aread(self) -> bytes:
return self._body
async def aiter_lines(self):
for line in self._lines:
yield line
class _FakeAsyncClient:
def __init__(self, response: _FakeStreamResponse, sink: dict[str, Any]) -> None:
self._response = response
self._sink = sink
async def __aenter__(self) -> "_FakeAsyncClient":
return self
async def __aexit__(self, exc_type, exc, tb) -> None:
return None
def stream(self, method: str, url: str, *, headers: dict[str, str], json: dict[str, Any]):
self._sink["method"] = method
self._sink["url"] = url
self._sink["headers"] = headers
self._sink["json"] = json
return self._response
def _b64url(data: dict[str, object]) -> str:
raw = json.dumps(data, separators=(",", ":")).encode("utf-8")
import base64
return base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
def _fake_codex_token() -> str:
payload = {"https://api.openai.com/auth": {"chatgpt_account_id": "acct_test"}}
return f"{_b64url({'alg': 'none', 'typ': 'JWT'})}.{_b64url(payload)}.sig"
def test_convert_messages_to_codex():
messages = [
ConversationMessage.from_user_text("Inspect file"),
ConversationMessage(
role="assistant",
content=[
TextBlock(text="I'll inspect it."),
ToolUseBlock(id="call_123", name="read_file", input={"path": "README.md"}),
],
),
ConversationMessage(
role="user",
content=[ToolResultBlock(tool_use_id="call_123", content="hello", is_error=False)],
),
]
converted = _convert_messages_to_codex(messages)
assert converted[0] == {
"role": "user",
"content": [{"type": "input_text", "text": "Inspect file"}],
}
assert converted[1]["type"] == "message"
assert converted[1]["role"] == "assistant"
assert converted[2]["type"] == "function_call"
assert converted[2]["call_id"] == "call_123"
assert json.loads(converted[2]["arguments"]) == {"path": "README.md"}
assert converted[3] == {
"type": "function_call_output",
"call_id": "call_123",
"output": "hello",
}
def test_convert_user_message_with_tool_result_before_text_to_codex():
messages = [
ConversationMessage(
role="user",
content=[
ToolResultBlock(tool_use_id="call_123", content="done", is_error=False),
TextBlock(text="next request"),
],
)
]
converted = _convert_messages_to_codex(messages)
assert converted == [
{"type": "function_call_output", "call_id": "call_123", "output": "done"},
{"role": "user", "content": [{"type": "input_text", "text": "next request"}]},
]
def test_convert_multimodal_user_message_to_codex():
messages = [
ConversationMessage(
role="user",
content=[
TextBlock(text="What is in this image?"),
ImageBlock(media_type="image/png", data="YWJj", source_path="/tmp/example.png"),
],
)
]
converted = _convert_messages_to_codex(messages)
assert converted == [{
"role": "user",
"content": [
{"type": "input_text", "text": "What is in this image?"},
{"type": "input_image", "image_url": "data:image/png;base64,YWJj"},
],
}]
def test_resolve_codex_url_ignores_unrelated_base_url():
assert _resolve_codex_url("https://api.moonshot.cn/anthropic") == "https://chatgpt.com/backend-api/codex/responses"
def test_format_codex_stream_error_includes_code_and_request_id():
message = _format_codex_stream_error(
{
"type": "error",
"message": "Upstream overloaded",
"code": "overloaded",
"request_id": "req_123",
},
fallback="Codex error",
)
assert message == "Upstream overloaded (code=overloaded) [request_id=req_123]"
@pytest.mark.asyncio
async def test_codex_client_streams_text(monkeypatch):
sink: dict[str, Any] = {}
response = _FakeStreamResponse(
lines=[
'event: response.output_item.added',
'data: {"type":"response.output_item.added","item":{"id":"msg_1","type":"message","content":[],"role":"assistant"}}',
"",
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","delta":"CODE"}',
"",
'event: response.output_text.delta',
'data: {"type":"response.output_text.delta","delta":"X_OK"}',
"",
'event: response.output_item.done',
'data: {"type":"response.output_item.done","item":{"id":"msg_1","type":"message","content":[{"type":"output_text","text":"CODEX_OK","annotations":[]}]}}',
"",
'event: response.completed',
'data: {"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":12,"output_tokens":3}}}',
"",
]
)
monkeypatch.setattr(
"openharness.api.codex_client.httpx.AsyncClient",
lambda *args, **kwargs: _FakeAsyncClient(response, sink),
)
client = CodexApiClient(_fake_codex_token())
request = ApiMessageRequest(
model="gpt-5.5",
messages=[ConversationMessage.from_user_text("hi")],
system_prompt="Be helpful.",
effort="xhigh",
)
events = [event async for event in client.stream_message(request)]
assert [event.text for event in events if isinstance(event, ApiTextDeltaEvent)] == ["CODE", "X_OK"]
complete = next(event for event in events if isinstance(event, ApiMessageCompleteEvent))
assert complete.message.text == "CODEX_OK"
assert complete.usage.input_tokens == 12
assert complete.usage.output_tokens == 3
assert sink["url"].endswith("/codex/responses")
assert sink["json"]["instructions"] == "Be helpful."
assert sink["json"]["model"] == "gpt-5.5"
assert sink["json"]["reasoning"] == {"effort": "xhigh"}
assert sink["headers"]["OpenAI-Beta"] == "responses=experimental"
@pytest.mark.asyncio
async def test_codex_client_emits_tool_use(monkeypatch):
sink: dict[str, Any] = {}
response = _FakeStreamResponse(
lines=[
'data: {"type":"response.output_item.added","item":{"id":"fc_1","type":"function_call","arguments":"","call_id":"call_abc","name":"glob"}}',
"",
'data: {"type":"response.output_item.done","item":{"id":"fc_1","type":"function_call","arguments":"{\\"pattern\\":\\"src/**/*.py\\"}","call_id":"call_abc","name":"glob"}}',
"",
'data: {"type":"response.completed","response":{"status":"completed","usage":{"input_tokens":7,"output_tokens":2}}}',
"",
]
)
monkeypatch.setattr(
"openharness.api.codex_client.httpx.AsyncClient",
lambda *args, **kwargs: _FakeAsyncClient(response, sink),
)
client = CodexApiClient(_fake_codex_token())
request = ApiMessageRequest(
model="gpt-5.4",
messages=[ConversationMessage.from_user_text("glob")],
system_prompt="Use tools.",
tools=[{"name": "glob", "description": "find files", "input_schema": {"type": "object"}}],
)
events = [event async for event in client.stream_message(request)]
complete = next(event for event in events if isinstance(event, ApiMessageCompleteEvent))
assert complete.stop_reason == "tool_use"
assert len(complete.message.tool_uses) == 1
tool_use = complete.message.tool_uses[0]
assert tool_use.id == "call_abc"
assert tool_use.name == "glob"
assert tool_use.input == {"pattern": "src/**/*.py"}
assert sink["json"]["tools"][0]["name"] == "glob"