mirror of
https://github.com/openai/codex.git
synced 2026-05-14 16:22:51 +00:00
## Why The Python SDK needs the same tight formatter/lint loop as the rest of the repo: a safe Ruff autofix pass, Ruff formatting, editor save behavior, and CI checks that catch drift. Without that loop, SDK changes can land with formatting or import ordering that differs from what reviewers and CI expect. ## What - Add Ruff configuration to `sdk/python/pyproject.toml`, excluding generated protocol code and notebooks from the normal lint/format pass. - Update `just fmt` so it still formats Rust and also runs Python SDK Ruff autofix and formatting. - Add Python SDK CI steps for `ruff check` and `ruff format --check` before pytest. - Recommend the Ruff VS Code extension and enable Python format/fix/organize-on-save so Cmd+S uses the same tooling. - Apply the resulting Ruff formatting to SDK Python files, examples, and the checked-in generated `v2_all.py` output emitted by the pinned generator. - Add a guard test for the `just fmt` recipe so it keeps working from both Rust and Python SDK working directories. ## Stack 1. #21891 `[1/8]` Pin Python SDK runtime dependency 2. #21893 `[2/8]` Generate Python SDK types from pinned runtime 3. #21895 `[3/8]` Run Python SDK tests in CI 4. #21896 `[4/8]` Define Python SDK public API surface 5. #21905 `[5/8]` Rename Python SDK package to `openai-codex` 6. #21910 `[6/8]` Add high-level Python SDK approval mode 7. #22014 `[7/8]` Add Python SDK app-server integration harness 8. This PR `[8/8]` Add Python SDK Ruff formatting ## Verification - Added `test_root_fmt_recipe_formats_rust_and_python_sdk` for the shared format recipe. - Ran `just fmt` after the recipe update. --------- Co-authored-by: Codex <noreply@openai.com>
217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
from app_server_harness import AppServerHarness
|
|
from app_server_helpers import request_kind
|
|
|
|
from openai_codex import AsyncCodex, Codex
|
|
|
|
|
|
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"],
|
|
}
|