mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
## Summary - Treat `sdk/python` as a development template with source version `0.0.0-dev`, matching the existing Python runtime packaging pattern. - Have `python-v*` tags supply the published SDK beta version through the existing `stage-sdk --sdk-version` path. - Remove the workflow check requiring a source version bump for each beta release and remove its now-unused host Python setup step. - Keep the reviewed runtime dependency pin at `openai-codex-cli-bin==0.132.0`. - Remove beta-number-specific documentation so it does not need editing for each publish. ## Why The package staging script already writes the release version into the artifact. Requiring the checked-in SDK template version to match every tag adds release-only source churn without changing the package users receive. ## Validation - Not run locally; relying on online CI for this workflow and metadata change. ## Release After this PR lands, publish the next beta by pushing tag `python-v0.1.0b2` from merged `main`.
784 lines
28 KiB
Python
784 lines
28 KiB
Python
import ast
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import tarfile
|
|
import urllib.error
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import tomllib
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _load_update_script_module():
|
|
"""Load the maintenance script as a module so tests exercise real helpers."""
|
|
script_path = ROOT / "scripts" / "update_sdk_artifacts.py"
|
|
spec = importlib.util.spec_from_file_location("update_sdk_artifacts", script_path)
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError(f"Failed to load script module: {script_path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _load_runtime_setup_module():
|
|
"""Load runtime setup without importing the SDK package under test."""
|
|
runtime_setup_path = ROOT / "_runtime_setup.py"
|
|
spec = importlib.util.spec_from_file_location("_runtime_setup", runtime_setup_path)
|
|
if spec is None or spec.loader is None:
|
|
raise AssertionError(f"Failed to load runtime setup module: {runtime_setup_path}")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _write_fake_codex_package(package_dir: Path, script) -> Path:
|
|
(package_dir / "bin").mkdir(parents=True)
|
|
(package_dir / "codex-resources").mkdir()
|
|
(package_dir / "codex-path").mkdir()
|
|
(package_dir / "codex-package.json").write_text('{"variant":"codex"}\n')
|
|
(package_dir / "bin" / script.runtime_binary_name()).write_text("fake codex\n")
|
|
(package_dir / "codex-resources" / "bwrap").write_text("fake bwrap\n")
|
|
(package_dir / "codex-path" / "rg").write_text("fake rg\n")
|
|
return package_dir
|
|
|
|
|
|
def _write_fake_codex_package_archive(tmp_path: Path, script) -> Path:
|
|
package_dir = _write_fake_codex_package(tmp_path / "codex-package", script)
|
|
archive_path = tmp_path / "codex-package.tar.gz"
|
|
_write_package_archive(package_dir, archive_path)
|
|
return archive_path
|
|
|
|
|
|
def _write_package_archive(package_dir: Path, archive_path: Path) -> None:
|
|
with tarfile.open(archive_path, "w:gz") as archive:
|
|
for path in package_dir.rglob("*"):
|
|
archive.add(path, arcname=path.relative_to(package_dir))
|
|
|
|
|
|
def test_generation_has_single_maintenance_entrypoint_script() -> None:
|
|
"""Keep artifact workflows routed through one script instead of side entrypoints."""
|
|
scripts = sorted(p.name for p in (ROOT / "scripts").glob("*.py"))
|
|
assert scripts == ["update_sdk_artifacts.py"]
|
|
|
|
|
|
def test_root_fmt_recipe_formats_rust_and_python_sdk() -> None:
|
|
"""The repo fmt command should work from Rust and Python SDK directories."""
|
|
justfile = ROOT.parents[1] / "justfile"
|
|
lines = justfile.read_text().splitlines()
|
|
fmt_index = lines.index("fmt:")
|
|
next_recipe_index = next(
|
|
index
|
|
for index in range(fmt_index + 1, len(lines))
|
|
if lines[index] and not lines[index].startswith((" ", "\t", "#"))
|
|
)
|
|
fmt_recipe = lines[fmt_index:next_recipe_index]
|
|
actual = {
|
|
"working_directory": lines[0],
|
|
"previous_attribute": lines[fmt_index - 1],
|
|
"commands": [line.strip() for line in fmt_recipe[1:] if line.strip()],
|
|
}
|
|
expected = {
|
|
"working_directory": 'set working-directory := "codex-rs"',
|
|
"previous_attribute": "# Format Rust and Python SDK code.",
|
|
"commands": [
|
|
"cargo fmt -- --config imports_granularity=Item 2>/dev/null",
|
|
"uv run --frozen --project ../sdk/python --extra dev ruff check --fix --fix-only ../sdk/python",
|
|
"uv run --frozen --project ../sdk/python --extra dev ruff format ../sdk/python",
|
|
],
|
|
}
|
|
|
|
assert actual == expected, (
|
|
"The root `just fmt` recipe must run Rust fmt and Python SDK Ruff. "
|
|
"Fix the `fmt` recipe in `justfile`, then run `just fmt`.\n"
|
|
f"Expected: {json.dumps(expected, indent=2)}\n"
|
|
f"Actual: {json.dumps(actual, indent=2)}"
|
|
)
|
|
|
|
|
|
def test_generate_types_wires_all_generation_steps() -> None:
|
|
"""The type generation command should refresh every schema-derived artifact."""
|
|
source = (ROOT / "scripts" / "update_sdk_artifacts.py").read_text()
|
|
tree = ast.parse(source)
|
|
|
|
generate_types_fn = next(
|
|
(
|
|
node
|
|
for node in tree.body
|
|
if isinstance(node, ast.FunctionDef) and node.name == "generate_types_from_schema_dir"
|
|
),
|
|
None,
|
|
)
|
|
assert generate_types_fn is not None
|
|
|
|
calls: list[str] = []
|
|
for node in generate_types_fn.body:
|
|
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
|
|
fn = node.value.func
|
|
if isinstance(fn, ast.Name):
|
|
calls.append(fn.id)
|
|
|
|
assert calls == [
|
|
"generate_v2_all",
|
|
"generate_notification_registry",
|
|
"generate_public_api_flat_methods",
|
|
]
|
|
|
|
|
|
def _load_runtime_schema_bundle(tmp_path: Path) -> dict:
|
|
"""Ask the pinned runtime package for a real schema bundle used by tests."""
|
|
script = _load_update_script_module()
|
|
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:
|
|
"""Schema normalization should only flatten the enum-shaped oneOf variants."""
|
|
script = _load_update_script_module()
|
|
schema = _load_runtime_schema_bundle(tmp_path)
|
|
definitions = schema["definitions"]
|
|
flattened = [
|
|
name
|
|
for name, definition in definitions.items()
|
|
if isinstance(definition, dict) and script._flatten_string_enum_one_of(definition.copy())
|
|
]
|
|
|
|
assert flattened == [
|
|
"MessagePhase",
|
|
"TurnItemsView",
|
|
"PluginAvailability",
|
|
"AuthMode",
|
|
"InputModality",
|
|
"ExperimentalFeatureStage",
|
|
"ProcessOutputStream",
|
|
"CommandExecOutputStream",
|
|
]
|
|
|
|
|
|
def test_python_codegen_schema_annotation_adds_stable_variant_titles(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Schema annotations should give generated protocol classes stable names."""
|
|
script = _load_update_script_module()
|
|
schema = _load_runtime_schema_bundle(tmp_path)
|
|
script._annotate_schema(schema)
|
|
definitions = schema["definitions"]
|
|
|
|
server_notification_titles = {
|
|
variant.get("title")
|
|
for variant in definitions["ServerNotification"]["oneOf"]
|
|
if isinstance(variant, dict)
|
|
}
|
|
assert "ErrorServerNotification" in server_notification_titles
|
|
assert "ThreadStartedServerNotification" in server_notification_titles
|
|
assert "ErrorNotification" not in server_notification_titles
|
|
assert "Thread/startedNotification" not in server_notification_titles
|
|
|
|
ask_for_approval_titles = [
|
|
variant.get("title") for variant in definitions["AskForApproval"]["oneOf"]
|
|
]
|
|
assert ask_for_approval_titles == [
|
|
"AskForApprovalValue",
|
|
"GranularAskForApproval",
|
|
]
|
|
|
|
reasoning_summary_titles = [
|
|
variant.get("title") for variant in definitions["ReasoningSummary"]["oneOf"]
|
|
]
|
|
assert reasoning_summary_titles == [
|
|
"ReasoningSummaryValue",
|
|
"NoneReasoningSummary",
|
|
]
|
|
|
|
|
|
def test_generate_v2_all_uses_titles_for_generated_names() -> None:
|
|
source = (ROOT / "scripts" / "update_sdk_artifacts.py").read_text()
|
|
assert "--use-title-as-name" in source
|
|
assert "--use-annotated" in source
|
|
assert "--formatters" in source
|
|
assert "ruff-format" in source
|
|
|
|
|
|
def test_runtime_package_template_has_no_checked_in_binaries() -> None:
|
|
runtime_root = ROOT.parent / "python-runtime" / "src" / "codex_cli_bin"
|
|
assert sorted(
|
|
path.name
|
|
for path in runtime_root.rglob("*")
|
|
if path.is_file() and "__pycache__" not in path.parts
|
|
) == ["__init__.py"]
|
|
|
|
|
|
def test_examples_readme_points_to_runtime_version_source_of_truth() -> None:
|
|
"""Document that examples should point at the dependency pin, not release lore."""
|
|
readme = (ROOT / "examples" / "README.md").read_text()
|
|
assert "The pinned runtime version comes from the SDK package dependency." in readme
|
|
|
|
|
|
def test_runtime_distribution_name_is_consistent() -> None:
|
|
script = _load_update_script_module()
|
|
runtime_setup = _load_runtime_setup_module()
|
|
from openai_codex import _version, client as client_module
|
|
|
|
assert script.SDK_DISTRIBUTION_NAME == "openai-codex"
|
|
assert runtime_setup.SDK_PACKAGE_NAME == "openai-codex"
|
|
assert _version.DISTRIBUTION_NAME == "openai-codex"
|
|
assert script.RUNTIME_DISTRIBUTION_NAME == "openai-codex-cli-bin"
|
|
assert runtime_setup.PACKAGE_NAME == "openai-codex-cli-bin"
|
|
assert client_module.RUNTIME_PKG_NAME == "openai-codex-cli-bin"
|
|
assert (
|
|
"importlib.metadata.version('codex-cli-bin')"
|
|
not in (ROOT / "_runtime_setup.py").read_text()
|
|
)
|
|
|
|
|
|
def test_source_sdk_template_pins_published_runtime() -> None:
|
|
"""The source template should carry a development version and reviewed runtime pin."""
|
|
script = _load_update_script_module()
|
|
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
|
|
|
assert {
|
|
"sdk_template_version": pyproject["project"]["version"],
|
|
"runtime_pin": script.pinned_runtime_version(),
|
|
"dependencies": pyproject["project"]["dependencies"],
|
|
} == {
|
|
"sdk_template_version": "0.0.0-dev",
|
|
"runtime_pin": "0.132.0",
|
|
"dependencies": [
|
|
"pydantic>=2.12",
|
|
"openai-codex-cli-bin==0.132.0",
|
|
],
|
|
}
|
|
|
|
|
|
def test_source_sdk_package_declares_beta_documentation_and_release_files() -> None:
|
|
"""Public package metadata should link beta docs and ship package metadata."""
|
|
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
|
readme = (ROOT / "README.md").read_text()
|
|
|
|
assert {
|
|
"description": pyproject["project"]["description"],
|
|
"is_beta": "Development Status :: 4 - Beta" in pyproject["project"]["classifiers"],
|
|
"license": pyproject["project"]["license"],
|
|
"documentation": pyproject["project"]["urls"]["Documentation"],
|
|
"sdist_include": pyproject["tool"]["hatch"]["build"]["targets"]["sdist"]["include"],
|
|
"readme_is_beta": "# OpenAI Codex Python SDK (Beta)" in readme,
|
|
"local_license_file": (ROOT / "LICENSE").exists(),
|
|
} == {
|
|
"description": "Python SDK for Codex",
|
|
"is_beta": True,
|
|
"license": "Apache-2.0",
|
|
"documentation": "https://github.com/openai/codex/tree/main/sdk/python/docs",
|
|
"sdist_include": [
|
|
"src/openai_codex/**",
|
|
"README.md",
|
|
"pyproject.toml",
|
|
],
|
|
"readme_is_beta": True,
|
|
"local_license_file": False,
|
|
}
|
|
|
|
|
|
def test_release_metadata_retries_without_invalid_auth(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
runtime_setup = _load_runtime_setup_module()
|
|
authorizations: list[str | None] = []
|
|
|
|
def fake_urlopen(request):
|
|
authorization = request.headers.get("Authorization")
|
|
authorizations.append(authorization)
|
|
if authorization is not None:
|
|
raise urllib.error.HTTPError(
|
|
request.full_url,
|
|
401,
|
|
"Unauthorized",
|
|
hdrs=None,
|
|
fp=None,
|
|
)
|
|
return io.StringIO('{"assets": []}')
|
|
|
|
monkeypatch.setenv("GH_TOKEN", "invalid-token")
|
|
monkeypatch.setattr(runtime_setup.urllib.request, "urlopen", fake_urlopen)
|
|
|
|
assert runtime_setup._release_metadata("1.2.3") == {"assets": []}
|
|
assert authorizations == ["Bearer invalid-token", None]
|
|
|
|
|
|
def test_runtime_setup_reads_independent_runtime_pin_and_release_tags() -> None:
|
|
"""Runtime package pins remain independent of the SDK beta version."""
|
|
runtime_setup = _load_runtime_setup_module()
|
|
pyproject = tomllib.loads((ROOT / "pyproject.toml").read_text())
|
|
|
|
assert {
|
|
"package_name": runtime_setup.PACKAGE_NAME,
|
|
"sdk_template_version": pyproject["project"]["version"],
|
|
"runtime_pin": runtime_setup.pinned_runtime_version(),
|
|
"normalized_release_version": runtime_setup._normalized_package_version(
|
|
"rust-v0.116.0-alpha.1"
|
|
),
|
|
"release_tag": runtime_setup._release_tag("0.116.0a1"),
|
|
} == {
|
|
"package_name": "openai-codex-cli-bin",
|
|
"sdk_template_version": "0.0.0-dev",
|
|
"runtime_pin": "0.132.0",
|
|
"normalized_release_version": "0.116.0a1",
|
|
"release_tag": "rust-v0.116.0-alpha.1",
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("system", "machine", "asset_name"),
|
|
[
|
|
("Darwin", "arm64", "codex-package-aarch64-apple-darwin.tar.gz"),
|
|
("Linux", "x86_64", "codex-package-x86_64-unknown-linux-musl.tar.gz"),
|
|
("Windows", "AMD64", "codex-package-x86_64-pc-windows-msvc.tar.gz"),
|
|
],
|
|
)
|
|
def test_runtime_setup_downloads_codex_package_archives(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
system: str,
|
|
machine: str,
|
|
asset_name: str,
|
|
) -> None:
|
|
runtime_setup = _load_runtime_setup_module()
|
|
monkeypatch.setattr(runtime_setup.platform, "system", lambda: system)
|
|
monkeypatch.setattr(runtime_setup.platform, "machine", lambda: machine)
|
|
|
|
assert runtime_setup.platform_asset_name() == asset_name
|
|
|
|
|
|
def test_runtime_package_is_wheel_only_and_builds_platform_specific_wheels() -> None:
|
|
pyproject = tomllib.loads((ROOT.parent / "python-runtime" / "pyproject.toml").read_text())
|
|
hook_source = (ROOT.parent / "python-runtime" / "hatch_build.py").read_text()
|
|
hook_tree = ast.parse(hook_source)
|
|
initialize_fn = next(
|
|
node
|
|
for node in ast.walk(hook_tree)
|
|
if isinstance(node, ast.FunctionDef) and node.name == "initialize"
|
|
)
|
|
|
|
sdist_guard = next(
|
|
(
|
|
node
|
|
for node in initialize_fn.body
|
|
if isinstance(node, ast.If)
|
|
and isinstance(node.test, ast.Compare)
|
|
and isinstance(node.test.left, ast.Attribute)
|
|
and isinstance(node.test.left.value, ast.Name)
|
|
and node.test.left.value.id == "self"
|
|
and node.test.left.attr == "target_name"
|
|
and len(node.test.ops) == 1
|
|
and isinstance(node.test.ops[0], ast.Eq)
|
|
and len(node.test.comparators) == 1
|
|
and isinstance(node.test.comparators[0], ast.Constant)
|
|
and node.test.comparators[0].value == "sdist"
|
|
),
|
|
None,
|
|
)
|
|
build_data_assignments = {}
|
|
for node in initialize_fn.body:
|
|
if (
|
|
not isinstance(node, ast.Assign)
|
|
or len(node.targets) != 1
|
|
or not isinstance(node.targets[0], ast.Subscript)
|
|
or not isinstance(node.targets[0].value, ast.Name)
|
|
or node.targets[0].value.id != "build_data"
|
|
or not isinstance(node.targets[0].slice, ast.Constant)
|
|
or not isinstance(node.targets[0].slice.value, str)
|
|
):
|
|
continue
|
|
if isinstance(node.value, ast.Constant):
|
|
build_data_assignments[node.targets[0].slice.value] = node.value.value
|
|
elif isinstance(node.value, ast.JoinedStr):
|
|
build_data_assignments[node.targets[0].slice.value] = "joined-string"
|
|
|
|
assert pyproject["project"]["name"] == "openai-codex-cli-bin"
|
|
assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"] == {
|
|
"packages": ["src/codex_cli_bin"],
|
|
"include": [
|
|
"src/codex_cli_bin/codex-package.json",
|
|
"src/codex_cli_bin/bin/**",
|
|
"src/codex_cli_bin/codex-resources/**",
|
|
"src/codex_cli_bin/codex-path/**",
|
|
],
|
|
"hooks": {"custom": {}},
|
|
}
|
|
assert pyproject["tool"]["hatch"]["build"]["targets"]["sdist"] == {
|
|
"hooks": {"custom": {}},
|
|
}
|
|
assert sdist_guard is not None
|
|
assert build_data_assignments == {
|
|
"pure_python": False,
|
|
"infer_tag": False,
|
|
"tag": "joined-string",
|
|
}
|
|
|
|
|
|
def test_stage_runtime_release_copies_package_layout_and_sets_version(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
script = _load_update_script_module()
|
|
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
|
|
|
staged = script.stage_python_runtime_package(
|
|
tmp_path / "runtime-stage",
|
|
"1.2.3",
|
|
package_archive,
|
|
)
|
|
package_root = script.staged_runtime_package_root(staged)
|
|
|
|
assert {
|
|
"metadata": (package_root / "codex-package.json").read_text(),
|
|
"codex": (package_root / "bin" / script.runtime_binary_name()).read_text(),
|
|
"bwrap": (package_root / "codex-resources" / "bwrap").read_text(),
|
|
"rg": (package_root / "codex-path" / "rg").read_text(),
|
|
} == {
|
|
"metadata": '{"variant":"codex"}\n',
|
|
"codex": "fake codex\n",
|
|
"bwrap": "fake bwrap\n",
|
|
"rg": "fake rg\n",
|
|
}
|
|
assert 'name = "openai-codex-cli-bin"' in (staged / "pyproject.toml").read_text()
|
|
assert 'version = "1.2.3"' in (staged / "pyproject.toml").read_text()
|
|
|
|
|
|
def test_normalize_codex_version_accepts_release_tags_and_pep440_versions() -> None:
|
|
script = _load_update_script_module()
|
|
|
|
assert script.normalize_codex_version("rust-v0.116.0-alpha.1") == "0.116.0a1"
|
|
assert script.normalize_codex_version("v0.116.0-beta.2") == "0.116.0b2"
|
|
assert script.normalize_codex_version("0.116.0rc3") == "0.116.0rc3"
|
|
assert script.normalize_codex_version("0.116.0") == "0.116.0"
|
|
|
|
|
|
def test_stage_runtime_release_replaces_existing_staging_dir(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
staging_dir = tmp_path / "runtime-stage"
|
|
old_file = staging_dir / "stale.txt"
|
|
old_file.parent.mkdir(parents=True)
|
|
old_file.write_text("stale")
|
|
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
|
|
|
staged = script.stage_python_runtime_package(
|
|
staging_dir,
|
|
"1.2.3",
|
|
package_archive,
|
|
)
|
|
|
|
assert staged == staging_dir
|
|
assert not old_file.exists()
|
|
package_root = script.staged_runtime_package_root(staged)
|
|
assert (package_root / "bin" / script.runtime_binary_name()).read_text() == "fake codex\n"
|
|
|
|
|
|
def test_stage_runtime_release_can_pin_wheel_platform_tag(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
|
|
|
staged = script.stage_python_runtime_package(
|
|
tmp_path / "runtime-stage",
|
|
"0.116.0a1",
|
|
package_archive,
|
|
platform_tag="musllinux_1_1_x86_64",
|
|
)
|
|
|
|
pyproject = (staged / "pyproject.toml").read_text()
|
|
assert 'platform-tag = "musllinux_1_1_x86_64"' in pyproject
|
|
|
|
|
|
def test_stage_runtime_release_rejects_incomplete_package_layout(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
package_dir = tmp_path / "codex-package"
|
|
(package_dir / "bin").mkdir(parents=True)
|
|
package_archive = tmp_path / "codex-package.tar.gz"
|
|
_write_package_archive(package_dir, package_archive)
|
|
|
|
with pytest.raises(RuntimeError, match="Missing Codex package layout entries"):
|
|
script.stage_python_runtime_package(tmp_path / "runtime-stage", "1.2.3", package_archive)
|
|
|
|
|
|
def test_runtime_package_layout_is_included_by_wheel_config(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
script = _load_update_script_module()
|
|
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
|
|
|
staged = script.stage_python_runtime_package(
|
|
tmp_path / "runtime-stage",
|
|
"1.2.3",
|
|
package_archive,
|
|
)
|
|
|
|
pyproject = tomllib.loads((staged / "pyproject.toml").read_text())
|
|
assert pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["include"] == [
|
|
"src/codex_cli_bin/codex-package.json",
|
|
"src/codex_cli_bin/bin/**",
|
|
"src/codex_cli_bin/codex-resources/**",
|
|
"src/codex_cli_bin/codex-path/**",
|
|
]
|
|
|
|
|
|
def test_stage_sdk_release_preserves_reviewed_runtime_pin(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
staged = script.stage_python_sdk_package(
|
|
tmp_path / "sdk-stage",
|
|
"0.1.0b1",
|
|
)
|
|
|
|
pyproject = tomllib.loads((staged / "pyproject.toml").read_text())
|
|
assert {
|
|
"name": pyproject["project"]["name"],
|
|
"version": pyproject["project"]["version"],
|
|
"dependencies": pyproject["project"]["dependencies"],
|
|
} == {
|
|
"name": "openai-codex",
|
|
"version": "0.1.0b1",
|
|
"dependencies": [
|
|
"pydantic>=2.12",
|
|
"openai-codex-cli-bin==0.132.0",
|
|
],
|
|
}
|
|
assert (
|
|
'__version__ = "0.1.0b1"'
|
|
not in (staged / "src" / "openai_codex" / "__init__.py").read_text()
|
|
)
|
|
assert (
|
|
'client_version: str = "0.1.0b1"'
|
|
not in (staged / "src" / "openai_codex" / "client.py").read_text()
|
|
)
|
|
assert not any((staged / "src" / "openai_codex").glob("bin/**"))
|
|
|
|
|
|
def test_stage_sdk_release_replaces_existing_staging_dir(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
staging_dir = tmp_path / "sdk-stage"
|
|
old_file = staging_dir / "stale.txt"
|
|
old_file.parent.mkdir(parents=True)
|
|
old_file.write_text("stale")
|
|
|
|
staged = script.stage_python_sdk_package(staging_dir, "0.1.0b1")
|
|
|
|
assert staged == staging_dir
|
|
assert not old_file.exists()
|
|
|
|
|
|
def test_sdk_beta_release_can_pin_stable_runtime(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
|
|
|
sdk_stage = script.stage_python_sdk_package(
|
|
tmp_path / "sdk-stage",
|
|
"0.1.0b1",
|
|
)
|
|
runtime_stage = script.stage_python_runtime_package(
|
|
tmp_path / "runtime-stage",
|
|
"0.132.0",
|
|
package_archive,
|
|
)
|
|
|
|
sdk_pyproject = tomllib.loads((sdk_stage / "pyproject.toml").read_text())
|
|
runtime_pyproject = tomllib.loads((runtime_stage / "pyproject.toml").read_text())
|
|
|
|
assert {
|
|
"sdk_version": sdk_pyproject["project"]["version"],
|
|
"runtime_version": runtime_pyproject["project"]["version"],
|
|
"sdk_dependencies": sdk_pyproject["project"]["dependencies"],
|
|
} == {
|
|
"sdk_version": "0.1.0b1",
|
|
"runtime_version": "0.132.0",
|
|
"sdk_dependencies": [
|
|
"pydantic>=2.12",
|
|
"openai-codex-cli-bin==0.132.0",
|
|
],
|
|
}
|
|
|
|
|
|
def test_stage_sdk_runs_type_generation_before_staging(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
calls: list[str] = []
|
|
args = script.parse_args(
|
|
[
|
|
"stage-sdk",
|
|
str(tmp_path / "sdk-stage"),
|
|
"--sdk-version",
|
|
"0.1.0b1",
|
|
]
|
|
)
|
|
|
|
def fake_generate_types() -> None:
|
|
calls.append("generate_types")
|
|
|
|
def fake_stage_sdk_package(_staging_dir: Path, sdk_version: str) -> Path:
|
|
calls.append(f"stage_sdk:{sdk_version}")
|
|
return tmp_path / "sdk-stage"
|
|
|
|
def fake_stage_runtime_package(
|
|
_staging_dir: Path,
|
|
_runtime_version: str,
|
|
_package_dir: Path,
|
|
_platform_tag: str | None,
|
|
) -> Path:
|
|
raise AssertionError("runtime staging should not run for stage-sdk")
|
|
|
|
def fake_current_sdk_version() -> str:
|
|
return "0.116.0a1"
|
|
|
|
ops = script.CliOps(
|
|
generate_types=fake_generate_types,
|
|
stage_python_sdk_package=fake_stage_sdk_package,
|
|
stage_python_runtime_package=fake_stage_runtime_package,
|
|
current_sdk_version=fake_current_sdk_version,
|
|
)
|
|
|
|
script.run_command(args, ops)
|
|
|
|
assert calls == ["generate_types", "stage_sdk:0.1.0b1"]
|
|
|
|
|
|
def test_stage_runtime_stages_package_without_type_generation(tmp_path: Path) -> None:
|
|
script = _load_update_script_module()
|
|
package_archive = _write_fake_codex_package_archive(tmp_path, script)
|
|
calls: list[str] = []
|
|
args = script.parse_args(
|
|
[
|
|
"stage-runtime",
|
|
str(tmp_path / "runtime-stage"),
|
|
str(package_archive),
|
|
"--codex-version",
|
|
"rust-v0.116.0-alpha.1",
|
|
"--platform-tag",
|
|
"musllinux_1_1_x86_64",
|
|
]
|
|
)
|
|
|
|
def fake_generate_types() -> None:
|
|
calls.append("generate_types")
|
|
|
|
def fake_stage_sdk_package(_staging_dir: Path, _codex_version: str) -> Path:
|
|
raise AssertionError("sdk staging should not run for stage-runtime")
|
|
|
|
def fake_stage_runtime_package(
|
|
_staging_dir: Path,
|
|
codex_version: str,
|
|
package_archive: Path,
|
|
platform_tag: str | None,
|
|
) -> Path:
|
|
calls.append(f"stage_runtime:{codex_version}:{platform_tag}:{package_archive.name}")
|
|
return tmp_path / "runtime-stage"
|
|
|
|
def fake_current_sdk_version() -> str:
|
|
return "0.116.0a1"
|
|
|
|
ops = script.CliOps(
|
|
generate_types=fake_generate_types,
|
|
stage_python_sdk_package=fake_stage_sdk_package,
|
|
stage_python_runtime_package=fake_stage_runtime_package,
|
|
current_sdk_version=fake_current_sdk_version,
|
|
)
|
|
|
|
script.run_command(args, ops)
|
|
|
|
assert calls == ["stage_runtime:0.116.0a1:musllinux_1_1_x86_64:codex-package.tar.gz"]
|
|
|
|
|
|
def test_default_runtime_is_resolved_from_installed_runtime_package(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
from openai_codex import client as client_module
|
|
|
|
fake_binary = tmp_path / ("codex.exe" if client_module.os.name == "nt" else "codex")
|
|
fake_binary.write_text("")
|
|
ops = client_module.CodexBinResolverOps(
|
|
installed_codex_path=lambda: fake_binary,
|
|
path_exists=lambda path: path == fake_binary,
|
|
)
|
|
|
|
config = client_module.CodexConfig()
|
|
assert config.codex_bin is None
|
|
assert client_module.resolve_codex_bin(config, ops) == fake_binary
|
|
|
|
|
|
def test_runtime_path_dir_is_prepended_without_duplicates(tmp_path: Path) -> None:
|
|
from openai_codex import client as client_module
|
|
|
|
path_dir = tmp_path / "codex-path"
|
|
env = {"PATH": os.pathsep.join(["/usr/bin", str(path_dir), "/bin"])}
|
|
|
|
client_module._prepend_path_dirs(env, (path_dir,))
|
|
|
|
assert env["PATH"] == os.pathsep.join([str(path_dir), "/usr/bin", "/bin"])
|
|
|
|
|
|
def test_runtime_path_dir_preserves_windows_path_key(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
from openai_codex import client as client_module
|
|
|
|
path_dir = tmp_path / "codex-path"
|
|
monkeypatch.setattr(client_module.os, "name", "nt")
|
|
env = {
|
|
"PATH": "/usr/bin",
|
|
"Path": os.pathsep.join(["C\\Windows", str(path_dir)]),
|
|
}
|
|
|
|
client_module._prepend_path_dirs(env, (path_dir,))
|
|
|
|
assert env == {"Path": os.pathsep.join([str(path_dir), "C\\Windows"])}
|
|
|
|
|
|
def test_explicit_codex_bin_override_takes_priority(tmp_path: Path) -> None:
|
|
from openai_codex import client as client_module
|
|
|
|
explicit_binary = tmp_path / (
|
|
"custom-codex.exe" if client_module.os.name == "nt" else "custom-codex"
|
|
)
|
|
explicit_binary.write_text("")
|
|
ops = client_module.CodexBinResolverOps(
|
|
installed_codex_path=lambda: (_ for _ in ()).throw(
|
|
AssertionError("packaged runtime should not be used")
|
|
),
|
|
path_exists=lambda path: path == explicit_binary,
|
|
)
|
|
|
|
config = client_module.CodexConfig(codex_bin=str(explicit_binary))
|
|
assert client_module.resolve_codex_bin(config, ops) == explicit_binary
|
|
|
|
|
|
def test_missing_runtime_package_requires_explicit_codex_bin() -> None:
|
|
from openai_codex import client as client_module
|
|
|
|
ops = client_module.CodexBinResolverOps(
|
|
installed_codex_path=lambda: (_ for _ in ()).throw(
|
|
FileNotFoundError("missing packaged runtime")
|
|
),
|
|
path_exists=lambda _path: False,
|
|
)
|
|
|
|
with pytest.raises(FileNotFoundError, match="missing packaged runtime"):
|
|
client_module.resolve_codex_bin(client_module.CodexConfig(), ops)
|
|
|
|
|
|
def test_broken_runtime_package_does_not_fall_back() -> None:
|
|
from openai_codex import client as client_module
|
|
|
|
ops = client_module.CodexBinResolverOps(
|
|
installed_codex_path=lambda: (_ for _ in ()).throw(
|
|
FileNotFoundError("missing packaged binary")
|
|
),
|
|
path_exists=lambda _path: False,
|
|
)
|
|
|
|
with pytest.raises(FileNotFoundError) as exc_info:
|
|
client_module.resolve_codex_bin(client_module.CodexConfig(), ops)
|
|
|
|
assert str(exc_info.value) == ("missing packaged binary")
|