Files
codex/sdk/python/tests/test_app_server_lifecycle.py
Ahmed Ibrahim d77f543654 Cover SDK app-server integration gaps
Add focused integration coverage for thread listing, persisted history reads, async lifecycle wrappers, skill input injection, and run override/usage behavior through the pinned app-server test harness.

Co-authored-by: Codex <noreply@openai.com>
2026-05-10 15:14:45 +03:00

218 lines
8.3 KiB
Python

from __future__ import annotations
import asyncio
from app_server_harness import AppServerHarness
from openai_codex import AsyncCodex, Codex
from app_server_helpers import request_kind
def _thread_message_summary(read_response) -> list[tuple[str, str]]:
"""Return persisted user/agent messages from a thread read response."""
messages: list[tuple[str, str]] = []
for turn in read_response.thread.turns:
for item in turn.items:
root = item.root
if root.type == "userMessage":
text = "\n".join(
input_item.root.text
for input_item in root.content
if input_item.root.type == "text"
)
messages.append(("user", text))
if root.type == "agentMessage":
messages.append(("agent", root.text))
return messages
def test_thread_set_name_and_read(tmp_path) -> None:
"""Thread naming should round-trip through app-server JSON-RPC."""
with AppServerHarness(tmp_path) as harness:
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
thread.set_name("sdk integration thread")
named = thread.read(include_turns=True)
assert {"thread_name": named.thread.name} == {
"thread_name": "sdk integration thread",
}
def test_thread_list_filters_archived_threads(tmp_path) -> None:
"""Thread listing should reflect archive state through app-server."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message("active", response_id="list-active")
harness.responses.enqueue_assistant_message(
"archived",
response_id="list-archived",
)
with Codex(config=harness.app_server_config()) as codex:
active_thread = codex.thread_start()
archived_thread = codex.thread_start()
active_thread.run("keep this listed")
archived_thread.run("archive this")
codex.thread_archive(archived_thread.id)
active_list = codex.thread_list(archived=False)
archived_list = codex.thread_list(archived=True)
expected_ids = {active_thread.id, archived_thread.id}
assert {
"active_ids": sorted(
thread.id for thread in active_list.data if thread.id in expected_ids
),
"archived_ids": sorted(
thread.id for thread in archived_list.data if thread.id in expected_ids
),
} == {
"active_ids": [active_thread.id],
"archived_ids": [archived_thread.id],
}
def test_read_include_turns_returns_persisted_history(tmp_path) -> None:
"""Thread.read(include_turns=True) should load real persisted turn items."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message("first answer", response_id="read-1")
harness.responses.enqueue_assistant_message("second answer", response_id="read-2")
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
thread.run("first question")
thread.run("second question")
read = thread.read(include_turns=True)
assert _thread_message_summary(read) == [
("user", "first question"),
("agent", "first answer"),
("user", "second question"),
("agent", "second answer"),
]
def test_async_lifecycle_methods_round_trip(tmp_path) -> None:
"""Async lifecycle helpers should preserve the same app-server thread state."""
async def scenario() -> None:
"""Exercise async wrappers over one materialized thread."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message(
"async materialized",
response_id="async-lifecycle",
)
async with AsyncCodex(config=harness.app_server_config()) as codex:
thread = await codex.thread_start()
run_result = await thread.run("materialize async thread")
await thread.set_name("async lifecycle")
named = await thread.read()
resumed = await codex.thread_resume(thread.id)
forked = await codex.thread_fork(thread.id)
archive_response = await codex.thread_archive(thread.id)
unarchived = await codex.thread_unarchive(thread.id)
assert {
"run_final_response": run_result.final_response,
"named_thread": named.thread.name,
"resumed_id": resumed.id,
"forked_is_distinct": forked.id != thread.id,
"archive_response": archive_response.model_dump(by_alias=True, mode="json"),
"unarchived_id": unarchived.id,
} == {
"run_final_response": "async materialized",
"named_thread": "async lifecycle",
"resumed_id": thread.id,
"forked_is_distinct": True,
"archive_response": {},
"unarchived_id": thread.id,
}
asyncio.run(scenario())
def test_thread_fork_returns_distinct_thread(tmp_path) -> None:
"""Thread fork should return a distinct thread for a persisted rollout."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message("materialized", response_id="fork-seed")
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
seeded = thread.run("materialize this thread before fork")
forked = codex.thread_fork(thread.id)
assert {
"seeded_response": seeded.final_response,
"forked_is_distinct": forked.id != thread.id,
} == {
"seeded_response": "materialized",
"forked_is_distinct": True,
}
def test_archive_unarchive_round_trip_uses_materialized_rollout(tmp_path) -> None:
"""Archive helpers should work once the app-server has persisted a rollout."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message("materialized", response_id="archive-seed")
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
seeded = thread.run("materialize this thread before archive")
archived = codex.thread_archive(thread.id)
unarchived = codex.thread_unarchive(thread.id)
read = unarchived.read()
assert {
"seeded_response": seeded.final_response,
"archive_response": archived.model_dump(by_alias=True, mode="json"),
"unarchived_id": unarchived.id,
"read_id": read.thread.id,
} == {
"seeded_response": "materialized",
"archive_response": {},
"unarchived_id": thread.id,
"read_id": thread.id,
}
def test_models_rpc(tmp_path) -> None:
"""Model listing should go through the pinned app-server method."""
with AppServerHarness(tmp_path) as harness:
with Codex(config=harness.app_server_config()) as codex:
models = codex.models(include_hidden=True)
assert {
"models_payload_has_data": isinstance(
models.model_dump(by_alias=True, mode="json").get("data"),
list,
),
} == {"models_payload_has_data": True}
def test_compact_rpc_hits_mock_responses(tmp_path) -> None:
"""Compaction should run through app-server and hit the mock Responses boundary."""
with AppServerHarness(tmp_path) as harness:
harness.responses.enqueue_assistant_message("history", response_id="compact-history")
harness.responses.enqueue_assistant_message(
"compact summary",
response_id="compact-summary",
)
with Codex(config=harness.app_server_config()) as codex:
thread = codex.thread_start()
run_result = thread.run("create history")
compact_response = thread.compact()
requests = harness.responses.wait_for_requests(2)
assert {
"run_final_response": run_result.final_response,
"compact_response": compact_response.model_dump(
by_alias=True,
mode="json",
),
"request_kinds": [request_kind(request.path) for request in requests],
} == {
"run_final_response": "history",
"compact_response": {},
"request_kinds": ["responses", "responses"],
}