mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Merge branch 'main' of github.com:openai/codex into etraut/tuiappserver-mcp-regression
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: "*"
|
||||
|
||||
@@ -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
@@ -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 }}/*
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}/*
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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 }}/*
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+8
@@ -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"
|
||||
}
|
||||
|
||||
Generated
-97
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
use codex_tui::ComposerInput;
|
||||
use codex_tui_app_server::ComposerInput;
|
||||
|
||||
pub struct NewTaskPage {
|
||||
pub composer: ComposerInput,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: "));
|
||||
}
|
||||
|
||||
@@ -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`)"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user