diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index f2e8fc7e12..18b6cb4552 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -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 diff --git a/sdk/python/tests/app_server_harness.py b/sdk/python/tests/app_server_harness.py index cf0c847394..9e6425bed3 100644 --- a/sdk/python/tests/app_server_harness.py +++ b/sdk/python/tests/app_server_harness.py @@ -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")): diff --git a/sdk/python/tests/installed_sdk_smoke.py b/sdk/python/tests/installed_sdk_smoke.py new file mode 100644 index 0000000000..352a047a10 --- /dev/null +++ b/sdk/python/tests/installed_sdk_smoke.py @@ -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()