mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
Test Python SDK against the built CLI and installed runtime (#44053)
## Why Python SDK CI needs to exercise the checkout's CLI while retaining coverage of the wheel installation and its default runtime. ## What changed - Run the Python SDK test suite against the same Bazel-built CLI as the TypeScript SDK tests. Build and stage `codex-code-mode-host` alongside the CLI so it can be discovered. - Make the Python test harness prefer `CODEX_EXEC_PATH`, then a local debug build, then the installed runtime. Exclude optional turn-cost probes from recorded model requests. - Add a separate installation job that builds the Python SDK wheel and installs it in a fresh environment. ## Testing The installation smoke test verifies that imports resolve to the installed SDK and that its default runtime completes a mocked turn with the expected user input and final response. GitOrigin-RevId: 8e3126742a014e2cf042afe799bc657e1ea2a282
This commit is contained in:
77
.github/workflows/sdk.yml
vendored
77
.github/workflows/sdk.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
python-sdk:
|
||||
python-sdk-installation:
|
||||
runs-on:
|
||||
group: ${{ github.event.repository.name }}-runners
|
||||
labels: ${{ github.event.repository.name }}-linux-x64
|
||||
@@ -15,13 +15,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Test Python SDK
|
||||
- name: Test the installed Python SDK and its default runtime
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Run inside a glibc Linux image so dependency resolution exercises
|
||||
# the pinned manylinux runtime wheel that users install.
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
-e HOME=/tmp/codex-python-sdk-home \
|
||||
@@ -30,23 +27,19 @@ jobs:
|
||||
-w "${GITHUB_WORKSPACE}/sdk/python" \
|
||||
python:3.12-slim \
|
||||
sh -euxc '
|
||||
python -m venv /tmp/uv
|
||||
/tmp/uv/bin/python -m pip install uv==0.11.3
|
||||
/tmp/uv/bin/uv sync --group dev --frozen
|
||||
/tmp/uv/bin/uv run --frozen --no-sync ruff check --output-format=github .
|
||||
/tmp/uv/bin/uv run --frozen --no-sync ruff format --check .
|
||||
/tmp/uv/bin/uv run --frozen --no-sync pytest
|
||||
python -m venv /tmp/build
|
||||
/tmp/build/bin/python -m pip install uv==0.11.3
|
||||
/tmp/build/bin/uv build --wheel --out-dir /tmp/dist
|
||||
python -m venv /tmp/installed
|
||||
/tmp/build/bin/uv pip install --python /tmp/installed/bin/python /tmp/dist/*.whl
|
||||
env -u PYTHONPATH -u CODEX_EXEC_PATH /tmp/installed/bin/python tests/installed_sdk_smoke.py
|
||||
'
|
||||
|
||||
- name: Check for a clean worktree
|
||||
if: always() && !cancelled()
|
||||
uses: ./.github/actions/check-clean-worktree
|
||||
|
||||
sdks:
|
||||
runs-on:
|
||||
group: ${{ github.event.repository.name }}-runners
|
||||
labels: ${{ github.event.repository.name }}-linux-x64
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
environment:
|
||||
name: bazel
|
||||
deployment: false
|
||||
@@ -80,7 +73,7 @@ jobs:
|
||||
with:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Build codex with Bazel
|
||||
- name: Build codex and the code-mode host with Bazel
|
||||
env:
|
||||
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
||||
shell: bash
|
||||
@@ -88,8 +81,8 @@ jobs:
|
||||
set -euo pipefail
|
||||
# Use the shared CI wrapper so fork PRs fall back cleanly when
|
||||
# BuildBuddy credentials are unavailable. This workflow needs the
|
||||
# built `codex` binary on disk afterwards, so ask the wrapper to
|
||||
# override CI's default remote_download_minimal behavior.
|
||||
# built binaries on disk afterwards, so ask the wrapper to override
|
||||
# CI's default remote_download_minimal behavior.
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
--remote-download-toplevel \
|
||||
-- \
|
||||
@@ -97,42 +90,29 @@ jobs:
|
||||
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
|
||||
--build_metadata=TAG_job=sdk \
|
||||
-- \
|
||||
//codex-rs/cli:codex
|
||||
//codex-rs/cli:codex \
|
||||
//codex-rs/code-mode-host:codex-code-mode-host
|
||||
|
||||
# Resolve the exact output file using the same wrapper/config path as
|
||||
# the build instead of guessing which Bazel convenience symlink is
|
||||
# available on the runner.
|
||||
cquery_output="$(
|
||||
./.github/scripts/run-bazel-ci.sh \
|
||||
-- \
|
||||
cquery \
|
||||
--output=files \
|
||||
-- \
|
||||
//codex-rs/cli:codex \
|
||||
| grep -E '^(/|bazel-out/)' \
|
||||
| tail -n 1
|
||||
'set(//codex-rs/cli:codex //codex-rs/code-mode-host:codex-code-mode-host)' \
|
||||
| grep -E '^(/|bazel-out/)'
|
||||
)"
|
||||
if [[ "${cquery_output}" = /* ]]; then
|
||||
codex_bazel_output_path="${cquery_output}"
|
||||
else
|
||||
codex_bazel_output_path="${GITHUB_WORKSPACE}/${cquery_output}"
|
||||
fi
|
||||
if [[ -z "${codex_bazel_output_path}" ]]; then
|
||||
echo "Bazel did not report an output path for //codex-rs/cli:codex." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -e "${codex_bazel_output_path}" ]]; then
|
||||
echo "Unable to locate the Bazel-built codex binary at ${codex_bazel_output_path}." >&2
|
||||
mapfile -t bazel_output_paths <<< "${cquery_output}"
|
||||
if [[ ${#bazel_output_paths[@]} -ne 2 ]]; then
|
||||
echo "Expected both Bazel-built binaries, found ${#bazel_output_paths[@]}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stage the binary into the workspace and point the SDK tests at that
|
||||
# stable path. The tests spawn `codex` directly many times, so using a
|
||||
# normal executable path is more reliable than invoking Bazel for each
|
||||
# test process.
|
||||
# Stage both binaries together so the CLI can discover its sibling
|
||||
# code-mode host while the SDK tests spawn the CLI directly.
|
||||
install_dir="${GITHUB_WORKSPACE}/.tmp/sdk-ci"
|
||||
mkdir -p "${install_dir}"
|
||||
install -m 755 "${codex_bazel_output_path}" "${install_dir}/codex"
|
||||
install -m 755 "${bazel_output_paths[@]}" "${install_dir}"
|
||||
echo "CODEX_EXEC_PATH=${install_dir}/codex" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Warm up Bazel-built codex
|
||||
@@ -153,6 +133,19 @@ jobs:
|
||||
- name: Test SDK packages
|
||||
run: pnpm -r --filter ./sdk/typescript run test
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: "0.11.3"
|
||||
|
||||
- name: Test Python SDK against the same CLI
|
||||
working-directory: sdk/python
|
||||
run: |
|
||||
uv sync --only-group dev --frozen
|
||||
uv run --only-group dev --frozen --no-sync ruff check --output-format=github .
|
||||
uv run --only-group dev --frozen --no-sync ruff format --check .
|
||||
uv run --only-group dev --frozen --no-sync pytest
|
||||
|
||||
- name: Save bazel repository cache
|
||||
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import shutil
|
||||
import threading
|
||||
@@ -204,7 +205,7 @@ class MockResponsesServer:
|
||||
|
||||
|
||||
class AppServerHarness:
|
||||
"""Test fixture that points a pinned runtime app-server at MockResponsesServer."""
|
||||
"""Test fixture that points the checkout's app-server at MockResponsesServer."""
|
||||
|
||||
def __init__(self, tmp_path: Path, *, requires_openai_auth: bool = False) -> None:
|
||||
self.tmp_path = tmp_path
|
||||
@@ -226,8 +227,14 @@ class AppServerHarness:
|
||||
shutil.rmtree(self.workspace, ignore_errors=True)
|
||||
|
||||
def app_server_config(self) -> CodexConfig:
|
||||
"""Build SDK config for an isolated pinned-runtime app-server process."""
|
||||
"""Prefer the CI binary, then a local debug build, then the installed runtime."""
|
||||
binary_name = "codex.exe" if os.name == "nt" else "codex"
|
||||
debug_binary = Path(__file__).resolve().parents[3] / "codex-rs/target/debug" / binary_name
|
||||
codex_bin = os.environ.get("CODEX_EXEC_PATH")
|
||||
if codex_bin is None and debug_binary.is_file():
|
||||
codex_bin = str(debug_binary)
|
||||
return CodexConfig(
|
||||
codex_bin=codex_bin,
|
||||
cwd=str(self.workspace),
|
||||
env={
|
||||
"CODEX_HOME": str(self.codex_home),
|
||||
@@ -304,6 +311,10 @@ class _ResponsesHandler(BaseHTTPRequestHandler):
|
||||
"""Serve queued SSE responses for `/v1/responses` requests."""
|
||||
length = int(self.headers.get("content-length", "0"))
|
||||
body = self.rfile.read(length)
|
||||
if self.path.endswith("/analytics/codex/turn-costs"):
|
||||
# Optional cost probes are not model requests.
|
||||
self.send_error(404, "turn costs are unavailable for the mock provider")
|
||||
return
|
||||
self.server.mock._record_request(self, body)
|
||||
|
||||
if not (self.path.endswith("/v1/responses") or self.path.endswith("/responses")):
|
||||
|
||||
34
sdk/python/tests/installed_sdk_smoke.py
Normal file
34
sdk/python/tests/installed_sdk_smoke.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Exercise a built SDK's default runtime in an otherwise isolated environment."""
|
||||
|
||||
from dataclasses import replace
|
||||
from importlib.metadata import distribution, version
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from app_server_harness import AppServerHarness
|
||||
|
||||
import openai_codex
|
||||
from openai_codex import Codex
|
||||
|
||||
|
||||
def main() -> None:
|
||||
installed_root = Path(distribution("openai-codex").locate_file("")).resolve()
|
||||
assert Path(openai_codex.__file__).resolve().is_relative_to(installed_root), (
|
||||
"The smoke test must import the installed SDK, not the source checkout"
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as directory, AppServerHarness(Path(directory)) as harness:
|
||||
harness.responses.enqueue_assistant_message("Installed SDK works")
|
||||
config = replace(harness.app_server_config(), codex_bin=None)
|
||||
with Codex(config=config) as codex:
|
||||
result = codex.thread_start().run("Check the installed SDK")
|
||||
assert result.final_response == "Installed SDK works"
|
||||
assert harness.responses.single_request().message_input_texts("user")[-1:] == [
|
||||
"Check the installed SDK"
|
||||
]
|
||||
|
||||
print(f"Installed SDK passed with CLI runtime {version('openai-codex-cli-bin')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user