mirror of
https://github.com/openai/codex.git
synced 2026-05-14 16:22:51 +00:00
Generate Python SDK types from pinned runtime
Make the SDK artifact generator fetch schema from the pinned runtime package, regenerate the checked-in Python types from that schema, and assert generated artifacts stay up to date. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
|
||||
Experimental Python SDK for `codex app-server` JSON-RPC v2 over stdio, with a small default surface optimized for real scripts and apps.
|
||||
|
||||
The generated wire-model layer is currently sourced from the bundled v2 schema and exposed as Pydantic models with snake_case Python fields that serialize back to the app-server’s camelCase wire format.
|
||||
The generated wire-model layer is sourced from the pinned `openai-codex-cli-bin`
|
||||
runtime package and exposed as Pydantic models with snake_case Python fields
|
||||
that serialize back to the app-server’s camelCase wire format.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -68,6 +70,7 @@ notebook bootstrap the pinned runtime package automatically.
|
||||
|
||||
```bash
|
||||
cd sdk/python
|
||||
uv sync
|
||||
python scripts/update_sdk_artifacts.py generate-types
|
||||
python scripts/update_sdk_artifacts.py \
|
||||
stage-sdk \
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
@@ -33,19 +34,12 @@ def python_runtime_root() -> Path:
|
||||
return repo_root() / "sdk" / "python-runtime"
|
||||
|
||||
|
||||
def schema_bundle_path() -> Path:
|
||||
return (
|
||||
repo_root()
|
||||
/ "codex-rs"
|
||||
/ "app-server-protocol"
|
||||
/ "schema"
|
||||
/ "json"
|
||||
/ "codex_app_server_protocol.v2.schemas.json"
|
||||
)
|
||||
def sdk_pyproject_path() -> Path:
|
||||
return sdk_root() / "pyproject.toml"
|
||||
|
||||
|
||||
def schema_root_dir() -> Path:
|
||||
return repo_root() / "codex-rs" / "app-server-protocol" / "schema" / "json"
|
||||
def schema_bundle_path(schema_dir: Path) -> Path:
|
||||
return schema_dir / "codex_app_server_protocol.v2.schemas.json"
|
||||
|
||||
|
||||
def _is_windows() -> bool:
|
||||
@@ -79,7 +73,7 @@ def run_python_module(module: str, args: list[str], cwd: Path) -> None:
|
||||
def current_sdk_version() -> str:
|
||||
match = re.search(
|
||||
r'^version = "([^"]+)"$',
|
||||
(sdk_root() / "pyproject.toml").read_text(),
|
||||
sdk_pyproject_path().read_text(),
|
||||
flags=re.MULTILINE,
|
||||
)
|
||||
if match is None:
|
||||
@@ -87,6 +81,57 @@ def current_sdk_version() -> str:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def pinned_runtime_version() -> str:
|
||||
pyproject_text = sdk_pyproject_path().read_text()
|
||||
match = re.search(r"(?ms)^dependencies = \[(.*?)\]$", pyproject_text)
|
||||
if match is None:
|
||||
raise RuntimeError(
|
||||
"Could not find dependencies array in sdk/python/pyproject.toml"
|
||||
)
|
||||
|
||||
pins = re.findall(
|
||||
rf'"{re.escape(RUNTIME_DISTRIBUTION_NAME)}==([^"]+)"',
|
||||
match.group(1),
|
||||
)
|
||||
if len(pins) != 1:
|
||||
raise RuntimeError(
|
||||
f"Expected exactly one {RUNTIME_DISTRIBUTION_NAME} dependency pin "
|
||||
"in sdk/python/pyproject.toml"
|
||||
)
|
||||
return normalize_codex_version(pins[0])
|
||||
|
||||
|
||||
def pinned_runtime_codex_path() -> Path:
|
||||
expected_version = pinned_runtime_version()
|
||||
try:
|
||||
installed_version = importlib.metadata.version(RUNTIME_DISTRIBUTION_NAME)
|
||||
except importlib.metadata.PackageNotFoundError as exc:
|
||||
raise RuntimeError(
|
||||
f"Install {RUNTIME_DISTRIBUTION_NAME}=={expected_version} before "
|
||||
"generating Python SDK types."
|
||||
) from exc
|
||||
|
||||
normalized_installed_version = normalize_codex_version(installed_version)
|
||||
if normalized_installed_version != expected_version:
|
||||
raise RuntimeError(
|
||||
f"Expected {RUNTIME_DISTRIBUTION_NAME}=={expected_version}, "
|
||||
f"but found {installed_version}."
|
||||
)
|
||||
|
||||
try:
|
||||
from codex_cli_bin import bundled_codex_path
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
f"Installed {RUNTIME_DISTRIBUTION_NAME} package does not expose "
|
||||
"bundled_codex_path."
|
||||
) from exc
|
||||
|
||||
codex_path = bundled_codex_path()
|
||||
if not codex_path.exists():
|
||||
raise RuntimeError(f"Pinned Codex runtime binary not found at {codex_path}.")
|
||||
return codex_path
|
||||
|
||||
|
||||
def normalize_codex_version(version: str) -> str:
|
||||
normalized = version.strip()
|
||||
if normalized.startswith("rust-v"):
|
||||
@@ -488,8 +533,26 @@ def _annotate_schema(value: Any, base: str | None = None) -> None:
|
||||
_annotate_schema(child, base)
|
||||
|
||||
|
||||
def _normalized_schema_bundle_text() -> str:
|
||||
schema = json.loads(schema_bundle_path().read_text())
|
||||
def generate_schema_from_pinned_runtime(schema_dir: Path) -> Path:
|
||||
codex_path = pinned_runtime_codex_path()
|
||||
if schema_dir.exists():
|
||||
shutil.rmtree(schema_dir)
|
||||
schema_dir.mkdir(parents=True)
|
||||
run(
|
||||
[
|
||||
str(codex_path),
|
||||
"app-server",
|
||||
"generate-json-schema",
|
||||
"--out",
|
||||
str(schema_dir),
|
||||
],
|
||||
cwd=sdk_root(),
|
||||
)
|
||||
return schema_dir
|
||||
|
||||
|
||||
def _normalized_schema_bundle_text(schema_dir: Path) -> str:
|
||||
schema = json.loads(schema_bundle_path(schema_dir).read_text())
|
||||
definitions = schema.get("definitions", {})
|
||||
if isinstance(definitions, dict):
|
||||
for definition in definitions.values():
|
||||
@@ -501,7 +564,7 @@ def _normalized_schema_bundle_text() -> str:
|
||||
return json.dumps(schema, indent=2, sort_keys=True) + "\n"
|
||||
|
||||
|
||||
def generate_v2_all() -> None:
|
||||
def generate_v2_all(schema_dir: Path) -> None:
|
||||
out_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py"
|
||||
out_dir = out_path.parent
|
||||
old_package_dir = out_dir / "v2_all"
|
||||
@@ -509,8 +572,8 @@ def generate_v2_all() -> None:
|
||||
shutil.rmtree(old_package_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
normalized_bundle = Path(td) / schema_bundle_path().name
|
||||
normalized_bundle.write_text(_normalized_schema_bundle_text())
|
||||
normalized_bundle = Path(td) / schema_bundle_path(schema_dir).name
|
||||
normalized_bundle.write_text(_normalized_schema_bundle_text(schema_dir))
|
||||
run_python_module(
|
||||
"datamodel_code_generator",
|
||||
[
|
||||
@@ -547,9 +610,9 @@ def generate_v2_all() -> None:
|
||||
_normalize_generated_timestamps(out_path)
|
||||
|
||||
|
||||
def _notification_specs() -> list[tuple[str, str]]:
|
||||
def _notification_specs(schema_dir: Path) -> list[tuple[str, str]]:
|
||||
server_notifications = json.loads(
|
||||
(schema_root_dir() / "ServerNotification.json").read_text()
|
||||
(schema_dir / "ServerNotification.json").read_text()
|
||||
)
|
||||
one_of = server_notifications.get("oneOf", [])
|
||||
generated_source = (
|
||||
@@ -587,11 +650,12 @@ def _notification_specs() -> list[tuple[str, str]]:
|
||||
|
||||
|
||||
def _notification_turn_id_specs(
|
||||
schema_dir: Path,
|
||||
specs: list[tuple[str, str]],
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""Classify generated notification payloads by where the turn id lives."""
|
||||
server_notifications = json.loads(
|
||||
(schema_root_dir() / "ServerNotification.json").read_text()
|
||||
(schema_dir / "ServerNotification.json").read_text()
|
||||
)
|
||||
definitions = server_notifications.get("definitions", {})
|
||||
if not isinstance(definitions, dict):
|
||||
@@ -625,7 +689,7 @@ def _type_tuple_source(class_names: list[str]) -> str:
|
||||
return "(\n" + "".join(f" {class_name},\n" for class_name in class_names) + ")"
|
||||
|
||||
|
||||
def generate_notification_registry() -> None:
|
||||
def generate_notification_registry(schema_dir: Path) -> None:
|
||||
"""Regenerate notification models and routing metadata from generated schemas."""
|
||||
out = (
|
||||
sdk_root()
|
||||
@@ -634,9 +698,12 @@ def generate_notification_registry() -> None:
|
||||
/ "generated"
|
||||
/ "notification_registry.py"
|
||||
)
|
||||
specs = _notification_specs()
|
||||
specs = _notification_specs(schema_dir)
|
||||
class_names = sorted({class_name for _, class_name in specs})
|
||||
direct_turn_id_types, nested_turn_types = _notification_turn_id_specs(specs)
|
||||
direct_turn_id_types, nested_turn_types = _notification_turn_id_specs(
|
||||
schema_dir,
|
||||
specs,
|
||||
)
|
||||
|
||||
lines = [
|
||||
"# Auto-generated by scripts/update_sdk_artifacts.py",
|
||||
@@ -758,7 +825,10 @@ def _load_public_fields(
|
||||
module_name: str, class_name: str, *, exclude: set[str] | None = None
|
||||
) -> list[PublicFieldSpec]:
|
||||
exclude = exclude or set()
|
||||
module = importlib.import_module(module_name)
|
||||
if module_name == "codex_app_server.generated.v2_all":
|
||||
module = _load_generated_v2_all_module()
|
||||
else:
|
||||
module = importlib.import_module(module_name)
|
||||
model = getattr(module, class_name)
|
||||
fields: list[PublicFieldSpec] = []
|
||||
for name, field in model.model_fields.items():
|
||||
@@ -780,6 +850,19 @@ def _load_public_fields(
|
||||
return fields
|
||||
|
||||
|
||||
def _load_generated_v2_all_module() -> types.ModuleType:
|
||||
module_name = "_codex_app_server_generated_v2_all_for_artifacts"
|
||||
sys.modules.pop(module_name, None)
|
||||
module_path = sdk_root() / "src" / "codex_app_server" / "generated" / "v2_all.py"
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Failed to load generated module from {module_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _kw_signature_lines(fields: list[PublicFieldSpec]) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for field in fields:
|
||||
@@ -1054,13 +1137,20 @@ def generate_public_api_flat_methods() -> None:
|
||||
_render_async_thread_block(turn_start_fields),
|
||||
)
|
||||
public_api_path.write_text(source)
|
||||
run_python_module("ruff", ["format", str(public_api_path)], cwd=sdk_root())
|
||||
|
||||
|
||||
def generate_types_from_schema_dir(schema_dir: Path) -> None:
|
||||
# v2_all is the authoritative generated surface.
|
||||
generate_v2_all(schema_dir)
|
||||
generate_notification_registry(schema_dir)
|
||||
generate_public_api_flat_methods()
|
||||
|
||||
|
||||
def generate_types() -> None:
|
||||
# v2_all is the authoritative generated surface.
|
||||
generate_v2_all()
|
||||
generate_notification_registry()
|
||||
generate_public_api_flat_methods()
|
||||
with tempfile.TemporaryDirectory(prefix="codex-python-schema-") as td:
|
||||
schema_dir = generate_schema_from_pinned_runtime(Path(td) / "schema")
|
||||
generate_types_from_schema_dir(schema_dir)
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
|
||||
@@ -22,12 +22,12 @@ from .generated.v2_all import (
|
||||
ReasoningSummary,
|
||||
SandboxMode,
|
||||
SandboxPolicy,
|
||||
ServiceTier,
|
||||
ThreadItem,
|
||||
ThreadForkParams,
|
||||
ThreadListParams,
|
||||
ThreadResumeParams,
|
||||
ThreadSortKey,
|
||||
ThreadSource,
|
||||
ThreadSourceKind,
|
||||
ThreadStartParams,
|
||||
ThreadTokenUsageUpdatedNotification,
|
||||
@@ -86,11 +86,11 @@ __all__ = [
|
||||
"ReasoningSummary",
|
||||
"SandboxMode",
|
||||
"SandboxPolicy",
|
||||
"ServiceTier",
|
||||
"ThreadStartParams",
|
||||
"ThreadResumeParams",
|
||||
"ThreadListParams",
|
||||
"ThreadSortKey",
|
||||
"ThreadSource",
|
||||
"ThreadSourceKind",
|
||||
"ThreadForkParams",
|
||||
"TurnStatus",
|
||||
|
||||
@@ -15,7 +15,6 @@ from .generated.v2_all import (
|
||||
ReasoningSummary,
|
||||
SandboxMode,
|
||||
SandboxPolicy,
|
||||
ServiceTier,
|
||||
SortDirection,
|
||||
ThreadArchiveResponse,
|
||||
ThreadCompactStartResponse,
|
||||
@@ -27,6 +26,7 @@ from .generated.v2_all import (
|
||||
ThreadResumeParams,
|
||||
ThreadSetNameResponse,
|
||||
ThreadSortKey,
|
||||
ThreadSource,
|
||||
ThreadSourceKind,
|
||||
ThreadStartSource,
|
||||
ThreadStartParams,
|
||||
@@ -152,8 +152,9 @@ class Codex:
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_name: str | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
session_start_source: ThreadStartSource | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> Thread:
|
||||
params = ThreadStartParams(
|
||||
approval_policy=approval_policy,
|
||||
@@ -170,6 +171,7 @@ class Codex:
|
||||
service_name=service_name,
|
||||
service_tier=service_tier,
|
||||
session_start_source=session_start_source,
|
||||
thread_source=thread_source,
|
||||
)
|
||||
started = self._client.thread_start(params)
|
||||
return Thread(self._client, started.thread.id)
|
||||
@@ -216,7 +218,7 @@ class Codex:
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
) -> Thread:
|
||||
params = ThreadResumeParams(
|
||||
thread_id=thread_id,
|
||||
@@ -249,7 +251,8 @@ class Codex:
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> Thread:
|
||||
params = ThreadForkParams(
|
||||
thread_id=thread_id,
|
||||
@@ -264,6 +267,7 @@ class Codex:
|
||||
model_provider=model_provider,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
thread_source=thread_source,
|
||||
)
|
||||
forked = self._client.thread_fork(thread_id, params)
|
||||
return Thread(self._client, forked.thread.id)
|
||||
@@ -349,8 +353,9 @@ class AsyncCodex:
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_name: str | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
session_start_source: ThreadStartSource | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> AsyncThread:
|
||||
await self._ensure_initialized()
|
||||
params = ThreadStartParams(
|
||||
@@ -368,6 +373,7 @@ class AsyncCodex:
|
||||
service_name=service_name,
|
||||
service_tier=service_tier,
|
||||
session_start_source=session_start_source,
|
||||
thread_source=thread_source,
|
||||
)
|
||||
started = await self._client.thread_start(params)
|
||||
return AsyncThread(self, started.thread.id)
|
||||
@@ -415,7 +421,7 @@ class AsyncCodex:
|
||||
model_provider: str | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
) -> AsyncThread:
|
||||
await self._ensure_initialized()
|
||||
params = ThreadResumeParams(
|
||||
@@ -449,7 +455,8 @@ class AsyncCodex:
|
||||
model: str | None = None,
|
||||
model_provider: str | None = None,
|
||||
sandbox: SandboxMode | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
thread_source: ThreadSource | None = None,
|
||||
) -> AsyncThread:
|
||||
await self._ensure_initialized()
|
||||
params = ThreadForkParams(
|
||||
@@ -465,6 +472,7 @@ class AsyncCodex:
|
||||
model_provider=model_provider,
|
||||
sandbox=sandbox,
|
||||
service_tier=service_tier,
|
||||
thread_source=thread_source,
|
||||
)
|
||||
forked = await self._client.thread_fork(thread_id, params)
|
||||
return AsyncThread(self, forked.thread.id)
|
||||
@@ -502,7 +510,7 @@ class Thread:
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> RunResult:
|
||||
turn = self.turn(
|
||||
@@ -537,7 +545,7 @@ class Thread:
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> TurnHandle:
|
||||
wire_input = _to_wire_input(input)
|
||||
@@ -587,7 +595,7 @@ class AsyncThread:
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> RunResult:
|
||||
turn = await self.turn(
|
||||
@@ -622,7 +630,7 @@ class AsyncThread:
|
||||
output_schema: JsonObject | None = None,
|
||||
personality: Personality | None = None,
|
||||
sandbox_policy: SandboxPolicy | None = None,
|
||||
service_tier: ServiceTier | None = None,
|
||||
service_tier: str | None = None,
|
||||
summary: ReasoningSummary | None = None,
|
||||
) -> AsyncTurnHandle:
|
||||
await self._codex._ensure_initialized()
|
||||
|
||||
@@ -35,6 +35,8 @@ from .v2_all import McpToolCallProgressNotification
|
||||
from .v2_all import ModelReroutedNotification
|
||||
from .v2_all import ModelVerificationNotification
|
||||
from .v2_all import PlanDeltaNotification
|
||||
from .v2_all import ProcessExitedNotification
|
||||
from .v2_all import ProcessOutputDeltaNotification
|
||||
from .v2_all import ReasoningSummaryPartAddedNotification
|
||||
from .v2_all import ReasoningSummaryTextDeltaNotification
|
||||
from .v2_all import ReasoningTextDeltaNotification
|
||||
@@ -101,6 +103,8 @@ NOTIFICATION_MODELS: dict[str, type[BaseModel]] = {
|
||||
"mcpServer/startupStatus/updated": McpServerStatusUpdatedNotification,
|
||||
"model/rerouted": ModelReroutedNotification,
|
||||
"model/verification": ModelVerificationNotification,
|
||||
"process/exited": ProcessExitedNotification,
|
||||
"process/outputDelta": ProcessOutputDeltaNotification,
|
||||
"remoteControl/status/changed": RemoteControlStatusChangedNotification,
|
||||
"serverRequest/resolved": ServerRequestResolvedNotification,
|
||||
"skills/changed": SkillsChangedNotification,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,7 +52,8 @@ def test_generate_types_wires_all_generation_steps() -> None:
|
||||
(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.FunctionDef) and node.name == "generate_types"
|
||||
if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "generate_types_from_schema_dir"
|
||||
),
|
||||
None,
|
||||
)
|
||||
@@ -72,19 +73,17 @@ def test_generate_types_wires_all_generation_steps() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_schema_normalization_only_flattens_string_literal_oneofs() -> None:
|
||||
def _load_runtime_schema_bundle(tmp_path: Path) -> dict:
|
||||
script = _load_update_script_module()
|
||||
schema = json.loads(
|
||||
(
|
||||
ROOT.parent.parent
|
||||
/ "codex-rs"
|
||||
/ "app-server-protocol"
|
||||
/ "schema"
|
||||
/ "json"
|
||||
/ "codex_app_server_protocol.v2.schemas.json"
|
||||
).read_text()
|
||||
)
|
||||
schema_dir = script.generate_schema_from_pinned_runtime(tmp_path / "schema")
|
||||
return json.loads(script.schema_bundle_path(schema_dir).read_text())
|
||||
|
||||
|
||||
def test_schema_normalization_only_flattens_string_literal_oneofs(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
script = _load_update_script_module()
|
||||
schema = _load_runtime_schema_bundle(tmp_path)
|
||||
definitions = schema["definitions"]
|
||||
flattened = [
|
||||
name
|
||||
@@ -94,27 +93,22 @@ def test_schema_normalization_only_flattens_string_literal_oneofs() -> None:
|
||||
]
|
||||
|
||||
assert flattened == [
|
||||
"AuthMode",
|
||||
"CommandExecOutputStream",
|
||||
"ExperimentalFeatureStage",
|
||||
"InputModality",
|
||||
"MessagePhase",
|
||||
"TurnItemsView",
|
||||
"PluginAvailability",
|
||||
"AuthMode",
|
||||
"InputModality",
|
||||
"ExperimentalFeatureStage",
|
||||
"CommandExecOutputStream",
|
||||
"ProcessOutputStream",
|
||||
]
|
||||
|
||||
|
||||
def test_python_codegen_schema_annotation_adds_stable_variant_titles() -> None:
|
||||
def test_python_codegen_schema_annotation_adds_stable_variant_titles(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
script = _load_update_script_module()
|
||||
schema = json.loads(
|
||||
(
|
||||
ROOT.parent.parent
|
||||
/ "codex-rs"
|
||||
/ "app-server-protocol"
|
||||
/ "schema"
|
||||
/ "json"
|
||||
/ "codex_app_server_protocol.v2.schemas.json"
|
||||
).read_text()
|
||||
)
|
||||
|
||||
schema = _load_runtime_schema_bundle(tmp_path)
|
||||
script._annotate_schema(schema)
|
||||
definitions = schema["definitions"]
|
||||
|
||||
@@ -186,6 +180,25 @@ def test_runtime_distribution_name_is_consistent() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_source_sdk_package_pins_published_runtime() -> None:
|
||||
"""The source package metadata should pin the runtime wheel that ships schemas."""
|
||||
script = _load_update_script_module()
|
||||
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
||||
|
||||
assert {
|
||||
"sdk_version": pyproject["project"]["version"],
|
||||
"runtime_pin": script.pinned_runtime_version(),
|
||||
"dependencies": pyproject["project"]["dependencies"],
|
||||
} == {
|
||||
"sdk_version": "0.131.0a4",
|
||||
"runtime_pin": "0.131.0a4",
|
||||
"dependencies": [
|
||||
"pydantic>=2.12",
|
||||
"openai-codex-cli-bin==0.131.0a4",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_release_metadata_retries_without_invalid_auth(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -212,22 +225,6 @@ def test_release_metadata_retries_without_invalid_auth(
|
||||
assert authorizations == ["Bearer invalid-token", None]
|
||||
|
||||
|
||||
def test_source_sdk_package_pins_published_runtime() -> None:
|
||||
"""The source package metadata should pin the runtime wheel that ships schemas."""
|
||||
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
||||
|
||||
assert {
|
||||
"sdk_version": pyproject["project"]["version"],
|
||||
"dependencies": pyproject["project"]["dependencies"],
|
||||
} == {
|
||||
"sdk_version": "0.131.0a4",
|
||||
"dependencies": [
|
||||
"pydantic>=2.12",
|
||||
"openai-codex-cli-bin==0.131.0a4",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_runtime_setup_uses_pep440_package_version_and_codex_release_tags() -> None:
|
||||
"""The SDK uses PEP 440 package pins and converts only when fetching releases."""
|
||||
runtime_setup = _load_runtime_setup_module()
|
||||
@@ -422,9 +419,7 @@ def test_runtime_resource_binaries_are_included_by_wheel_config(
|
||||
pyproject = tomllib.loads((staged / "pyproject.toml").read_text())
|
||||
assert {
|
||||
"include": pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["include"],
|
||||
"helper": (
|
||||
staged / "src" / "codex_cli_bin" / "bin" / "helper"
|
||||
).read_text(),
|
||||
"helper": (staged / "src" / "codex_cli_bin" / "bin" / "helper").read_text(),
|
||||
} == {
|
||||
"include": ["src/codex_cli_bin/bin/**"],
|
||||
"helper": "fake helper\n",
|
||||
|
||||
@@ -66,6 +66,7 @@ def test_thread_resume_response_accepts_auto_review_reviewer() -> None:
|
||||
"id": "thread-1",
|
||||
"modelProvider": "openai",
|
||||
"preview": "",
|
||||
"sessionId": "session-1",
|
||||
"source": "cli",
|
||||
"status": {"type": "idle"},
|
||||
"turns": [],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -29,15 +30,19 @@ def _snapshot_target(root: Path, rel_path: Path) -> dict[str, bytes] | bytes | N
|
||||
|
||||
def _snapshot_targets(root: Path) -> dict[str, dict[str, bytes] | bytes | None]:
|
||||
return {
|
||||
str(rel_path): _snapshot_target(root, rel_path) for rel_path in GENERATED_TARGETS
|
||||
str(rel_path): _snapshot_target(root, rel_path)
|
||||
for rel_path in GENERATED_TARGETS
|
||||
}
|
||||
|
||||
|
||||
def test_generated_files_are_up_to_date():
|
||||
before = _snapshot_targets(ROOT)
|
||||
|
||||
# Regenerate contract artifacts via single maintenance entrypoint.
|
||||
# Regenerate contract artifacts via the pinned runtime package, not a local
|
||||
# app-server binary from the checkout or CI environment.
|
||||
assert importlib.metadata.version("openai-codex-cli-bin") == "0.131.0a4"
|
||||
env = os.environ.copy()
|
||||
env.pop("CODEX_EXEC_PATH", None)
|
||||
python_bin = str(Path(sys.executable).parent)
|
||||
env["PATH"] = f"{python_bin}{os.pathsep}{env.get('PATH', '')}"
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ def _item_completed_notification(
|
||||
method="item/completed",
|
||||
payload=ItemCompletedNotification.model_validate(
|
||||
{
|
||||
"completedAtMs": 1,
|
||||
"item": item,
|
||||
"threadId": thread_id,
|
||||
"turnId": turn_id,
|
||||
|
||||
@@ -70,6 +70,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"service_name",
|
||||
"service_tier",
|
||||
"session_start_source",
|
||||
"thread_source",
|
||||
],
|
||||
Codex.thread_list: [
|
||||
"archived",
|
||||
@@ -108,6 +109,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"model_provider",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"thread_source",
|
||||
],
|
||||
Thread.turn: [
|
||||
"approval_policy",
|
||||
@@ -148,6 +150,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"service_name",
|
||||
"service_tier",
|
||||
"session_start_source",
|
||||
"thread_source",
|
||||
],
|
||||
AsyncCodex.thread_list: [
|
||||
"archived",
|
||||
@@ -186,6 +189,7 @@ def test_generated_public_signatures_are_snake_case_and_typed() -> None:
|
||||
"model_provider",
|
||||
"sandbox",
|
||||
"service_tier",
|
||||
"thread_source",
|
||||
],
|
||||
AsyncThread.turn: [
|
||||
"approval_policy",
|
||||
|
||||
Reference in New Issue
Block a user