Merge branch 'main' of github.com:openai/codex into etraut/tuiappserver-mcp-regression

This commit is contained in:
Eric Traut
2026-03-28 00:38:06 -06:00
1210 changed files with 11235 additions and 136881 deletions
-3
View File
@@ -20,9 +20,6 @@ common:windows --host_platform=//:local_windows
common --@rules_cc//cc/toolchains/args/archiver_flags:use_libtool_on_macos=False
common --@llvm//config:experimental_stub_libgcc_s
# We need to use the sh toolchain on windows so we don't send host bash paths to the linux executor.
common:windows --@rules_rust//rust/settings:experimental_use_sh_toolchain_for_bootstrap_process_wrapper
# TODO(zbarsky): rules_rust doesn't implement this flag properly with remote exec...
# common --@rules_rust//rust/settings:pipelined_compilation
+1 -1
View File
@@ -3,4 +3,4 @@
skip = .git*,vendor,*-lock.yaml,*.lock,.codespellrc,*test.ts,*.jsonl,frame*.txt,*.snap,*.snap.new,*meriyah.umd.min.js
check-hidden = true
ignore-regex = ^\s*"image/\S+": ".*|\b(afterAll)\b
ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE
ignore-words-list = ratatui,ser,iTerm,iterm2,iterm,te,TE,PASE,SEH
+8 -2
View File
@@ -57,5 +57,11 @@ runs:
if: runner.os == 'Windows'
shell: pwsh
run: |
# Use a very short path to reduce argv/path length issues.
"BAZEL_OUTPUT_USER_ROOT=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
# Use a very short path to reduce argv/path length issues, but avoid the
# drive root because some Windows test launchers mis-handle MANIFEST paths there.
"BAZEL_OUTPUT_USER_ROOT=D:\b" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Enable Git long paths (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: git config --global core.longpaths true
+31 -7
View File
@@ -4,6 +4,7 @@ set -euo pipefail
print_failed_bazel_test_logs=0
use_node_test_env=0
remote_download_toplevel=0
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -15,6 +16,10 @@ while [[ $# -gt 0 ]]; do
use_node_test_env=1
shift
;;
--remote-download-toplevel)
remote_download_toplevel=1
shift
;;
--)
shift
break
@@ -27,7 +32,7 @@ while [[ $# -gt 0 ]]; do
done
if [[ $# -eq 0 ]]; then
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] -- <bazel args> -- <targets>" >&2
echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] [--remote-download-toplevel] -- <bazel args> -- <targets>" >&2
exit 1
fi
@@ -114,6 +119,13 @@ if [[ $use_node_test_env -eq 1 && "${RUNNER_OS:-}" != "Windows" ]]; then
bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}")
fi
post_config_bazel_args=()
if [[ $remote_download_toplevel -eq 1 ]]; then
# Override the CI config's remote_download_minimal setting when callers need
# the built artifact to exist on disk after the command completes.
post_config_bazel_args+=(--remote_download_toplevel)
fi
bazel_console_log="$(mktemp)"
trap 'rm -f "$bazel_console_log"' EXIT
@@ -128,12 +140,18 @@ if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then
# seen in CI (for example "is not a symlink" or permission errors while
# materializing external repos such as rules_perl). We still use BuildBuddy for
# remote execution/cache; this only disables the startup-level repo contents cache.
bazel_run_args=(
"${bazel_args[@]}"
"--config=${ci_config}"
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
)
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
"${bazel_cmd[@]}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_args[@]}" \
"--config=${ci_config}" \
"--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
@@ -157,12 +175,18 @@ else
# clear remote cache/execution endpoints configured in .bazelrc.
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache
# https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor
bazel_run_args=(
"${bazel_args[@]}"
--remote_cache=
--remote_executor=
)
if (( ${#post_config_bazel_args[@]} > 0 )); then
bazel_run_args+=("${post_config_bazel_args[@]}")
fi
set +e
"${bazel_cmd[@]}" \
--noexperimental_remote_repo_contents_cache \
"${bazel_args[@]}" \
--remote_cache= \
--remote_executor= \
"${bazel_run_args[@]}" \
-- \
"${bazel_targets[@]}" \
2>&1 | tee "$bazel_console_log"
+33
View File
@@ -0,0 +1,33 @@
# Workflow Strategy
The workflows in this directory are split so that pull requests get fast, review-friendly signal while `main` still gets the full cross-platform verification pass.
## Pull Requests
- `bazel.yml` is the main pre-merge verification path for Rust code.
It runs Bazel `test` and Bazel `clippy` on the supported Bazel targets.
- `rust-ci.yml` keeps the Cargo-native PR checks intentionally small:
- `cargo fmt --check`
- `cargo shear`
- `argument-comment-lint` on Linux, macOS, and Windows
- `tools/argument-comment-lint` package tests when the lint or its workflow wiring changes
The PR workflow still keeps the Linux lint lane on the default-targets-only invocation for now, but the released linter runs on Linux, macOS, and Windows before merge.
## Post-Merge On `main`
- `bazel.yml` also runs on pushes to `main`.
This re-verifies the merged Bazel path and helps keep the BuildBuddy caches warm.
- `rust-ci-full.yml` is the full Cargo-native verification workflow.
It keeps the heavier checks off the PR path while still validating them after merge:
- the full Cargo `clippy` matrix
- the full Cargo `nextest` matrix
- release-profile Cargo builds
- cross-platform `argument-comment-lint`
- Linux remote-env tests
## Rule Of Thumb
- If a build/test/clippy check can be expressed in Bazel, prefer putting the PR-time version in `bazel.yml`.
- Keep `rust-ci.yml` fast enough that it usually does not dominate PR latency.
- Reserve `rust-ci-full.yml` for heavyweight Cargo-native coverage that Bazel does not replace yet.
+22 -12
View File
@@ -17,6 +17,7 @@ concurrency:
cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
test:
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -39,16 +40,16 @@ jobs:
# - os: ubuntu-24.04-arm
# target: aarch64-unknown-linux-gnu
# TODO: Enable Windows once we fix the toolchain issues there.
#- os: windows-latest
# target: x86_64-pc-windows-gnullvm
# Windows
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs-on: ${{ matrix.os }}
# Configure a human readable name for each job
name: Local Bazel build on ${{ matrix.os }} for ${{ matrix.target }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Bazel CI
id: setup_bazel
@@ -67,8 +68,14 @@ jobs:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
shell: bash
run: |
# Keep V8 out of the ordinary Bazel CI path. Only the dedicated
# canary and release workflows should build `third_party/v8`.
bazel_targets=(
//...
# Keep standalone V8 library targets out of the ordinary Bazel CI
# path. V8 consumers under `//codex-rs/...` still participate
# transitively through `//...`.
-//third_party/v8:all
)
./.github/scripts/run-bazel-ci.sh \
--print-failed-test-logs \
--use-node-test-env \
@@ -77,15 +84,14 @@ jobs:
--test_verbose_timeout_warnings \
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
-- \
//... \
-//third_party/v8:all
"${bazel_targets[@]}"
# Save bazel repository cache explicitly; make non-fatal so cache uploading
# never fails the overall job. Only save when key wasn't hit.
- name: Save bazel repository cache
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cache/bazel-repo-cache
@@ -97,16 +103,20 @@ jobs:
matrix:
include:
# Keep Linux lint coverage on x64 and add the arm64 macOS path that
# the Bazel test job already exercises.
# the Bazel test job already exercises. Add Windows gnullvm as well
# so PRs get Bazel-native lint signal on the same Windows toolchain
# that the Bazel test job uses.
- os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
- os: macos-15-xlarge
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs-on: ${{ matrix.os }}
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Bazel CI
id: setup_bazel
@@ -136,7 +146,7 @@ jobs:
- name: Save bazel repository cache
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cache/bazel-repo-cache
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
name: Blob size policy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
+3 -3
View File
@@ -14,13 +14,13 @@ jobs:
working-directory: ./codex-rs
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@v2
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
with:
rust-version: stable
manifest-path: ./codex-rs/Cargo.toml
+5 -5
View File
@@ -12,15 +12,15 @@ jobs:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 22
@@ -28,7 +28,7 @@ jobs:
run: pnpm install --frozen-lockfile
# stage_npm_packages.py requires DotSlash when staging releases.
- uses: facebook/install-dotslash@v2
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Stage npm package
id: stage_npm_package
@@ -47,7 +47,7 @@ jobs:
echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
- name: Upload staged npm package artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: codex-npm-staging
path: ${{ steps.stage_npm_package.outputs.pack_output }}
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
if: ${{ github.repository_owner == 'openai' }}
runs-on: ubuntu-latest
steps:
- uses: contributor-assistant/github-action@v2.6.1
- uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
# Run on close only if the PR was merged. This will lock the PR to preserve
# the CLA agreement. We don't want to lock PRs that have been closed without
# merging because the contributor may want to respond with additional comments.
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Close inactive PRs from contributors
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Annotate locations with typos
uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
- name: Codespell
+5 -5
View File
@@ -19,7 +19,7 @@ jobs:
reason: ${{ steps.normalize-all.outputs.reason }}
has_matches: ${{ steps.normalize-all.outputs.has_matches }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Prepare Codex inputs
env:
@@ -61,7 +61,7 @@ jobs:
# .github/prompts/issue-deduplicator.txt file is obsolete and removed.
- id: codex-all
name: Find duplicates (pass 1, all issues)
uses: openai/codex-action@main
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
@@ -155,7 +155,7 @@ jobs:
reason: ${{ steps.normalize-open.outputs.reason }}
has_matches: ${{ steps.normalize-open.outputs.has_matches }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Prepare Codex inputs
env:
@@ -195,7 +195,7 @@ jobs:
- id: codex-open
name: Find duplicates (pass 2, open issues)
uses: openai/codex-action@main
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
@@ -342,7 +342,7 @@ jobs:
issues: write
steps:
- name: Comment on issue
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
with:
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
outputs:
codex_output: ${{ steps.codex.outputs.final-message }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- id: codex
uses: openai/codex-action@main
uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
with:
openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
allow-users: "*"
+761
View File
@@ -0,0 +1,761 @@
name: rust-ci-full
on:
push:
branches:
- main
workflow_dispatch:
# CI builds in debug (dev) for faster signal.
jobs:
# --- CI that doesn't need specific targets ---------------------------------
general:
name: Format / etc
runs-on: ubuntu-24.04
defaults:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
components: rustfmt
- name: cargo fmt
run: cargo fmt -- --config imports_granularity=Item --check
cargo_shear:
name: cargo shear
runs-on: ubuntu-24.04
defaults:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-shear
version: 1.5.1
- name: cargo shear
run: cargo shear
argument_comment_lint_package:
name: Argument comment lint package
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
- name: Cache cargo-dylint tooling
id: cargo_dylint_cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cargo/bin/cargo-dylint
~/.cargo/bin/dylint-link
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml', '.github/workflows/rust-ci-full.yml') }}
- name: Install cargo-dylint tooling
if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
run: cargo install --locked cargo-dylint dylint-link
- name: Check Python wrapper syntax
run: python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
- name: Test Python wrapper helpers
run: python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
- name: Test argument comment lint package
working-directory: tools/argument-comment-lint
run: cargo test
argument_comment_lint_prebuilt:
name: Argument comment lint - ${{ matrix.name }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- name: Linux
runner: ubuntu-24.04
- name: macOS
runner: macos-15-xlarge
- name: Windows
runner: windows-x64
runs_on:
group: codex-runners
labels: codex-windows-x64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install Linux sandbox build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Run argument comment lint on codex-rs
if: ${{ runner.os == 'macOS' }}
shell: bash
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py
- name: Run argument comment lint on codex-rs (default targets only)
if: ${{ runner.os == 'Linux' }}
shell: bash
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py -- --lib --bins
- name: Run argument comment lint on codex-rs
if: ${{ runner.os == 'Windows' }}
shell: bash
run: python ./tools/argument-comment-lint/run-prebuilt-linter.py
# --- CI to validate on different os/targets --------------------------------
lint_build:
name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: 30
defaults:
run:
working-directory: codex-rs
env:
# Speed up repeated builds across CI runs by caching compiled objects, except on
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
# mixed-architecture archives under sccache.
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
# In rust-ci, representative release-profile checks use thin LTO for faster feedback.
CARGO_PROFILE_RELEASE_LTO: ${{ matrix.profile == 'release' && 'thin' || 'fat' }}
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: dev
- runner: macos-15-xlarge
target: x86_64-apple-darwin
profile: dev
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-arm64
# Also run representative release builds on Mac and Linux because
# there could be release-only build errors we want to catch.
# Hopefully this also pre-populates the build cache to speed up
# releases.
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: release
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: release
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: release
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: release
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: release
runs_on:
group: codex-runners
labels: codex-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
packages=(pkg-config libcap-dev)
if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
packages+=(libubsan1)
fi
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
fi
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
components: clippy
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Use hermetic Cargo home (musl)
shell: bash
run: |
set -euo pipefail
cargo_home="${GITHUB_WORKSPACE}/.cargo-home"
mkdir -p "${cargo_home}/bin"
echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV"
echo "${cargo_home}/bin" >> "$GITHUB_PATH"
: > "${cargo_home}/config.toml"
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
# Explicit cache restore: split cargo home vs target, so we can
# avoid caching the large target dir on the gnu-dev job.
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
${{ github.workspace }}/.cargo-home/bin/
${{ github.workspace }}/.cargo-home/registry/index/
${{ github.workspace }}/.cargo-home/registry/cache/
${{ github.workspace }}/.cargo-home/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
# Install and restore sccache cache
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "Using sccache GitHub backend"
else
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Disable sccache wrapper (musl)
shell: bash
run: |
set -euo pipefail
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Prepare APT cache directories (musl)
shell: bash
run: |
set -euo pipefail
sudo mkdir -p /var/cache/apt/archives /var/lib/apt/lists
sudo chown -R "$USER:$USER" /var/cache/apt /var/lib/apt/lists
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Restore APT cache (musl)
id: cache_apt_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
/var/cache/apt
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Install Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
with:
version: 0.14.0
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Install musl build tools
env:
DEBIAN_FRONTEND: noninteractive
TARGET: ${{ matrix.target }}
APT_UPDATE_ARGS: -o Acquire::Retries=3
APT_INSTALL_ARGS: --no-install-recommends
shell: bash
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Configure rustc UBSan wrapper (musl host)
shell: bash
run: |
set -euo pipefail
ubsan=""
if command -v ldconfig >/dev/null 2>&1; then
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
fi
wrapper_root="${RUNNER_TEMP:-/tmp}"
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
cat > "${wrapper}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${ubsan}" ]]; then
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
fi
exec "\$1" "\${@:2}"
EOF
chmod +x "${wrapper}"
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Clear sanitizer flags (musl)
shell: bash
run: |
set -euo pipefail
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
# Override any runner-level Cargo config rustflags as well.
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
sanitize_flags() {
local input="$1"
input="${input//-fsanitize=undefined/}"
input="${input//-fno-sanitize-recover=undefined/}"
input="${input//-fno-sanitize-trap=undefined/}"
echo "$input"
}
cflags="$(sanitize_flags "${CFLAGS-}")"
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
name: Configure musl rusty_v8 artifact overrides
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
release_tag="rusty-v8-v${version}"
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
binding_dir="${RUNNER_TEMP}/rusty_v8"
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
mkdir -p "${binding_dir}"
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"
- name: Install cargo-chef
if: ${{ matrix.profile == 'release' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-chef
version: 0.1.71
- name: Pre-warm dependency cache (cargo-chef)
if: ${{ matrix.profile == 'release' }}
shell: bash
run: |
set -euo pipefail
RECIPE="${RUNNER_TEMP}/chef-recipe.json"
cargo chef prepare --recipe-path "$RECIPE"
cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release --all-features
- name: cargo clippy
run: cargo clippy --target ${{ matrix.target }} --all-features --tests --profile ${{ matrix.profile }} --timings -- -D warnings
- name: Upload Cargo timings (clippy)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
if-no-files-found: warn
# Save caches explicitly; make non-fatal so cache packaging
# never fails the overall job. Only save when key wasn't hit.
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
${{ github.workspace }}/.cargo-home/bin/
${{ github.workspace }}/.cargo-home/registry/index/
${{ github.workspace }}/.cargo-home/registry/cache/
${{ github.workspace }}/.cargo-home/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
echo "### sccache stats — ${{ matrix.target }} (${{ matrix.profile }})";
echo;
echo '```';
sccache --show-stats || true;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
- name: Save APT cache (musl)
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
/var/cache/apt
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
tests:
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
# Perhaps we can bring this back down to 30m once we finish the cutover
# from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
# offender for exceeding the timeout.
timeout-minutes: 45
defaults:
run:
working-directory: codex-rs
env:
# Speed up repeated builds across CI runs by caching compiled objects, except on
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
# mixed-architecture archives under sccache.
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: dev
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
remote_env: "true"
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Node.js for js_repl tests
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version-file: codex-rs/node-version.txt
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
fi
# Some integration tests rely on DotSlash being installed.
# See https://github.com/openai/codex/pull/7617.
- name: Install DotSlash
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "Using sccache GitHub backend"
else
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: nextest
version: 0.9.103
- name: Enable unprivileged user namespaces (Linux)
if: runner.os == 'Linux'
run: |
# Required for bubblewrap to work on Linux CI runners.
sudo sysctl -w kernel.unprivileged_userns_clone=1
# Ubuntu 24.04+ can additionally gate unprivileged user namespaces
# behind AppArmor.
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Set up remote test env (Docker)
if: ${{ runner.os == 'Linux' && matrix.remote_env == 'true' }}
shell: bash
run: |
set -euo pipefail
export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME=codex-remote-test-env
source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"
- name: tests
id: test
run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
env:
RUST_BACKTRACE: 1
NEXTEST_STATUS_LEVEL: leak
- name: Upload Cargo timings (nextest)
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
if-no-files-found: warn
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
echo "### sccache stats — ${{ matrix.target }} (tests)";
echo;
echo '```';
sccache --show-stats || true;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
- name: Tear down remote test env
if: ${{ always() && runner.os == 'Linux' && matrix.remote_env == 'true' }}
shell: bash
run: |
set +e
if [[ "${{ steps.test.outcome }}" != "success" ]]; then
docker logs codex-remote-test-env || true
fi
docker rm -f codex-remote-test-env >/dev/null 2>&1 || true
- name: verify tests passed
if: steps.test.outcome == 'failure'
run: |
echo "Tests failed. See logs for details."
exit 1
# --- Gatherer job for the full post-merge workflow --------------------------
results:
name: Full CI results
needs:
[
general,
cargo_shear,
argument_comment_lint_package,
argument_comment_lint_prebuilt,
lint_build,
tests,
]
if: always()
runs-on: ubuntu-24.04
steps:
- name: Summarize
shell: bash
run: |
echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
echo "general: ${{ needs.general.result }}"
echo "shear : ${{ needs.cargo_shear.result }}"
echo "lint : ${{ needs.lint_build.result }}"
echo "tests : ${{ needs.tests.result }}"
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
[[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }
- name: sccache summary note
if: always()
run: |
echo "Per-job sccache stats are attached to each matrix job's Step Summary."
+38 -659
View File
@@ -1,15 +1,10 @@
name: rust-ci
on:
pull_request: {}
push:
branches:
- main
workflow_dispatch:
# CI builds in debug (dev) for faster signal.
jobs:
# --- Detect what changed to detect which tests to run (always runs) -------------------------------------
# --- Detect what changed so the fast PR workflow only runs relevant jobs ----
changed:
name: Detect changed areas
runs-on: ubuntu-24.04
@@ -19,7 +14,7 @@ jobs:
codex: ${{ steps.detect.outputs.codex }}
workflows: ${{ steps.detect.outputs.workflows }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Detect changed paths (no external action)
@@ -33,11 +28,10 @@ jobs:
HEAD_SHA='${{ github.event.pull_request.head.sha }}'
echo "Base SHA: $BASE_SHA"
echo "Head SHA: $HEAD_SHA"
# List files changed between base and PR head
mapfile -t files < <(git diff --name-only --no-renames "$BASE_SHA" "$HEAD_SHA")
else
# On push / manual runs, default to running everything
files=("codex-rs/force" ".github/force")
# On manual runs, default to the full fast-PR bundle.
files=("codex-rs/force" "tools/argument-comment-lint/force" ".github/force")
fi
codex=false
@@ -47,7 +41,7 @@ jobs:
for f in "${files[@]}"; do
[[ $f == codex-rs/* ]] && codex=true
[[ $f == codex-rs/* || $f == tools/argument-comment-lint/* || $f == justfile ]] && argument_comment_lint=true
[[ $f == tools/argument-comment-lint/* || $f == .github/workflows/rust-ci.yml ]] && argument_comment_lint_package=true
[[ $f == tools/argument-comment-lint/* || $f == .github/workflows/rust-ci.yml || $f == .github/workflows/rust-ci-full.yml ]] && argument_comment_lint_package=true
[[ $f == .github/* ]] && workflows=true
done
@@ -56,18 +50,18 @@ jobs:
echo "codex=$codex" >> "$GITHUB_OUTPUT"
echo "workflows=$workflows" >> "$GITHUB_OUTPUT"
# --- CI that doesn't need specific targets ---------------------------------
# --- Fast Cargo-native PR checks -------------------------------------------
general:
name: Format / etc
runs-on: ubuntu-24.04
needs: changed
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }}
defaults:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.93.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
components: rustfmt
- name: cargo fmt
@@ -77,13 +71,13 @@ jobs:
name: cargo shear
runs-on: ubuntu-24.04
needs: changed
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }}
defaults:
run:
working-directory: codex-rs
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.93.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-shear
@@ -95,16 +89,16 @@ jobs:
name: Argument comment lint package
runs-on: ubuntu-24.04
needs: changed
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' || github.event_name == 'push' }}
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' }}
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.93.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
- name: Cache cargo-dylint tooling
id: cargo_dylint_cache
uses: actions/cache@v5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cargo/bin/cargo-dylint
@@ -112,12 +106,14 @@ jobs:
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml') }}
key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml', '.github/workflows/rust-ci-full.yml') }}
- name: Install cargo-dylint tooling
if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
run: cargo install --locked cargo-dylint dylint-link
- name: Check source wrapper syntax
run: bash -n tools/argument-comment-lint/run.sh
- name: Check Python wrapper syntax
run: python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
- name: Test Python wrapper helpers
run: python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
- name: Test argument comment lint package
working-directory: tools/argument-comment-lint
run: cargo test
@@ -126,7 +122,7 @@ jobs:
name: Argument comment lint - ${{ matrix.name }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
needs: changed
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' }}
strategy:
fail-fast: false
matrix:
@@ -141,638 +137,32 @@ jobs:
group: codex-runners
labels: codex-windows-x64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install Linux sandbox build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
- uses: dtolnay/rust-toolchain@1.93.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
- uses: facebook/install-dotslash@v2
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Run argument comment lint on codex-rs
if: ${{ runner.os == 'macOS' }}
shell: bash
run: ./tools/argument-comment-lint/run-prebuilt-linter.sh
# --- CI to validate on different os/targets --------------------------------
lint_build:
name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: 30
needs: changed
# Keep job-level if to avoid spinning up runners when not needed
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
defaults:
run:
working-directory: codex-rs
env:
# Speed up repeated builds across CI runs by caching compiled objects, except on
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
# mixed-architecture archives under sccache.
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
# In rust-ci, representative release-profile checks use thin LTO for faster feedback.
CARGO_PROFILE_RELEASE_LTO: ${{ matrix.profile == 'release' && 'thin' || 'fat' }}
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: dev
- runner: macos-15-xlarge
target: x86_64-apple-darwin
profile: dev
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-arm64
# Also run representative release builds on Mac and Linux because
# there could be release-only build errors we want to catch.
# Hopefully this also pre-populates the build cache to speed up
# releases.
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: release
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: release
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: release
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: release
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: release
runs_on:
group: codex-runners
labels: codex-windows-arm64
steps:
- uses: actions/checkout@v6
- name: Install Linux build dependencies
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py
# Linux still uses the default-targets-only form for now, but PRs run the
# released linter on all three platforms so wrapper regressions surface pre-merge.
- name: Run argument comment lint on codex-rs (default targets only)
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
packages=(pkg-config libcap-dev)
if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
packages+=(libubsan1)
fi
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
fi
- uses: dtolnay/rust-toolchain@1.93.0
with:
targets: ${{ matrix.target }}
components: clippy
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Use hermetic Cargo home (musl)
run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py -- --lib --bins
- name: Run argument comment lint on codex-rs
if: ${{ runner.os == 'Windows' }}
shell: bash
run: |
set -euo pipefail
cargo_home="${GITHUB_WORKSPACE}/.cargo-home"
mkdir -p "${cargo_home}/bin"
echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV"
echo "${cargo_home}/bin" >> "$GITHUB_PATH"
: > "${cargo_home}/config.toml"
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
# Explicit cache restore: split cargo home vs target, so we can
# avoid caching the large target dir on the gnu-dev job.
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
${{ github.workspace }}/.cargo-home/bin/
${{ github.workspace }}/.cargo-home/registry/index/
${{ github.workspace }}/.cargo-home/registry/cache/
${{ github.workspace }}/.cargo-home/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
# Install and restore sccache cache
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "Using sccache GitHub backend"
else
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Disable sccache wrapper (musl)
shell: bash
run: |
set -euo pipefail
echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Prepare APT cache directories (musl)
shell: bash
run: |
set -euo pipefail
sudo mkdir -p /var/cache/apt/archives /var/lib/apt/lists
sudo chown -R "$USER:$USER" /var/cache/apt /var/lib/apt/lists
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Restore APT cache (musl)
id: cache_apt_restore
uses: actions/cache/restore@v5
with:
path: |
/var/cache/apt
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Install Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
with:
version: 0.14.0
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Install musl build tools
env:
DEBIAN_FRONTEND: noninteractive
TARGET: ${{ matrix.target }}
APT_UPDATE_ARGS: -o Acquire::Retries=3
APT_INSTALL_ARGS: --no-install-recommends
shell: bash
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Configure rustc UBSan wrapper (musl host)
shell: bash
run: |
set -euo pipefail
ubsan=""
if command -v ldconfig >/dev/null 2>&1; then
ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
fi
wrapper_root="${RUNNER_TEMP:-/tmp}"
wrapper="${wrapper_root}/rustc-ubsan-wrapper"
cat > "${wrapper}" <<EOF
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${ubsan}" ]]; then
export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
fi
exec "\$1" "\${@:2}"
EOF
chmod +x "${wrapper}"
echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
name: Clear sanitizer flags (musl)
shell: bash
run: |
set -euo pipefail
# Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
echo "RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
# Override any runner-level Cargo config rustflags as well.
echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
sanitize_flags() {
local input="$1"
input="${input//-fsanitize=undefined/}"
input="${input//-fno-sanitize-recover=undefined/}"
input="${input//-fno-sanitize-trap=undefined/}"
echo "$input"
}
cflags="$(sanitize_flags "${CFLAGS-}")"
cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"
- if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
name: Configure musl rusty_v8 artifact overrides
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
release_tag="rusty-v8-v${version}"
base_url="https://github.com/openai/codex/releases/download/${release_tag}"
archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
binding_dir="${RUNNER_TEMP}/rusty_v8"
binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
mkdir -p "${binding_dir}"
curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"
- name: Install cargo-chef
if: ${{ matrix.profile == 'release' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: cargo-chef
version: 0.1.71
- name: Pre-warm dependency cache (cargo-chef)
if: ${{ matrix.profile == 'release' }}
shell: bash
run: |
set -euo pipefail
RECIPE="${RUNNER_TEMP}/chef-recipe.json"
cargo chef prepare --recipe-path "$RECIPE"
cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release --all-features
- name: cargo clippy
run: cargo clippy --target ${{ matrix.target }} --all-features --tests --profile ${{ matrix.profile }} --timings -- -D warnings
- name: Upload Cargo timings (clippy)
if: always()
uses: actions/upload-artifact@v7
with:
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
if-no-files-found: warn
# Save caches explicitly; make non-fatal so cache packaging
# never fails the overall job. Only save when key wasn't hit.
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
${{ github.workspace }}/.cargo-home/bin/
${{ github.workspace }}/.cargo-home/registry/index/
${{ github.workspace }}/.cargo-home/registry/cache/
${{ github.workspace }}/.cargo-home/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
echo "### sccache stats — ${{ matrix.target }} (${{ matrix.profile }})";
echo;
echo '```';
sccache --show-stats || true;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
- name: Save APT cache (musl)
if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
/var/cache/apt
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
tests:
name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
# Perhaps we can bring this back down to 30m once we finish the cutover
# from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
# offender for exceeding the timeout.
timeout-minutes: 45
needs: changed
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
defaults:
run:
working-directory: codex-rs
env:
# Speed up repeated builds across CI runs by caching compiled objects, except on
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
# mixed-architecture archives under sccache.
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: dev
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
remote_env: "true"
runs_on:
group: codex-runners
labels: codex-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-arm64
steps:
- uses: actions/checkout@v6
- name: Set up Node.js for js_repl tests
uses: actions/setup-node@v6
with:
node-version-file: codex-rs/node-version.txt
- name: Install Linux build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
set -euo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
fi
# Some integration tests rely on DotSlash being installed.
# See https://github.com/openai/codex/pull/7617.
- name: Install DotSlash
uses: facebook/install-dotslash@v2
- uses: dtolnay/rust-toolchain@1.93.0
with:
targets: ${{ matrix.target }}
- name: Compute lockfile hash
id: lockhash
working-directory: codex-rs
shell: bash
run: |
set -euo pipefail
echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- name: Restore cargo home cache
id: cache_cargo_home_restore
uses: actions/cache/restore@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
restore-keys: |
cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- name: Install sccache
if: ${{ env.USE_SCCACHE == 'true' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: sccache
version: 0.7.5
- name: Configure sccache backend
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: |
set -euo pipefail
if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
echo "Using sccache GitHub backend"
else
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
echo "Using sccache local disk + actions/cache fallback"
fi
- name: Enable sccache wrapper
if: ${{ env.USE_SCCACHE == 'true' }}
shell: bash
run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
- name: Restore sccache cache (fallback)
if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
id: cache_sccache_restore
uses: actions/cache/restore@v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
restore-keys: |
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-
- uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
with:
tool: nextest
version: 0.9.103
- name: Enable unprivileged user namespaces (Linux)
if: runner.os == 'Linux'
run: |
# Required for bubblewrap to work on Linux CI runners.
sudo sysctl -w kernel.unprivileged_userns_clone=1
# Ubuntu 24.04+ can additionally gate unprivileged user namespaces
# behind AppArmor.
if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- name: Set up remote test env (Docker)
if: ${{ runner.os == 'Linux' && matrix.remote_env == 'true' }}
shell: bash
run: |
set -euo pipefail
export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME=codex-remote-test-env
source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"
- name: tests
id: test
run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
env:
RUST_BACKTRACE: 1
NEXTEST_STATUS_LEVEL: leak
- name: Upload Cargo timings (nextest)
if: always()
uses: actions/upload-artifact@v7
with:
name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
if-no-files-found: warn
- name: Save cargo home cache
if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
- name: Save sccache cache (fallback)
if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
continue-on-error: true
uses: actions/cache/save@v5
with:
path: ${{ github.workspace }}/.sccache/
key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
- name: sccache stats
if: always() && env.USE_SCCACHE == 'true'
continue-on-error: true
run: sccache --show-stats || true
- name: sccache summary
if: always() && env.USE_SCCACHE == 'true'
shell: bash
run: |
{
echo "### sccache stats — ${{ matrix.target }} (tests)";
echo;
echo '```';
sccache --show-stats || true;
echo '```';
} >> "$GITHUB_STEP_SUMMARY"
- name: Tear down remote test env
if: ${{ always() && runner.os == 'Linux' && matrix.remote_env == 'true' }}
shell: bash
run: |
set +e
if [[ "${{ steps.test.outcome }}" != "success" ]]; then
docker logs codex-remote-test-env || true
fi
docker rm -f codex-remote-test-env >/dev/null 2>&1 || true
- name: verify tests passed
if: steps.test.outcome == 'failure'
run: |
echo "Tests failed. See logs for details."
exit 1
run: python ./tools/argument-comment-lint/run-prebuilt-linter.py
# --- Gatherer job that you mark as the ONLY required status -----------------
results:
@@ -784,8 +174,6 @@ jobs:
cargo_shear,
argument_comment_lint_package,
argument_comment_lint_prebuilt,
lint_build,
tests,
]
if: always()
runs-on: ubuntu-24.04
@@ -797,32 +185,23 @@ jobs:
echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
echo "general: ${{ needs.general.result }}"
echo "shear : ${{ needs.cargo_shear.result }}"
echo "lint : ${{ needs.lint_build.result }}"
echo "tests : ${{ needs.tests.result }}"
# If nothing relevant changed (PR touching only root README, etc.),
# declare success regardless of other jobs.
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' != 'true' && '${{ needs.changed.outputs.codex }}' != 'true' && '${{ needs.changed.outputs.workflows }}' != 'true' && '${{ github.event_name }}' != 'push' ]]; then
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' != 'true' && '${{ needs.changed.outputs.codex }}' != 'true' && '${{ needs.changed.outputs.workflows }}' != 'true' ]]; then
echo 'No relevant changes -> CI not required.'
exit 0
fi
if [[ '${{ needs.changed.outputs.argument_comment_lint_package }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
if [[ '${{ needs.changed.outputs.argument_comment_lint_package }}' == 'true' ]]; then
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
fi
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' ]]; then
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
fi
if [[ '${{ needs.changed.outputs.codex }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
if [[ '${{ needs.changed.outputs.codex }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' ]]; then
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
[[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }
fi
- name: sccache summary note
if: always()
run: |
echo "Per-job sccache stats are attached to each matrix job's Step Summary."
@@ -53,9 +53,9 @@ jobs:
labels: codex-windows-x64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@1.93.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
toolchain: nightly-2025-09-18
targets: ${{ matrix.target }}
@@ -97,7 +97,7 @@ jobs:
(cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
fi
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: argument-comment-lint-${{ matrix.target }}
path: dist/argument-comment-lint/${{ matrix.target }}/*
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
if: github.repository == 'openai/codex'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: main
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/core/models.json
- name: Open pull request (if changed)
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
with:
commit-message: "Update models.json"
title: "Update models.json"
+9 -9
View File
@@ -67,7 +67,7 @@ jobs:
labels: codex-windows-arm64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Print runner specs (Windows)
shell: powershell
run: |
@@ -82,7 +82,7 @@ jobs:
Write-Host "Total RAM: $ramGiB GiB"
Write-Host "Disk usage:"
Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}}
- uses: dtolnay/rust-toolchain@1.93.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
@@ -92,7 +92,7 @@ jobs:
cargo build --target ${{ matrix.target }} --release --timings ${{ matrix.build_args }}
- name: Upload Cargo timings
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
@@ -112,7 +112,7 @@ jobs:
fi
- name: Upload Windows binaries
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
path: |
@@ -147,16 +147,16 @@ jobs:
labels: codex-windows-arm64
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Download prebuilt Windows primary binaries
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-binaries-${{ matrix.target }}-primary
path: codex-rs/target/${{ matrix.target }}/release
- name: Download prebuilt Windows helper binaries
uses: actions/download-artifact@v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-binaries-${{ matrix.target }}-helpers
path: codex-rs/target/${{ matrix.target }}/release
@@ -193,7 +193,7 @@ jobs:
cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe"
- name: Install DotSlash
uses: facebook/install-dotslash@v2
uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Compress artifacts
shell: bash
@@ -257,7 +257,7 @@ jobs:
"${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base"
done
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ matrix.target }}
path: |
+4 -4
View File
@@ -45,7 +45,7 @@ jobs:
git \
libncursesw5-dev
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Build, smoke-test, and stage zsh artifact
shell: bash
@@ -53,7 +53,7 @@ jobs:
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: codex-zsh-${{ matrix.target }}
path: dist/zsh/${{ matrix.target }}/*
@@ -81,7 +81,7 @@ jobs:
brew install autoconf
fi
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Build, smoke-test, and stage zsh artifact
shell: bash
@@ -89,7 +89,7 @@ jobs:
"${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
"dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: codex-zsh-${{ matrix.target }}
path: dist/zsh/${{ matrix.target }}/*
+16 -16
View File
@@ -19,8 +19,8 @@ jobs:
tag-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.92
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: dtolnay/rust-toolchain@c2b55edffaf41a251c410bb32bed22afefa800f1 # 1.92
- name: Validate tag matches Cargo.toml version
shell: bash
run: |
@@ -79,7 +79,7 @@ jobs:
target: aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Print runner specs (Linux)
if: ${{ runner.os == 'Linux' }}
shell: bash
@@ -125,7 +125,7 @@ jobs:
sudo apt-get update -y
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
fi
- uses: dtolnay/rust-toolchain@1.93.0
- uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
with:
targets: ${{ matrix.target }}
@@ -235,7 +235,7 @@ jobs:
cargo build --target ${{ matrix.target }} --release --timings --bin codex --bin codex-responses-api-proxy
- name: Upload Cargo timings
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: cargo-timings-rust-release-${{ matrix.target }}
path: codex-rs/target/**/cargo-timings/cargo-timing.html
@@ -374,7 +374,7 @@ jobs:
zstd -T0 -19 --rm "$dest/$base"
done
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: ${{ matrix.target }}
# Upload the per-binary .zst files as well as the new .tar.gz
@@ -420,7 +420,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Generate release notes from tag commit message
id: release_notes
@@ -442,7 +442,7 @@ jobs:
echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: dist
@@ -492,12 +492,12 @@ jobs:
fi
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
run_install: false
- name: Setup Node.js for npm packaging
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 22
@@ -505,7 +505,7 @@ jobs:
run: pnpm install --frozen-lockfile
# stage_npm_packages.py requires DotSlash when staging releases.
- uses: facebook/install-dotslash@v2
- uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
- name: Stage npm packages
env:
GH_TOKEN: ${{ github.token }}
@@ -523,7 +523,7 @@ jobs:
cp scripts/install/install.ps1 dist/install.ps1
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
with:
name: ${{ steps.release_name.outputs.name }}
tag_name: ${{ github.ref_name }}
@@ -533,21 +533,21 @@ jobs:
# (e.g. -alpha, -beta). Otherwise publish a normal release.
prerelease: ${{ contains(steps.release_name.outputs.name, '-') }}
- uses: facebook/dotslash-publish-release@v2
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ github.ref_name }}
config: .github/dotslash-config.json
- uses: facebook/dotslash-publish-release@v2
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ github.ref_name }}
config: .github/dotslash-zsh-config.json
- uses: facebook/dotslash-publish-release@v2
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -582,7 +582,7 @@ jobs:
steps:
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
+8 -8
View File
@@ -25,10 +25,10 @@ jobs:
v8_version: ${{ steps.v8_version.outputs.version }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
@@ -75,13 +75,13 @@ jobs:
target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Bazel
uses: bazelbuild/setup-bazelisk@v3
uses: bazelbuild/setup-bazelisk@6ecf4fd8b7d1f9721785f1dd656a689acf9add47 # v3
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
@@ -135,7 +135,7 @@ jobs:
--output-dir "dist/${TARGET}"
- name: Upload staged musl artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
@@ -174,12 +174,12 @@ jobs:
exit 1
fi
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: dist
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
with:
tag_name: ${{ needs.metadata.outputs.release_tag }}
name: ${{ needs.metadata.outputs.release_tag }}
+77 -7
View File
@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install Linux bwrap build dependencies
shell: bash
@@ -23,21 +23,82 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
- name: Setup pnpm
uses: pnpm/action-setup@v5
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 22
cache: pnpm
- uses: dtolnay/rust-toolchain@1.93.0
- name: Set up Bazel CI
id: setup_bazel
uses: ./.github/actions/setup-bazel-ci
with:
target: x86_64-unknown-linux-gnu
- name: build codex
run: cargo build --bin codex
working-directory: codex-rs
- name: Build codex with Bazel
env:
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
shell: bash
run: |
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.
./.github/scripts/run-bazel-ci.sh \
--remote-download-toplevel \
-- \
build \
--build_metadata=COMMIT_SHA=${GITHUB_SHA} \
--build_metadata=TAG_job=sdk \
-- \
//codex-rs/cli:codex
# 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
)"
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
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.
install_dir="${GITHUB_WORKSPACE}/.tmp/sdk-ci"
mkdir -p "${install_dir}"
install -m 755 "${codex_bazel_output_path}" "${install_dir}/codex"
echo "CODEX_EXEC_PATH=${install_dir}/codex" >> "$GITHUB_ENV"
- name: Warm up Bazel-built codex
shell: bash
run: |
set -euo pipefail
"${CODEX_EXEC_PATH}" --version
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -50,3 +111,12 @@ jobs:
- name: Test SDK packages
run: pnpm -r --filter ./sdk/typescript run test
- name: Save bazel repository cache
if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
continue-on-error: true
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with:
path: |
~/.cache/bazel-repo-cache
key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
+6 -6
View File
@@ -38,10 +38,10 @@ jobs:
v8_version: ${{ steps.v8_version.outputs.version }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
@@ -72,13 +72,13 @@ jobs:
target: aarch64-unknown-linux-musl
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up Bazel
uses: bazelbuild/setup-bazelisk@v3
uses: bazelbuild/setup-bazelisk@6ecf4fd8b7d1f9721785f1dd656a689acf9add47 # v3
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.12"
@@ -126,7 +126,7 @@ jobs:
--output-dir "dist/${TARGET}"
- name: Upload staged musl artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
+4 -4
View File
@@ -35,9 +35,9 @@ In the codex-rs folder where the rust code lives:
- If a file exceeds roughly 800 LoC, add new functionality in a new module instead of extending
the existing file unless there is a strong documented reason not to.
- This rule applies especially to high-touch files that already attract unrelated changes, such
as `codex-rs/tui/src/app.rs`, `codex-rs/tui/src/bottom_pane/chat_composer.rs`,
`codex-rs/tui/src/bottom_pane/footer.rs`, `codex-rs/tui/src/chatwidget.rs`,
`codex-rs/tui/src/bottom_pane/mod.rs`, and similarly central orchestration modules.
as `codex-rs/tui_app_server/src/app.rs`, `codex-rs/tui_app_server/src/bottom_pane/chat_composer.rs`,
`codex-rs/tui_app_server/src/bottom_pane/footer.rs`, `codex-rs/tui_app_server/src/chatwidget.rs`,
`codex-rs/tui_app_server/src/bottom_pane/mod.rs`, and similarly central orchestration modules.
- When extracting code from a large module, move the related tests and module/type docs toward
the new implementation so the invariants stay close to the code that owns them.
- When running Rust commands (e.g. `just fix` or `cargo test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected.
@@ -66,7 +66,7 @@ Likewise, when reviewing code, do not hesitate to push back on PRs that would un
## TUI style conventions
See `codex-rs/tui/styles.md`.
See `codex-rs/tui_app_server/styles.md`.
## TUI code conventions
+44
View File
@@ -3,6 +3,25 @@ module(name = "codex")
bazel_dep(name = "bazel_skylib", version = "1.8.2")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "llvm", version = "0.6.8")
# The upstream LLVM archive contains a few unix-only symlink entries and is
# missing a couple of MinGW compatibility archives that windows-gnullvm needs
# during extraction and linking, so patch it until upstream grows native support.
single_version_override(
module_name = "llvm",
patch_strip = 1,
patches = [
"//patches:llvm_windows_symlink_extract.patch",
],
)
# Abseil picks a MinGW pthread TLS path that does not match our hermetic
# windows-gnullvm toolchain; force it onto the portable C++11 thread-local path.
single_version_override(
module_name = "abseil-cpp",
patch_strip = 1,
patches = [
"//patches:abseil_windows_gnullvm_thread_identity.patch",
],
)
register_toolchains("@llvm//toolchain:all")
@@ -44,8 +63,28 @@ bazel_dep(name = "apple_support", version = "2.1.0")
bazel_dep(name = "rules_cc", version = "0.2.16")
bazel_dep(name = "rules_platform", version = "0.1.0")
bazel_dep(name = "rules_rs", version = "0.0.43")
# `rules_rs` 0.0.43 does not model `windows-gnullvm` as a distinct Windows exec
# platform, so patch it until upstream grows that support for both x86_64 and
# aarch64.
single_version_override(
module_name = "rules_rs",
patch_strip = 1,
patches = [
"//patches:rules_rs_windows_gnullvm_exec.patch",
],
version = "0.0.43",
)
rules_rust = use_extension("@rules_rs//rs/experimental:rules_rust.bzl", "rules_rust")
# Build-script probe binaries inherit CFLAGS/CXXFLAGS from Bazel's C++
# toolchain. On `windows-gnullvm`, llvm-mingw does not ship
# `libssp_nonshared`, so strip the forwarded stack-protector flags there.
rules_rust.patch(
patches = [
"//patches:rules_rust_windows_gnullvm_build_script.patch",
],
strip = 1,
)
use_repo(rules_rust, "rules_rust")
toolchains = use_extension("@rules_rs//rs/experimental/toolchains:module_extension.bzl", "toolchains")
@@ -65,10 +104,15 @@ crate.from_cargo(
"aarch64-unknown-linux-gnu",
"aarch64-unknown-linux-musl",
"aarch64-apple-darwin",
# Keep both Windows ABIs in the generated Cargo metadata: the V8
# experiment still consumes release assets that only exist under the
# MSVC names while targeting the GNU toolchain.
"aarch64-pc-windows-msvc",
"aarch64-pc-windows-gnullvm",
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-pc-windows-gnullvm",
],
use_experimental_platforms = True,
+8
View File
@@ -1576,15 +1576,19 @@
},
"@@rules_rs+//rs/experimental/toolchains:module_extension.bzl%toolchains": {
"cargo-1.93.0-aarch64-apple-darwin.tar.xz": "6443909350322ad07f09bb5edfd9ff29268e6fe88c7d78bfba7a5e254248dc25",
"cargo-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "387832b989c8eb96c9ebd66402a87962167633bc6d91a49ffb8c7903c45f3476",
"cargo-1.93.0-aarch64-pc-windows-msvc.tar.xz": "155bff7a16aa7054e7ed7c3a82e362d4b302b3882d751b823e06ff63ae3f103d",
"cargo-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "5998940b8b97286bb67facb1a85535eeb3d4d7a61e36a85e386e5c0c5cfe5266",
"cargo-1.93.0-x86_64-apple-darwin.tar.xz": "95a47c5ed797c35419908f04188d8b7de09946e71073c4b72632b16f5b10dfae",
"cargo-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "f19766837559f90476508140cb95cc708220012ec00a854fa9f99187b1f246b6",
"cargo-1.93.0-x86_64-pc-windows-msvc.tar.xz": "e59c5e2baa9ec17261f2cda6676ebf7b68b21a860e3f7451c4d964728951da75",
"cargo-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "c23de3ae709ff33eed5e4ae59d1f9bcd75fa4dbaa9fb92f7b06bfb534b8db880",
"clippy-1.93.0-aarch64-apple-darwin.tar.xz": "0b6e943a8d12be0e68575acf59c9ea102daf795055fcbbf862b0bfd35ec40039",
"clippy-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "296949f49be2de77ce9d1c5023a5f0a58e28e329eec03642a3da0e175a67beeb",
"clippy-1.93.0-aarch64-pc-windows-msvc.tar.xz": "07bcf2edb88cdf5ead2f02e4a8493e9b0ef935a31253fac6f9f3378d8023f113",
"clippy-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "872ae6d68d625946d281b91d928332e6b74f6ab269b6af842338df4338805a60",
"clippy-1.93.0-x86_64-apple-darwin.tar.xz": "e6d0b1afb9607c14a1172d09ee194a032bbb3e48af913d55c5a473e0559eddde",
"clippy-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "b6f1f7264ed6943c59dedfb9531fbadcc3c0fcf273c940a63d58898b14a1060f",
"clippy-1.93.0-x86_64-pc-windows-msvc.tar.xz": "25fb103390bf392980b4689ac09b2ec2ab4beefb7022a983215b613ad05eab57",
"clippy-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "793108977514b15c0f45ade28ae35c58b05370cb0f22e89bd98fdfa61eabf55d",
"rust-std-1.93.0-aarch64-apple-darwin.tar.xz": "8603c63715349636ed85b4fe716c4e827a727918c840e54aff5b243cedadf19b",
@@ -1653,15 +1657,19 @@
"rust-std-1.93.0-x86_64-unknown-none.tar.xz": "01dcca7ae4b7e82fbfa399adb5e160afaa13143e5a17e1e0737c38cf07365fb3",
"rust-std-1.93.0-x86_64-unknown-uefi.tar.xz": "ec4e439d9485ce752b56999e8e41ed82373fc833a005cf2531c6f7ef7e785392",
"rustc-1.93.0-aarch64-apple-darwin.tar.xz": "092be03c02b44c405dab1232541c84f32b2d9e8295747568c3d531dd137221dc",
"rustc-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "d3bc0cdaf157e20b1f23e510b5e3c4c6e9117d08f5284c04dee60aecff1bc851",
"rustc-1.93.0-aarch64-pc-windows-msvc.tar.xz": "a3ac1a8e411de8470f71b366f89d187718c431526912b181692ed0a18c56c7ad",
"rustc-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "1a9045695892ec08d8e9751bf7cf7db71fe27a6202dd12ce13aca48d0602dbde",
"rustc-1.93.0-x86_64-apple-darwin.tar.xz": "594bb293f0a4f444656cf8dec2149fcb979c606260efee9e09bcf8c9c6ed6ae7",
"rustc-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "0cdaa8de66f5ce21d1ea73917efc5c64f408bda49f678ddde19465ced9d5ec63",
"rustc-1.93.0-x86_64-pc-windows-msvc.tar.xz": "fa17677eee0d83eb055b309953184bf87ba634923d8897f860cda65d55c6e350",
"rustc-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "00c6e6740ea6a795e33568cd7514855d58408a1180cd820284a7bbf7c46af715",
"rustfmt-1.93.0-aarch64-apple-darwin.tar.xz": "0dd1faedf0768ef362f4aae4424b34e8266f2b9cf5e76ea4fcaf780220b363a0",
"rustfmt-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "5888827e7fbd7d59930870b4856fce8d6d8fca5e02f6535f8ae3d7ad0ccf2d4a",
"rustfmt-1.93.0-aarch64-pc-windows-msvc.tar.xz": "24eed108489567133bbfe40c8eacda1567be55fae4c526911b39eb33eb27a6cb",
"rustfmt-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "92e1acb45ae642136258b4dabb39302af2d53c83e56ebd5858bc969f9e5c141a",
"rustfmt-1.93.0-x86_64-apple-darwin.tar.xz": "c8453b4c5758eb39423042ffa9c23ed6128cbed2b15b581e5e1192c9cc0b1d4e",
"rustfmt-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "47167e9e78db9be4503a060dee02f4df2cda252da32175dbf44331f965a747b9",
"rustfmt-1.93.0-x86_64-pc-windows-msvc.tar.xz": "5becc7c2dba4b9ab5199012cad30829235a7f7fb5d85a238697e8f0e44cbd9af",
"rustfmt-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "7f81f6c17d11a7fda5b4e1b111942fb3b23d30dcec767e13e340ebfb762a5e33"
}
-97
View File
@@ -1636,7 +1636,6 @@ dependencies = [
"codex-stdio-to-uds",
"codex-terminal-detection",
"codex-tui",
"codex-tui-app-server",
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-windows-sandbox",
@@ -2624,102 +2623,6 @@ dependencies = [
[[package]]
name = "codex-tui"
version = "0.0.0"
dependencies = [
"anyhow",
"arboard",
"assert_matches",
"base64 0.22.1",
"chrono",
"clap",
"codex-ansi-escape",
"codex-app-server-client",
"codex-app-server-protocol",
"codex-arg0",
"codex-backend-client",
"codex-chatgpt",
"codex-cli",
"codex-client",
"codex-cloud-requirements",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-feedback",
"codex-file-search",
"codex-git-utils",
"codex-login",
"codex-otel",
"codex-protocol",
"codex-shell-command",
"codex-state",
"codex-terminal-detection",
"codex-tui-app-server",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-utils-elapsed",
"codex-utils-fuzzy-match",
"codex-utils-oss",
"codex-utils-pty",
"codex-utils-sandbox-summary",
"codex-utils-sleep-inhibitor",
"codex-utils-string",
"codex-windows-sandbox",
"color-eyre",
"cpal",
"crossterm",
"derive_more 2.1.1",
"diffy",
"dirs",
"dunce",
"hound",
"image",
"insta",
"itertools 0.14.0",
"lazy_static",
"libc",
"pathdiff",
"pretty_assertions",
"pulldown-cmark",
"rand 0.9.2",
"ratatui",
"ratatui-macros",
"regex-lite",
"reqwest",
"rmcp",
"serde",
"serde_json",
"serial_test",
"shlex",
"strum 0.27.2",
"strum_macros 0.28.0",
"supports-color 3.0.2",
"syntect",
"tempfile",
"textwrap 0.16.2",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
"toml 0.9.11+spec-1.1.0",
"tracing",
"tracing-appender",
"tracing-subscriber",
"two-face",
"unicode-segmentation",
"unicode-width 0.2.1",
"url",
"uuid",
"vt100",
"webbrowser",
"which 8.0.0",
"windows-sys 0.52.0",
"winsplit",
]
[[package]]
name = "codex-tui-app-server"
version = "0.0.0"
dependencies = [
"anyhow",
"arboard",
+1 -3
View File
@@ -49,7 +49,6 @@ members = [
"sandboxing",
"stdio-to-uds",
"otel",
"tui",
"tui_app_server",
"tools",
"v8-poc",
@@ -150,8 +149,7 @@ codex-state = { path = "state" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-terminal-detection = { path = "terminal-detection" }
codex-tools = { path = "tools" }
codex-tui = { path = "tui" }
codex-tui-app-server = { path = "tui_app_server" }
codex-tui = { path = "tui_app_server" }
codex-v8-poc = { path = "v8-poc" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
codex-utils-approval-presets = { path = "utils/approval-presets" }
+1 -1
View File
@@ -50,7 +50,7 @@ You can enable notifications by configuring a script that is run whenever the ag
### `codex exec` to run Codex programmatically/non-interactively
To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on.
To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. If you provide both a prompt argument and piped stdin, Codex appends stdin as a `<stdin>` block after the prompt so patterns like `echo "my output" | codex exec "Summarize this concisely"` work naturally. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on.
Use `codex exec --ephemeral ...` to run without persisting session rollout files to disk.
### Experimenting with the Codex Sandbox
@@ -49,7 +49,11 @@ fn normalize_path_for_skill_id_repo_scoped_uses_relative_path() {
fn normalize_path_for_skill_id_user_scoped_uses_absolute_path() {
let skill_path = PathBuf::from("/Users/abc/.codex/skills/doc/SKILL.md");
let path = normalize_path_for_skill_id(None, None, skill_path.as_path());
let path = normalize_path_for_skill_id(
/*repo_url*/ None,
/*repo_root*/ None,
skill_path.as_path(),
);
let expected = expected_absolute_path(&skill_path);
assert_eq!(path, expected);
@@ -59,7 +63,11 @@ fn normalize_path_for_skill_id_user_scoped_uses_absolute_path() {
fn normalize_path_for_skill_id_admin_scoped_uses_absolute_path() {
let skill_path = PathBuf::from("/etc/codex/skills/doc/SKILL.md");
let path = normalize_path_for_skill_id(None, None, skill_path.as_path());
let path = normalize_path_for_skill_id(
/*repo_url*/ None,
/*repo_root*/ None,
skill_path.as_path(),
);
let expected = expected_absolute_path(&skill_path);
assert_eq!(path, expected);
+3 -2
View File
@@ -917,7 +917,7 @@ mod tests {
+ 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
start_test_remote_server_with_auth(None, handler).await
start_test_remote_server_with_auth(/*expected_auth_token*/ None, handler).await
}
async fn start_test_remote_server_with_auth<F, Fut>(
@@ -1164,7 +1164,8 @@ mod tests {
#[tokio::test]
async fn tiny_channel_capacity_still_supports_request_roundtrip() {
let client = start_test_client_with_capacity(SessionSource::Exec, 1).await;
let client =
start_test_client_with_capacity(SessionSource::Exec, /*channel_capacity*/ 1).await;
let _response: ConfigRequirementsReadResponse = client
.request_typed(ClientRequest::ConfigRequirementsRead {
request_id: RequestId::Integer(1),
+2 -3
View File
@@ -4,9 +4,8 @@ This module implements the websocket-backed app-server client transport.
It owns the remote connection lifecycle, including the initialize/initialized
handshake, JSON-RPC request/response routing, server-request resolution, and
notification streaming. The rest of the crate uses the same `AppServerEvent`
surface for both in-process and remote transports, so callers such as
`tui_app_server` can switch between them without changing their higher-level
session logic.
surface for both in-process and remote transports, so callers such as the TUI
can switch between them without changing their higher-level session logic.
*/
use std::collections::HashMap;
+1 -1
View File
@@ -2690,7 +2690,7 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k
fn generate_json_filters_experimental_fields_and_methods() -> Result<()> {
let output_dir = std::env::temp_dir().join(format!("codex_schema_{}", Uuid::now_v7()));
fs::create_dir(&output_dir)?;
generate_json_with_experimental(&output_dir, false)?;
generate_json_with_experimental(&output_dir, /*experimental_api*/ false)?;
let thread_start_json =
fs::read_to_string(output_dir.join("v2").join("ThreadStartParams.json"))?;
@@ -23,7 +23,7 @@ fn typescript_schema_fixtures_match_generated() -> Result<()> {
#[test]
fn json_schema_fixtures_match_generated() -> Result<()> {
assert_schema_fixtures_match_generated("json", |output_dir| {
generate_json_with_experimental(output_dir, false)
generate_json_with_experimental(output_dir, /*experimental_api*/ false)
})
}
@@ -8747,7 +8747,7 @@ mod tests {
fn config_load_error_marks_non_auth_cloud_requirements_failures_without_relogin() {
let err = std::io::Error::other(CloudRequirementsLoadError::new(
CloudRequirementsLoadErrorCode::RequestFailed,
None,
/*status_code*/ None,
"failed to load your workspace-managed config",
));
@@ -8961,7 +8961,8 @@ mod tests {
fn merge_persisted_resume_metadata_skips_missing_values() -> Result<()> {
let mut request_overrides = None;
let mut typesafe_overrides = ConfigOverrides::default();
let persisted_metadata = test_thread_metadata(None, None)?;
let persisted_metadata =
test_thread_metadata(/*model*/ None, /*reasoning_effort*/ None)?;
merge_persisted_resume_metadata(
&mut request_overrides,
@@ -9013,7 +9014,7 @@ mod tests {
path.clone(),
&head,
&session_meta,
None,
/*git*/ None,
"test-provider",
timestamp.clone(),
)
@@ -9223,9 +9224,9 @@ mod tests {
source,
Some("atlas".to_string()),
Some("explorer".to_string()),
None,
None,
None,
/*git_sha*/ None,
/*git_branch*/ None,
/*git_origin_url*/ None,
);
let thread = summary_to_thread(summary);
@@ -9244,7 +9245,9 @@ mod tests {
manager.connection_initialized(connection).await;
manager
.try_ensure_connection_subscribed(thread_id, connection, false)
.try_ensure_connection_subscribed(
thread_id, connection, /*experimental_raw_events*/ false,
)
.await
.expect("connection should be live");
{
@@ -9288,11 +9291,19 @@ mod tests {
manager.connection_initialized(connection_a).await;
manager.connection_initialized(connection_b).await;
manager
.try_ensure_connection_subscribed(thread_id, connection_a, false)
.try_ensure_connection_subscribed(
thread_id,
connection_a,
/*experimental_raw_events*/ false,
)
.await
.expect("connection_a should be live");
manager
.try_ensure_connection_subscribed(thread_id, connection_b, false)
.try_ensure_connection_subscribed(
thread_id,
connection_b,
/*experimental_raw_events*/ false,
)
.await
.expect("connection_b should be live");
{
@@ -9325,7 +9336,9 @@ mod tests {
assert!(
manager
.try_ensure_connection_subscribed(thread_id, connection, false)
.try_ensure_connection_subscribed(
thread_id, connection, /*experimental_raw_events*/ false
)
.await
.is_none()
);
@@ -137,7 +137,7 @@ mod tests {
&all_connectors,
&[],
&[AppConnectorId("alpha".to_string())],
false,
/*codex_apps_ready*/ false,
),
Vec::new()
);
+1 -1
View File
@@ -90,7 +90,7 @@ mod tests {
#[test]
fn compute_source_filters_defaults_to_interactive_sources() {
let (allowed_sources, filter) = compute_source_filters(None);
let (allowed_sources, filter) = compute_source_filters(/*source_kinds*/ None);
assert_eq!(allowed_sources, INTERACTIVE_SESSION_SOURCES.to_vec());
assert_eq!(filter, None);
+2 -1
View File
@@ -786,7 +786,8 @@ mod tests {
#[tokio::test]
async fn in_process_start_clamps_zero_channel_capacity() {
let client = start_test_client_with_capacity(SessionSource::Cli, 0).await;
let client =
start_test_client_with_capacity(SessionSource::Cli, /*channel_capacity*/ 0).await;
let response = loop {
match client
.request(ClientRequest::ConfigRequirementsRead {
+4 -1
View File
@@ -860,7 +860,10 @@ mod tests {
#[test]
fn log_format_from_env_value_defaults_for_non_json_values() {
assert_eq!(LogFormat::from_env_value(None), LogFormat::Default);
assert_eq!(
LogFormat::from_env_value(/*value*/ None),
LogFormat::Default
);
assert_eq!(LogFormat::from_env_value(Some("")), LogFormat::Default);
assert_eq!(LogFormat::from_env_value(Some("text")), LogFormat::Default);
assert_eq!(LogFormat::from_env_value(Some("jsonl")), LogFormat::Default);
+4 -4
View File
@@ -88,7 +88,7 @@ use toml::Value as TomlValue;
use tracing::Instrument;
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
const TUI_APP_SERVER_CLIENT_NAME: &str = "codex-tui";
const TUI_CLIENT_NAME: &str = "codex-tui";
#[derive(Clone)]
struct ExternalAuthRefreshBridge {
@@ -569,9 +569,9 @@ impl MessageProcessor {
} = params.client_info;
session.app_server_client_name = Some(name.clone());
session.client_version = Some(version.clone());
let originator = if name == TUI_APP_SERVER_CLIENT_NAME {
// TODO: Remove this temporary workaround once app-server clients no longer
// need to retain the legacy TUI `codex_cli_rs` originator behavior.
let originator = if name == TUI_CLIENT_NAME {
// TODO: Remove this temporary workaround once the TUI no longer
// needs to retain the `codex_cli_rs` originator behavior.
DEFAULT_ORIGINATOR.to_string()
} else {
name.clone()
@@ -147,7 +147,7 @@ impl TracingHarness {
}),
},
},
None,
/*trace*/ None,
)
.await;
assert!(harness.session.initialized);
@@ -213,7 +213,7 @@ async fn build_test_config(codex_home: &Path, server_uri: &str) -> Result<Config
codex_home,
server_uri,
&BTreeMap::new(),
8_192,
/*auto_compact_limit*/ 8_192,
Some(false),
"mock_provider",
"compact",
@@ -509,7 +509,9 @@ async fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() ->
..
} = RemoteTrace::new("00000000000000000000000000000011", "0000000000000022");
let _: ThreadStartResponse = harness.start_thread(20_002, None).await;
let _: ThreadStartResponse = harness
.start_thread(/*request_id*/ 20_002, /*trace*/ None)
.await;
let untraced_spans = wait_for_exported_spans(harness.tracing, |spans| {
spans.iter().any(|span| {
span.span_kind == SpanKind::Server
@@ -538,10 +540,16 @@ async fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() ->
.span_context
.trace_id(),
);
assert_has_internal_descendant_at_min_depth(&untraced_spans, untraced_server_span, 1);
assert_has_internal_descendant_at_min_depth(
&untraced_spans,
untraced_server_span,
/*min_depth*/ 1,
);
let baseline_len = untraced_spans.len();
let _: ThreadStartResponse = harness.start_thread(20_003, Some(remote_trace)).await;
let _: ThreadStartResponse = harness
.start_thread(/*request_id*/ 20_003, Some(remote_trace))
.await;
let spans = wait_for_new_exported_spans(harness.tracing, baseline_len, |spans| {
spans.iter().any(|span| {
span.span_kind == SpanKind::Server
@@ -561,8 +569,8 @@ async fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() ->
assert!(server_request_span.parent_span_is_remote);
assert_eq!(server_request_span.span_context.trace_id(), remote_trace_id);
assert_ne!(server_request_span.span_context.span_id(), SpanId::INVALID);
assert_has_internal_descendant_at_min_depth(&spans, server_request_span, 1);
assert_has_internal_descendant_at_min_depth(&spans, server_request_span, 2);
assert_has_internal_descendant_at_min_depth(&spans, server_request_span, /*min_depth*/ 1);
assert_has_internal_descendant_at_min_depth(&spans, server_request_span, /*min_depth*/ 2);
harness.shutdown().await;
Ok(())
@@ -572,7 +580,7 @@ async fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() ->
async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
let _guard = tracing_test_guard().lock().await;
let mut harness = TracingHarness::new().await?;
let thread_start_response = harness.start_thread(2, None).await;
let thread_start_response = harness.start_thread(/*request_id*/ 2, /*trace*/ None).await;
let thread_id = thread_start_response.thread.id.clone();
harness.reset_tracing();
+3 -3
View File
@@ -886,7 +886,7 @@ mod tests {
.register_request_context(RequestContext::new(
request_id.clone(),
tracing::info_span!("app_server.request", rpc.method = "thread/start"),
None,
/*parent_trace*/ None,
))
.await;
assert_eq!(outgoing.request_context_count().await, 1);
@@ -997,14 +997,14 @@ mod tests {
.register_request_context(RequestContext::new(
closed_connection_request,
tracing::info_span!("app_server.request", rpc.method = "turn/interrupt"),
None,
/*parent_trace*/ None,
))
.await;
outgoing
.register_request_context(RequestContext::new(
open_connection_request,
tracing::info_span!("app_server.request", rpc.method = "turn/start"),
None,
/*parent_trace*/ None,
))
.await;
assert_eq!(outgoing.request_context_count().await, 2);
+8 -4
View File
@@ -530,7 +530,7 @@ mod tests {
#[test]
fn resolves_in_progress_turn_to_active_status() {
let status = resolve_thread_status(ThreadStatus::Idle, true);
let status = resolve_thread_status(ThreadStatus::Idle, /*has_in_progress_turn*/ true);
assert_eq!(
status,
ThreadStatus::Active {
@@ -538,7 +538,8 @@ mod tests {
}
);
let status = resolve_thread_status(ThreadStatus::NotLoaded, true);
let status =
resolve_thread_status(ThreadStatus::NotLoaded, /*has_in_progress_turn*/ true);
assert_eq!(
status,
ThreadStatus::Active {
@@ -550,11 +551,14 @@ mod tests {
#[test]
fn keeps_status_when_no_in_progress_turn() {
assert_eq!(
resolve_thread_status(ThreadStatus::Idle, false),
resolve_thread_status(ThreadStatus::Idle, /*has_in_progress_turn*/ false),
ThreadStatus::Idle
);
assert_eq!(
resolve_thread_status(ThreadStatus::SystemError, false),
resolve_thread_status(
ThreadStatus::SystemError,
/*has_in_progress_turn*/ false
),
ThreadStatus::SystemError
);
}
+40 -11
View File
@@ -506,8 +506,14 @@ mod tests {
}),
);
let tampered = token.replace(".eyJleHAi", ".eyJleHBi");
let err = verify_signed_bearer_token(&tampered, shared_secret, None, None, 30)
.expect_err("tampered jwt should fail");
let err = verify_signed_bearer_token(
&tampered,
shared_secret,
/*issuer*/ None,
/*audience*/ None,
/*max_clock_skew_seconds*/ 30,
)
.expect_err("tampered jwt should fail");
assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
}
@@ -522,8 +528,14 @@ mod tests {
"aud": "audience",
}),
);
verify_signed_bearer_token(&token, shared_secret, Some("issuer"), Some("audience"), 30)
.expect("valid signed token should verify");
verify_signed_bearer_token(
&token,
shared_secret,
Some("issuer"),
Some("audience"),
/*max_clock_skew_seconds*/ 30,
)
.expect("valid signed token should verify");
}
#[test]
@@ -536,8 +548,14 @@ mod tests {
"aud": ["other-audience", "audience"],
}),
);
verify_signed_bearer_token(&token, shared_secret, None, Some("audience"), 30)
.expect("jwt audience arrays should verify");
verify_signed_bearer_token(
&token,
shared_secret,
/*issuer*/ None,
Some("audience"),
/*max_clock_skew_seconds*/ 30,
)
.expect("jwt audience arrays should verify");
}
#[test]
@@ -550,9 +568,14 @@ mod tests {
);
let header_segment = URL_SAFE_NO_PAD.encode(br#"{"alg":"none","typ":"JWT"}"#);
let token = format!("{header_segment}.{claims_segment}.");
let err =
verify_signed_bearer_token(&token, b"0123456789abcdef0123456789abcdef", None, None, 30)
.expect_err("alg=none jwt should be rejected");
let err = verify_signed_bearer_token(
&token,
b"0123456789abcdef0123456789abcdef",
/*issuer*/ None,
/*audience*/ None,
/*max_clock_skew_seconds*/ 30,
)
.expect_err("alg=none jwt should be rejected");
assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
}
@@ -565,8 +588,14 @@ mod tests {
"iss": "issuer",
}),
);
let err = verify_signed_bearer_token(&token, shared_secret, None, None, 30)
.expect_err("jwt without exp should be rejected");
let err = verify_signed_bearer_token(
&token,
shared_secret,
/*issuer*/ None,
/*audience*/ None,
/*max_clock_skew_seconds*/ 30,
)
.expect_err("jwt without exp should be rejected");
assert_eq!(err.status_code(), StatusCode::UNAUTHORIZED);
}
+6 -6
View File
@@ -646,7 +646,7 @@ mod tests {
initialized,
Arc::new(AtomicBool::new(true)),
opted_out_notification_methods,
None,
/*disconnect_sender*/ None,
),
);
@@ -686,7 +686,7 @@ mod tests {
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::from(["configWarning".to_string()]))),
None,
/*disconnect_sender*/ None,
),
);
@@ -726,7 +726,7 @@ mod tests {
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
None,
/*disconnect_sender*/ None,
),
);
@@ -772,7 +772,7 @@ mod tests {
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(false)),
Arc::new(RwLock::new(HashSet::new())),
None,
/*disconnect_sender*/ None,
),
);
@@ -834,7 +834,7 @@ mod tests {
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
None,
/*disconnect_sender*/ None,
),
);
@@ -1006,7 +1006,7 @@ mod tests {
Arc::new(AtomicBool::new(true)),
Arc::new(AtomicBool::new(true)),
Arc::new(RwLock::new(HashSet::new())),
None,
/*disconnect_sender*/ None,
),
);
+1 -1
View File
@@ -160,7 +160,7 @@ async fn get_auth_status_with_api_key() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn get_auth_status_with_api_key_when_auth_not_required() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml_custom_provider(codex_home.path(), false)?;
create_config_toml_custom_provider(codex_home.path(), /*requires_openai_auth*/ false)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
@@ -45,7 +45,7 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> {
META_RFC3339,
PREVIEW,
Some(MODEL_PROVIDER),
None,
/*git_info*/ None,
)?;
let thread_id = ThreadId::from_string(&conversation_id)?;
let expected = expected_summary(
@@ -86,7 +86,7 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h
META_RFC3339,
PREVIEW,
Some(MODEL_PROVIDER),
None,
/*git_info*/ None,
)?;
let thread_id = ThreadId::from_string(&conversation_id)?;
let rollout_path = rollout_path(codex_home.path(), FILENAME_TS, &conversation_id);
@@ -237,7 +237,11 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
let root_path = root.path().to_string_lossy().to_string();
// Send fuzzyFileSearch request.
let request_id = mcp
.send_fuzzy_file_search_request("abe", vec![root_path.clone()], None)
.send_fuzzy_file_search_request(
"abe",
vec![root_path.clone()],
/*cancellation_token*/ None,
)
.await?;
// Read response and verify shape and ordering.
@@ -298,7 +302,11 @@ async fn test_fuzzy_file_search_accepts_cancellation_token() -> Result<()> {
let root_path = root.path().to_string_lossy().to_string();
let request_id = mcp
.send_fuzzy_file_search_request("alp", vec![root_path.clone()], None)
.send_fuzzy_file_search_request(
"alp",
vec![root_path.clone()],
/*cancellation_token*/ None,
)
.await?;
let request_id_2 = mcp
@@ -981,7 +981,7 @@ async fn login_account_chatgpt_device_code_returns_error_when_disabled() -> Resu
},
)?;
write_models_cache(codex_home.path())?;
mock_device_code_usercode_failure(&mock_server, 404).await;
mock_device_code_usercode_failure(&mock_server, /*status*/ 404).await;
let issuer = mock_server.uri();
let mut mcp = McpProcess::new_with_env(
@@ -1038,7 +1038,7 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
)?;
write_models_cache(codex_home.path())?;
mock_device_code_usercode(&mock_server, 0).await;
mock_device_code_usercode(&mock_server, /*interval_seconds*/ 0).await;
mock_device_code_token_success(&mock_server).await;
let id_token = encode_id_token(
&ChatGptIdTokenClaims::new()
@@ -1122,8 +1122,8 @@ async fn login_account_chatgpt_device_code_failure_notifies_without_account_upda
)?;
write_models_cache(codex_home.path())?;
mock_device_code_usercode(&mock_server, 0).await;
mock_device_code_token_failure(&mock_server, 500).await;
mock_device_code_usercode(&mock_server, /*interval_seconds*/ 0).await;
mock_device_code_token_failure(&mock_server, /*status*/ 500).await;
let issuer = mock_server.uri();
let mut mcp = McpProcess::new_with_env(
@@ -1197,8 +1197,8 @@ async fn login_account_chatgpt_device_code_can_be_cancelled() -> Result<()> {
)?;
write_models_cache(codex_home.path())?;
mock_device_code_usercode(&mock_server, 1).await;
mock_device_code_token_failure(&mock_server, 404).await;
mock_device_code_usercode(&mock_server, /*interval_seconds*/ 1).await;
mock_device_code_token_failure(&mock_server, /*status*/ 404).await;
let issuer = mock_server.uri();
let mut mcp = McpProcess::new_with_env(
@@ -31,7 +31,7 @@ async fn app_server_default_analytics_disabled_without_flag() -> Result<()> {
&config,
SERVICE_VERSION,
Some("codex-app-server"),
false,
/*default_analytics_enabled*/ false,
)
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
@@ -56,7 +56,7 @@ async fn app_server_default_analytics_enabled_with_flag() -> Result<()> {
&config,
SERVICE_VERSION,
Some("codex-app-server"),
true,
/*default_analytics_enabled*/ true,
)
.map_err(|err| anyhow::anyhow!(err.to_string()))?;
@@ -722,15 +722,15 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
let mut ws1 = connect_websocket(bind_addr).await?;
let mut ws2 = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws1, 1, "ws_client_one").await?;
read_initialize_response(&mut ws1, 1).await?;
send_initialize_request(&mut ws2, 2, "ws_client_two").await?;
read_initialize_response(&mut ws2, 2).await?;
send_initialize_request(&mut ws1, /*id*/ 1, "ws_client_one").await?;
read_initialize_response(&mut ws1, /*request_id*/ 1).await?;
send_initialize_request(&mut ws2, /*id*/ 2, "ws_client_two").await?;
read_initialize_response(&mut ws2, /*request_id*/ 2).await?;
send_request(
&mut ws1,
"command/exec",
101,
/*id*/ 101,
Some(serde_json::json!({
"command": [
"python3",
@@ -749,12 +749,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
assert_eq!(delta.stream, CommandExecOutputStream::Stdout);
let delta_text = String::from_utf8(STANDARD.decode(&delta.delta_base64)?)?;
assert!(delta_text.contains("ready"));
wait_for_process_marker(&marker, true).await?;
wait_for_process_marker(&marker, /*should_exist*/ true).await?;
send_request(
&mut ws2,
"command/exec/terminate",
102,
/*id*/ 102,
Some(serde_json::json!({
"processId": "shared-process",
})),
@@ -773,12 +773,12 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
terminate_error.error.message,
"no active command/exec for process id \"shared-process\""
);
wait_for_process_marker(&marker, true).await?;
wait_for_process_marker(&marker, /*should_exist*/ true).await?;
assert_no_message(&mut ws2, Duration::from_millis(250)).await?;
ws1.close(None).await?;
wait_for_process_marker(&marker, false).await?;
wait_for_process_marker(&marker, /*should_exist*/ false).await?;
process
.kill()
@@ -50,19 +50,19 @@ async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()>
let server = responses::start_mock_server().await;
let sse1 = responses::sse(vec![
responses::ev_assistant_message("m1", "FIRST_REPLY"),
responses::ev_completed_with_tokens("r1", 70_000),
responses::ev_completed_with_tokens("r1", /*total_tokens*/ 70_000),
]);
let sse2 = responses::sse(vec![
responses::ev_assistant_message("m2", "SECOND_REPLY"),
responses::ev_completed_with_tokens("r2", 330_000),
responses::ev_completed_with_tokens("r2", /*total_tokens*/ 330_000),
]);
let sse3 = responses::sse(vec![
responses::ev_assistant_message("m3", "LOCAL_SUMMARY"),
responses::ev_completed_with_tokens("r3", 200),
responses::ev_completed_with_tokens("r3", /*total_tokens*/ 200),
]);
let sse4 = responses::sse(vec![
responses::ev_assistant_message("m4", "FINAL_REPLY"),
responses::ev_completed_with_tokens("r4", 120),
responses::ev_completed_with_tokens("r4", /*total_tokens*/ 120),
]);
responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await;
@@ -72,7 +72,7 @@ async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()>
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
/*requires_openai_auth*/ None,
"mock_provider",
COMPACT_PROMPT,
)?;
@@ -110,15 +110,15 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
let server = responses::start_mock_server().await;
let sse1 = responses::sse(vec![
responses::ev_assistant_message("m1", "FIRST_REPLY"),
responses::ev_completed_with_tokens("r1", 70_000),
responses::ev_completed_with_tokens("r1", /*total_tokens*/ 70_000),
]);
let sse2 = responses::sse(vec![
responses::ev_assistant_message("m2", "SECOND_REPLY"),
responses::ev_completed_with_tokens("r2", 330_000),
responses::ev_completed_with_tokens("r2", /*total_tokens*/ 330_000),
]);
let sse3 = responses::sse(vec![
responses::ev_assistant_message("m3", "FINAL_REPLY"),
responses::ev_completed_with_tokens("r3", 120),
responses::ev_completed_with_tokens("r3", /*total_tokens*/ 120),
]);
let responses_log = responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3]).await;
@@ -197,7 +197,7 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
let server = responses::start_mock_server().await;
let sse = responses::sse(vec![
responses::ev_assistant_message("m1", "MANUAL_COMPACT_SUMMARY"),
responses::ev_completed_with_tokens("r1", 200),
responses::ev_completed_with_tokens("r1", /*total_tokens*/ 200),
]);
responses::mount_sse_sequence(&server, vec![sse]).await;
@@ -207,7 +207,7 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
/*requires_openai_auth*/ None,
"mock_provider",
COMPACT_PROMPT,
)?;
@@ -257,7 +257,7 @@ async fn thread_compact_start_rejects_invalid_thread_id() -> Result<()> {
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
/*requires_openai_auth*/ None,
"mock_provider",
COMPACT_PROMPT,
)?;
@@ -293,7 +293,7 @@ async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()> {
&server.uri(),
&BTreeMap::default(),
AUTO_COMPACT_LIMIT,
None,
/*requires_openai_auth*/ None,
"mock_provider",
COMPACT_PROMPT,
)?;
@@ -58,27 +58,27 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() ->
let mut ws1 = connect_websocket(bind_addr).await?;
let mut ws2 = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws1, 1, "ws_client_one").await?;
let first_init = read_response_for_id(&mut ws1, 1).await?;
send_initialize_request(&mut ws1, /*id*/ 1, "ws_client_one").await?;
let first_init = read_response_for_id(&mut ws1, /*id*/ 1).await?;
assert_eq!(first_init.id, RequestId::Integer(1));
// Initialize responses are request-scoped and must not leak to other
// connections.
assert_no_message(&mut ws2, Duration::from_millis(250)).await?;
send_config_read_request(&mut ws2, 2).await?;
let not_initialized = read_error_for_id(&mut ws2, 2).await?;
send_config_read_request(&mut ws2, /*id*/ 2).await?;
let not_initialized = read_error_for_id(&mut ws2, /*id*/ 2).await?;
assert_eq!(not_initialized.error.message, "Not initialized");
send_initialize_request(&mut ws2, 3, "ws_client_two").await?;
let second_init = read_response_for_id(&mut ws2, 3).await?;
send_initialize_request(&mut ws2, /*id*/ 3, "ws_client_two").await?;
let second_init = read_response_for_id(&mut ws2, /*id*/ 3).await?;
assert_eq!(second_init.id, RequestId::Integer(3));
// Same request-id on different connections must route independently.
send_config_read_request(&mut ws1, 77).await?;
send_config_read_request(&mut ws2, 77).await?;
let ws1_config = read_response_for_id(&mut ws1, 77).await?;
let ws2_config = read_response_for_id(&mut ws2, 77).await?;
send_config_read_request(&mut ws1, /*id*/ 77).await?;
send_config_read_request(&mut ws2, /*id*/ 77).await?;
let ws1_config = read_response_for_id(&mut ws1, /*id*/ 77).await?;
let ws2_config = read_response_for_id(&mut ws2, /*id*/ 77).await?;
assert_eq!(ws1_config.id, RequestId::Integer(77));
assert_eq!(ws2_config.id, RequestId::Integer(77));
@@ -108,8 +108,8 @@ async fn websocket_transport_serves_health_endpoints_on_same_listener() -> Resul
assert_eq!(healthz.status(), StatusCode::OK);
let mut ws = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws, 1, "ws_health_client").await?;
let init = read_response_for_id(&mut ws, 1).await?;
send_initialize_request(&mut ws, /*id*/ 1, "ws_health_client").await?;
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
assert_eq!(init.id, RequestId::Integer(1));
process
@@ -128,14 +128,14 @@ async fn websocket_transport_rejects_browser_origin_without_auth() -> Result<()>
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
let mut ws = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws, 1, "ws_loopback_client").await?;
let init = read_response_for_id(&mut ws, 1).await?;
send_initialize_request(&mut ws, /*id*/ 1, "ws_loopback_client").await?;
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
assert_eq!(init.id, RequestId::Integer(1));
drop(ws);
assert_websocket_connect_rejected_with_headers(
bind_addr,
None,
/*bearer_token*/ None,
Some("https://evil.example"),
StatusCode::FORBIDDEN,
)
@@ -165,12 +165,12 @@ async fn websocket_transport_rejects_missing_and_invalid_capability_tokens() ->
let (mut process, bind_addr) =
spawn_websocket_server_with_args(codex_home.path(), "ws://127.0.0.1:0", &auth_args).await?;
assert_websocket_connect_rejected(bind_addr, None).await?;
assert_websocket_connect_rejected(bind_addr, /*bearer_token*/ None).await?;
assert_websocket_connect_rejected(bind_addr, Some("wrong-token")).await?;
let mut ws = connect_websocket_with_bearer(bind_addr, Some("super-secret-token")).await?;
send_initialize_request(&mut ws, 1, "ws_auth_client").await?;
let init = read_response_for_id(&mut ws, 1).await?;
send_initialize_request(&mut ws, /*id*/ 1, "ws_auth_client").await?;
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
assert_eq!(init.id, RequestId::Integer(1));
process
@@ -266,8 +266,8 @@ async fn websocket_transport_verifies_signed_short_lived_bearer_tokens() -> Resu
}),
)?;
let mut ws = connect_websocket_with_bearer(bind_addr, Some(valid_token.as_str())).await?;
send_initialize_request(&mut ws, 1, "ws_signed_auth_client").await?;
let init = read_response_for_id(&mut ws, 1).await?;
send_initialize_request(&mut ws, /*id*/ 1, "ws_signed_auth_client").await?;
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
assert_eq!(init.id, RequestId::Integer(1));
process
@@ -320,8 +320,8 @@ async fn websocket_transport_allows_unauthenticated_non_loopback_startup_by_defa
spawn_websocket_server_with_args(codex_home.path(), "ws://0.0.0.0:0", &[]).await?;
let mut ws = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws, 1, "ws_non_loopback_default_client").await?;
let init = read_response_for_id(&mut ws, 1).await?;
send_initialize_request(&mut ws, /*id*/ 1, "ws_non_loopback_default_client").await?;
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
assert_eq!(init.id, RequestId::Integer(1));
process
@@ -411,7 +411,7 @@ pub(super) async fn spawn_websocket_server_with_args(
}
pub(super) async fn connect_websocket(bind_addr: SocketAddr) -> Result<WsClient> {
connect_websocket_with_bearer(bind_addr, None).await
connect_websocket_with_bearer(bind_addr, /*bearer_token*/ None).await
}
pub(super) async fn connect_websocket_with_bearer(
@@ -419,7 +419,7 @@ pub(super) async fn connect_websocket_with_bearer(
bearer_token: Option<&str>,
) -> Result<WsClient> {
let url = format!("ws://{}", connectable_bind_addr(bind_addr));
let request = websocket_request(url.as_str(), bearer_token, None)?;
let request = websocket_request(url.as_str(), bearer_token, /*origin*/ None)?;
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match connect_async(request.clone()).await {
@@ -441,7 +441,7 @@ async fn assert_websocket_connect_rejected(
assert_websocket_connect_rejected_with_headers(
bind_addr,
bearer_token,
None,
/*origin*/ None,
StatusCode::UNAUTHORIZED,
)
.await
@@ -156,16 +156,16 @@ async fn start_ctrl_c_restart_fixture(turn_delay: Duration) -> Result<GracefulCt
let (process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
let mut ws = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws, 1, "ws_graceful_shutdown").await?;
let init_response = read_response_for_id(&mut ws, 1).await?;
send_initialize_request(&mut ws, /*id*/ 1, "ws_graceful_shutdown").await?;
let init_response = read_response_for_id(&mut ws, /*id*/ 1).await?;
assert_eq!(init_response.id, RequestId::Integer(1));
send_thread_start_request(&mut ws, 2).await?;
let thread_start_response = read_response_for_id(&mut ws, 2).await?;
send_thread_start_request(&mut ws, /*id*/ 2).await?;
let thread_start_response = read_response_for_id(&mut ws, /*id*/ 2).await?;
let ThreadStartResponse { thread, .. } = to_response(thread_start_response)?;
send_turn_start_request(&mut ws, 3, &thread.id).await?;
let turn_start_response = read_response_for_id(&mut ws, 3).await?;
send_turn_start_request(&mut ws, /*id*/ 3, &thread.id).await?;
let turn_start_response = read_response_for_id(&mut ws, /*id*/ 3).await?;
assert_eq!(turn_start_response.id, RequestId::Integer(3));
wait_for_responses_post(&server, Duration::from_secs(5)).await?;
@@ -56,8 +56,10 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
fn expected_visible_models() -> Vec<Model> {
// Filter by supported_in_api to support testing with both ChatGPT and non-ChatGPT auth modes.
let mut presets =
ModelPreset::filter_by_auth(codex_core::test_support::all_model_presets().clone(), false);
let mut presets = ModelPreset::filter_by_auth(
codex_core::test_support::all_model_presets().clone(),
/*chatgpt_mode*/ false,
);
// Mirror `ModelsManager::build_available_models()` default selection after auth filtering.
ModelPreset::mark_default_by_picker_visibility(&mut presets);
@@ -59,7 +59,7 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> {
let turn = start_plan_mode_turn(&mut mcp).await?;
let (_, completed_items, plan_deltas, turn_completed) =
collect_turn_notifications(&mut mcp).await?;
wait_for_responses_request_count(&server, 1).await?;
wait_for_responses_request_count(&server, /*expected_count*/ 1).await?;
assert_eq!(turn_completed.turn.id, turn.id);
assert_eq!(turn_completed.turn.status, TurnStatus::Completed);
@@ -116,7 +116,7 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()>
let _turn = start_plan_mode_turn(&mut mcp).await?;
let (_, completed_items, plan_deltas, _) = collect_turn_notifications(&mut mcp).await?;
wait_for_responses_request_count(&server, 1).await?;
wait_for_responses_request_count(&server, /*expected_count*/ 1).await?;
let has_plan_item = completed_items
.iter()
@@ -118,7 +118,7 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re
"sample-plugin",
"./sample-plugin",
Some("NOT_AVAILABLE"),
None,
/*auth_policy*/ None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
let marketplace_path =
@@ -217,8 +217,8 @@ async fn plugin_install_force_remote_sync_enables_remote_plugin_before_local_ins
"debug",
"sample-plugin",
"./sample-plugin",
None,
None,
/*install_policy*/ None,
/*auth_policy*/ None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
let marketplace_path =
@@ -286,8 +286,8 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
"debug",
"sample-plugin",
"./sample-plugin",
None,
None,
/*install_policy*/ None,
/*auth_policy*/ None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
let marketplace_path =
@@ -401,8 +401,8 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
"debug",
"sample-plugin",
"./sample-plugin",
None,
None,
/*install_policy*/ None,
/*auth_policy*/ None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &["alpha", "beta"])?;
let marketplace_path =
@@ -481,7 +481,7 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> {
"debug",
"sample-plugin",
"./sample-plugin",
None,
/*install_policy*/ None,
Some("ON_USE"),
)?;
write_plugin_source(
@@ -542,8 +542,8 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests
"debug",
"sample-plugin",
"./sample-plugin",
None,
None,
/*install_policy*/ None,
/*auth_policy*/ None,
)?;
write_plugin_source(repo_root.path(), "sample-plugin", &[])?;
std::fs::write(
@@ -931,7 +931,8 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_path_exists(&marker_path).await?;
wait_for_remote_plugin_request_count(&server, "/plugins/list", 1).await?;
wait_for_remote_plugin_request_count(&server, "/plugins/list", /*expected_count*/ 1)
.await?;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: None,
@@ -957,7 +958,8 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
.collect::<Vec<_>>(),
vec![("linear@openai-curated".to_string(), true, true)]
);
wait_for_remote_plugin_request_count(&server, "/plugins/list", 1).await?;
wait_for_remote_plugin_request_count(&server, "/plugins/list", /*expected_count*/ 1)
.await?;
}
let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?;
@@ -969,7 +971,7 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
}
tokio::time::sleep(Duration::from_millis(250)).await;
wait_for_remote_plugin_request_count(&server, "/plugins/list", 1).await?;
wait_for_remote_plugin_request_count(&server, "/plugins/list", /*expected_count*/ 1).await?;
Ok(())
}
@@ -1029,7 +1031,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() ->
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_featured_plugin_request_count(&server, 1).await?;
wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
@@ -99,7 +99,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
codex_home.path(),
&responses_server.uri(),
realtime_server.uri(),
true,
/*realtime_enabled*/ true,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -137,7 +137,9 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
assert!(started.session_id.is_some());
assert_eq!(started.version, RealtimeConversationVersion::V2);
let startup_context_request = realtime_server.wait_for_request(0, 0).await;
let startup_context_request = realtime_server
.wait_for_request(/*connection_index*/ 0, /*request_index*/ 0)
.await;
assert_eq!(
startup_context_request.body_json()["type"].as_str(),
Some("session.update")
@@ -306,7 +308,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
codex_home.path(),
&responses_server.uri(),
realtime_server.uri(),
true,
/*realtime_enabled*/ true,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -378,7 +380,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> {
codex_home.path(),
&responses_server.uri(),
realtime_server.uri(),
false,
/*realtime_enabled*/ false,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
+1 -1
View File
@@ -149,7 +149,7 @@ async fn review_start_exec_approval_item_id_matches_command_execution_item() ->
"rev-parse".to_string(),
"HEAD".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"review-call-1",
)?,
@@ -53,7 +53,7 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> {
"2025-01-05T12:00:00Z",
preview,
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let original_path = codex_home
@@ -267,7 +267,7 @@ async fn thread_fork_surfaces_cloud_requirements_load_errors() -> Result<()> {
"2025-01-05T12:00:00Z",
"Saved user message",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let refresh_token_url = format!("{}/oauth/token", server.uri());
@@ -331,7 +331,7 @@ async fn thread_fork_ephemeral_remains_pathless_and_omits_listing() -> Result<()
"2025-01-05T12:00:00Z",
preview,
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
+142 -106
View File
@@ -58,7 +58,16 @@ async fn list_threads(
source_kinds: Option<Vec<ThreadSourceKind>>,
archived: Option<bool>,
) -> Result<ThreadListResponse> {
list_threads_with_sort(mcp, cursor, limit, providers, source_kinds, None, archived).await
list_threads_with_sort(
mcp,
cursor,
limit,
providers,
source_kinds,
/*sort_key*/ None,
archived,
)
.await
}
async fn list_threads_with_sort(
@@ -110,7 +119,7 @@ where
&ts_rfc,
preview,
Some(provider_for_index(i)),
None,
/*git_info*/ None,
)?);
}
Ok(ids)
@@ -172,11 +181,11 @@ async fn thread_list_basic_empty() -> Result<()> {
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert!(data.is_empty());
@@ -256,7 +265,7 @@ async fn thread_list_reports_system_error_idle_flag_after_failed_turn() -> Resul
let ThreadListResponse { data, .. } = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(vec![
@@ -264,7 +273,7 @@ async fn thread_list_reports_system_error_idle_flag_after_failed_turn() -> Resul
ThreadSourceKind::Cli,
ThreadSourceKind::VsCode,
]),
None,
/*archived*/ None,
)
.await?;
let listed = data
@@ -323,7 +332,7 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
"2025-01-02T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let _b = create_fake_rollout(
codex_home.path(),
@@ -331,7 +340,7 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
"2025-01-01T13:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let _c = create_fake_rollout(
codex_home.path(),
@@ -339,7 +348,7 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
"2025-01-01T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = init_mcp(codex_home.path()).await?;
@@ -350,11 +359,11 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
next_cursor: cursor1,
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(2),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert_eq!(data1.len(), 2);
@@ -380,8 +389,8 @@ async fn thread_list_pagination_next_cursor_none_on_last_page() -> Result<()> {
Some(cursor1),
Some(2),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert!(data2.len() <= 2);
@@ -413,7 +422,7 @@ async fn thread_list_respects_provider_filter() -> Result<()> {
"2025-01-02T10:00:00Z",
"X",
Some("mock_provider"),
None,
/*git_info*/ None,
)?; // mock_provider
let _b = create_fake_rollout(
codex_home.path(),
@@ -421,7 +430,7 @@ async fn thread_list_respects_provider_filter() -> Result<()> {
"2025-01-02T11:00:00Z",
"X",
Some("other_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = init_mcp(codex_home.path()).await?;
@@ -431,11 +440,11 @@ async fn thread_list_respects_provider_filter() -> Result<()> {
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["other_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert_eq!(data.len(), 1);
@@ -465,7 +474,7 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
"2025-01-02T10:00:00Z",
"filtered",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let unfiltered_id = create_fake_rollout(
codex_home.path(),
@@ -473,7 +482,7 @@ async fn thread_list_respects_cwd_filter() -> Result<()> {
"2025-01-02T11:00:00Z",
"unfiltered",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let target_cwd = codex_home.path().join("target-cwd");
@@ -535,7 +544,7 @@ sqlite = true
"2025-01-02T10:00:00Z",
"match: needle",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let _non_match = create_fake_rollout(
codex_home.path(),
@@ -543,7 +552,7 @@ sqlite = true
"2025-01-02T11:00:00Z",
"no hit here",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let newer_match = create_fake_rollout(
codex_home.path(),
@@ -551,7 +560,7 @@ sqlite = true
"2025-01-02T12:00:00Z",
"needle suffix",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
// `thread/list` only applies `search_term` on the sqlite path. In this test we
@@ -560,7 +569,9 @@ sqlite = true
let state_db =
codex_state::StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into())
.await?;
state_db.mark_backfill_complete(None).await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
let mut mcp = init_mcp(codex_home.path()).await?;
let request_id = mcp
@@ -602,7 +613,7 @@ async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result
"2025-02-01T10:00:00Z",
"CLI",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let exec_id = create_fake_rollout_with_source(
codex_home.path(),
@@ -610,7 +621,7 @@ async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result
"2025-02-01T11:00:00Z",
"Exec",
Some("mock_provider"),
None,
/*git_info*/ None,
CoreSessionSource::Exec,
)?;
@@ -620,11 +631,11 @@ async fn thread_list_empty_source_kinds_defaults_to_interactive_only() -> Result
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(Vec::new()),
None,
/*archived*/ None,
)
.await?;
@@ -648,7 +659,7 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
"2025-02-01T10:00:00Z",
"CLI",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let parent_thread_id = ThreadId::from_string(&Uuid::new_v4().to_string())?;
@@ -658,7 +669,7 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
"2025-02-01T11:00:00Z",
"SubAgent",
Some("mock_provider"),
None,
/*git_info*/ None,
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
@@ -674,11 +685,11 @@ async fn thread_list_filters_by_source_kind_subagent_thread_spawn() -> Result<()
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(vec![ThreadSourceKind::SubAgentThreadSpawn]),
None,
/*archived*/ None,
)
.await?;
@@ -704,7 +715,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
"2025-02-02T09:00:00Z",
"Review",
Some("mock_provider"),
None,
/*git_info*/ None,
CoreSessionSource::SubAgent(SubAgentSource::Review),
)?;
let compact_id = create_fake_rollout_with_source(
@@ -713,7 +724,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
"2025-02-02T10:00:00Z",
"Compact",
Some("mock_provider"),
None,
/*git_info*/ None,
CoreSessionSource::SubAgent(SubAgentSource::Compact),
)?;
let spawn_id = create_fake_rollout_with_source(
@@ -722,7 +733,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
"2025-02-02T11:00:00Z",
"Spawn",
Some("mock_provider"),
None,
/*git_info*/ None,
CoreSessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
@@ -737,7 +748,7 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
"2025-02-02T12:00:00Z",
"Other",
Some("mock_provider"),
None,
/*git_info*/ None,
CoreSessionSource::SubAgent(SubAgentSource::Other("custom".to_string())),
)?;
@@ -745,11 +756,11 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
let review = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(vec![ThreadSourceKind::SubAgentReview]),
None,
/*archived*/ None,
)
.await?;
let review_ids: Vec<_> = review
@@ -761,11 +772,11 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
let compact = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(vec![ThreadSourceKind::SubAgentCompact]),
None,
/*archived*/ None,
)
.await?;
let compact_ids: Vec<_> = compact
@@ -777,11 +788,11 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
let spawn = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(vec![ThreadSourceKind::SubAgentThreadSpawn]),
None,
/*archived*/ None,
)
.await?;
let spawn_ids: Vec<_> = spawn.data.iter().map(|thread| thread.id.as_str()).collect();
@@ -789,11 +800,11 @@ async fn thread_list_filters_by_subagent_variant() -> Result<()> {
let other = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
Some(vec![ThreadSourceKind::SubAgentOther]),
None,
/*archived*/ None,
)
.await?;
let other_ids: Vec<_> = other.data.iter().map(|thread| thread.id.as_str()).collect();
@@ -812,7 +823,7 @@ async fn thread_list_fetches_until_limit_or_exhausted() -> Result<()> {
// paging past the first two pages to reach the desired count.
create_fake_rollouts(
codex_home.path(),
24,
/*count*/ 24,
|i| {
if i < 16 {
"skip_provider"
@@ -820,7 +831,16 @@ async fn thread_list_fetches_until_limit_or_exhausted() -> Result<()> {
"target_provider"
}
},
|i| timestamp_at(2025, 3, 30 - i as u32, 12, 0, 0),
|i| {
timestamp_at(
/*year*/ 2025,
/*month*/ 3,
30 - i as u32,
/*hour*/ 12,
/*minute*/ 0,
/*second*/ 0,
)
},
"Hello",
)?;
@@ -832,11 +852,11 @@ async fn thread_list_fetches_until_limit_or_exhausted() -> Result<()> {
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(8),
Some(vec!["target_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert_eq!(
@@ -864,12 +884,19 @@ async fn thread_list_enforces_max_limit() -> Result<()> {
create_fake_rollouts(
codex_home.path(),
105,
/*count*/ 105,
|_| "mock_provider",
|i| {
let month = 5 + (i / 28);
let day = (i % 28) + 1;
timestamp_at(2025, month as u32, day as u32, 0, 0, 0)
timestamp_at(
/*year*/ 2025,
month as u32,
day as u32,
/*hour*/ 0,
/*minute*/ 0,
/*second*/ 0,
)
},
"Hello",
)?;
@@ -880,11 +907,11 @@ async fn thread_list_enforces_max_limit() -> Result<()> {
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(200),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert_eq!(
@@ -909,7 +936,7 @@ async fn thread_list_stops_when_not_enough_filtered_results_exist() -> Result<()
// ensure the server exhausts pagination without looping forever.
create_fake_rollouts(
codex_home.path(),
22,
/*count*/ 22,
|i| {
if i < 15 {
"skip_provider"
@@ -917,7 +944,16 @@ async fn thread_list_stops_when_not_enough_filtered_results_exist() -> Result<()
"target_provider"
}
},
|i| timestamp_at(2025, 4, 28 - i as u32, 8, 0, 0),
|i| {
timestamp_at(
/*year*/ 2025,
/*month*/ 4,
28 - i as u32,
/*hour*/ 8,
/*minute*/ 0,
/*second*/ 0,
)
},
"Hello",
)?;
@@ -929,11 +965,11 @@ async fn thread_list_stops_when_not_enough_filtered_results_exist() -> Result<()
data, next_cursor, ..
} = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["target_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert_eq!(
@@ -977,11 +1013,11 @@ async fn thread_list_includes_git_info() -> Result<()> {
let ThreadListResponse { data, .. } = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
let thread = data
@@ -1013,7 +1049,7 @@ async fn thread_list_default_sorts_by_created_at() -> Result<()> {
"2025-01-02T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_b = create_fake_rollout(
codex_home.path(),
@@ -1021,7 +1057,7 @@ async fn thread_list_default_sorts_by_created_at() -> Result<()> {
"2025-01-01T13:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_c = create_fake_rollout(
codex_home.path(),
@@ -1029,19 +1065,19 @@ async fn thread_list_default_sorts_by_created_at() -> Result<()> {
"2025-01-01T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = init_mcp(codex_home.path()).await?;
let ThreadListResponse { data, .. } = list_threads_with_sort(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
None,
None,
/*source_kinds*/ None,
/*sort_key*/ None,
/*archived*/ None,
)
.await?;
@@ -1062,7 +1098,7 @@ async fn thread_list_sort_updated_at_orders_by_mtime() -> Result<()> {
"2025-01-01T10:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_mid = create_fake_rollout(
codex_home.path(),
@@ -1070,7 +1106,7 @@ async fn thread_list_sort_updated_at_orders_by_mtime() -> Result<()> {
"2025-01-01T11:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_new = create_fake_rollout(
codex_home.path(),
@@ -1078,7 +1114,7 @@ async fn thread_list_sort_updated_at_orders_by_mtime() -> Result<()> {
"2025-01-01T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
set_rollout_mtime(
@@ -1098,12 +1134,12 @@ async fn thread_list_sort_updated_at_orders_by_mtime() -> Result<()> {
let ThreadListResponse { data, .. } = list_threads_with_sort(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
/*source_kinds*/ None,
Some(ThreadSortKey::UpdatedAt),
None,
/*archived*/ None,
)
.await?;
@@ -1124,7 +1160,7 @@ async fn thread_list_updated_at_paginates_with_cursor() -> Result<()> {
"2025-02-01T10:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_b = create_fake_rollout(
codex_home.path(),
@@ -1132,7 +1168,7 @@ async fn thread_list_updated_at_paginates_with_cursor() -> Result<()> {
"2025-02-01T11:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_c = create_fake_rollout(
codex_home.path(),
@@ -1140,7 +1176,7 @@ async fn thread_list_updated_at_paginates_with_cursor() -> Result<()> {
"2025-02-01T12:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
set_rollout_mtime(
@@ -1164,12 +1200,12 @@ async fn thread_list_updated_at_paginates_with_cursor() -> Result<()> {
..
} = list_threads_with_sort(
&mut mcp,
None,
/*cursor*/ None,
Some(2),
Some(vec!["mock_provider".to_string()]),
None,
/*source_kinds*/ None,
Some(ThreadSortKey::UpdatedAt),
None,
/*archived*/ None,
)
.await?;
let ids_page1: Vec<_> = page1.iter().map(|thread| thread.id.as_str()).collect();
@@ -1185,9 +1221,9 @@ async fn thread_list_updated_at_paginates_with_cursor() -> Result<()> {
Some(cursor1),
Some(2),
Some(vec!["mock_provider".to_string()]),
None,
/*source_kinds*/ None,
Some(ThreadSortKey::UpdatedAt),
None,
/*archived*/ None,
)
.await?;
let ids_page2: Vec<_> = page2.iter().map(|thread| thread.id.as_str()).collect();
@@ -1208,7 +1244,7 @@ async fn thread_list_created_at_tie_breaks_by_uuid() -> Result<()> {
"2025-02-01T10:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_b = create_fake_rollout(
codex_home.path(),
@@ -1216,18 +1252,18 @@ async fn thread_list_created_at_tie_breaks_by_uuid() -> Result<()> {
"2025-02-01T10:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = init_mcp(codex_home.path()).await?;
let ThreadListResponse { data, .. } = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
@@ -1251,7 +1287,7 @@ async fn thread_list_updated_at_tie_breaks_by_uuid() -> Result<()> {
"2025-02-01T10:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let id_b = create_fake_rollout(
codex_home.path(),
@@ -1259,7 +1295,7 @@ async fn thread_list_updated_at_tie_breaks_by_uuid() -> Result<()> {
"2025-02-01T11:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let updated_at = "2025-02-03T00:00:00Z";
@@ -1276,12 +1312,12 @@ async fn thread_list_updated_at_tie_breaks_by_uuid() -> Result<()> {
let ThreadListResponse { data, .. } = list_threads_with_sort(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
/*source_kinds*/ None,
Some(ThreadSortKey::UpdatedAt),
None,
/*archived*/ None,
)
.await?;
@@ -1305,7 +1341,7 @@ async fn thread_list_updated_at_uses_mtime() -> Result<()> {
"2025-02-01T10:00:00Z",
"Hello",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
set_rollout_mtime(
@@ -1317,12 +1353,12 @@ async fn thread_list_updated_at_uses_mtime() -> Result<()> {
let ThreadListResponse { data, .. } = list_threads_with_sort(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
/*source_kinds*/ None,
Some(ThreadSortKey::UpdatedAt),
None,
/*archived*/ None,
)
.await?;
@@ -1351,7 +1387,7 @@ async fn thread_list_archived_filter() -> Result<()> {
"2025-03-01T10:00:00Z",
"Active",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let archived_id = create_fake_rollout(
codex_home.path(),
@@ -1359,7 +1395,7 @@ async fn thread_list_archived_filter() -> Result<()> {
"2025-03-01T09:00:00Z",
"Archived",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let archived_dir = codex_home.path().join(ARCHIVED_SESSIONS_SUBDIR);
@@ -1376,11 +1412,11 @@ async fn thread_list_archived_filter() -> Result<()> {
let ThreadListResponse { data, .. } = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
None,
/*source_kinds*/ None,
/*archived*/ None,
)
.await?;
assert_eq!(data.len(), 1);
@@ -1388,10 +1424,10 @@ async fn thread_list_archived_filter() -> Result<()> {
let ThreadListResponse { data, .. } = list_threads(
&mut mcp,
None,
/*cursor*/ None,
Some(10),
Some(vec!["mock_provider".to_string()]),
None,
/*source_kinds*/ None,
Some(true),
)
.await?;
@@ -184,7 +184,7 @@ async fn thread_metadata_update_repairs_missing_sqlite_row_for_stored_thread() -
"2025-01-05T12:00:00Z",
preview,
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -237,7 +237,7 @@ async fn thread_metadata_update_repairs_loaded_thread_without_resetting_summary(
"2025-01-06T08:30:00Z",
preview,
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let thread_uuid = ThreadId::from_string(&thread_id)?;
let rollout_path = rollout_path(codex_home.path(), "2025-01-06T08-30-00", &thread_id);
@@ -245,10 +245,10 @@ async fn thread_metadata_update_repairs_loaded_thread_without_resetting_summary(
Some(&state_db),
rollout_path.as_path(),
"mock_provider",
None,
/*builder*/ None,
&[],
None,
None,
/*archived_only*/ None,
/*new_thread_memory_mode*/ None,
)
.await;
@@ -317,7 +317,7 @@ async fn thread_metadata_update_repairs_missing_sqlite_row_for_archived_thread()
"2025-01-06T08:30:00Z",
preview,
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let archived_dir = codex_home.path().join(ARCHIVED_SESSIONS_SUBDIR);
@@ -430,7 +430,9 @@ async fn thread_metadata_update_can_clear_stored_git_fields() -> Result<()> {
async fn init_state_db(codex_home: &Path) -> Result<Arc<StateRuntime>> {
let state_db = StateRuntime::init(codex_home.to_path_buf(), "mock_provider".into()).await?;
state_db.mark_backfill_complete(None).await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
Ok(state_db)
}
@@ -43,14 +43,14 @@ async fn thread_name_updated_broadcasts_for_loaded_threads() -> Result<()> {
send_request(
&mut ws1,
"thread/resume",
10,
/*id*/ 10,
Some(serde_json::to_value(ThreadResumeParams {
thread_id: conversation_id.clone(),
..Default::default()
})?),
)
.await?;
let resume_resp: JSONRPCResponse = read_response_for_id(&mut ws1, 10).await?;
let resume_resp: JSONRPCResponse = read_response_for_id(&mut ws1, /*id*/ 10).await?;
let resume: ThreadResumeResponse = to_response::<ThreadResumeResponse>(resume_resp)?;
assert_eq!(resume.thread.id, conversation_id);
@@ -58,15 +58,19 @@ async fn thread_name_updated_broadcasts_for_loaded_threads() -> Result<()> {
send_request(
&mut ws1,
"thread/name/set",
11,
/*id*/ 11,
Some(serde_json::to_value(ThreadSetNameParams {
thread_id: conversation_id.clone(),
name: renamed.to_string(),
})?),
)
.await?;
let (rename_resp, ws1_notification) =
read_response_and_notification_for_method(&mut ws1, 11, "thread/name/updated").await?;
let (rename_resp, ws1_notification) = read_response_and_notification_for_method(
&mut ws1,
/*id*/ 11,
"thread/name/updated",
)
.await?;
let _: ThreadSetNameResponse = to_response::<ThreadSetNameResponse>(rename_resp)?;
assert_thread_name_updated(ws1_notification, &conversation_id, renamed)?;
@@ -105,15 +109,19 @@ async fn thread_name_updated_broadcasts_for_not_loaded_threads() -> Result<()> {
send_request(
&mut ws1,
"thread/name/set",
20,
/*id*/ 20,
Some(serde_json::to_value(ThreadSetNameParams {
thread_id: conversation_id.clone(),
name: renamed.to_string(),
})?),
)
.await?;
let (rename_resp, ws1_notification) =
read_response_and_notification_for_method(&mut ws1, 20, "thread/name/updated").await?;
let (rename_resp, ws1_notification) = read_response_and_notification_for_method(
&mut ws1,
/*id*/ 20,
"thread/name/updated",
)
.await?;
let _: ThreadSetNameResponse = to_response::<ThreadSetNameResponse>(rename_resp)?;
assert_thread_name_updated(ws1_notification, &conversation_id, renamed)?;
@@ -135,11 +143,11 @@ async fn thread_name_updated_broadcasts_for_not_loaded_threads() -> Result<()> {
}
async fn initialize_both_clients(ws1: &mut WsClient, ws2: &mut WsClient) -> Result<()> {
send_initialize_request(ws1, 1, "ws_client_one").await?;
timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(ws1, 1)).await??;
send_initialize_request(ws1, /*id*/ 1, "ws_client_one").await?;
timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(ws1, /*id*/ 1)).await??;
send_initialize_request(ws2, 2, "ws_client_two").await?;
timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(ws2, 2)).await??;
send_initialize_request(ws2, /*id*/ 2, "ws_client_two").await?;
timeout(DEFAULT_READ_TIMEOUT, read_response_for_id(ws2, /*id*/ 2)).await??;
Ok(())
}
@@ -151,7 +159,7 @@ fn create_rollout(codex_home: &std::path::Path, filename_ts: &str) -> Result<Str
"Saved user message",
Vec::new(),
Some("mock_provider"),
None,
/*git_info*/ None,
)
}
@@ -57,7 +57,7 @@ async fn thread_read_returns_summary_without_turns() -> Result<()> {
.map(|elem| serde_json::to_value(elem).expect("serialize text element"))
.collect(),
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -112,7 +112,7 @@ async fn thread_read_can_include_turns() -> Result<()> {
.map(|elem| serde_json::to_value(elem).expect("serialize text element"))
.collect(),
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -215,7 +215,7 @@ async fn thread_name_set_is_reflected_in_read_list_and_resume() -> Result<()> {
preview,
vec![],
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -171,7 +171,7 @@ async fn thread_resume_returns_rollout_history() -> Result<()> {
.map(|elem| serde_json::to_value(elem).expect("serialize text element"))
.collect(),
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -368,7 +368,9 @@ stream_max_retries = 0
)?;
let state_db =
StateRuntime::init(codex_home.path().to_path_buf(), "mock_provider".into()).await?;
state_db.mark_backfill_complete(None).await?;
state_db
.mark_backfill_complete(/*last_watermark*/ None)
.await?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
@@ -429,7 +431,7 @@ async fn thread_resume_and_read_interrupt_incomplete_rollout_turn_when_thread_is
"Saved user message",
Vec::new(),
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let rollout_file_path = rollout_path(codex_home.path(), filename_ts, &conversation_id);
let persisted_rollout = std::fs::read_to_string(&rollout_file_path)?;
@@ -1022,7 +1024,7 @@ async fn thread_resume_replays_pending_command_execution_request_approval() -> R
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call-1",
)?,
@@ -1143,7 +1145,7 @@ async fn thread_resume_replays_pending_command_execution_request_approval() -> R
primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
wait_for_responses_request_count(&server, 3).await?;
wait_for_responses_request_count(&server, /*expected_count*/ 3).await?;
Ok(())
}
@@ -1309,7 +1311,7 @@ async fn thread_resume_replays_pending_file_change_request_approval() -> Result<
primary.read_stream_until_notification_message("turn/completed"),
)
.await??;
wait_for_responses_request_count(&server, 3).await?;
wait_for_responses_request_count(&server, /*expected_count*/ 3).await?;
Ok(())
}
@@ -1465,7 +1467,7 @@ async fn thread_resume_surfaces_cloud_requirements_load_errors() -> Result<()> {
"Saved user message",
Vec::new(),
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let refresh_token_url = format!("{}/oauth/token", server.uri());
let mut mcp = McpProcess::new_with_env(
@@ -1933,7 +1935,7 @@ fn setup_rollout_fixture(codex_home: &Path, server_uri: &str) -> Result<RolloutF
preview,
Vec::new(),
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let rollout_file_path = rollout_path(codex_home, filename_ts, &conversation_id);
set_rollout_mtime(rollout_file_path.as_path(), expected_updated_at_rfc3339)?;
@@ -81,7 +81,7 @@ async fn thread_shell_command_runs_as_standalone_turn_and_persists_history() ->
.await??;
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
let started = wait_for_command_execution_started(&mut mcp, None).await?;
let started = wait_for_command_execution_started(&mut mcp, /*expected_id*/ None).await?;
let ThreadItem::CommandExecution {
id, source, status, ..
} = &started.item
@@ -167,7 +167,7 @@ async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call-approve",
)?,
@@ -345,7 +345,7 @@ async fn wait_for_command_execution_started_by_source(
expected_source: CommandExecutionSource,
) -> Result<ItemStartedNotification> {
loop {
let started = wait_for_command_execution_started(mcp, None).await?;
let started = wait_for_command_execution_started(mcp, /*expected_id*/ None).await?;
let ThreadItem::CommandExecution { source, .. } = &started.item else {
continue;
};
@@ -219,7 +219,7 @@ async fn thread_unsubscribe_during_turn_interrupts_turn_and_emits_thread_closed(
wait_for_responses_request_count_to_stabilize(
&server,
1,
/*expected_count*/ 1,
std::time::Duration::from_millis(200),
)
.await?;
@@ -888,7 +888,7 @@ async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() ->
"2025-01-01T00:00:00Z",
"history user message",
Some("mock_provider"),
None,
/*git_info*/ None,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -1031,7 +1031,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call1",
)?,
@@ -1042,7 +1042,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> {
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call2",
)?,
@@ -1186,7 +1186,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> {
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call-decline",
)?,
@@ -1331,14 +1331,14 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> {
let responses = vec![
create_shell_command_sse_response(
vec!["echo".to_string(), "first".to_string(), "turn".to_string()],
None,
/*workdir*/ None,
Some(5000),
"call-first",
)?,
create_final_assistant_message_sse_response("done first")?,
create_shell_command_sse_response(
vec!["echo".to_string(), "second".to_string(), "turn".to_string()],
None,
/*workdir*/ None,
Some(5000),
"call-second",
)?,
@@ -71,7 +71,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
format!("while [ ! -f '{release_marker_escaped}' ]; do sleep 0.01; done");
let response = create_shell_command_sse_response(
vec!["/bin/sh".to_string(), "-c".to_string(), wait_for_interrupt],
None,
/*workdir*/ None,
Some(5000),
"call-zsh-fork",
)?;
@@ -197,7 +197,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> {
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call-zsh-fork-decline",
)?,
@@ -332,7 +332,7 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> {
"-c".to_string(),
"print(42)".to_string(),
],
None,
/*workdir*/ None,
Some(5000),
"call-zsh-fork-cancel",
)?];
@@ -805,7 +805,7 @@ fn find_test_zsh_path() -> Result<Option<std::path::PathBuf>> {
);
return Ok(None);
}
match core_test_support::fetch_dotslash_file(&dotslash_zsh, None) {
match core_test_support::fetch_dotslash_file(&dotslash_zsh, /*dotslash_cache*/ None) {
Ok(path) => return Ok(Some(path)),
Err(error) => {
eprintln!("failed to fetch vendored zsh via dotslash: {error:#}");
@@ -26,7 +26,7 @@ async fn windows_sandbox_setup_start_emits_completion_notification() -> Result<(
codex_home.path(),
&server.uri(),
&BTreeMap::new(),
500_000,
/*auto_compact_limit*/ 500_000,
Some(false),
"mock_provider",
"compact prompt",
+8 -5
View File
@@ -524,14 +524,14 @@ mod tests {
#[test]
fn test_heredoc() {
assert_match(&heredoc_script(""), None);
assert_match(&heredoc_script(""), /*expected_workdir*/ None);
}
#[test]
fn test_heredoc_non_login_shell() {
let script = heredoc_script("");
let args = strs_to_strings(&["bash", "-c", &script]);
assert_match_args(args, None);
assert_match_args(args, /*expected_workdir*/ None);
}
#[test]
@@ -565,17 +565,20 @@ PATCH"#,
#[test]
fn test_powershell_heredoc() {
let script = heredoc_script("");
assert_match_args(args_powershell(&script), None);
assert_match_args(args_powershell(&script), /*expected_workdir*/ None);
}
#[test]
fn test_powershell_heredoc_no_profile() {
let script = heredoc_script("");
assert_match_args(args_powershell_no_profile(&script), None);
assert_match_args(
args_powershell_no_profile(&script),
/*expected_workdir*/ None,
);
}
#[test]
fn test_pwsh_heredoc() {
let script = heredoc_script("");
assert_match_args(args_pwsh(&script), None);
assert_match_args(args_pwsh(&script), /*expected_workdir*/ None);
}
#[test]
+24 -8
View File
@@ -672,7 +672,7 @@ fn test_parse_patch_lenient() {
#[test]
fn test_parse_one_hunk() {
assert_eq!(
parse_one_hunk(&["bad"], 234),
parse_one_hunk(&["bad"], /*line_number*/ 234),
Err(InvalidHunkError {
message: "'bad' is not a valid hunk header. \
Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'".to_string(),
@@ -685,7 +685,11 @@ fn test_parse_one_hunk() {
#[test]
fn test_update_file_chunk() {
assert_eq!(
parse_update_file_chunk(&["bad"], 123, false),
parse_update_file_chunk(
&["bad"],
/*line_number*/ 123,
/*allow_missing_context*/ false
),
Err(InvalidHunkError {
message: "Expected update hunk to start with a @@ context marker, got: 'bad'"
.to_string(),
@@ -693,14 +697,18 @@ fn test_update_file_chunk() {
})
);
assert_eq!(
parse_update_file_chunk(&["@@"], 123, false),
parse_update_file_chunk(
&["@@"],
/*line_number*/ 123,
/*allow_missing_context*/ false
),
Err(InvalidHunkError {
message: "Update hunk does not contain any lines".to_string(),
line_number: 124
})
);
assert_eq!(
parse_update_file_chunk(&["@@", "bad"], 123, false),
parse_update_file_chunk(&["@@", "bad"], /*line_number*/ 123, /*allow_missing_context*/ false),
Err(InvalidHunkError {
message: "Unexpected line found in update hunk: 'bad'. \
Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)".to_string(),
@@ -708,7 +716,11 @@ fn test_update_file_chunk() {
})
);
assert_eq!(
parse_update_file_chunk(&["@@", "*** End of File"], 123, false),
parse_update_file_chunk(
&["@@", "*** End of File"],
/*line_number*/ 123,
/*allow_missing_context*/ false
),
Err(InvalidHunkError {
message: "Update hunk does not contain any lines".to_string(),
line_number: 124
@@ -725,8 +737,8 @@ fn test_update_file_chunk() {
" context2",
"*** End Patch",
],
123,
false
/*line_number*/ 123,
/*allow_missing_context*/ false
),
Ok((
(UpdateFileChunk {
@@ -749,7 +761,11 @@ fn test_update_file_chunk() {
))
);
assert_eq!(
parse_update_file_chunk(&["@@", "+line", "*** End of File"], 123, false),
parse_update_file_chunk(
&["@@", "+line", "*** End of File"],
/*line_number*/ 123,
/*allow_missing_context*/ false
),
Ok((
(UpdateFileChunk {
change_context: None,
+16 -4
View File
@@ -122,7 +122,10 @@ mod tests {
fn test_exact_match_finds_sequence() {
let lines = to_vec(&["foo", "bar", "baz"]);
let pattern = to_vec(&["bar", "baz"]);
assert_eq!(seek_sequence(&lines, &pattern, 0, false), Some(1));
assert_eq!(
seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false),
Some(1)
);
}
#[test]
@@ -130,7 +133,10 @@ mod tests {
let lines = to_vec(&["foo ", "bar\t\t"]);
// Pattern omits trailing whitespace.
let pattern = to_vec(&["foo", "bar"]);
assert_eq!(seek_sequence(&lines, &pattern, 0, false), Some(0));
assert_eq!(
seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false),
Some(0)
);
}
#[test]
@@ -138,7 +144,10 @@ mod tests {
let lines = to_vec(&[" foo ", " bar\t"]);
// Pattern omits any additional whitespace.
let pattern = to_vec(&["foo", "bar"]);
assert_eq!(seek_sequence(&lines, &pattern, 0, false), Some(0));
assert_eq!(
seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false),
Some(0)
);
}
#[test]
@@ -146,6 +155,9 @@ mod tests {
let lines = to_vec(&["just one line"]);
let pattern = to_vec(&["too", "many", "lines"]);
// Should not panic – must return None when pattern cannot possibly fit.
assert_eq!(seek_sequence(&lines, &pattern, 0, false), None);
assert_eq!(
seek_sequence(&lines, &pattern, /*start*/ 0, /*eof*/ false),
None
);
}
}
+11 -5
View File
@@ -245,9 +245,9 @@ mod tests {
let merged = merge_connectors_with_accessible(
vec![app("alpha")],
vec![app("alpha"), app("beta")],
true,
/*all_connectors_loaded*/ true,
);
assert_eq!(merged, vec![merged_app("alpha", true)]);
assert_eq!(merged, vec![merged_app("alpha", /*is_accessible*/ true)]);
}
#[test]
@@ -255,11 +255,14 @@ mod tests {
let merged = merge_connectors_with_accessible(
vec![app("alpha")],
vec![app("alpha"), app("beta")],
false,
/*all_connectors_loaded*/ false,
);
assert_eq!(
merged,
vec![merged_app("alpha", true), merged_app("beta", true)]
vec![
merged_app("alpha", /*is_accessible*/ true),
merged_app("beta", /*is_accessible*/ true)
]
);
}
@@ -272,7 +275,10 @@ mod tests {
AppConnectorId("gmail".to_string()),
],
);
assert_eq!(connectors, vec![app("alpha"), merged_app("gmail", false)]);
assert_eq!(
connectors,
vec![app("alpha"), merged_app("gmail", /*is_accessible*/ false)]
);
}
#[test]
-1
View File
@@ -41,7 +41,6 @@ codex-state = { workspace = true }
codex-stdio-to-uds = { workspace = true }
codex-terminal-detection = { workspace = true }
codex-tui = { workspace = true }
codex-tui-app-server = { workspace = true }
libc = { workspace = true }
owo-colors = { workspace = true }
regex-lite = { workspace = true }
+5 -5
View File
@@ -498,7 +498,7 @@ mod tests {
let legacy_config = build_debug_sandbox_config(
Vec::new(),
ConfigOverrides {
sandbox_mode: Some(create_sandbox_mode(false)),
sandbox_mode: Some(create_sandbox_mode(/*full_auto*/ false)),
..Default::default()
},
Some(codex_home_path.clone()),
@@ -507,8 +507,8 @@ mod tests {
let config = load_debug_sandbox_config_with_codex_home(
Vec::new(),
None,
false,
/*codex_linux_sandbox_exe*/ None,
/*full_auto*/ false,
Some(codex_home_path),
)
.await?;
@@ -541,8 +541,8 @@ mod tests {
let err = load_debug_sandbox_config_with_codex_home(
Vec::new(),
None,
true,
/*codex_linux_sandbox_exe*/ None,
/*full_auto*/ true,
Some(codex_home.path().to_path_buf()),
)
.await
+43 -104
View File
@@ -24,10 +24,10 @@ use codex_execpolicy::ExecPolicyCheckCommand;
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
use codex_state::StateRuntime;
use codex_state::state_db_path;
use codex_tui::AppExitInfo;
use codex_tui::Cli as TuiCli;
use codex_tui::ExitReason;
use codex_tui::update_action::UpdateAction;
use codex_tui_app_server::AppExitInfo;
use codex_tui_app_server::Cli as TuiCli;
use codex_tui_app_server::ExitReason;
use codex_tui_app_server::update_action::UpdateAction;
use codex_utils_cli::CliConfigOverrides;
use owo_colors::OwoColorize;
use std::io::IsTerminal;
@@ -525,7 +525,7 @@ struct FeatureToggles {
#[derive(Debug, Default, Parser, Clone)]
struct InteractiveRemoteOptions {
/// Connect the app-server-backed TUI to a remote app server websocket endpoint.
/// Connect the TUI to a remote app server websocket endpoint.
///
/// Accepted forms: `ws://host:port` or `wss://host:port`.
#[arg(long = "remote", value_name = "ADDR")]
@@ -1226,7 +1226,6 @@ async fn run_interactive_tui(
}
}
let use_app_server_tui = codex_tui::should_use_app_server_tui(&interactive).await?;
let normalized_remote = remote
.as_deref()
.map(codex_tui_app_server::normalize_remote_addr)
@@ -1237,93 +1236,19 @@ async fn run_interactive_tui(
"`--remote-auth-token-env` requires `--remote`.",
));
}
if normalized_remote.is_some() && !use_app_server_tui {
return Ok(AppExitInfo::fatal(
"`--remote` requires the `tui_app_server` feature flag to be enabled.",
));
}
if use_app_server_tui {
let remote_auth_token = remote_auth_token_env
.as_deref()
.map(read_remote_auth_token_from_env_var)
.transpose()
.map_err(std::io::Error::other)?;
codex_tui_app_server::run_main(
into_app_server_tui_cli(interactive),
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
normalized_remote,
remote_auth_token,
)
.await
.map(into_legacy_app_exit_info)
} else {
codex_tui::run_main(
interactive,
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
)
.await
}
}
fn into_app_server_tui_cli(cli: TuiCli) -> codex_tui_app_server::Cli {
codex_tui_app_server::Cli {
prompt: cli.prompt,
images: cli.images,
resume_picker: cli.resume_picker,
resume_last: cli.resume_last,
resume_session_id: cli.resume_session_id,
resume_show_all: cli.resume_show_all,
fork_picker: cli.fork_picker,
fork_last: cli.fork_last,
fork_session_id: cli.fork_session_id,
fork_show_all: cli.fork_show_all,
model: cli.model,
oss: cli.oss,
oss_provider: cli.oss_provider,
config_profile: cli.config_profile,
sandbox_mode: cli.sandbox_mode,
approval_policy: cli.approval_policy,
full_auto: cli.full_auto,
dangerously_bypass_approvals_and_sandbox: cli.dangerously_bypass_approvals_and_sandbox,
cwd: cli.cwd,
web_search: cli.web_search,
add_dir: cli.add_dir,
no_alt_screen: cli.no_alt_screen,
config_overrides: cli.config_overrides,
}
}
fn into_legacy_update_action(
action: codex_tui_app_server::update_action::UpdateAction,
) -> UpdateAction {
match action {
codex_tui_app_server::update_action::UpdateAction::NpmGlobalLatest => {
UpdateAction::NpmGlobalLatest
}
codex_tui_app_server::update_action::UpdateAction::BunGlobalLatest => {
UpdateAction::BunGlobalLatest
}
codex_tui_app_server::update_action::UpdateAction::BrewUpgrade => UpdateAction::BrewUpgrade,
}
}
fn into_legacy_exit_reason(reason: codex_tui_app_server::ExitReason) -> ExitReason {
match reason {
codex_tui_app_server::ExitReason::UserRequested => ExitReason::UserRequested,
codex_tui_app_server::ExitReason::Fatal(message) => ExitReason::Fatal(message),
}
}
fn into_legacy_app_exit_info(exit_info: codex_tui_app_server::AppExitInfo) -> AppExitInfo {
AppExitInfo {
token_usage: exit_info.token_usage,
thread_id: exit_info.thread_id,
thread_name: exit_info.thread_name,
update_action: exit_info.update_action.map(into_legacy_update_action),
exit_reason: into_legacy_exit_reason(exit_info.exit_reason),
}
let remote_auth_token = remote_auth_token_env
.as_deref()
.map(read_remote_auth_token_from_env_var)
.transpose()
.map_err(std::io::Error::other)?;
codex_tui_app_server::run_main(
interactive,
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
normalized_remote,
remote_auth_token,
)
.await
}
fn confirm(prompt: &str) -> std::io::Result<bool> {
@@ -1588,14 +1513,17 @@ mod tests {
update_action: None,
exit_reason: ExitReason::UserRequested,
};
let lines = format_exit_messages(exit_info, false);
let lines = format_exit_messages(exit_info, /*color_enabled*/ false);
assert!(lines.is_empty());
}
#[test]
fn format_exit_messages_includes_resume_hint_without_color() {
let exit_info = sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000"), None);
let lines = format_exit_messages(exit_info, false);
let exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
let lines = format_exit_messages(exit_info, /*color_enabled*/ false);
assert_eq!(
lines,
vec![
@@ -1608,8 +1536,11 @@ mod tests {
#[test]
fn format_exit_messages_applies_color_when_enabled() {
let exit_info = sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000"), None);
let lines = format_exit_messages(exit_info, true);
let exit_info = sample_exit_info(
Some("123e4567-e89b-12d3-a456-426614174000"),
/*thread_name*/ None,
);
let lines = format_exit_messages(exit_info, /*color_enabled*/ true);
assert_eq!(lines.len(), 2);
assert!(lines[1].contains("\u{1b}[36m"));
}
@@ -1620,7 +1551,7 @@ mod tests {
Some("123e4567-e89b-12d3-a456-426614174000"),
Some("my-thread"),
);
let lines = format_exit_messages(exit_info, false);
let lines = format_exit_messages(exit_info, /*color_enabled*/ false);
assert_eq!(
lines,
vec![
@@ -1846,8 +1777,12 @@ mod tests {
#[test]
fn reject_remote_mode_for_non_interactive_subcommands() {
let err = reject_remote_mode_for_subcommand(Some("127.0.0.1:4500"), None, "exec")
.expect_err("non-interactive subcommands should reject --remote");
let err = reject_remote_mode_for_subcommand(
Some("127.0.0.1:4500"),
/*remote_auth_token_env*/ None,
"exec",
)
.expect_err("non-interactive subcommands should reject --remote");
assert!(
err.to_string()
.contains("only supported for interactive TUI commands")
@@ -1856,8 +1791,12 @@ mod tests {
#[test]
fn reject_remote_auth_token_env_for_non_interactive_subcommands() {
let err = reject_remote_mode_for_subcommand(None, Some("CODEX_REMOTE_AUTH_TOKEN"), "exec")
.expect_err("non-interactive subcommands should reject --remote-auth-token-env");
let err = reject_remote_mode_for_subcommand(
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
"exec",
)
.expect_err("non-interactive subcommands should reject --remote-auth-token-env");
assert!(
err.to_string()
.contains("only supported for interactive TUI commands")
@@ -1871,7 +1810,7 @@ mod tests {
out_dir: PathBuf::from("/tmp/out"),
});
let err = reject_remote_mode_for_app_server_subcommand(
None,
/*remote*/ None,
Some("CODEX_REMOTE_AUTH_TOKEN"),
Some(&subcommand),
)
+18 -10
View File
@@ -848,7 +848,7 @@ mod tests {
write_auth_json(tmp.path(), auth_json).expect("write auth");
Arc::new(AuthManager::new(
tmp.path().to_path_buf(),
false,
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
))
}
@@ -872,7 +872,7 @@ mod tests {
.expect("write auth");
Arc::new(AuthManager::new(
tmp.path().to_path_buf(),
false,
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
))
}
@@ -909,7 +909,7 @@ mod tests {
access_token,
refresh_token,
last_refresh,
None,
/*auth_mode*/ None,
)
}
@@ -980,7 +980,7 @@ mod tests {
ManagedAuthContext {
manager: Arc::new(AuthManager::new(
home.path().to_path_buf(),
false,
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
)),
_home: home,
@@ -1225,7 +1225,7 @@ mod tests {
#[tokio::test]
async fn fetch_cloud_requirements_handles_missing_contents() {
let result = parse_for_fetch(None);
let result = parse_for_fetch(/*contents*/ None);
assert!(result.is_none());
}
@@ -1369,7 +1369,7 @@ enabled = false
.expect("write initial auth");
let auth_manager = Arc::new(AuthManager::new(
auth_home.path().to_path_buf(),
false,
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
));
@@ -1438,7 +1438,7 @@ enabled = false
.expect("write initial auth");
let auth_manager = Arc::new(AuthManager::new(
auth_home.path().to_path_buf(),
false,
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
));
@@ -1565,7 +1565,7 @@ enabled = false
.expect("write auth");
let auth_manager = Arc::new(AuthManager::new(
auth_home.path().to_path_buf(),
false,
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
));
@@ -1657,7 +1657,11 @@ enabled = false
async fn fetch_cloud_requirements_writes_cache_when_identity_is_incomplete() {
let codex_home = tempdir().expect("tempdir");
let service = CloudRequirementsService::new(
auth_manager_with_plan_and_identity("business", None, Some("account-12345")),
auth_manager_with_plan_and_identity(
"business",
/*chatgpt_user_id*/ None,
Some("account-12345"),
),
Arc::new(StaticFetcher {
contents: Some("allowed_approval_policies = [\"never\"]".to_string()),
}),
@@ -1709,7 +1713,11 @@ enabled = false
"allowed_approval_policies = [\"on-request\"]".to_string(),
))]));
let service = CloudRequirementsService::new(
auth_manager_with_plan_and_identity("business", None, Some("account-12345")),
auth_manager_with_plan_and_identity(
"business",
/*chatgpt_user_id*/ None,
Some("account-12345"),
),
fetcher.clone(),
codex_home.path().to_path_buf(),
CLOUD_REQUIREMENTS_TIMEOUT,
+1 -1
View File
@@ -24,7 +24,7 @@ codex-client = { workspace = true }
codex-core = { path = "../core" }
codex-git-utils = { workspace = true }
codex-login = { path = "../login" }
codex-tui = { path = "../tui" }
codex-tui = { workspace = true }
codex-utils-cli = { workspace = true }
crossterm = { workspace = true, features = ["event-stream"] }
ratatui = { workspace = true }
+2 -2
View File
@@ -408,7 +408,7 @@ mod tests {
&self,
id: TaskId,
) -> codex_cloud_tasks_client::Result<TaskSummary> {
self.list_tasks(None, None, None)
self.list_tasks(/*env*/ None, /*limit*/ None, /*cursor*/ None)
.await?
.tasks
.into_iter()
@@ -497,7 +497,7 @@ mod tests {
let backend = FakeBackend { by_env };
// Act + Assert
let root = load_tasks(&backend, None).await.unwrap();
let root = load_tasks(&backend, /*env*/ None).await.unwrap();
assert_eq!(root.len(), 2);
assert_eq!(root[0].title, "root-1");
+29 -14
View File
@@ -930,7 +930,8 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
if let Some(page) = app.new_task.as_mut() {
if page.composer.flush_paste_burst_if_due() { needs_redraw = true; }
if page.composer.is_in_paste_burst() {
let _ = frame_tx.send(Instant::now() + codex_tui::ComposerInput::recommended_flush_delay());
let _ = frame_tx
.send(Instant::now() + codex_tui_app_server::ComposerInput::recommended_flush_delay());
}
}
// Keep spinner pulsing only while loading.
@@ -1491,7 +1492,9 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
_ => {
if page.submitting {
// Ignore input while submitting
} else if let codex_tui::ComposerAction::Submitted(text) = page.composer.input(key) {
} else if let codex_tui_app_server::ComposerAction::Submitted(text) =
page.composer.input(key)
{
// Submit only if we have an env id
if let Some(env) = page.env_id.clone() {
append_error_log(format!(
@@ -1521,7 +1524,10 @@ pub async fn run_main(cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> an
needs_redraw = true;
// If paste‑burst is active, schedule a micro‑flush frame.
if page.composer.is_in_paste_burst() {
let _ = frame_tx.send(Instant::now() + codex_tui::ComposerInput::recommended_flush_delay());
let _ = frame_tx.send(
Instant::now()
+ codex_tui_app_server::ComposerInput::recommended_flush_delay(),
);
}
// Always schedule an immediate redraw for key edits in the composer.
let _ = frame_tx.send(Instant::now());
@@ -2130,8 +2136,8 @@ mod tests {
use codex_cloud_tasks_client::TaskId;
use codex_cloud_tasks_client::TaskStatus;
use codex_cloud_tasks_client::TaskSummary;
use codex_tui::ComposerAction;
use codex_tui::ComposerInput;
use codex_tui_app_server::ComposerAction;
use codex_tui_app_server::ComposerInput;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
@@ -2168,7 +2174,7 @@ mod tests {
async fn branch_override_is_used_when_provided() {
let git_ref = resolve_git_ref_with_git_info(
Some(&"feature/override".to_string()),
&StubGitInfo::new(None, None),
&StubGitInfo::new(/*default_branch*/ None, /*current_branch*/ None),
)
.await;
@@ -2179,7 +2185,7 @@ mod tests {
async fn trims_override_whitespace() {
let git_ref = resolve_git_ref_with_git_info(
Some(&" feature/spaces ".to_string()),
&StubGitInfo::new(None, None),
&StubGitInfo::new(/*default_branch*/ None, /*current_branch*/ None),
)
.await;
@@ -2189,7 +2195,7 @@ mod tests {
#[tokio::test]
async fn prefers_current_branch_when_available() {
let git_ref = resolve_git_ref_with_git_info(
None,
/*branch_override*/ None,
&StubGitInfo::new(
Some("default-main".to_string()),
Some("feature/current".to_string()),
@@ -2203,8 +2209,8 @@ mod tests {
#[tokio::test]
async fn falls_back_to_current_branch_when_default_is_missing() {
let git_ref = resolve_git_ref_with_git_info(
None,
&StubGitInfo::new(None, Some("develop".to_string())),
/*branch_override*/ None,
&StubGitInfo::new(/*default_branch*/ None, Some("develop".to_string())),
)
.await;
@@ -2213,7 +2219,11 @@ mod tests {
#[tokio::test]
async fn falls_back_to_main_when_no_git_info_is_available() {
let git_ref = resolve_git_ref_with_git_info(None, &StubGitInfo::new(None, None)).await;
let git_ref = resolve_git_ref_with_git_info(
/*branch_override*/ None,
&StubGitInfo::new(/*default_branch*/ None, /*current_branch*/ None),
)
.await;
assert_eq!(git_ref, "main");
}
@@ -2236,7 +2246,7 @@ mod tests {
is_review: false,
attempt_total: None,
};
let lines = format_task_status_lines(&task, now, false);
let lines = format_task_status_lines(&task, now, /*colorize*/ false);
assert_eq!(
lines,
vec![
@@ -2261,7 +2271,7 @@ mod tests {
is_review: false,
attempt_total: Some(1),
};
let lines = format_task_status_lines(&task, now, false);
let lines = format_task_status_lines(&task, now, /*colorize*/ false);
assert_eq!(
lines,
vec![
@@ -2303,7 +2313,12 @@ mod tests {
attempt_total: Some(1),
},
];
let lines = format_task_list_lines(&tasks, "https://chatgpt.com/backend-api", now, false);
let lines = format_task_list_lines(
&tasks,
"https://chatgpt.com/backend-api",
now,
/*colorize*/ false,
);
assert_eq!(
lines,
vec![
+1 -1
View File
@@ -1,4 +1,4 @@
use codex_tui::ComposerInput;
use codex_tui_app_server::ComposerInput;
pub struct NewTaskPage {
pub composer: ComposerInput,
+1 -1
View File
@@ -23,7 +23,7 @@ use crate::app::AttemptView;
use crate::util::format_relative_time_now;
use codex_cloud_tasks_client::AttemptStatus;
use codex_cloud_tasks_client::TaskStatus;
use codex_tui::render_markdown_text;
use codex_tui_app_server::render_markdown_text;
pub fn draw(frame: &mut Frame, app: &mut App) {
let area = frame.area();
+24 -12
View File
@@ -5,23 +5,35 @@ use codex_cloud_tasks_client::MockClient;
async fn mock_backend_varies_by_env() {
let client = MockClient;
let root = CloudBackend::list_tasks(&client, None, None, None)
.await
.unwrap()
.tasks;
let root = CloudBackend::list_tasks(
&client, /*env*/ None, /*limit*/ None, /*cursor*/ None,
)
.await
.unwrap()
.tasks;
assert!(root.iter().any(|t| t.title.contains("Update README")));
let a = CloudBackend::list_tasks(&client, Some("env-A"), None, None)
.await
.unwrap()
.tasks;
let a = CloudBackend::list_tasks(
&client,
Some("env-A"),
/*limit*/ None,
/*cursor*/ None,
)
.await
.unwrap()
.tasks;
assert_eq!(a.len(), 1);
assert_eq!(a[0].title, "A: First");
let b = CloudBackend::list_tasks(&client, Some("env-B"), None, None)
.await
.unwrap()
.tasks;
let b = CloudBackend::list_tasks(
&client,
Some("env-B"),
/*limit*/ None,
/*cursor*/ None,
)
.await
.unwrap()
.tasks;
assert_eq!(b.len(), 2);
assert!(b[0].title.starts_with("B: "));
}
+4 -2
View File
@@ -548,8 +548,10 @@ mod tests {
#[test]
fn code_mode_only_description_includes_nested_tools() {
let description =
build_exec_tool_description(&[("foo".to_string(), "bar".to_string())], true);
let description = build_exec_tool_description(
&[("foo".to_string(), "bar".to_string())],
/*code_mode_only*/ true,
);
assert!(description.contains("### `foo` (`foo`)"));
}
}
+1 -1
View File
@@ -631,7 +631,7 @@ text(JSON.stringify(returnsUndefined));
event_rx,
control_rx,
initial_response_tx,
60_000,
/*initial_yield_time_ms*/ 60_000,
));
event_tx.send(RuntimeEvent::Started).unwrap();
@@ -967,8 +967,8 @@ mod tests {
fn websocket_url_from_http_base_defaults_to_ws_path() {
let url = websocket_url_from_api_url(
"http://127.0.0.1:8011",
None,
None,
/*query_params*/ None,
/*model*/ None,
RealtimeEventParser::V1,
RealtimeSessionMode::Conversational,
)
@@ -983,7 +983,7 @@ mod tests {
fn websocket_url_from_ws_base_defaults_to_ws_path() {
let url = websocket_url_from_api_url(
"wss://example.com",
None,
/*query_params*/ None,
Some("realtime-test-model"),
RealtimeEventParser::V1,
RealtimeSessionMode::Conversational,
@@ -999,7 +999,7 @@ mod tests {
fn websocket_url_from_v1_base_appends_realtime_path() {
let url = websocket_url_from_api_url(
"https://api.openai.com/v1",
None,
/*query_params*/ None,
Some("snapshot"),
RealtimeEventParser::V1,
RealtimeSessionMode::Conversational,
@@ -1015,7 +1015,7 @@ mod tests {
fn websocket_url_from_nested_v1_base_appends_realtime_path() {
let url = websocket_url_from_api_url(
"https://example.com/openai/v1",
None,
/*query_params*/ None,
Some("snapshot"),
RealtimeEventParser::V1,
RealtimeSessionMode::Conversational,
@@ -1050,8 +1050,8 @@ mod tests {
fn websocket_url_v1_ignores_transcription_mode() {
let url = websocket_url_from_api_url(
"https://example.com",
None,
None,
/*query_params*/ None,
/*model*/ None,
RealtimeEventParser::V1,
RealtimeSessionMode::Transcription,
)
@@ -1085,8 +1085,8 @@ mod tests {
fn websocket_url_omits_intent_for_realtime_v2_transcription_mode() {
let url = websocket_url_from_api_url(
"https://example.com",
None,
None,
/*query_params*/ None,
/*model*/ None,
RealtimeEventParser::RealtimeV2,
RealtimeSessionMode::Transcription,
)
+1 -1
View File
@@ -277,7 +277,7 @@ mod tests {
HeaderValue::from_static("1704069000"),
);
let snapshot = parse_rate_limit_for_limit(&headers, None).expect("snapshot");
let snapshot = parse_rate_limit_for_limit(&headers, /*limit_id*/ None).expect("snapshot");
assert_eq!(snapshot.limit_id.as_deref(), Some("codex"));
assert_eq!(snapshot.limit_name, None);
let primary = snapshot.primary.expect("primary");
+24 -4
View File
@@ -515,7 +515,12 @@ mod tests {
let stream =
ReaderStream::new(reader).map_err(|err| TransportError::Network(err.to_string()));
let (tx, mut rx) = mpsc::channel::<Result<ResponseEvent, ApiError>>(16);
tokio::spawn(process_sse(Box::pin(stream), tx, idle_timeout(), None));
tokio::spawn(process_sse(
Box::pin(stream),
tx,
idle_timeout(),
/*telemetry*/ None,
));
let mut events = Vec::new();
while let Some(ev) = rx.recv().await {
@@ -541,7 +546,12 @@ mod tests {
let (tx, mut rx) = mpsc::channel::<Result<ResponseEvent, ApiError>>(8);
let stream = ReaderStream::new(std::io::Cursor::new(body))
.map_err(|err| TransportError::Network(err.to_string()));
tokio::spawn(process_sse(Box::pin(stream), tx, idle_timeout(), None));
tokio::spawn(process_sse(
Box::pin(stream),
tx,
idle_timeout(),
/*telemetry*/ None,
));
let mut out = Vec::new();
while let Some(ev) = rx.recv().await {
@@ -695,7 +705,12 @@ mod tests {
let stream: ByteStream = Box::pin(stream);
let (tx, mut rx) = mpsc::channel::<Result<ResponseEvent, ApiError>>(8);
tokio::spawn(process_sse(stream, tx, idle_timeout(), None));
tokio::spawn(process_sse(
stream,
tx,
idle_timeout(),
/*telemetry*/ None,
));
let events = tokio::time::timeout(Duration::from_millis(1000), async {
let mut events = Vec::new();
@@ -894,7 +909,12 @@ mod tests {
bytes: Box::pin(bytes),
};
let mut stream = spawn_response_stream(stream_response, idle_timeout(), None, None);
let mut stream = spawn_response_stream(
stream_response,
idle_timeout(),
/*telemetry*/ None,
/*turn_state*/ None,
);
let event = stream
.rx_event
.recv()
+12 -2
View File
@@ -203,7 +203,12 @@ async fn responses_client_uses_responses_path() -> Result<()> {
let body = serde_json::json!({ "echo": true });
let _stream = client
.stream(body, HeaderMap::new(), Compression::None, None)
.stream(
body,
HeaderMap::new(),
Compression::None,
/*turn_state*/ None,
)
.await?;
let requests = state.take_stream_requests();
@@ -220,7 +225,12 @@ async fn streaming_client_adds_auth_headers() -> Result<()> {
let body = serde_json::json!({ "model": "gpt-test" });
let _stream = client
.stream(body, HeaderMap::new(), Compression::None, None)
.stream(
body,
HeaderMap::new(),
Compression::None,
/*turn_state*/ None,
)
.await?;
let requests = state.take_stream_requests();
@@ -85,7 +85,7 @@ async fn models_client_hits_models_endpoint() {
availability_nux: None,
apply_patch_tool_type: None,
web_search_tool_type: Default::default(),
truncation_policy: TruncationPolicyConfig::bytes(10_000),
truncation_policy: TruncationPolicyConfig::bytes(/*limit*/ 10_000),
supports_parallel_tool_calls: false,
supports_image_detail_original: false,
context_window: Some(272_000),
+1 -1
View File
@@ -125,7 +125,7 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()>
serde_json::json!({"echo": true}),
HeaderMap::new(),
Compression::None,
None,
/*turn_state*/ None,
)
.await?;
+15 -12
View File
@@ -188,8 +188,10 @@ mod tests {
#[test]
fn constrained_allow_any_accepts_any_value() {
let mut constrained = Constrained::allow_any(5);
constrained.set(-10).expect("allow any accepts all values");
let mut constrained = Constrained::allow_any(/*initial_value*/ 5);
constrained
.set(/*value*/ -10)
.expect("allow any accepts all values");
assert_eq!(constrained.value(), -10);
}
@@ -201,13 +203,13 @@ mod tests {
#[test]
fn constrained_allow_only_rejects_different_values() {
let mut constrained = Constrained::allow_only(5);
let mut constrained = Constrained::allow_only(/*only_value*/ 5);
constrained
.set(5)
.set(/*value*/ 5)
.expect("allowed value should be accepted");
let err = constrained
.set(6)
.set(/*value*/ 6)
.expect_err("different value should be rejected");
assert_eq!(err, invalid_value("6", "[5]"));
assert_eq!(constrained.value(), 5);
@@ -215,18 +217,19 @@ mod tests {
#[test]
fn constrained_normalizer_applies_on_init_and_set() -> anyhow::Result<()> {
let mut constrained = Constrained::normalized(-1, |value| value.max(0))?;
let mut constrained =
Constrained::normalized(/*initial_value*/ -1, |value| value.max(0))?;
assert_eq!(constrained.value(), 0);
constrained.set(-5)?;
constrained.set(/*value*/ -5)?;
assert_eq!(constrained.value(), 0);
constrained.set(10)?;
constrained.set(/*value*/ 10)?;
assert_eq!(constrained.value(), 10);
Ok(())
}
#[test]
fn constrained_new_rejects_invalid_initial_value() {
let result = Constrained::new(0, |value| {
let result = Constrained::new(/*initial_value*/ 0, |value| {
if *value > 0 {
Ok(())
} else {
@@ -239,7 +242,7 @@ mod tests {
#[test]
fn constrained_set_rejects_invalid_value_and_leaves_previous() {
let mut constrained = Constrained::new(1, |value| {
let mut constrained = Constrained::new(/*initial_value*/ 1, |value| {
if *value > 0 {
Ok(())
} else {
@@ -249,7 +252,7 @@ mod tests {
.expect("initial value should be accepted");
let err = constrained
.set(-5)
.set(/*value*/ -5)
.expect_err("negative values should be rejected");
assert_eq!(err, invalid_value("-5", "positive values"));
assert_eq!(constrained.value(), 1);
@@ -257,7 +260,7 @@ mod tests {
#[test]
fn constrained_can_set_allows_probe_without_setting() {
let constrained = Constrained::new(1, |value| {
let constrained = Constrained::new(/*initial_value*/ 1, |value| {
if *value > 0 {
Ok(())
} else {
+62 -47
View File
@@ -419,7 +419,7 @@ mod tests {
"https://chatgpt.example".to_string(),
Some(format!("account-{id}")),
Some(format!("user-{id}")),
true,
/*is_workspace_account*/ true,
)
}
@@ -444,21 +444,31 @@ mod tests {
let call_counter = Arc::clone(&calls);
let key = cache_key("shared");
let first = list_all_connectors_with_options(key.clone(), false, false, move |_path| {
let call_counter = Arc::clone(&call_counter);
async move {
call_counter.fetch_add(1, Ordering::SeqCst);
Ok(DirectoryListResponse {
apps: vec![app("alpha", "Alpha")],
next_token: None,
})
}
})
let first = list_all_connectors_with_options(
key.clone(),
/*is_workspace_account*/ false,
/*force_refetch*/ false,
move |_path| {
let call_counter = Arc::clone(&call_counter);
async move {
call_counter.fetch_add(1, Ordering::SeqCst);
Ok(DirectoryListResponse {
apps: vec![app("alpha", "Alpha")],
next_token: None,
})
}
},
)
.await?;
let second = list_all_connectors_with_options(key, false, false, move |_path| async move {
anyhow::bail!("cache should have been used");
})
let second = list_all_connectors_with_options(
key,
/*is_workspace_account*/ false,
/*force_refetch*/ false,
move |_path| async move {
anyhow::bail!("cache should have been used");
},
)
.await?;
assert_eq!(calls.load(Ordering::SeqCst), 1);
@@ -472,40 +482,45 @@ mod tests {
let calls = Arc::new(AtomicUsize::new(0));
let call_counter = Arc::clone(&calls);
let connectors = list_all_connectors_with_options(key, true, true, move |path| {
let call_counter = Arc::clone(&call_counter);
async move {
call_counter.fetch_add(1, Ordering::SeqCst);
if path.starts_with("/connectors/directory/list_workspace") {
Ok(DirectoryListResponse {
apps: vec![
DirectoryApp {
description: Some("Merged description".to_string()),
branding: Some(AppBranding {
category: Some("calendar".to_string()),
developer: None,
website: None,
privacy_policy: None,
terms_of_service: None,
is_discoverable_app: true,
}),
..app("alpha", "")
},
DirectoryApp {
visibility: Some("HIDDEN".to_string()),
..app("hidden", "Hidden")
},
],
next_token: None,
})
} else {
Ok(DirectoryListResponse {
apps: vec![app("alpha", " Alpha "), app("beta", "Beta")],
next_token: None,
})
let connectors = list_all_connectors_with_options(
key,
/*is_workspace_account*/ true,
/*force_refetch*/ true,
move |path| {
let call_counter = Arc::clone(&call_counter);
async move {
call_counter.fetch_add(1, Ordering::SeqCst);
if path.starts_with("/connectors/directory/list_workspace") {
Ok(DirectoryListResponse {
apps: vec![
DirectoryApp {
description: Some("Merged description".to_string()),
branding: Some(AppBranding {
category: Some("calendar".to_string()),
developer: None,
website: None,
privacy_policy: None,
terms_of_service: None,
is_discoverable_app: true,
}),
..app("alpha", "")
},
DirectoryApp {
visibility: Some("HIDDEN".to_string()),
..app("hidden", "Hidden")
},
],
next_token: None,
})
} else {
Ok(DirectoryListResponse {
apps: vec![app("alpha", " Alpha "), app("beta", "Beta")],
next_token: None,
})
}
}
}
})
},
)
.await?;
assert_eq!(calls.load(Ordering::SeqCst), 2);
+1 -1
View File
@@ -115,7 +115,7 @@ fn load_skills_for_test(config: &TestConfig) -> SkillLoadOutcome {
super::load_skills_from_roots(super::skill_roots_with_home_dir(
&config.config_layer_stack,
&config.cwd,
None,
/*home_dir*/ None,
Vec::new(),
))
}
+55 -23
View File
@@ -141,7 +141,10 @@ fn new_with_disabled_bundled_skills_removes_stale_cached_system_skills() {
fs::write(stale_system_skill_dir.join("SKILL.md"), "# stale\n")
.expect("write stale system skill");
let _skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), false);
let _skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ false,
);
assert!(
!codex_home.path().join("skills/.system").exists(),
@@ -154,7 +157,10 @@ async fn skills_for_config_reuses_cache_for_same_effective_config() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
let config_layer_stack = config_stack(&codex_home, "");
let skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), true);
let skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ true,
);
write_user_skill(&codex_home, "a", "skill-a", "from a");
let outcome1 = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]);
@@ -185,14 +191,17 @@ async fn skills_for_config_disables_plugin_skills_by_name() {
);
let config_layer_stack = config_stack(
&codex_home,
&name_toggle_config("sample:sample-search", false),
&name_toggle_config("sample:sample-search", /*enabled*/ false),
);
let plugin_skill_root = skill_path
.parent()
.and_then(std::path::Path::parent)
.expect("plugin skill should live under a skills root")
.to_path_buf();
let skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), true);
let skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ true,
);
let outcome = skills_for_config_with_stack(
&skills_manager,
@@ -223,7 +232,10 @@ async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
let cwd = tempfile::tempdir().expect("tempdir");
let extra_root = tempfile::tempdir().expect("tempdir");
let config_layer_stack = config_stack(&codex_home, "");
let skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), true);
let skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ true,
);
let _ = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]);
write_user_skill(&extra_root, "x", "extra-skill", "from extra root");
@@ -237,7 +249,7 @@ async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
let outcome_with_extra = skills_manager
.skills_for_cwd_with_extra_user_roots(
&base_input,
true,
/*force_reload*/ true,
std::slice::from_ref(&extra_root_path),
)
.await;
@@ -262,7 +274,9 @@ async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
config_layer_stack.clone(),
bundled_skills_enabled_from_stack(&config_layer_stack),
);
let outcome_without_extra = skills_manager.skills_for_cwd(&base_input, false).await;
let outcome_without_extra = skills_manager
.skills_for_cwd(&base_input, /*force_reload*/ false)
.await;
assert_eq!(outcome_without_extra.skills, outcome_with_extra.skills);
assert_eq!(outcome_without_extra.errors, outcome_with_extra.errors);
}
@@ -279,7 +293,10 @@ async fn skills_for_config_excludes_bundled_skills_when_disabled_in_config() {
)
.expect("write bundled skill");
let config_layer_stack = config_stack(&codex_home, "[skills.bundled]\nenabled = false\n");
let skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), false);
let skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ false,
);
// Recreate the cached bundled skill after startup cleanup so this assertion exercises
// root selection rather than relying on directory removal succeeding.
@@ -312,7 +329,10 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
let extra_root_a = tempfile::tempdir().expect("tempdir");
let extra_root_b = tempfile::tempdir().expect("tempdir");
let config_layer_stack = config_stack(&codex_home, "");
let skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), true);
let skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ true,
);
let _ = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]);
write_user_skill(&extra_root_a, "x", "extra-skill-a", "from extra root a");
@@ -328,7 +348,7 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
let outcome_a = skills_manager
.skills_for_cwd_with_extra_user_roots(
&base_input,
true,
/*force_reload*/ true,
std::slice::from_ref(&extra_root_a_path),
)
.await;
@@ -349,7 +369,7 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
let outcome_b = skills_manager
.skills_for_cwd_with_extra_user_roots(
&base_input,
false,
/*force_reload*/ false,
std::slice::from_ref(&extra_root_b_path),
)
.await;
@@ -369,7 +389,7 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
let outcome_reloaded = skills_manager
.skills_for_cwd_with_extra_user_roots(
&base_input,
true,
/*force_reload*/ true,
std::slice::from_ref(&extra_root_b_path),
)
.await;
@@ -408,11 +428,13 @@ fn disabled_paths_for_skills_allows_session_flags_to_override_user_layer() {
.expect("user config path should be absolute");
let user_layer = ConfigLayerEntry::new(
ConfigLayerSource::User { file: user_file },
toml::from_str(&path_toggle_config(&skill_path, false)).expect("user layer toml"),
toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ false))
.expect("user layer toml"),
);
let session_layer = ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
toml::from_str(&path_toggle_config(&skill_path, true)).expect("session layer toml"),
toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ true))
.expect("session layer toml"),
);
let stack = ConfigLayerStack::new(
vec![user_layer, session_layer],
@@ -438,11 +460,13 @@ fn disabled_paths_for_skills_allows_session_flags_to_disable_user_enabled_skill(
.expect("user config path should be absolute");
let user_layer = ConfigLayerEntry::new(
ConfigLayerSource::User { file: user_file },
toml::from_str(&path_toggle_config(&skill_path, true)).expect("user layer toml"),
toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ true))
.expect("user layer toml"),
);
let session_layer = ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
toml::from_str(&path_toggle_config(&skill_path, false)).expect("session layer toml"),
toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ false))
.expect("session layer toml"),
);
let stack = ConfigLayerStack::new(
vec![user_layer, session_layer],
@@ -468,7 +492,8 @@ fn disabled_paths_for_skills_disables_matching_name_selectors() {
.expect("user config path should be absolute");
let user_layer = ConfigLayerEntry::new(
ConfigLayerSource::User { file: user_file },
toml::from_str(&name_toggle_config("github:yeet", false)).expect("user layer toml"),
toml::from_str(&name_toggle_config("github:yeet", /*enabled*/ false))
.expect("user layer toml"),
);
let stack = ConfigLayerStack::new(
vec![user_layer],
@@ -494,11 +519,13 @@ fn disabled_paths_for_skills_allows_name_selector_to_override_path_selector() {
.expect("user config path should be absolute");
let user_layer = ConfigLayerEntry::new(
ConfigLayerSource::User { file: user_file },
toml::from_str(&path_toggle_config(&skill_path, false)).expect("user layer toml"),
toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ false))
.expect("user layer toml"),
);
let session_layer = ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
toml::from_str(&name_toggle_config("github:yeet", true)).expect("session layer toml"),
toml::from_str(&name_toggle_config("github:yeet", /*enabled*/ true))
.expect("session layer toml"),
);
let stack = ConfigLayerStack::new(
vec![user_layer, session_layer],
@@ -527,12 +554,15 @@ async fn skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill()
"---\nname: demo-skill\ndescription: demo description\n---\n\n# Body\n",
)
.expect("write skill");
let disabled_skill_config = path_toggle_config(&skill_path, false);
let enabled_skill_config = path_toggle_config(&skill_path, true);
let disabled_skill_config = path_toggle_config(&skill_path, /*enabled*/ false);
let enabled_skill_config = path_toggle_config(&skill_path, /*enabled*/ true);
let parent_stack = config_stack(&codex_home, &disabled_skill_config);
let child_stack =
config_stack_with_session_flags(&codex_home, &disabled_skill_config, &enabled_skill_config);
let skills_manager = SkillsManager::new(codex_home.path().to_path_buf(), true);
let skills_manager = SkillsManager::new(
codex_home.path().to_path_buf(),
/*bundled_skills_enabled*/ true,
);
let parent_input = SkillsLoadInput::new(
cwd.path().to_path_buf(),
Vec::new(),
@@ -540,7 +570,9 @@ async fn skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill()
bundled_skills_enabled_from_stack(&parent_stack),
);
let parent_outcome = skills_manager.skills_for_cwd(&parent_input, true).await;
let parent_outcome = skills_manager
.skills_for_cwd(&parent_input, /*force_reload*/ true)
.await;
let parent_skill = parent_outcome
.skills
.iter()
+57 -17
View File
@@ -289,7 +289,7 @@ async fn spawn_agent_errors_when_manager_dropped() {
let control = AgentControl::default();
let (_home, config) = test_config().await;
let err = control
.spawn_agent(config, text_input("hello"), None)
.spawn_agent(config, text_input("hello"), /*session_source*/ None)
.await
.expect_err("spawn_agent should fail without a manager");
assert_eq!(
@@ -423,7 +423,7 @@ async fn send_inter_agent_communication_without_turn_queues_message_without_trig
AgentPath::try_from("/root/worker").expect("agent path"),
Vec::new(),
"hello from tests".to_string(),
false,
/*trigger_turn*/ false,
);
let submission_id = harness
@@ -535,7 +535,11 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
let harness = AgentControlHarness::new().await;
let thread_id = harness
.control
.spawn_agent(harness.config.clone(), text_input("spawned"), None)
.spawn_agent(
harness.config.clone(),
text_input("spawned"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed");
let _thread = harness
@@ -830,12 +834,20 @@ async fn spawn_agent_respects_max_threads_limit() {
.expect("start thread");
let first_agent_id = control
.spawn_agent(config.clone(), text_input("hello"), None)
.spawn_agent(
config.clone(),
text_input("hello"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed");
let err = control
.spawn_agent(config, text_input("hello again"), None)
.spawn_agent(
config,
text_input("hello again"),
/*session_source*/ None,
)
.await
.expect_err("spawn_agent should respect max threads");
let CodexErr::AgentLimitReached {
@@ -871,7 +883,11 @@ async fn spawn_agent_releases_slot_after_shutdown() {
let control = manager.agent_control();
let first_agent_id = control
.spawn_agent(config.clone(), text_input("hello"), None)
.spawn_agent(
config.clone(),
text_input("hello"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed");
let _ = control
@@ -880,7 +896,11 @@ async fn spawn_agent_releases_slot_after_shutdown() {
.expect("shutdown agent");
let second_agent_id = control
.spawn_agent(config.clone(), text_input("hello again"), None)
.spawn_agent(
config.clone(),
text_input("hello again"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed after shutdown");
let _ = control
@@ -909,12 +929,20 @@ async fn spawn_agent_limit_shared_across_clones() {
let cloned = control.clone();
let first_agent_id = cloned
.spawn_agent(config.clone(), text_input("hello"), None)
.spawn_agent(
config.clone(),
text_input("hello"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed");
let err = control
.spawn_agent(config, text_input("hello again"), None)
.spawn_agent(
config,
text_input("hello again"),
/*session_source*/ None,
)
.await
.expect_err("spawn_agent should respect shared guard");
let CodexErr::AgentLimitReached { max_threads } = err else {
@@ -947,7 +975,11 @@ async fn resume_agent_respects_max_threads_limit() {
let control = manager.agent_control();
let resumable_id = control
.spawn_agent(config.clone(), text_input("hello"), None)
.spawn_agent(
config.clone(),
text_input("hello"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed");
let _ = control
@@ -956,7 +988,11 @@ async fn resume_agent_respects_max_threads_limit() {
.expect("shutdown resumable thread");
let active_id = control
.spawn_agent(config.clone(), text_input("occupy"), None)
.spawn_agent(
config.clone(),
text_input("occupy"),
/*session_source*/ None,
)
.await
.expect("spawn_agent should succeed for active slot");
@@ -1002,7 +1038,7 @@ async fn resume_agent_releases_slot_after_resume_failure() {
.expect_err("resume should fail for missing rollout path");
let resumed_id = control
.spawn_agent(config, text_input("hello"), None)
.spawn_agent(config, text_input("hello"), /*session_source*/ None)
.await
.expect("spawn should succeed after failed resume");
let _ = control
@@ -1140,7 +1176,7 @@ async fn multi_agent_v2_completion_ignores_dead_direct_parent() {
AgentPath::root(),
Vec::new(),
"done".to_string(),
true,
/*trigger_turn*/ true,
)
));
assert!(!has_subagent_notification(&root_history_items));
@@ -1203,7 +1239,7 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
worker_path.clone(),
Vec::new(),
expected_message.clone(),
false,
/*trigger_turn*/ false,
),
},
);
@@ -1238,7 +1274,7 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
AgentPath::root(),
Vec::new(),
expected_message,
false,
/*trigger_turn*/ false,
)
));
}
@@ -1259,7 +1295,7 @@ async fn completion_watcher_notifies_parent_when_child_is_missing() {
agent_role: Some("explorer".to_string()),
})),
child_thread_id.to_string(),
None,
/*child_agent_path*/ None,
);
assert_eq!(wait_for_subagent_notification(&parent_thread).await, true);
@@ -1520,7 +1556,11 @@ async fn resume_agent_from_rollout_reads_archived_rollout_path() {
let harness = AgentControlHarness::new().await;
let child_thread_id = harness
.control
.spawn_agent(harness.config.clone(), text_input("hello"), None)
.spawn_agent(
harness.config.clone(),
text_input("hello"),
/*session_source*/ None,
)
.await
.expect("child spawn should succeed");
+46 -20
View File
@@ -16,11 +16,26 @@ fn agent_metadata(thread_id: ThreadId) -> AgentMetadata {
#[test]
fn format_agent_nickname_adds_ordinals_after_reset() {
assert_eq!(format_agent_nickname("Plato", 0), "Plato");
assert_eq!(format_agent_nickname("Plato", 1), "Plato the 2nd");
assert_eq!(format_agent_nickname("Plato", 2), "Plato the 3rd");
assert_eq!(format_agent_nickname("Plato", 10), "Plato the 11th");
assert_eq!(format_agent_nickname("Plato", 20), "Plato the 21st");
assert_eq!(
format_agent_nickname("Plato", /*nickname_reset_count*/ 0),
"Plato"
);
assert_eq!(
format_agent_nickname("Plato", /*nickname_reset_count*/ 1),
"Plato the 2nd"
);
assert_eq!(
format_agent_nickname("Plato", /*nickname_reset_count*/ 2),
"Plato the 3rd"
);
assert_eq!(
format_agent_nickname("Plato", /*nickname_reset_count*/ 10),
"Plato the 11th"
);
assert_eq!(
format_agent_nickname("Plato", /*nickname_reset_count*/ 20),
"Plato the 21st"
);
}
#[test]
@@ -39,7 +54,10 @@ fn thread_spawn_depth_increments_and_enforces_limit() {
});
let child_depth = next_thread_spawn_depth(&session_source);
assert_eq!(child_depth, 2);
assert!(exceeds_thread_spawn_depth_limit(child_depth, 1));
assert!(exceeds_thread_spawn_depth_limit(
child_depth,
/*max_depth*/ 1
));
}
#[test]
@@ -47,7 +65,9 @@ fn non_thread_spawn_subagents_default_to_depth_zero() {
let session_source = SessionSource::SubAgent(SubAgentSource::Review);
assert_eq!(session_depth(&session_source), 0);
assert_eq!(next_thread_spawn_depth(&session_source), 1);
assert!(!exceeds_thread_spawn_depth_limit(1, 1));
assert!(!exceeds_thread_spawn_depth_limit(
/*depth*/ 1, /*max_depth*/ 1
));
}
#[test]
@@ -142,14 +162,18 @@ fn release_is_idempotent_for_registered_threads() {
#[test]
fn failed_spawn_keeps_nickname_marked_used() {
let registry = Arc::new(AgentRegistry::default());
let mut reservation = registry.reserve_spawn_slot(None).expect("reserve slot");
let mut reservation = registry
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve slot");
let agent_nickname = reservation
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
.expect("reserve agent name");
assert_eq!(agent_nickname, "alpha");
drop(reservation);
let mut reservation = registry.reserve_spawn_slot(None).expect("reserve slot");
let mut reservation = registry
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve slot");
let agent_nickname = reservation
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
.expect("unused name should still be preferred");
@@ -160,7 +184,7 @@ fn failed_spawn_keeps_nickname_marked_used() {
fn agent_nickname_resets_used_pool_when_exhausted() {
let registry = Arc::new(AgentRegistry::default());
let mut first = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve first slot");
let first_name = first
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
@@ -170,7 +194,7 @@ fn agent_nickname_resets_used_pool_when_exhausted() {
assert_eq!(first_name, "alpha");
let mut second = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve second slot");
let second_name = second
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
@@ -188,7 +212,7 @@ fn released_nickname_stays_used_until_pool_reset() {
let registry = Arc::new(AgentRegistry::default());
let mut first = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve first slot");
let first_name = first
.reserve_agent_nickname_with_preference(&["alpha"], /*preferred*/ None)
@@ -200,7 +224,7 @@ fn released_nickname_stays_used_until_pool_reset() {
registry.release_spawned_thread(first_id);
let mut second = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve second slot");
let second_name = second
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
@@ -211,7 +235,7 @@ fn released_nickname_stays_used_until_pool_reset() {
registry.release_spawned_thread(second_id);
let mut third = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve third slot");
let third_name = third
.reserve_agent_nickname_with_preference(&["alpha", "beta"], /*preferred*/ None)
@@ -230,7 +254,7 @@ fn repeated_resets_advance_the_ordinal_suffix() {
let registry = Arc::new(AgentRegistry::default());
let mut first = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve first slot");
let first_name = first
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
@@ -241,7 +265,7 @@ fn repeated_resets_advance_the_ordinal_suffix() {
registry.release_spawned_thread(first_id);
let mut second = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve second slot");
let second_name = second
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
@@ -252,7 +276,7 @@ fn repeated_resets_advance_the_ordinal_suffix() {
registry.release_spawned_thread(second_id);
let mut third = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve third slot");
let third_name = third
.reserve_agent_nickname_with_preference(&["Plato"], /*preferred*/ None)
@@ -282,7 +306,7 @@ fn register_root_thread_indexes_root_path() {
fn reserved_agent_path_is_released_when_spawn_fails() {
let registry = Arc::new(AgentRegistry::default());
let mut first = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve first slot");
first
.reserve_agent_path(&agent_path("/root/researcher"))
@@ -290,7 +314,7 @@ fn reserved_agent_path_is_released_when_spawn_fails() {
drop(first);
let mut second = registry
.reserve_spawn_slot(None)
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve second slot");
second
.reserve_agent_path(&agent_path("/root/researcher"))
@@ -301,7 +325,9 @@ fn reserved_agent_path_is_released_when_spawn_fails() {
fn committed_agent_path_is_indexed_until_release() {
let registry = Arc::new(AgentRegistry::default());
let thread_id = ThreadId::new();
let mut reservation = registry.reserve_spawn_slot(None).expect("reserve slot");
let mut reservation = registry
.reserve_spawn_slot(/*max_threads*/ None)
.expect("reserve slot");
reservation
.reserve_agent_path(&agent_path("/root/researcher"))
.expect("reserve path");

Some files were not shown because too many files have changed in this diff Show More