mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
Merge branch 'main' into compaction-count-plumbing
This commit is contained in:
@@ -12,7 +12,8 @@ outputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- id: setup_ci
|
||||
uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Set up Bazel
|
||||
uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0
|
||||
@@ -20,30 +21,15 @@ runs:
|
||||
# and Windows can use the runner's standalone Bazel, ignoring .bazelversion.
|
||||
with:
|
||||
bazelisk-version: 1.28.1
|
||||
# setup-bazel writes an explicit output_base, which otherwise overrides
|
||||
# BAZEL_OUTPUT_USER_ROOT and leaves Bazel's I/O-heavy trees on C:.
|
||||
output-base: ${{ steps.setup_ci.outputs.bazel-output-base }}
|
||||
|
||||
- name: Configure Bazel repository cache
|
||||
id: configure_bazel_repository_cache
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Keep the repository cache under HOME on all runners. Windows `D:\a`
|
||||
# cache paths match `.bazelrc`, but `actions/cache/restore` currently
|
||||
# returns HTTP 400 for that path in the Windows clippy job.
|
||||
$repositoryCachePath = Join-Path $HOME '.cache/bazel-repo-cache'
|
||||
"repository-cache-path=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
"BAZEL_REPOSITORY_CACHE=$repositoryCachePath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
- name: Configure Bazel output root (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use the shortest available drive to reduce argv/path length issues,
|
||||
# but avoid the drive root because some Windows test launchers mis-handle
|
||||
# MANIFEST paths there.
|
||||
$hasDDrive = Test-Path 'D:\'
|
||||
$bazelOutputUserRoot = if ($hasDDrive) { 'D:\b' } else { 'C:\b' }
|
||||
$repoContentsCache = Join-Path $env:RUNNER_TEMP "bazel-repo-contents-cache-$env:GITHUB_RUN_ID-$env:GITHUB_JOB"
|
||||
"BAZEL_OUTPUT_USER_ROOT=$bazelOutputUserRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"BAZEL_REPO_CONTENTS_CACHE=$repoContentsCache" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"repository-cache-path=$env:BAZEL_REPOSITORY_CACHE" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
||||
|
||||
- name: Expose MSVC SDK environment (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
@@ -1,9 +1,67 @@
|
||||
name: setup-ci
|
||||
description: Prepare common tools and environment shared by CI jobs.
|
||||
outputs:
|
||||
bazel-output-base:
|
||||
description: Filesystem path used for Bazel's output base.
|
||||
value: ${{ steps.configure_ci_build_paths.outputs.bazel-output-base }}
|
||||
cargo-target-dir:
|
||||
description: Filesystem path used for Cargo's target directory.
|
||||
value: ${{ steps.configure_ci_build_paths.outputs.cargo-target-dir }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
# setup-ci expects either this step or the Unix fallback below to define
|
||||
# CI_BUILD_ROOT. Windows puts it on a Dev Drive because Cargo and Bazel
|
||||
# spend significant time reading and writing build/cache trees.
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./.github/scripts/setup-dev-drive.ps1
|
||||
|
||||
- name: Configure CI build root (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: echo "CI_BUILD_ROOT=$HOME/.cache/codex-ci" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Configure CI build paths
|
||||
id: configure_ci_build_paths
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# setup-bazel passes output_base explicitly, so keep both it and the
|
||||
# user root under the shared build root. Keep these directory names tiny
|
||||
# on every platform so Windows Bazel paths do not overflow argv or
|
||||
# confuse test MANIFEST handling.
|
||||
bazel_output_base="$CI_BUILD_ROOT/o"
|
||||
bazel_output_user_root="$CI_BUILD_ROOT/b"
|
||||
bazel_repository_cache="$CI_BUILD_ROOT/bazel-repository-cache"
|
||||
bazel_repo_contents_cache="$CI_BUILD_ROOT/bazel-repo-contents-cache-$GITHUB_RUN_ID-$GITHUB_JOB"
|
||||
cargo_target_dir="$CI_BUILD_ROOT/cargo-target"
|
||||
tmp="$CI_BUILD_ROOT/tmp"
|
||||
|
||||
build_dirs=(
|
||||
"$bazel_output_base"
|
||||
"$bazel_output_user_root"
|
||||
"$bazel_repository_cache"
|
||||
"$bazel_repo_contents_cache"
|
||||
"$cargo_target_dir"
|
||||
"$tmp"
|
||||
)
|
||||
mkdir -p "${build_dirs[@]}"
|
||||
echo "bazel-output-base=$bazel_output_base" >> "$GITHUB_OUTPUT"
|
||||
echo "cargo-target-dir=$cargo_target_dir" >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo "BAZEL_OUTPUT_BASE=$bazel_output_base"
|
||||
echo "BAZEL_OUTPUT_USER_ROOT=$bazel_output_user_root"
|
||||
echo "BAZEL_REPOSITORY_CACHE=$bazel_repository_cache"
|
||||
echo "BAZEL_REPO_CONTENTS_CACHE=$bazel_repo_contents_cache"
|
||||
echo "CARGO_TARGET_DIR=$cargo_target_dir"
|
||||
echo "TEMP=$tmp"
|
||||
echo "TMP=$tmp"
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Prefer the Git CLI for Cargo git dependencies
|
||||
shell: bash
|
||||
run: echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -48,7 +48,7 @@ runs:
|
||||
{
|
||||
echo "files<<EOF"
|
||||
for binary in ${BINARIES}; do
|
||||
echo "${GITHUB_WORKSPACE}/codex-rs/target/${TARGET}/release/${binary}.exe"
|
||||
echo "${CARGO_TARGET_DIR}/${TARGET}/release/${binary}.exe"
|
||||
done
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -131,25 +131,54 @@ def bazel_args_without_remote_execution(args: Sequence[str]) -> list[str]:
|
||||
def bazel_args_with_remote_config(
|
||||
args: Sequence[str], env: Mapping[str, str]
|
||||
) -> list[str]:
|
||||
command_idx = next(
|
||||
(idx for idx, arg in enumerate(args) if not arg.startswith("-")),
|
||||
None,
|
||||
)
|
||||
if command_idx is None:
|
||||
raise ValueError("expected a Bazel command")
|
||||
|
||||
config = remote_config(args, env)
|
||||
if config is None:
|
||||
return bazel_args_without_remote_execution(args)
|
||||
configured_args = bazel_args_without_remote_execution(args)
|
||||
else:
|
||||
# `remote_config()` returns a configuration only when this key is present.
|
||||
api_key = env["BUILDBUDDY_API_KEY"]
|
||||
remote_args = [
|
||||
f"--config={config}",
|
||||
f"--remote_header=x-buildbuddy-api-key={api_key}",
|
||||
]
|
||||
|
||||
# `remote_config()` returns a configuration only when this key is present.
|
||||
api_key = env["BUILDBUDDY_API_KEY"]
|
||||
remote_args = [
|
||||
f"--config={config}",
|
||||
f"--remote_header=x-buildbuddy-api-key={api_key}",
|
||||
# Insert immediately after the Bazel command. This keeps wrapper-added
|
||||
# options out of positional payloads and lets later CI configs override
|
||||
# shared RBE defaults such as the Windows cross-compilation exec platforms.
|
||||
configured_args = [
|
||||
*args[: command_idx + 1],
|
||||
*remote_args,
|
||||
*args[command_idx + 1 :],
|
||||
]
|
||||
|
||||
try:
|
||||
separator_idx = configured_args.index("--")
|
||||
except ValueError:
|
||||
separator_idx = len(configured_args)
|
||||
|
||||
cache_args = [
|
||||
f"{option_prefix}{env[env_name]}"
|
||||
for env_name, option_prefix in (
|
||||
("BAZEL_REPO_CONTENTS_CACHE", "--repo_contents_cache="),
|
||||
("BAZEL_REPOSITORY_CACHE", "--repository_cache="),
|
||||
)
|
||||
if env.get(env_name)
|
||||
and not any(
|
||||
arg.startswith(option_prefix) for arg in configured_args[:separator_idx]
|
||||
)
|
||||
]
|
||||
return [
|
||||
*configured_args[:separator_idx],
|
||||
*cache_args,
|
||||
*configured_args[separator_idx:],
|
||||
]
|
||||
|
||||
# Insert immediately after the Bazel command. This keeps wrapper-added
|
||||
# options out of positional payloads and lets later CI configs override
|
||||
# shared RBE defaults such as the Windows cross-compilation exec platforms.
|
||||
insertion_idx = next(
|
||||
(idx + 1 for idx, arg in enumerate(args) if not arg.startswith("-")),
|
||||
len(args),
|
||||
)
|
||||
return [*args[:insertion_idx], *remote_args, *args[insertion_idx:]]
|
||||
|
||||
|
||||
def bazel_command(*args: str, env: Mapping[str, str] | None = None) -> list[str]:
|
||||
|
||||
@@ -258,21 +258,27 @@ def stage_artifacts(
|
||||
print(staged_checksums)
|
||||
|
||||
|
||||
def upstream_release_pair_paths(source_root: Path, target: str) -> tuple[Path, Path]:
|
||||
def upstream_release_pair_paths(
|
||||
target: str,
|
||||
target_dir: Path,
|
||||
) -> tuple[Path, Path]:
|
||||
lib_name = (
|
||||
"rusty_v8.lib" if target.endswith("-pc-windows-msvc") else "librusty_v8.a"
|
||||
)
|
||||
gn_out = source_root / "target" / target / "release" / "gn_out"
|
||||
gn_out = target_dir / target / "release" / "gn_out"
|
||||
return gn_out / "obj" / lib_name, gn_out / "src_binding.rs"
|
||||
|
||||
|
||||
def stage_upstream_release_pair(
|
||||
source_root: Path,
|
||||
target: str,
|
||||
output_dir: Path,
|
||||
target_dir: Path,
|
||||
sandbox: bool = False,
|
||||
) -> None:
|
||||
lib_path, binding_path = upstream_release_pair_paths(source_root, target)
|
||||
lib_path, binding_path = upstream_release_pair_paths(
|
||||
target,
|
||||
target_dir,
|
||||
)
|
||||
stage_artifacts(target, lib_path, binding_path, output_dir, sandbox)
|
||||
|
||||
|
||||
@@ -330,7 +336,7 @@ def parse_args() -> argparse.Namespace:
|
||||
"stage-upstream-release-pair"
|
||||
)
|
||||
stage_upstream_release_pair_parser.add_argument(
|
||||
"--source-root", type=Path, required=True
|
||||
"--target-dir", type=Path, required=True
|
||||
)
|
||||
stage_upstream_release_pair_parser.add_argument("--target", required=True)
|
||||
stage_upstream_release_pair_parser.add_argument("--output-dir", required=True)
|
||||
@@ -373,9 +379,9 @@ def main() -> int:
|
||||
return 0
|
||||
if args.command == "stage-upstream-release-pair":
|
||||
stage_upstream_release_pair(
|
||||
source_root=args.source_root,
|
||||
target=args.target,
|
||||
output_dir=Path(args.output_dir),
|
||||
target_dir=args.target_dir,
|
||||
sandbox=args.sandbox,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# Configure a fast drive for Windows CI jobs.
|
||||
#
|
||||
# GitHub-hosted Windows runners do not always expose a secondary D: volume. When
|
||||
# they do not, try to create a Dev Drive VHD and fall back to C: if the runner
|
||||
# image does not allow that provisioning path.
|
||||
# they do not, create a Dev Drive VHD. CI depends on this path for its
|
||||
# build directories where CI spends significant time doing I/O, so fail the
|
||||
# job if no real Dev Drive is available.
|
||||
|
||||
function Use-FallbackDrive {
|
||||
param([string]$Reason)
|
||||
function Test-DevDrive {
|
||||
param([string]$Drive)
|
||||
|
||||
Write-Warning "$Reason Falling back to C:"
|
||||
return "C:"
|
||||
& fsutil devdrv query $Drive *> $null
|
||||
return $LASTEXITCODE -eq 0
|
||||
}
|
||||
|
||||
function Invoke-BestEffort {
|
||||
@@ -21,10 +22,14 @@ function Invoke-BestEffort {
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path "D:\") {
|
||||
Write-Output "Using existing drive at D:"
|
||||
if ((Test-Path "D:\") -and (Test-DevDrive "D:")) {
|
||||
Write-Output "Using existing Dev Drive at D:"
|
||||
$Drive = "D:"
|
||||
} else {
|
||||
if (Test-Path "D:\") {
|
||||
Write-Output "Existing D: volume is not a Dev Drive; provisioning a new Dev Drive VHD."
|
||||
}
|
||||
|
||||
try {
|
||||
$VhdPath = Join-Path $env:RUNNER_TEMP "codex-dev-drive.vhdx"
|
||||
$SizeBytes = 64GB
|
||||
@@ -42,21 +47,17 @@ if (Test-Path "D:\") {
|
||||
|
||||
$Drive = "$($Volume.DriveLetter):"
|
||||
|
||||
if (-not (Test-DevDrive $Drive)) {
|
||||
throw "Provisioned volume at $Drive did not pass Dev Drive verification."
|
||||
}
|
||||
|
||||
Invoke-BestEffort { fsutil devdrv trust $Drive } "Trusting Dev Drive $Drive"
|
||||
Invoke-BestEffort { fsutil devdrv enable /disallowAv } "Disabling AV filter attachment for Dev Drives"
|
||||
Invoke-BestEffort { fsutil devdrv query $Drive } "Querying Dev Drive $Drive"
|
||||
|
||||
Write-Output "Using Dev Drive at $Drive"
|
||||
} catch {
|
||||
$Drive = Use-FallbackDrive "Failed to create Dev Drive: $($_.Exception.Message)"
|
||||
throw "Failed to create Dev Drive: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$Tmp = "$Drive\codex-tmp"
|
||||
New-Item -Path $Tmp -ItemType Directory -Force | Out-Null
|
||||
|
||||
@(
|
||||
"DEV_DRIVE=$Drive"
|
||||
"TMP=$Tmp"
|
||||
"TEMP=$Tmp"
|
||||
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
"CI_BUILD_ROOT=$Drive" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
|
||||
@@ -214,6 +214,48 @@ class RunBazelWithBuildBuddyTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_bazel_command_uses_configured_local_caches(self) -> None:
|
||||
env = {
|
||||
"BAZEL_REPO_CONTENTS_CACHE": "/tmp/bazel-repo-contents",
|
||||
"BAZEL_REPOSITORY_CACHE": "/tmp/bazel-repository",
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
run_bazel_with_buildbuddy.bazel_command(
|
||||
"build",
|
||||
"--config=local",
|
||||
"//codex-rs/...",
|
||||
env=env,
|
||||
),
|
||||
[
|
||||
"bazel",
|
||||
"build",
|
||||
"--config=local",
|
||||
"//codex-rs/...",
|
||||
"--repo_contents_cache=/tmp/bazel-repo-contents",
|
||||
"--repository_cache=/tmp/bazel-repository",
|
||||
],
|
||||
)
|
||||
|
||||
def test_bazel_command_adds_local_caches_before_separator(self) -> None:
|
||||
self.assertEqual(
|
||||
run_bazel_with_buildbuddy.bazel_command(
|
||||
"build",
|
||||
"//codex-rs/...",
|
||||
"--",
|
||||
"--program-arg",
|
||||
env={"BAZEL_REPOSITORY_CACHE": "/tmp/bazel-repository"},
|
||||
),
|
||||
[
|
||||
"bazel",
|
||||
"build",
|
||||
"//codex-rs/...",
|
||||
"--repository_cache=/tmp/bazel-repository",
|
||||
"--",
|
||||
"--program-arg",
|
||||
],
|
||||
)
|
||||
|
||||
def test_main_preserves_spaced_argument_and_child_exit_status(self) -> None:
|
||||
spaced_arg = (
|
||||
r"--test_env=PATH=C:\Program Files\PowerShell\7;C:\Program Files\Git\bin"
|
||||
|
||||
@@ -205,8 +205,8 @@ class RustyV8BazelTest(unittest.TestCase):
|
||||
),
|
||||
),
|
||||
rusty_v8_bazel.upstream_release_pair_paths(
|
||||
Path("/tmp/rusty_v8"),
|
||||
"x86_64-apple-darwin",
|
||||
Path("/tmp/rusty_v8/target"),
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -221,25 +221,30 @@ class RustyV8BazelTest(unittest.TestCase):
|
||||
),
|
||||
),
|
||||
rusty_v8_bazel.upstream_release_pair_paths(
|
||||
Path("/tmp/rusty_v8"),
|
||||
"x86_64-pc-windows-msvc",
|
||||
Path("/tmp/rusty_v8/target"),
|
||||
),
|
||||
)
|
||||
|
||||
def test_stage_upstream_release_pair(self) -> None:
|
||||
with TemporaryDirectory() as source_dir, TemporaryDirectory() as output_dir:
|
||||
source_root = Path(source_dir)
|
||||
with (
|
||||
TemporaryDirectory() as target_dir,
|
||||
TemporaryDirectory() as output_dir,
|
||||
):
|
||||
gn_out = (
|
||||
source_root / "target" / "x86_64-pc-windows-msvc" / "release" / "gn_out"
|
||||
Path(target_dir)
|
||||
/ "x86_64-pc-windows-msvc"
|
||||
/ "release"
|
||||
/ "gn_out"
|
||||
)
|
||||
(gn_out / "obj").mkdir(parents=True)
|
||||
(gn_out / "obj" / "rusty_v8.lib").write_bytes(b"archive")
|
||||
(gn_out / "src_binding.rs").write_text("binding")
|
||||
|
||||
rusty_v8_bazel.stage_upstream_release_pair(
|
||||
source_root,
|
||||
"x86_64-pc-windows-msvc",
|
||||
Path(output_dir),
|
||||
Path(target_dir),
|
||||
sandbox=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,13 +47,17 @@ version = "149.2.0"
|
||||
)
|
||||
)
|
||||
|
||||
def test_setup_ci_change_requires_canary_and_source_build(self) -> None:
|
||||
changed_files = {".github/actions/setup-ci/action.yml"}
|
||||
|
||||
self.assertTrue(canary_required(changed_files, "149.2.0", "149.2.0"))
|
||||
self.assertTrue(
|
||||
windows_source_required(changed_files, "149.2.0", "149.2.0")
|
||||
)
|
||||
def test_shared_ci_setup_changes_require_canary_and_source_build(self) -> None:
|
||||
for path in (
|
||||
".github/actions/setup-ci/action.yml",
|
||||
".github/scripts/setup-dev-drive.ps1",
|
||||
):
|
||||
with self.subTest(path=path):
|
||||
changed_files = {path}
|
||||
self.assertTrue(canary_required(changed_files, "149.2.0", "149.2.0"))
|
||||
self.assertTrue(
|
||||
windows_source_required(changed_files, "149.2.0", "149.2.0")
|
||||
)
|
||||
|
||||
def test_manual_dispatch_requires_source_build(self) -> None:
|
||||
self.assertTrue(
|
||||
|
||||
@@ -25,6 +25,7 @@ CANARY_PATH_PATTERNS = {
|
||||
".github/scripts/run_bazel_with_buildbuddy.py",
|
||||
".github/scripts/rusty_v8_bazel.py",
|
||||
".github/scripts/rusty_v8_module_bazel.py",
|
||||
".github/scripts/setup-dev-drive.ps1",
|
||||
".github/scripts/v8_canary_changes.py",
|
||||
".github/workflows/postmerge-ci.yml",
|
||||
".github/workflows/rusty-v8-release.yml",
|
||||
@@ -44,6 +45,7 @@ WINDOWS_SOURCE_BUILD_PATHS = {
|
||||
".github/actions/setup-ci/**",
|
||||
".github/scripts/rusty_v8_bazel.py",
|
||||
".github/scripts/rusty_v8_module_bazel.py",
|
||||
".github/scripts/setup-dev-drive.ps1",
|
||||
".github/scripts/v8_canary_changes.py",
|
||||
".github/workflows/rusty-v8-release.yml",
|
||||
".github/workflows/v8-canary.yml",
|
||||
|
||||
@@ -71,11 +71,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure Dev Drive (Windows)
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
shell: pwsh
|
||||
run: ../.github/scripts/setup-dev-drive.ps1
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Install Linux build dependencies
|
||||
@@ -136,11 +131,7 @@ jobs:
|
||||
echo "Using sccache GitHub backend"
|
||||
else
|
||||
echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
|
||||
if [[ -n "${DEV_DRIVE:-}" ]]; then
|
||||
echo "SCCACHE_DIR=${DEV_DRIVE}\\.sccache" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
|
||||
fi
|
||||
echo "SCCACHE_DIR=${CI_BUILD_ROOT}/sccache" >> "$GITHUB_ENV"
|
||||
echo "Using sccache local disk + actions/cache fallback"
|
||||
fi
|
||||
|
||||
@@ -206,7 +197,7 @@ jobs:
|
||||
--profile ${{ inputs.profile }} \
|
||||
-p codex-linux-sandbox \
|
||||
--bin codex-linux-sandbox
|
||||
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-linux-sandbox" "${helper_dir}/"
|
||||
cp "${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}/codex-linux-sandbox" "${helper_dir}/"
|
||||
else
|
||||
cargo build \
|
||||
--target ${{ inputs.target }} \
|
||||
@@ -214,8 +205,8 @@ jobs:
|
||||
-p codex-windows-sandbox \
|
||||
--bin codex-windows-sandbox-setup \
|
||||
--bin codex-command-runner
|
||||
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-windows-sandbox-setup.exe" "${helper_dir}/"
|
||||
cp "target/${{ inputs.target }}/${{ inputs.profile }}/codex-command-runner.exe" "${helper_dir}/"
|
||||
cp "${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}/codex-windows-sandbox-setup.exe" "${helper_dir}/"
|
||||
cp "${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}/codex-command-runner.exe" "${helper_dir}/"
|
||||
fi
|
||||
|
||||
- name: Upload Cargo timings (nextest)
|
||||
@@ -223,7 +214,7 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-nextest-${{ inputs.target }}-${{ inputs.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Upload nextest archive
|
||||
@@ -369,13 +360,13 @@ jobs:
|
||||
|
||||
if [[ "${RUNNER_OS}" == "Linux" ]]; then
|
||||
helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
|
||||
helper_target_dir="$(pwd)/target/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
helper_target_dir="${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
mkdir -p "${helper_target_dir}"
|
||||
cp "${helper_dir}/codex-linux-sandbox" "${helper_target_dir}/"
|
||||
chmod +x "${helper_target_dir}/codex-linux-sandbox"
|
||||
elif [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
helper_dir="${RUNNER_TEMP}/${TEST_HELPERS_ARTIFACT}"
|
||||
helper_target_dir="$(pwd)/target/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
helper_target_dir="${CARGO_TARGET_DIR}/${{ inputs.target }}/${{ inputs.profile }}"
|
||||
mkdir -p "${helper_target_dir}"
|
||||
cp "${helper_dir}/codex-windows-sandbox-setup.exe" "${helper_target_dir}/"
|
||||
cp "${helper_dir}/codex-command-runner.exe" "${helper_target_dir}/"
|
||||
@@ -423,7 +414,7 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: nextest-junit-rust-ci-${{ inputs.artifact_id }}-shard-${{ matrix.shard }}
|
||||
path: codex-rs/target/nextest/default/junit.xml
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/nextest/default/junit.xml
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Tear down remote test env
|
||||
|
||||
@@ -403,7 +403,7 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
# Save caches explicitly; make non-fatal so cache packaging
|
||||
|
||||
@@ -110,13 +110,13 @@ jobs:
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: codex-rs/target/**/cargo-timings/cargo-timing.html
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/**/cargo-timings/cargo-timing.html
|
||||
if-no-files-found: warn
|
||||
|
||||
- name: Stage Windows binaries
|
||||
shell: bash
|
||||
run: |
|
||||
release_dir="target/${{ matrix.target }}/release"
|
||||
release_dir="$CARGO_TARGET_DIR/${{ matrix.target }}/release"
|
||||
output_dir="$release_dir/staged-${{ matrix.bundle }}"
|
||||
mkdir -p "$output_dir"
|
||||
for binary in ${{ matrix.binaries }}; do
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
|
||||
path: |
|
||||
codex-rs/target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}/*
|
||||
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/staged-${{ matrix.bundle }}/*
|
||||
|
||||
build-windows-symbols:
|
||||
needs:
|
||||
@@ -168,14 +168,14 @@ jobs:
|
||||
with:
|
||||
pattern: windows-binaries-${{ matrix.target }}-*
|
||||
merge-multiple: true
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
- name: Build symbols archive
|
||||
shell: bash
|
||||
run: |
|
||||
bash "${GITHUB_WORKSPACE}/.github/scripts/archive-release-symbols-and-strip-binaries.sh" \
|
||||
--target "${{ matrix.target }}" \
|
||||
--artifact-name "${{ matrix.target }}" \
|
||||
--release-dir "target/${{ matrix.target }}/release" \
|
||||
--release-dir "${CARGO_TARGET_DIR}/${{ matrix.target }}/release" \
|
||||
--archive-dir "symbols-dist/${{ matrix.target }}" \
|
||||
--binaries "${WINDOWS_BINARIES}"
|
||||
- name: Upload symbols archive
|
||||
@@ -226,26 +226,26 @@ jobs:
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-primary
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows helper binaries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-helpers
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
|
||||
- name: Download prebuilt Windows app-server binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: windows-binaries-${{ matrix.target }}-app-server
|
||||
path: codex-rs/target/${{ matrix.target }}/release
|
||||
path: ${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release
|
||||
|
||||
- name: Verify binaries
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for binary in ${WINDOWS_BINARIES}; do
|
||||
ls -lh "target/${{ matrix.target }}/release/${binary}.exe"
|
||||
ls -lh "$CARGO_TARGET_DIR/${{ matrix.target }}/release/${binary}.exe"
|
||||
done
|
||||
|
||||
- name: Sign Windows binaries with Azure Trusted Signing
|
||||
@@ -267,7 +267,7 @@ jobs:
|
||||
mkdir -p "$dest"
|
||||
|
||||
for binary in ${WINDOWS_BINARIES}; do
|
||||
cp "target/${{ matrix.target }}/release/${binary}.exe" \
|
||||
cp "$CARGO_TARGET_DIR/${{ matrix.target }}/release/${binary}.exe" \
|
||||
"$dest/${binary}-${{ matrix.target }}.exe"
|
||||
done
|
||||
|
||||
@@ -291,7 +291,7 @@ jobs:
|
||||
bash "$archive_script" \
|
||||
--target "$target" \
|
||||
--bundle "{}" \
|
||||
--entrypoint-dir "target/$target/release" \
|
||||
--entrypoint-dir "$CARGO_TARGET_DIR/$target/release" \
|
||||
--archive-dir "dist/$target"
|
||||
|
||||
- name: Build Python runtime wheel
|
||||
|
||||
@@ -392,7 +392,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-upstream-release-pair \
|
||||
--source-root upstream-rusty-v8 \
|
||||
--target-dir upstream-rusty-v8/target \
|
||||
--target "${TARGET}" \
|
||||
--output-dir "dist/${TARGET}" \
|
||||
--sandbox
|
||||
|
||||
@@ -160,8 +160,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/bazel-repo-cache
|
||||
path: ${{ steps.setup_bazel.outputs.repository-cache-path }}
|
||||
key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
|
||||
|
||||
- name: Check for a clean worktree
|
||||
|
||||
@@ -312,15 +312,16 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: windows-2022
|
||||
- runner: windows-2025
|
||||
target: x86_64-pc-windows-msvc
|
||||
- runner: windows-2022
|
||||
- runner: windows-2025
|
||||
target: aarch64-pc-windows-msvc
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- uses: ./.github/actions/setup-ci
|
||||
- id: setup_ci
|
||||
uses: ./.github/actions/setup-ci
|
||||
|
||||
- name: Configure git for upstream checkout
|
||||
shell: bash
|
||||
@@ -364,17 +365,17 @@ jobs:
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: |
|
||||
upstream-rusty-v8/target/sccache
|
||||
upstream-rusty-v8/target/${{ matrix.target }}/release/gn_out
|
||||
key: rusty-v8-source-${{ matrix.target }}-sandbox-${{ hashFiles('upstream-rusty-v8/Cargo.lock', 'upstream-rusty-v8/build.rs', 'upstream-rusty-v8/git_submodule_status.txt') }}
|
||||
${{ steps.setup_ci.outputs.cargo-target-dir }}/sccache
|
||||
${{ steps.setup_ci.outputs.cargo-target-dir }}/${{ matrix.target }}/release/gn_out
|
||||
key: rusty-v8-source-${{ matrix.target }}-${{ matrix.runner }}-sandbox-${{ hashFiles('upstream-rusty-v8/Cargo.lock', 'upstream-rusty-v8/build.rs', 'upstream-rusty-v8/git_submodule_status.txt') }}
|
||||
restore-keys: |
|
||||
rusty-v8-source-${{ matrix.target }}-sandbox-
|
||||
rusty-v8-source-${{ matrix.target }}-${{ matrix.runner }}-sandbox-
|
||||
|
||||
- name: Install and start sccache
|
||||
shell: pwsh
|
||||
env:
|
||||
SCCACHE_CACHE_SIZE: 256M
|
||||
SCCACHE_DIR: ${{ github.workspace }}/upstream-rusty-v8/target/sccache
|
||||
SCCACHE_DIR: ${{ steps.setup_ci.outputs.cargo-target-dir }}/sccache
|
||||
SCCACHE_IDLE_TIMEOUT: 0
|
||||
run: |
|
||||
$version = "v0.8.2"
|
||||
@@ -409,7 +410,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 .github/scripts/rusty_v8_bazel.py stage-upstream-release-pair \
|
||||
--source-root upstream-rusty-v8 \
|
||||
--target-dir "${CARGO_TARGET_DIR}" \
|
||||
--target "${TARGET}" \
|
||||
--output-dir "dist/${TARGET}" \
|
||||
--sandbox
|
||||
|
||||
@@ -253,7 +253,7 @@ Use `just bench-smoke` to dry-run the benchmark for a single iteration to ensure
|
||||
|
||||
- Tests should exercise app-server's public JSON-RPC API.
|
||||
- Use similar server mocking as for core integration tests.
|
||||
- Use `TestAppServer::new_with_auto_env()` and `TestAppServer::send_thread_start_request_with_auto_env()`
|
||||
- Use `TestAppServer::builder().build()` and `TestAppServer::send_thread_start_request_with_auto_env()`
|
||||
by default to ensure that new tests work with foreign app/exec OSes. See `$remote-tests` for
|
||||
details.
|
||||
|
||||
|
||||
+50
-5
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
const codexPackageRoot = realpathSync(path.join(__dirname, ".."));
|
||||
|
||||
const PLATFORM_PACKAGE_BY_TARGET = {
|
||||
"x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
|
||||
@@ -98,7 +99,9 @@ function findCodexExecutable() {
|
||||
const updateCommand =
|
||||
packageManager === "bun"
|
||||
? "bun install -g @openai/codex@latest"
|
||||
: "npm install -g @openai/codex@latest";
|
||||
: packageManager === "pnpm"
|
||||
? "pnpm add -g @openai/codex@latest"
|
||||
: "npm install -g @openai/codex@latest";
|
||||
throw new Error(
|
||||
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
|
||||
);
|
||||
@@ -112,11 +115,47 @@ const binaryPath = findCodexExecutable();
|
||||
// and guarantees that when either the child terminates or the parent
|
||||
// receives a fatal signal, both processes exit in a predictable manner.
|
||||
|
||||
function isPnpmOwnedCodexInstall(nodeModulesDir) {
|
||||
if (!existsSync(path.join(nodeModulesDir, ".modules.yaml"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return (
|
||||
realpathSync(path.join(nodeModulesDir, "@openai", "codex")) ===
|
||||
codexPackageRoot
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use heuristics to detect the package manager that was used to install Codex
|
||||
* in order to give the user a hint about how to update it.
|
||||
*/
|
||||
function detectPackageManager() {
|
||||
// pnpm's owning node_modules directory can be several parents above the
|
||||
// package in isolated global layouts. Search ancestors of both the canonical
|
||||
// package root and lexical entrypoint because pnpm may link either path.
|
||||
const entrypointDir = path.dirname(path.resolve(process.argv[1]));
|
||||
for (const startDir of new Set([codexPackageRoot, entrypointDir])) {
|
||||
const filesystemRoot = path.parse(startDir).root;
|
||||
for (
|
||||
let currentDir = startDir;
|
||||
currentDir !== filesystemRoot;
|
||||
currentDir = path.dirname(currentDir)
|
||||
) {
|
||||
if (isPnpmOwnedCodexInstall(path.join(currentDir, "node_modules"))) {
|
||||
return "pnpm";
|
||||
}
|
||||
}
|
||||
|
||||
if (isPnpmOwnedCodexInstall(path.join(filesystemRoot, "node_modules"))) {
|
||||
return "pnpm";
|
||||
}
|
||||
}
|
||||
|
||||
const userAgent = process.env.npm_config_user_agent || "";
|
||||
if (/\bbun\//.test(userAgent)) {
|
||||
return "bun";
|
||||
@@ -137,15 +176,21 @@ function detectPackageManager() {
|
||||
return userAgent ? "npm" : null;
|
||||
}
|
||||
|
||||
const packageManager = detectPackageManager();
|
||||
const packageManagerEnvVar =
|
||||
detectPackageManager() === "bun"
|
||||
packageManager === "bun"
|
||||
? "CODEX_MANAGED_BY_BUN"
|
||||
: "CODEX_MANAGED_BY_NPM";
|
||||
: packageManager === "pnpm"
|
||||
? "CODEX_MANAGED_BY_PNPM"
|
||||
: "CODEX_MANAGED_BY_NPM";
|
||||
const env = {
|
||||
...process.env,
|
||||
[packageManagerEnvVar]: "1",
|
||||
CODEX_MANAGED_PACKAGE_ROOT: realpathSync(path.join(__dirname, "..")),
|
||||
CODEX_MANAGED_PACKAGE_ROOT: codexPackageRoot,
|
||||
};
|
||||
delete env.CODEX_MANAGED_BY_NPM;
|
||||
delete env.CODEX_MANAGED_BY_BUN;
|
||||
delete env.CODEX_MANAGED_BY_PNPM;
|
||||
env[packageManagerEnvVar] = "1";
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
name: Cargo audit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
||||
- name: Install cargo-audit
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-audit
|
||||
- name: Run cargo audit
|
||||
run: cargo audit --deny warnings
|
||||
Generated
+37
@@ -412,6 +412,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
@@ -1939,6 +1940,7 @@ dependencies = [
|
||||
"codex-http-client",
|
||||
"codex-protocol",
|
||||
"codex-utils-rustls-provider",
|
||||
"codex-websocket-client",
|
||||
"eventsource-stream",
|
||||
"futures",
|
||||
"http 1.4.0",
|
||||
@@ -2107,6 +2109,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"codex-experimental-api-macros",
|
||||
"codex-extension-items",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2641,6 +2644,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-features",
|
||||
"codex-feedback",
|
||||
"codex-file-system",
|
||||
@@ -2802,6 +2806,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"tracing-test",
|
||||
"url",
|
||||
"which 8.0.0",
|
||||
"wiremock",
|
||||
"zip 2.4.2",
|
||||
]
|
||||
@@ -3013,6 +3018,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-extension-items"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-external-agent-migration"
|
||||
version = "0.0.0"
|
||||
@@ -3235,6 +3252,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
@@ -3714,6 +3732,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"codex-async-utils",
|
||||
"codex-execpolicy",
|
||||
"codex-extension-items",
|
||||
"codex-network-proxy",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
@@ -4071,6 +4090,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-code-mode",
|
||||
"codex-connectors",
|
||||
"codex-extension-items",
|
||||
"codex-features",
|
||||
"codex-file-system",
|
||||
"codex-protocol",
|
||||
@@ -4453,6 +4473,7 @@ dependencies = [
|
||||
"codex-api",
|
||||
"codex-core",
|
||||
"codex-extension-api",
|
||||
"codex-extension-items",
|
||||
"codex-login",
|
||||
"codex-model-provider",
|
||||
"codex-model-provider-info",
|
||||
@@ -4465,6 +4486,22 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-websocket-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-http-client",
|
||||
"codex-utils-rustls-provider",
|
||||
"futures",
|
||||
"pretty_assertions",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-windows-sandbox"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -54,6 +54,7 @@ members = [
|
||||
"ext/goal",
|
||||
"ext/guardian",
|
||||
"ext/image-generation",
|
||||
"ext/items",
|
||||
"ext/memories",
|
||||
"ext/mcp",
|
||||
"ext/skills",
|
||||
@@ -89,6 +90,7 @@ members = [
|
||||
"tui",
|
||||
"tools",
|
||||
"v8-poc",
|
||||
"websocket-client",
|
||||
"utils/absolute-path",
|
||||
"utils/path-uri",
|
||||
"utils/cargo-bin",
|
||||
@@ -166,6 +168,7 @@ codex-code-mode = { path = "code-mode" }
|
||||
codex-code-mode-protocol = { path = "code-mode-protocol" }
|
||||
codex-home = { path = "codex-home" }
|
||||
codex-http-client = { path = "http-client" }
|
||||
codex-websocket-client = { path = "websocket-client" }
|
||||
codex-config = { path = "config" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-connectors-extension = { path = "ext/connectors" }
|
||||
@@ -180,6 +183,7 @@ codex-exec-server-protocol = { path = "exec-server-protocol" }
|
||||
codex-exec-server = { path = "exec-server" }
|
||||
codex-execpolicy = { path = "execpolicy" }
|
||||
codex-extension-api = { path = "ext/extension-api" }
|
||||
codex-extension-items = { path = "ext/items" }
|
||||
codex-goal-extension = { path = "ext/goal" }
|
||||
codex-guardian = { path = "ext/guardian" }
|
||||
codex-image-generation-extension = { path = "ext/image-generation" }
|
||||
@@ -425,6 +429,7 @@ thiserror = "2.0.17"
|
||||
time = "0.3.47"
|
||||
tiny_http = "0.12"
|
||||
tokio = "1"
|
||||
tokio-rustls = "0.26.4"
|
||||
tokio-stream = "0.1.18"
|
||||
tokio-test = "0.4"
|
||||
tokio-tungstenite = { version = "0.28.0", features = [
|
||||
|
||||
@@ -103,6 +103,7 @@ use codex_app_server_protocol::GuardianApprovalReview;
|
||||
use codex_app_server_protocol::GuardianApprovalReviewAction;
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::GuardianCommandSource as AppServerGuardianCommandSource;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
@@ -139,6 +140,7 @@ use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_app_server_protocol::WebSearchItem;
|
||||
use codex_login::default_client::DEFAULT_ORIGINATOR;
|
||||
use codex_login::default_client::originator;
|
||||
use codex_plugin::AppConnectorId;
|
||||
@@ -4400,18 +4402,18 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
agent_thread_id: "thread-child".to_string(),
|
||||
agent_path: "/root/child".to_string(),
|
||||
},
|
||||
ThreadItem::WebSearch {
|
||||
ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "web-1".to_string(),
|
||||
query: "codex".to_string(),
|
||||
action: None,
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
}),
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
result: "ok".to_string(),
|
||||
saved_path: None,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
for item in completed_tool_items {
|
||||
|
||||
@@ -402,8 +402,8 @@ impl TurnToolCounts {
|
||||
ThreadItem::CollabAgentToolCall { .. } | ThreadItem::SubAgentActivity { .. } => {
|
||||
self.subagent_tool_call += 1;
|
||||
}
|
||||
ThreadItem::WebSearch { .. } => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration { .. } => self.image_generation += 1,
|
||||
ThreadItem::WebSearch(_) => self.web_search += 1,
|
||||
ThreadItem::ImageGeneration(_) => self.image_generation += 1,
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
@@ -1732,9 +1732,9 @@ fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> {
|
||||
| ThreadItem::FileChange { id, .. }
|
||||
| ThreadItem::McpToolCall { id, .. }
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. } => Some(id),
|
||||
| ThreadItem::CollabAgentToolCall { id, .. } => Some(id),
|
||||
ThreadItem::WebSearch(item) => Some(&item.id),
|
||||
ThreadItem::ImageGeneration(item) => Some(&item.id),
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::HookPrompt { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
@@ -2027,11 +2027,11 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
},
|
||||
))
|
||||
}
|
||||
ThreadItem::WebSearch { id, query, action } => {
|
||||
ThreadItem::WebSearch(item) => {
|
||||
let base = tool_item_base(
|
||||
thread_id,
|
||||
turn_id,
|
||||
id.clone(),
|
||||
item.id.clone(),
|
||||
"web_search".to_string(),
|
||||
ToolItemOutcome {
|
||||
terminal_status: ToolItemTerminalStatus::Completed,
|
||||
@@ -2051,24 +2051,18 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
event_type: "codex_web_search_event",
|
||||
event_params: CodexWebSearchEventParams {
|
||||
base,
|
||||
web_search_action: action.as_ref().map(web_search_action_kind),
|
||||
query_present: !query.trim().is_empty(),
|
||||
query_count: web_search_query_count(query, action.as_ref()),
|
||||
web_search_action: item.action.as_ref().map(web_search_action_kind),
|
||||
query_present: !item.query.trim().is_empty(),
|
||||
query_count: web_search_query_count(&item.query, item.action.as_ref()),
|
||||
},
|
||||
}))
|
||||
}
|
||||
ThreadItem::ImageGeneration {
|
||||
id,
|
||||
status,
|
||||
revised_prompt,
|
||||
saved_path,
|
||||
..
|
||||
} => {
|
||||
let (terminal_status, failure_kind) = image_generation_outcome(status.as_str());
|
||||
ThreadItem::ImageGeneration(item) => {
|
||||
let (terminal_status, failure_kind) = image_generation_outcome(item.status.as_str());
|
||||
let base = tool_item_base(
|
||||
thread_id,
|
||||
turn_id,
|
||||
id.clone(),
|
||||
item.id.clone(),
|
||||
"image_generation".to_string(),
|
||||
ToolItemOutcome {
|
||||
terminal_status,
|
||||
@@ -2089,8 +2083,8 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
||||
event_type: "codex_image_generation_event",
|
||||
event_params: CodexImageGenerationEventParams {
|
||||
base,
|
||||
revised_prompt_present: revised_prompt.is_some(),
|
||||
saved_path_present: saved_path.is_some(),
|
||||
revised_prompt_present: item.revised_prompt.is_some(),
|
||||
saved_path_present: item.saved_path.is_some(),
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
@@ -16,6 +16,7 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-experimental-api-macros = { workspace = true }
|
||||
codex-extension-items = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
|
||||
@@ -1463,6 +1463,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -1472,6 +1483,9 @@
|
||||
],
|
||||
"title": "ChatgptLoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -1532,6 +1546,13 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"MarketplaceAddParams": {
|
||||
"properties": {
|
||||
"refName": {
|
||||
|
||||
@@ -536,6 +536,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
+28
@@ -6951,6 +6951,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
@@ -11504,6 +11511,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -11513,6 +11531,9 @@
|
||||
],
|
||||
"title": "Chatgptv2::LoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11667,6 +11688,13 @@
|
||||
],
|
||||
"title": "LoginAccountResponse"
|
||||
},
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LogoutAccountResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LogoutAccountResponse",
|
||||
|
||||
+28
@@ -1077,6 +1077,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
@@ -7908,6 +7915,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -7917,6 +7935,9 @@
|
||||
],
|
||||
"title": "Chatgptv2::LoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -8071,6 +8092,13 @@
|
||||
],
|
||||
"title": "LoginAccountResponse"
|
||||
},
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"LogoutAccountResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LogoutAccountResponse",
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Backend auth supplied as request headers.",
|
||||
"enum": [
|
||||
"headers"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "Programmatic Codex auth backed by a registered Agent Identity.",
|
||||
"enum": [
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"LoginAppBrand": {
|
||||
"enum": [
|
||||
"codex",
|
||||
"chatgpt"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
@@ -23,6 +32,17 @@
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"appBrand": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/LoginAppBrand"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"codexStreamlinedLogin": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -32,6 +52,9 @@
|
||||
],
|
||||
"title": "Chatgptv2::LoginAccountParamsType",
|
||||
"type": "string"
|
||||
},
|
||||
"useHostedLoginSuccessPage": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+1
-1
@@ -5,4 +5,4 @@
|
||||
/**
|
||||
* Authentication mode for OpenAI-backed providers.
|
||||
*/
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "headers" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "./AbsolutePathBuf";
|
||||
|
||||
export type ImageGenerationItem = { id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { WebSearchAction } from "./v2/WebSearchAction";
|
||||
|
||||
export type WebSearchItem = { id: string, query: string, action: WebSearchAction | null, };
|
||||
@@ -37,6 +37,7 @@ export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams";
|
||||
export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse";
|
||||
export type { GitSha } from "./GitSha";
|
||||
export type { ImageDetail } from "./ImageDetail";
|
||||
export type { ImageGenerationItem } from "./ImageGenerationItem";
|
||||
export type { InitializeCapabilities } from "./InitializeCapabilities";
|
||||
export type { InitializeParams } from "./InitializeParams";
|
||||
export type { InitializeResponse } from "./InitializeResponse";
|
||||
@@ -81,6 +82,7 @@ export type { Tool } from "./Tool";
|
||||
export type { Verbosity } from "./Verbosity";
|
||||
export type { WebSearchAction } from "./WebSearchAction";
|
||||
export type { WebSearchContextSize } from "./WebSearchContextSize";
|
||||
export type { WebSearchItem } from "./WebSearchItem";
|
||||
export type { WebSearchLocation } from "./WebSearchLocation";
|
||||
export type { WebSearchMode } from "./WebSearchMode";
|
||||
export type { WebSearchToolConfig } from "./WebSearchToolConfig";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { LoginAppBrand } from "./LoginAppBrand";
|
||||
|
||||
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens",
|
||||
export type LoginAccountParams = { "type": "apiKey", apiKey: string, } | { "type": "chatgpt", codexStreamlinedLogin?: boolean, useHostedLoginSuccessPage?: boolean, appBrand?: LoginAppBrand | null, } | { "type": "chatgptDeviceCode" } | { "type": "chatgptAuthTokens",
|
||||
/**
|
||||
* Access token (JWT) supplied by the client.
|
||||
* This token is used for backend API requests and email extraction.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type LoginAppBrand = "codex" | "chatgpt";
|
||||
@@ -1,10 +1,11 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { ImageGenerationItem } from "../ImageGenerationItem";
|
||||
import type { LegacyAppPathString } from "../LegacyAppPathString";
|
||||
import type { MessagePhase } from "../MessagePhase";
|
||||
import type { ReasoningEffort } from "../ReasoningEffort";
|
||||
import type { WebSearchItem } from "../WebSearchItem";
|
||||
import type { JsonValue } from "../serde_json/JsonValue";
|
||||
import type { CollabAgentState } from "./CollabAgentState";
|
||||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
@@ -24,7 +25,6 @@ import type { MemoryCitation } from "./MemoryCitation";
|
||||
import type { PatchApplyStatus } from "./PatchApplyStatus";
|
||||
import type { SubAgentActivityKind } from "./SubAgentActivityKind";
|
||||
import type { UserInput } from "./UserInput";
|
||||
import type { WebSearchAction } from "./WebSearchAction";
|
||||
|
||||
export type ThreadItem = { "type": "userMessage", id: string, clientId: string | null, content: Array<UserInput>, } | { "type": "hookPrompt", id: string, fragments: Array<HookPromptFragment>, } | { "type": "agentMessage", id: string, text: string, phase: MessagePhase | null, memoryCitation: MemoryCitation | null, } | { "type": "plan", id: string, text: string, } | { "type": "reasoning", id: string, summary: Array<string>, content: Array<string>, } | { "type": "commandExecution", id: string,
|
||||
/**
|
||||
@@ -105,4 +105,4 @@ reasoningEffort: ReasoningEffort | null,
|
||||
/**
|
||||
* Last known status of the target agents, when available.
|
||||
*/
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch", id: string, query: string, action: WebSearchAction | null, } | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration", id: string, status: string, revisedPrompt: string | null, result: string, savedPath?: AbsolutePathBuf, } | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, };
|
||||
|
||||
@@ -194,6 +194,7 @@ export type { ListMcpServerStatusParams } from "./ListMcpServerStatusParams";
|
||||
export type { ListMcpServerStatusResponse } from "./ListMcpServerStatusResponse";
|
||||
export type { LoginAccountParams } from "./LoginAccountParams";
|
||||
export type { LoginAccountResponse } from "./LoginAccountResponse";
|
||||
export type { LoginAppBrand } from "./LoginAppBrand";
|
||||
export type { LogoutAccountResponse } from "./LogoutAccountResponse";
|
||||
export type { ManagedHooksRequirements } from "./ManagedHooksRequirements";
|
||||
export type { MarketplaceAddParams } from "./MarketplaceAddParams";
|
||||
|
||||
@@ -31,6 +31,11 @@ pub enum AuthMode {
|
||||
#[ts(rename = "chatgptAuthTokens")]
|
||||
#[strum(serialize = "chatgptAuthTokens")]
|
||||
ChatgptAuthTokens,
|
||||
/// Backend auth supplied as request headers.
|
||||
#[serde(rename = "headers")]
|
||||
#[ts(rename = "headers")]
|
||||
#[strum(serialize = "headers")]
|
||||
Headers,
|
||||
/// Programmatic Codex auth backed by a registered Agent Identity.
|
||||
#[serde(rename = "agentIdentity")]
|
||||
#[ts(rename = "agentIdentity")]
|
||||
@@ -53,7 +58,7 @@ impl AuthMode {
|
||||
pub fn has_chatgpt_account(self) -> bool {
|
||||
match self {
|
||||
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
Self::ApiKey | Self::Headers | Self::AgentIdentity | Self::BedrockApiKey => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +67,7 @@ impl AuthMode {
|
||||
match self {
|
||||
Self::Chatgpt
|
||||
| Self::ChatgptAuthTokens
|
||||
| Self::Headers
|
||||
| Self::AgentIdentity
|
||||
| Self::PersonalAccessToken => true,
|
||||
Self::ApiKey | Self::BedrockApiKey => false,
|
||||
@@ -2721,7 +2727,9 @@ mod tests {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
app_brand: None,
|
||||
codex_streamlined_login: false,
|
||||
use_hosted_login_success_page: false,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -2729,7 +2737,8 @@ mod tests {
|
||||
"method": "account/login/start",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt"
|
||||
"type": "chatgpt",
|
||||
"appBrand": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
@@ -2742,7 +2751,9 @@ mod tests {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
app_brand: None,
|
||||
codex_streamlined_login: true,
|
||||
use_hosted_login_success_page: false,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -2751,6 +2762,7 @@ mod tests {
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt",
|
||||
"appBrand": null,
|
||||
"codexStreamlinedLogin": true
|
||||
}
|
||||
}),
|
||||
@@ -2759,6 +2771,32 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_account_login_chatgpt_with_hosted_success_page() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: RequestId::Integer(3),
|
||||
params: v2::LoginAccountParams::Chatgpt {
|
||||
app_brand: Some(v2::LoginAppBrand::Chatgpt),
|
||||
codex_streamlined_login: true,
|
||||
use_hosted_login_success_page: true,
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
json!({
|
||||
"method": "account/login/start",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"type": "chatgpt",
|
||||
"appBrand": "chatgpt",
|
||||
"codexStreamlinedLogin": true,
|
||||
"useHostedLoginSuccessPage": true
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&request)?,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_account_login_chatgpt_device_code() -> Result<()> {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
|
||||
@@ -33,6 +33,9 @@ use codex_protocol::protocol::GuardianAssessmentAction;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
|
||||
use codex_protocol::review_format::render_review_output_text;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
@@ -41,6 +44,12 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
|
||||
pub(crate) fn review_output_text(output: Option<&ReviewOutputEvent>) -> String {
|
||||
output
|
||||
.map(render_review_output_text)
|
||||
.unwrap_or_else(|| REVIEW_FALLBACK_MESSAGE.to_string())
|
||||
}
|
||||
|
||||
pub fn build_file_change_approval_request_item(
|
||||
payload: &ApplyPatchApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::protocol::item_builders::build_file_change_approval_request_item;
|
||||
use crate::protocol::item_builders::build_file_change_begin_item;
|
||||
use crate::protocol::item_builders::build_file_change_end_item;
|
||||
use crate::protocol::item_builders::build_item_from_guardian_event;
|
||||
use crate::protocol::item_builders::review_output_text;
|
||||
use crate::protocol::v2::CollabAgentState;
|
||||
use crate::protocol::v2::CollabAgentTool;
|
||||
use crate::protocol::v2::CollabAgentToolCallStatus;
|
||||
@@ -21,7 +22,11 @@ use crate::protocol::v2::TurnError;
|
||||
use crate::protocol::v2::TurnItemsView;
|
||||
use crate::protocol::v2::TurnStatus;
|
||||
use crate::protocol::v2::UserInput;
|
||||
#[cfg(test)]
|
||||
use crate::protocol::v2::WebSearchAction;
|
||||
use crate::protocol::v2::WebSearchItem;
|
||||
use crate::protocol::v2::web_search_action_from_core;
|
||||
use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::protocol::AgentReasoningEvent;
|
||||
@@ -45,7 +50,6 @@ use codex_protocol::protocol::McpToolCallBeginEvent;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::PatchApplyEndEvent;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -55,6 +59,8 @@ use codex_protocol::protocol::UserMessageEvent;
|
||||
use codex_protocol::protocol::ViewImageToolCallEvent;
|
||||
use codex_protocol::protocol::WebSearchBeginEvent;
|
||||
use codex_protocol::protocol::WebSearchEndEvent;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::review_format::REVIEW_FALLBACK_MESSAGE;
|
||||
use std::collections::HashMap;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
@@ -586,15 +592,23 @@ impl ThreadHistoryBuilder {
|
||||
turn_id: &str,
|
||||
item: &codex_protocol::items::TurnItem,
|
||||
) {
|
||||
let is_review_mode_item = matches!(
|
||||
item,
|
||||
codex_protocol::items::TurnItem::EnteredReviewMode(_)
|
||||
| codex_protocol::items::TurnItem::ExitedReviewMode(_)
|
||||
);
|
||||
let should_upsert = match item {
|
||||
codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(),
|
||||
codex_protocol::items::TurnItem::Sleep(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::CommandExecution(_)
|
||||
| codex_protocol::items::TurnItem::DynamicToolCall(_)
|
||||
| codex_protocol::items::TurnItem::CollabAgentToolCall(_)
|
||||
| codex_protocol::items::TurnItem::SubAgentActivity(_) => true,
|
||||
| codex_protocol::items::TurnItem::SubAgentActivity(_)
|
||||
| codex_protocol::items::TurnItem::Extension(_)
|
||||
| codex_protocol::items::TurnItem::EnteredReviewMode(_)
|
||||
| codex_protocol::items::TurnItem::ExitedReviewMode(_) => true,
|
||||
codex_protocol::items::TurnItem::UserMessage(_)
|
||||
| codex_protocol::items::TurnItem::HookPrompt(_)
|
||||
| codex_protocol::items::TurnItem::AgentMessage(_)
|
||||
| codex_protocol::items::TurnItem::Reasoning(_)
|
||||
| codex_protocol::items::TurnItem::WebSearch(_)
|
||||
@@ -606,25 +620,30 @@ impl ThreadHistoryBuilder {
|
||||
};
|
||||
|
||||
if should_upsert {
|
||||
self.upsert_item_in_turn_id(turn_id, ThreadItem::from(item.clone()));
|
||||
let item = ThreadItem::from(item.clone());
|
||||
if is_review_mode_item {
|
||||
self.upsert_review_mode_item(Some(turn_id), item);
|
||||
} else {
|
||||
self.upsert_item_in_turn_id(turn_id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_web_search_begin(&mut self, payload: &WebSearchBeginEvent) {
|
||||
let item = ThreadItem::WebSearch {
|
||||
let item = ThreadItem::WebSearch(WebSearchItem {
|
||||
id: payload.call_id.clone(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
};
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_web_search_end(&mut self, payload: &WebSearchEndEvent) {
|
||||
let item = ThreadItem::WebSearch {
|
||||
let item = ThreadItem::WebSearch(WebSearchItem {
|
||||
id: payload.call_id.clone(),
|
||||
query: payload.query.clone(),
|
||||
action: Some(WebSearchAction::from(payload.action.clone())),
|
||||
};
|
||||
action: Some(web_search_action_from_core(payload.action.clone())),
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
@@ -827,24 +846,24 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
|
||||
fn handle_image_generation_begin(&mut self, payload: &ImageGenerationBeginEvent) {
|
||||
let item = ThreadItem::ImageGeneration {
|
||||
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: payload.call_id.clone(),
|
||||
status: String::new(),
|
||||
revised_prompt: None,
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
};
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
fn handle_image_generation_end(&mut self, payload: &ImageGenerationEndEvent) {
|
||||
let item = ThreadItem::ImageGeneration {
|
||||
let item = ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: payload.call_id.clone(),
|
||||
status: payload.status.clone(),
|
||||
revised_prompt: payload.revised_prompt.clone(),
|
||||
result: payload.result.clone(),
|
||||
saved_path: payload.saved_path.clone(),
|
||||
};
|
||||
});
|
||||
self.upsert_item_in_current_turn(item);
|
||||
}
|
||||
|
||||
@@ -1103,26 +1122,55 @@ impl ThreadHistoryBuilder {
|
||||
self.push_item_in_current_turn(ThreadItem::ContextCompaction { id });
|
||||
}
|
||||
|
||||
fn handle_entered_review_mode(&mut self, payload: &codex_protocol::protocol::ReviewRequest) {
|
||||
fn handle_entered_review_mode(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::EnteredReviewModeEvent,
|
||||
) {
|
||||
let review = payload
|
||||
.user_facing_hint
|
||||
.clone()
|
||||
.unwrap_or_else(|| "Review requested.".to_string());
|
||||
let id = self.next_item_id();
|
||||
self.push_item_in_current_turn(ThreadItem::EnteredReviewMode { id, review });
|
||||
let id = payload
|
||||
.item_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.next_item_id());
|
||||
self.upsert_review_mode_item(
|
||||
payload.turn_id.as_deref(),
|
||||
ThreadItem::EnteredReviewMode { id, review },
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_exited_review_mode(
|
||||
&mut self,
|
||||
payload: &codex_protocol::protocol::ExitedReviewModeEvent,
|
||||
) {
|
||||
let review = payload
|
||||
.review_output
|
||||
let review = review_output_text(payload.review_output.as_ref());
|
||||
let id = payload
|
||||
.item_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.next_item_id());
|
||||
self.upsert_review_mode_item(
|
||||
payload.turn_id.as_deref(),
|
||||
ThreadItem::ExitedReviewMode { id, review },
|
||||
);
|
||||
}
|
||||
|
||||
fn upsert_review_mode_item(&mut self, turn_id: Option<&str>, item: ThreadItem) {
|
||||
let Some(turn_id) = turn_id else {
|
||||
self.upsert_item_in_current_turn(item);
|
||||
return;
|
||||
};
|
||||
let current_turn_matches = self
|
||||
.current_turn
|
||||
.as_ref()
|
||||
.map(render_review_output_text)
|
||||
.unwrap_or_else(|| REVIEW_FALLBACK_MESSAGE.to_string());
|
||||
let id = self.next_item_id();
|
||||
self.push_item_in_current_turn(ThreadItem::ExitedReviewMode { id, review });
|
||||
.is_some_and(|turn| turn.id == turn_id);
|
||||
if !current_turn_matches && !self.turns.iter().any(|turn| turn.id == turn_id) {
|
||||
self.finish_current_turn();
|
||||
let turn = self.new_turn(Some(turn_id.to_string()));
|
||||
self.record_changed_pending_turn(&turn);
|
||||
self.current_turn = Some(turn);
|
||||
}
|
||||
self.upsert_item_in_turn_id(turn_id, item);
|
||||
}
|
||||
|
||||
fn handle_error(&mut self, payload: &ErrorEvent) {
|
||||
@@ -1438,17 +1486,6 @@ impl ThreadHistoryBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
|
||||
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
let explanation = output.overall_explanation.trim();
|
||||
if explanation.is_empty() {
|
||||
REVIEW_FALLBACK_MESSAGE.to_string()
|
||||
} else {
|
||||
explanation.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_dynamic_tool_content_items(
|
||||
items: &[codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem],
|
||||
) -> Vec<DynamicToolCallOutputContentItem> {
|
||||
@@ -1548,10 +1585,13 @@ impl From<&PendingTurn> for Turn {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_extension_items::ExtensionItem as CoreExtensionItem;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::items::CommandExecutionItem as CoreCommandExecutionItem;
|
||||
use codex_protocol::items::CommandExecutionStatus as CoreCommandExecutionStatus;
|
||||
use codex_protocol::items::EnteredReviewModeItem as CoreEnteredReviewModeItem;
|
||||
use codex_protocol::items::ExitedReviewModeItem as CoreExitedReviewModeItem;
|
||||
use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment;
|
||||
use codex_protocol::items::SleepItem as CoreSleepItem;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
@@ -1569,12 +1609,15 @@ mod tests {
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
use codex_protocol::protocol::CompactedItem;
|
||||
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
||||
use codex_protocol::protocol::EnteredReviewModeEvent;
|
||||
use codex_protocol::protocol::ExecCommandEndEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use codex_protocol::protocol::ExitedReviewModeEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::McpToolCallEndEvent;
|
||||
use codex_protocol::protocol::PatchApplyBeginEvent;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -1699,6 +1742,111 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_mode_events_replay_persisted_ids() {
|
||||
let events = vec![
|
||||
EventMsg::EnteredReviewMode(EnteredReviewModeEvent {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "review this".into(),
|
||||
},
|
||||
user_facing_hint: Some("Review requested.".into()),
|
||||
turn_id: Some("turn-1".into()),
|
||||
item_id: Some("entered-review".into()),
|
||||
}),
|
||||
EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
|
||||
turn_id: Some("turn-1".into()),
|
||||
item_id: Some("exited-review".into()),
|
||||
review_output: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
let turns = builder.finish();
|
||||
|
||||
assert_eq!(turns[0].id, "turn-1");
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![
|
||||
ThreadItem::EnteredReviewMode {
|
||||
id: "entered-review".into(),
|
||||
review: "Review requested.".into(),
|
||||
},
|
||||
ThreadItem::ExitedReviewMode {
|
||||
id: "exited-review".into(),
|
||||
review: REVIEW_FALLBACK_MESSAGE.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_mode_items_replay_without_turn_started() {
|
||||
let thread_id = ThreadId::new();
|
||||
let entered = CoreTurnItem::EnteredReviewMode(CoreEnteredReviewModeItem {
|
||||
id: "entered-review".into(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "review this".into(),
|
||||
},
|
||||
user_facing_hint: "Review requested.".into(),
|
||||
});
|
||||
let exited = CoreTurnItem::ExitedReviewMode(CoreExitedReviewModeItem {
|
||||
id: "exited-review".into(),
|
||||
review_output: None,
|
||||
});
|
||||
let events = vec![
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id,
|
||||
turn_id: "turn-1".into(),
|
||||
item: entered,
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id,
|
||||
turn_id: "turn-1".into(),
|
||||
item: exited,
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: "turn-1".into(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
];
|
||||
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
for event in &events {
|
||||
builder.handle_event(event);
|
||||
}
|
||||
let turns = builder.finish();
|
||||
|
||||
assert_eq!(turns[0].id, "turn-1");
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![
|
||||
ThreadItem::EnteredReviewMode {
|
||||
id: "entered-review".into(),
|
||||
review: "Review requested.".into(),
|
||||
},
|
||||
ThreadItem::ExitedReviewMode {
|
||||
id: "exited-review".into(),
|
||||
review: REVIEW_FALLBACK_MESSAGE.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_user_message_image_details_from_legacy_events() {
|
||||
let local_path = PathBuf::from("/tmp/local.png");
|
||||
@@ -1846,6 +1994,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_extension_image_generation_item_from_persisted_completion() {
|
||||
let turn_id = "turn-1";
|
||||
let thread_id = ThreadId::new();
|
||||
let saved_path = test_path_buf("/tmp/image-1.png").abs();
|
||||
let events = vec![
|
||||
EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}),
|
||||
EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id,
|
||||
turn_id: turn_id.to_string(),
|
||||
item: CoreTurnItem::Extension(CoreExtensionItem::ImageGeneration(
|
||||
ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("A blue square".to_string()),
|
||||
result: "cG5n".to_string(),
|
||||
saved_path: Some(saved_path.clone()),
|
||||
},
|
||||
)),
|
||||
completed_at_ms: 1_000,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: turn_id.to_string(),
|
||||
last_agent_message: None,
|
||||
completed_at: None,
|
||||
duration_ms: None,
|
||||
time_to_first_token_ms: None,
|
||||
}),
|
||||
];
|
||||
let items = events
|
||||
.into_iter()
|
||||
.map(RolloutItem::EventMsg)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let turns = build_turns_from_rollout_items(&items);
|
||||
|
||||
assert_eq!(
|
||||
turns[0].items,
|
||||
vec![ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("A blue square".to_string()),
|
||||
result: "cG5n".to_string(),
|
||||
saved_path: Some(saved_path),
|
||||
})]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuilds_command_execution_item_from_persisted_completion() {
|
||||
let turn_id = "turn-1";
|
||||
@@ -2058,13 +2260,13 @@ mod tests {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "ig_123".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("final prompt".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
);
|
||||
@@ -2492,14 +2694,14 @@ mod tests {
|
||||
assert_eq!(turns[0].items.len(), 4);
|
||||
assert_eq!(
|
||||
turns[0].items[1],
|
||||
ThreadItem::WebSearch {
|
||||
ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "search-1".into(),
|
||||
query: "codex".into(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("codex".into()),
|
||||
queries: None,
|
||||
}),
|
||||
}
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
turns[0].items[2],
|
||||
@@ -3825,6 +4027,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_hook_prompt_completion_updates_turn_history() {
|
||||
let hook_prompt = CoreTurnItem::HookPrompt(codex_protocol::items::HookPromptItem {
|
||||
id: "hook-prompt-1".into(),
|
||||
fragments: vec![CoreHookPromptFragment::from_single_hook(
|
||||
"Retry with tests.",
|
||||
"hook-run-1",
|
||||
)],
|
||||
});
|
||||
let expected_item = ThreadItem::from(hook_prompt.clone());
|
||||
let mut builder = ThreadHistoryBuilder::new();
|
||||
builder.handle_event(&EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_id: "turn-a".into(),
|
||||
trace_id: None,
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
}));
|
||||
builder.handle_event(&EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: ThreadId::new(),
|
||||
turn_id: "turn-a".into(),
|
||||
item: hook_prompt,
|
||||
completed_at_ms: 0,
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
builder.active_turn_snapshot().expect("active turn").items,
|
||||
vec![expected_item]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_plain_user_response_items_in_rollout_replay() {
|
||||
let items = vec![
|
||||
@@ -3872,7 +4105,6 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
@@ -3919,20 +4151,19 @@ mod tests {
|
||||
},
|
||||
}),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
changed_items: vec![ThreadHistoryItemChange {
|
||||
turn_id: "rollout-0".into(),
|
||||
item: ThreadItem::WebSearch {
|
||||
item: ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "search-1".into(),
|
||||
query: "codex".into(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("codex".into()),
|
||||
queries: None,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}],
|
||||
changed_turns: Vec::new(),
|
||||
removed_turn_ids: Vec::new(),
|
||||
@@ -3954,7 +4185,6 @@ mod tests {
|
||||
text: "raw content".into(),
|
||||
}),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
@@ -4054,20 +4284,19 @@ mod tests {
|
||||
},
|
||||
})),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
changes,
|
||||
ThreadHistoryChangeSet {
|
||||
changed_items: vec![ThreadHistoryItemChange {
|
||||
turn_id: "rollout-0".into(),
|
||||
item: ThreadItem::WebSearch {
|
||||
item: ThreadItem::WebSearch(WebSearchItem {
|
||||
id: "search-1".into(),
|
||||
query: "codex".into(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("codex".into()),
|
||||
queries: None,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}],
|
||||
changed_turns: vec![ThreadHistoryTurnChange {
|
||||
turn_id: "rollout-0".into(),
|
||||
|
||||
@@ -78,6 +78,11 @@ pub enum LoginAccountParams {
|
||||
Chatgpt {
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
codex_streamlined_login: bool,
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
use_hosted_login_success_page: bool,
|
||||
#[serde(default)]
|
||||
#[ts(optional = nullable)]
|
||||
app_brand: Option<LoginAppBrand>,
|
||||
},
|
||||
#[serde(rename = "chatgptDeviceCode")]
|
||||
#[ts(rename = "chatgptDeviceCode")]
|
||||
@@ -102,6 +107,16 @@ pub enum LoginAccountParams {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(rename_all = "lowercase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum LoginAppBrand {
|
||||
#[default]
|
||||
Codex,
|
||||
Chatgpt,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
|
||||
@@ -10,7 +10,12 @@ use super::UserInput;
|
||||
use super::shared::v2_enum_from_core;
|
||||
use crate::protocol::item_builders::command_actions_for_path_uri;
|
||||
use crate::protocol::item_builders::convert_patch_changes;
|
||||
use crate::protocol::item_builders::review_output_text;
|
||||
use codex_experimental_api_macros::ExperimentalApi;
|
||||
use codex_extension_items::ExtensionItem;
|
||||
pub use codex_extension_items::image_generation::ImageGenerationItem;
|
||||
pub use codex_extension_items::web_search::WebSearchAction;
|
||||
pub use codex_extension_items::web_search::WebSearchItem;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianAssessmentDecisionSource as CoreGuardianAssessmentDecisionSource;
|
||||
use codex_protocol::approvals::GuardianCommandSource as CoreGuardianCommandSource;
|
||||
@@ -246,7 +251,10 @@ pub enum ThreadItem {
|
||||
#[ts(rename_all = "camelCase")]
|
||||
/// EXPERIMENTAL - proposed plan item content. The completed plan item is
|
||||
/// authoritative and may not match the concatenation of `PlanDelta` text.
|
||||
Plan { id: String, text: String },
|
||||
Plan {
|
||||
id: String,
|
||||
text: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Reasoning {
|
||||
@@ -353,13 +361,7 @@ pub enum ThreadItem {
|
||||
agent_thread_id: String,
|
||||
agent_path: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
WebSearch {
|
||||
id: String,
|
||||
query: String,
|
||||
action: Option<WebSearchAction>,
|
||||
},
|
||||
WebSearch(WebSearchItem),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ImageView {
|
||||
@@ -373,26 +375,24 @@ pub enum ThreadItem {
|
||||
#[ts(type = "number")]
|
||||
duration_ms: u64,
|
||||
},
|
||||
ImageGeneration(ImageGenerationItem),
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ImageGeneration {
|
||||
EnteredReviewMode {
|
||||
id: String,
|
||||
status: String,
|
||||
revised_prompt: Option<String>,
|
||||
result: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
review: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
EnteredReviewMode { id: String, review: String },
|
||||
ExitedReviewMode {
|
||||
id: String,
|
||||
review: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ExitedReviewMode { id: String, review: String },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
ContextCompaction { id: String },
|
||||
ContextCompaction {
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
@@ -429,13 +429,13 @@ impl ThreadItem {
|
||||
| ThreadItem::DynamicToolCall { id, .. }
|
||||
| ThreadItem::CollabAgentToolCall { id, .. }
|
||||
| ThreadItem::SubAgentActivity { id, .. }
|
||||
| ThreadItem::WebSearch { id, .. }
|
||||
| ThreadItem::ImageView { id, .. }
|
||||
| ThreadItem::Sleep { id, .. }
|
||||
| ThreadItem::ImageGeneration { id, .. }
|
||||
| ThreadItem::EnteredReviewMode { id, .. }
|
||||
| ThreadItem::ExitedReviewMode { id, .. }
|
||||
| ThreadItem::ContextCompaction { id, .. } => id,
|
||||
ThreadItem::WebSearch(item) => &item.id,
|
||||
ThreadItem::ImageGeneration(item) => &item.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -783,40 +783,20 @@ impl TryFrom<GuardianApprovalReviewAction> for CoreGuardianAssessmentAction {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum WebSearchAction {
|
||||
Search {
|
||||
query: Option<String>,
|
||||
queries: Option<Vec<String>>,
|
||||
},
|
||||
OpenPage {
|
||||
url: Option<String>,
|
||||
},
|
||||
FindInPage {
|
||||
url: Option<String>,
|
||||
pattern: Option<String>,
|
||||
},
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<codex_protocol::models::WebSearchAction> for WebSearchAction {
|
||||
fn from(value: codex_protocol::models::WebSearchAction) -> Self {
|
||||
match value {
|
||||
codex_protocol::models::WebSearchAction::Search { query, queries } => {
|
||||
WebSearchAction::Search { query, queries }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::OpenPage { url } => {
|
||||
WebSearchAction::OpenPage { url }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => {
|
||||
WebSearchAction::FindInPage { url, pattern }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other,
|
||||
pub(crate) fn web_search_action_from_core(
|
||||
value: codex_protocol::models::WebSearchAction,
|
||||
) -> WebSearchAction {
|
||||
match value {
|
||||
codex_protocol::models::WebSearchAction::Search { query, queries } => {
|
||||
WebSearchAction::Search { query, queries }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::OpenPage { url } => {
|
||||
WebSearchAction::OpenPage { url }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::FindInPage { url, pattern } => {
|
||||
WebSearchAction::FindInPage { url, pattern }
|
||||
}
|
||||
codex_protocol::models::WebSearchAction::Other => WebSearchAction::Other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,11 +898,11 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
agent_thread_id: activity.agent_thread_id.to_string(),
|
||||
agent_path: String::from(activity.agent_path),
|
||||
},
|
||||
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch {
|
||||
CoreTurnItem::WebSearch(search) => ThreadItem::WebSearch(WebSearchItem {
|
||||
id: search.id,
|
||||
query: search.query,
|
||||
action: Some(WebSearchAction::from(search.action)),
|
||||
},
|
||||
action: Some(web_search_action_from_core(search.action)),
|
||||
}),
|
||||
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
|
||||
id: image.id,
|
||||
path: image.path.into(),
|
||||
@@ -931,12 +911,26 @@ impl From<CoreTurnItem> for ThreadItem {
|
||||
id: sleep.id,
|
||||
duration_ms: sleep.duration_ms,
|
||||
},
|
||||
CoreTurnItem::ImageGeneration(image) => ThreadItem::ImageGeneration {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
CoreTurnItem::Extension(extension) => match extension {
|
||||
ExtensionItem::ImageGeneration(item) => ThreadItem::ImageGeneration(item),
|
||||
ExtensionItem::WebSearch(item) => ThreadItem::WebSearch(item),
|
||||
},
|
||||
CoreTurnItem::ImageGeneration(image) => {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: image.id,
|
||||
status: image.status,
|
||||
revised_prompt: image.revised_prompt,
|
||||
result: image.result,
|
||||
saved_path: image.saved_path,
|
||||
})
|
||||
}
|
||||
CoreTurnItem::EnteredReviewMode(review) => ThreadItem::EnteredReviewMode {
|
||||
id: review.id,
|
||||
review: review.user_facing_hint,
|
||||
},
|
||||
CoreTurnItem::ExitedReviewMode(review) => ThreadItem::ExitedReviewMode {
|
||||
id: review.id,
|
||||
review: review_output_text(review.review_output.as_ref()),
|
||||
},
|
||||
CoreTurnItem::FileChange(file_change) => ThreadItem::FileChange {
|
||||
id: file_change.id,
|
||||
|
||||
@@ -19,7 +19,7 @@ use codex_protocol::items::ReasoningItem;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::items::WebSearchItem as CoreWebSearchItem;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::mcp::McpServerInfo;
|
||||
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
|
||||
@@ -2770,7 +2770,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
}
|
||||
);
|
||||
|
||||
let search_item = TurnItem::WebSearch(WebSearchItem {
|
||||
let search_item = TurnItem::WebSearch(CoreWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: "docs".to_string(),
|
||||
action: CoreWebSearchAction::Search {
|
||||
@@ -2779,16 +2779,24 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
|
||||
},
|
||||
});
|
||||
|
||||
let expected_search_item = WebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: "docs".to_string(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("docs".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ThreadItem::from(search_item),
|
||||
ThreadItem::WebSearch {
|
||||
id: "search-1".to_string(),
|
||||
query: "docs".to_string(),
|
||||
action: Some(WebSearchAction::Search {
|
||||
query: Some("docs".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
}
|
||||
ThreadItem::WebSearch(expected_search_item.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
ThreadItem::from(TurnItem::Extension(
|
||||
codex_extension_items::ExtensionItem::WebSearch(expected_search_item.clone()),
|
||||
)),
|
||||
ThreadItem::WebSearch(expected_search_item)
|
||||
);
|
||||
|
||||
let image_view_item = TurnItem::ImageView(ImageViewItem {
|
||||
|
||||
@@ -1722,7 +1722,9 @@ impl CodexClient {
|
||||
let request = ClientRequest::LoginAccount {
|
||||
request_id: request_id.clone(),
|
||||
params: codex_app_server_protocol::LoginAccountParams::Chatgpt {
|
||||
app_brand: None,
|
||||
codex_streamlined_login: false,
|
||||
use_hosted_login_success_page: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ codex_rust_crate(
|
||||
crate_name = "codex_app_server",
|
||||
extra_binaries = [
|
||||
"//codex-rs/bwrap:bwrap",
|
||||
"//codex-rs/code-mode-host:codex-code-mode-host",
|
||||
],
|
||||
extra_binaries_non_windows = [
|
||||
"//codex-rs/cli:codex",
|
||||
|
||||
@@ -1995,6 +1995,11 @@ Field notes:
|
||||
{ "id": 3, "result": { "type": "chatgpt", "loginId": "<uuid>", "authUrl": "https://chatgpt.com/…&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fauth%2Fcallback" } }
|
||||
```
|
||||
2. Open `authUrl` in a browser; the app-server hosts the local callback.
|
||||
By default, a successful callback redirects to the local success page. Clients may set
|
||||
`useHostedLoginSuccessPage: true` to redirect successful callbacks that do not require
|
||||
organization setup to the hosted Codex success page instead. When hosted login success is
|
||||
enabled, clients may set `appBrand` to `"codex"` or `"chatgpt"` to select the matching hosted
|
||||
page artwork; omitted or `null` values default to `"codex"`.
|
||||
3. Wait for notifications:
|
||||
```json
|
||||
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } }
|
||||
|
||||
@@ -12,6 +12,7 @@ pub(crate) fn auth_mode_to_api(auth_mode: AuthMode) -> ApiAuthMode {
|
||||
AuthMode::ApiKey => ApiAuthMode::ApiKey,
|
||||
AuthMode::Chatgpt => ApiAuthMode::Chatgpt,
|
||||
AuthMode::ChatgptAuthTokens => ApiAuthMode::ChatgptAuthTokens,
|
||||
AuthMode::Headers => ApiAuthMode::Headers,
|
||||
AuthMode::AgentIdentity => ApiAuthMode::AgentIdentity,
|
||||
AuthMode::PersonalAccessToken => ApiAuthMode::PersonalAccessToken,
|
||||
AuthMode::BedrockApiKey => ApiAuthMode::BedrockApiKey,
|
||||
|
||||
@@ -86,11 +86,9 @@ use codex_app_server_protocol::guardian_auto_approval_review_notification;
|
||||
use codex_app_server_protocol::item_event_to_server_notification;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::review_format::format_review_findings_block;
|
||||
use codex_core::review_prompts;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::CollabAgentTool as CoreCollabAgentTool;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::parse_hook_prompt_message;
|
||||
use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
@@ -100,7 +98,6 @@ use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RealtimeEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::protocol::SubAgentActivityKind;
|
||||
use codex_protocol::protocol::TokenCountEvent;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
@@ -822,25 +819,34 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
on_request_permissions_response(pending_response, conversation, thread_state).await;
|
||||
});
|
||||
}
|
||||
EventMsg::DynamicToolCallRequest(_) | EventMsg::DynamicToolCallResponse(_) => {
|
||||
// Deprecated dynamic-tool events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers. App-server v2 receives the canonical DynamicToolCall
|
||||
// item lifecycle and dispatches client requests from canonical starts instead.
|
||||
}
|
||||
EventMsg::McpToolCallBegin(_) | EventMsg::McpToolCallEnd(_) => {
|
||||
// Deprecated MCP tool-call events are still fanned out for legacy clients.
|
||||
// App-server v2 receives the canonical TurnItem::McpToolCall lifecycle instead.
|
||||
}
|
||||
msg @ (EventMsg::CollabAgentSpawnBegin(_)
|
||||
EventMsg::DynamicToolCallRequest(_)
|
||||
| EventMsg::DynamicToolCallResponse(_)
|
||||
| EventMsg::CollabAgentSpawnBegin(_)
|
||||
| EventMsg::CollabAgentSpawnEnd(_)
|
||||
| EventMsg::CollabAgentInteractionBegin(_)
|
||||
| EventMsg::CollabAgentInteractionEnd(_)
|
||||
| EventMsg::CollabWaitingBegin(_)
|
||||
| EventMsg::CollabWaitingEnd(_)
|
||||
| EventMsg::CollabCloseBegin(_)
|
||||
| EventMsg::CollabCloseEnd(_)
|
||||
| EventMsg::CollabResumeBegin(_)
|
||||
| EventMsg::CollabResumeEnd(_)
|
||||
| EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::SubAgentActivity(_)
|
||||
| EventMsg::ExecCommandBegin(_)
|
||||
| EventMsg::ExecCommandEnd(_)
|
||||
| EventMsg::EnteredReviewMode(_)
|
||||
| EventMsg::ExitedReviewMode(_) => {
|
||||
// Deprecated item lifecycle events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers.
|
||||
// App-server v2 receives TurnItem lifecycle instead, and dispatches dynamic tool
|
||||
// requests from DynamicToolCall starts.
|
||||
}
|
||||
EventMsg::McpToolCallBegin(_) | EventMsg::McpToolCallEnd(_) => {
|
||||
// Deprecated MCP tool-call events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers.
|
||||
// App-server v2 receives the canonical TurnItem::McpToolCall lifecycle instead.
|
||||
}
|
||||
msg @ (EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::PlanDelta(_)
|
||||
| EventMsg::ReasoningContentDelta(_)
|
||||
| EventMsg::ReasoningRawContentDelta(_)
|
||||
@@ -852,43 +858,9 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::SubAgentActivity(activity) => {
|
||||
if activity.kind == SubAgentActivityKind::Interrupted
|
||||
&& thread_manager
|
||||
.get_thread(activity.agent_thread_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
thread_watch_manager
|
||||
.remove_thread(&activity.agent_thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::SubAgentActivity(activity),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::CollabCloseEnd(end_event) => {
|
||||
if thread_manager
|
||||
.get_thread(end_event.receiver_thread_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
thread_watch_manager
|
||||
.remove_thread(&end_event.receiver_thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::CollabCloseEnd(end_event),
|
||||
&conversation_id.to_string(),
|
||||
&event_turn_id,
|
||||
);
|
||||
outgoing.send_server_notification(notification).await;
|
||||
}
|
||||
EventMsg::ContextCompacted(..) => {
|
||||
// Core still fans out this deprecated event for legacy clients;
|
||||
// Core still fans out this deprecated event for raw-event and rollout compatibility
|
||||
// consumers;
|
||||
// v2 clients receive the canonical ContextCompaction item instead.
|
||||
}
|
||||
EventMsg::DeprecationNotice(event) => {
|
||||
@@ -971,33 +943,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::ViewImageToolCall(_) => {}
|
||||
EventMsg::EnteredReviewMode(review_request) => {
|
||||
let review = review_request
|
||||
.user_facing_hint
|
||||
.unwrap_or_else(|| review_prompts::user_facing_hint(&review_request.target));
|
||||
let item = ThreadItem::EnteredReviewMode {
|
||||
id: event_turn_id.clone(),
|
||||
review,
|
||||
};
|
||||
let started = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
item: item.clone(),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(started))
|
||||
.await;
|
||||
let completed = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(completed))
|
||||
.await;
|
||||
}
|
||||
EventMsg::ItemStarted(event) => {
|
||||
let should_emit = match &event.item {
|
||||
// Approval and guardian flows can emit the command start notification before core
|
||||
@@ -1040,7 +985,13 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
}
|
||||
}
|
||||
EventMsg::ItemCompleted(event) => {
|
||||
apply_canonical_item_completed_side_effects(&thread_state, &event.item).await;
|
||||
apply_canonical_item_completed_side_effects(
|
||||
&thread_manager,
|
||||
&thread_watch_manager,
|
||||
&thread_state,
|
||||
&event.item,
|
||||
)
|
||||
.await;
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ItemCompleted(event),
|
||||
&conversation_id.to_string(),
|
||||
@@ -1076,42 +1027,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.send_server_notification(ServerNotification::HookCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
EventMsg::ExitedReviewMode(review_event) => {
|
||||
let review = match review_event.review_output {
|
||||
Some(output) => render_review_output_text(&output),
|
||||
None => REVIEW_FALLBACK_MESSAGE.to_string(),
|
||||
};
|
||||
let item = ThreadItem::ExitedReviewMode {
|
||||
id: event_turn_id.clone(),
|
||||
review,
|
||||
};
|
||||
let started = ItemStartedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
started_at_ms: now_unix_timestamp_ms(),
|
||||
item: item.clone(),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemStarted(started))
|
||||
.await;
|
||||
let completed = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item,
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(completed))
|
||||
.await;
|
||||
}
|
||||
EventMsg::RawResponseItem(raw_response_item_event) => {
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
&raw_response_item_event.item,
|
||||
&outgoing,
|
||||
)
|
||||
.await;
|
||||
maybe_emit_raw_response_item_completed(
|
||||
conversation_id,
|
||||
&event_turn_id,
|
||||
@@ -1121,14 +1037,10 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
.await;
|
||||
}
|
||||
EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyEnd(_) => {
|
||||
// Core still fans out these deprecated events for legacy clients;
|
||||
// Core still fans out these deprecated events for raw-event and rollout compatibility
|
||||
// consumers;
|
||||
// v2 clients receive the canonical FileChange item instead.
|
||||
}
|
||||
EventMsg::ExecCommandBegin(_) | EventMsg::ExecCommandEnd(_) => {
|
||||
// Deprecated command-execution events are still fanned out for raw-event and rollout
|
||||
// compatibility consumers. App-server v2 receives the canonical CommandExecution
|
||||
// item lifecycle instead.
|
||||
}
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
|
||||
let notification = item_event_to_server_notification(
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event),
|
||||
@@ -1341,16 +1253,48 @@ async fn emit_turn_completed_with_status(
|
||||
}
|
||||
|
||||
async fn apply_canonical_item_completed_side_effects(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
item: &CoreTurnItem,
|
||||
) {
|
||||
if let CoreTurnItem::CommandExecution(item) = item {
|
||||
thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.remove(&item.id);
|
||||
match item {
|
||||
CoreTurnItem::CommandExecution(item) => {
|
||||
thread_state
|
||||
.lock()
|
||||
.await
|
||||
.turn_summary
|
||||
.command_execution_started
|
||||
.remove(&item.id);
|
||||
}
|
||||
CoreTurnItem::SubAgentActivity(activity)
|
||||
if activity.kind == SubAgentActivityKind::Interrupted =>
|
||||
{
|
||||
remove_missing_thread_watch(
|
||||
thread_manager,
|
||||
thread_watch_manager,
|
||||
activity.agent_thread_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
CoreTurnItem::CollabAgentToolCall(item) if item.tool == CoreCollabAgentTool::CloseAgent => {
|
||||
for thread_id in &item.receiver_thread_ids {
|
||||
remove_missing_thread_watch(thread_manager, thread_watch_manager, *thread_id).await;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_missing_thread_watch(
|
||||
thread_manager: &Arc<ThreadManager>,
|
||||
thread_watch_manager: &ThreadWatchManager,
|
||||
thread_id: ThreadId,
|
||||
) {
|
||||
if thread_manager.get_thread(thread_id).await.is_err() {
|
||||
thread_watch_manager
|
||||
.remove_thread(&thread_id.to_string())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1461,45 +1405,6 @@ async fn maybe_emit_raw_response_item_completed(
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_emit_hook_prompt_item_completed(
|
||||
conversation_id: ThreadId,
|
||||
turn_id: &str,
|
||||
item: &codex_protocol::models::ResponseItem,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
) {
|
||||
let codex_protocol::models::ResponseItem::Message {
|
||||
role, content, id, ..
|
||||
} = item
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if role != "user" {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(hook_prompt) = parse_hook_prompt_message(id.as_ref(), content) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let notification = ItemCompletedNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
completed_at_ms: now_unix_timestamp_ms(),
|
||||
item: ThreadItem::HookPrompt {
|
||||
id: hook_prompt.id,
|
||||
fragments: hook_prompt
|
||||
.fragments
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::HookPromptFragment::from)
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::ItemCompleted(notification))
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn find_and_remove_turn_summary(
|
||||
_conversation_id: ThreadId,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
@@ -1953,28 +1858,6 @@ fn request_permissions_response_from_client_result(
|
||||
}))
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
|
||||
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
let mut sections = Vec::new();
|
||||
let explanation = output.overall_explanation.trim();
|
||||
if !explanation.is_empty() {
|
||||
sections.push(explanation.to_string());
|
||||
}
|
||||
if !output.findings.is_empty() {
|
||||
let findings = format_review_findings_block(&output.findings, /*selection*/ None);
|
||||
let trimmed = findings.trim();
|
||||
if !trimmed.is_empty() {
|
||||
sections.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
if sections.is_empty() {
|
||||
REVIEW_FALLBACK_MESSAGE.to_string()
|
||||
} else {
|
||||
sections.join("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
fn map_file_change_approval_decision(decision: FileChangeApprovalDecision) -> ReviewDecision {
|
||||
match decision {
|
||||
FileChangeApprovalDecision::Accept => ReviewDecision::Approved,
|
||||
@@ -2183,9 +2066,8 @@ mod tests {
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::items::DynamicToolCallItem;
|
||||
use codex_protocol::items::DynamicToolCallStatus as CoreDynamicToolCallStatus;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::SubAgentActivityItem;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -2201,12 +2083,12 @@ mod tests {
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::GuardianAssessmentEvent;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::ItemStartedEvent;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentActivityEvent;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_protocol::protocol::TokenUsageInfo;
|
||||
use codex_protocol::protocol::UserMessageEvent;
|
||||
@@ -3429,13 +3311,17 @@ mod tests {
|
||||
apply_bespoke_event_handling(
|
||||
Event {
|
||||
id: "turn-1".to_string(),
|
||||
msg: EventMsg::SubAgentActivity(SubAgentActivityEvent {
|
||||
event_id: "activity-1".to_string(),
|
||||
occurred_at_ms: 42,
|
||||
agent_thread_id: child_thread_id,
|
||||
agent_path: AgentPath::try_from("/root/worker")
|
||||
.expect("agent path should parse"),
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: conversation_id,
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: CoreTurnItem::SubAgentActivity(SubAgentActivityItem {
|
||||
id: "activity-1".to_string(),
|
||||
kind: SubAgentActivityKind::Interrupted,
|
||||
agent_thread_id: child_thread_id,
|
||||
agent_path: AgentPath::try_from("/root/worker")
|
||||
.expect("agent path should parse"),
|
||||
}),
|
||||
completed_at_ms: 42,
|
||||
}),
|
||||
},
|
||||
conversation_id,
|
||||
@@ -4070,55 +3956,4 @@ mod tests {
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_hook_prompt_raw_response_emits_item_completed() -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
|
||||
let outgoing = Arc::new(OutgoingMessageSender::new(
|
||||
tx,
|
||||
codex_analytics::AnalyticsEventsClient::disabled(),
|
||||
));
|
||||
let conversation_id = ThreadId::new();
|
||||
let outgoing = ThreadScopedOutgoingMessageSender::new(
|
||||
outgoing,
|
||||
vec![ConnectionId(1)],
|
||||
conversation_id,
|
||||
);
|
||||
let item = build_hook_prompt_message(&[
|
||||
HookPromptFragment::from_single_hook("Retry with tests.", "hook-run-1"),
|
||||
HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
|
||||
])
|
||||
.expect("hook prompt message");
|
||||
|
||||
maybe_emit_hook_prompt_item_completed(conversation_id, "turn-1", &item, &outgoing).await;
|
||||
|
||||
let msg = recv_broadcast_message(&mut rx).await?;
|
||||
match msg {
|
||||
OutgoingMessage::AppServerNotification(ServerNotification::ItemCompleted(
|
||||
notification,
|
||||
)) => {
|
||||
assert_eq!(notification.thread_id, conversation_id.to_string());
|
||||
assert_eq!(notification.turn_id, "turn-1");
|
||||
assert_eq!(
|
||||
notification.item,
|
||||
ThreadItem::HookPrompt {
|
||||
id: notification.item.id().to_string(),
|
||||
fragments: vec![
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Retry with tests.".into(),
|
||||
hook_run_id: "hook-run-1".into(),
|
||||
},
|
||||
codex_app_server_protocol::HookPromptFragment {
|
||||
text: "Then summarize cleanly.".into(),
|
||||
hook_run_id: "hook-run-2".into(),
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
other => bail!("unexpected message: {other:?}"),
|
||||
}
|
||||
assert!(rx.try_recv().is_err(), "no extra messages expected");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::ExternalAuthFuture;
|
||||
use codex_login::auth::ExternalAuth;
|
||||
use codex_login::auth::ExternalAuthRefreshContext;
|
||||
use codex_login::auth::ExternalAuthRefreshReason;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
|
||||
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub(crate) struct ExternalAuthBridge {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
auth: RwLock<CodexAuth>,
|
||||
}
|
||||
|
||||
impl ExternalAuthBridge {
|
||||
pub(crate) fn new(outgoing: Arc<OutgoingMessageSender>, auth: CodexAuth) -> Self {
|
||||
Self {
|
||||
outgoing,
|
||||
auth: RwLock::new(auth),
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh(&self, context: ExternalAuthRefreshContext) -> std::io::Result<CodexAuth> {
|
||||
let reason = match context.reason {
|
||||
ExternalAuthRefreshReason::Unauthorized => ChatgptAuthTokensRefreshReason::Unauthorized,
|
||||
};
|
||||
let params = ChatgptAuthTokensRefreshParams {
|
||||
reason,
|
||||
previous_account_id: context.previous_account_id,
|
||||
};
|
||||
|
||||
let (request_id, rx) = self
|
||||
.outgoing
|
||||
.send_request(ServerRequestPayload::ChatgptAuthTokensRefresh(params))
|
||||
.await;
|
||||
let result = match timeout(EXTERNAL_AUTH_REFRESH_TIMEOUT, rx).await {
|
||||
Ok(result) => {
|
||||
let result = result.map_err(|err| {
|
||||
std::io::Error::other(format!("auth refresh request canceled: {err}"))
|
||||
})?;
|
||||
result.map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"auth refresh request failed: code={} message={}",
|
||||
err.code, err.message
|
||||
))
|
||||
})?
|
||||
}
|
||||
Err(_) => {
|
||||
let _canceled = self.outgoing.cancel_request(&request_id).await;
|
||||
return Err(std::io::Error::other(format!(
|
||||
"auth refresh request timed out after {}s",
|
||||
EXTERNAL_AUTH_REFRESH_TIMEOUT.as_secs()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let response: ChatgptAuthTokensRefreshResponse =
|
||||
serde_json::from_value(result).map_err(std::io::Error::other)?;
|
||||
let auth = CodexAuth::from_external_chatgpt_tokens(
|
||||
response.access_token.as_str(),
|
||||
response.chatgpt_account_id.as_str(),
|
||||
response.chatgpt_plan_type.as_deref(),
|
||||
)?;
|
||||
*self
|
||||
.auth
|
||||
.write()
|
||||
.map_err(|_| std::io::Error::other("external auth lock is poisoned"))? = auth.clone();
|
||||
Ok(auth)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalAuth for ExternalAuthBridge {
|
||||
fn resolve(&self) -> ExternalAuthFuture<'_, CodexAuth> {
|
||||
Box::pin(async {
|
||||
self.auth
|
||||
.read()
|
||||
.map(|auth| auth.clone())
|
||||
.map_err(|_| std::io::Error::other("external auth lock is poisoned"))
|
||||
})
|
||||
}
|
||||
|
||||
fn refresh(&self, context: ExternalAuthRefreshContext) -> ExternalAuthFuture<'_, CodexAuth> {
|
||||
Box::pin(ExternalAuthBridge::refresh(self, context))
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ mod current_time;
|
||||
mod dynamic_tools;
|
||||
mod error_code;
|
||||
mod extensions;
|
||||
mod external_auth;
|
||||
mod filters;
|
||||
mod fs_watch;
|
||||
mod fuzzy_file_search;
|
||||
|
||||
@@ -50,9 +50,6 @@ use crate::transport::AppServerTransport;
|
||||
use crate::transport::RemoteControlHandle;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshParams;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
|
||||
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
|
||||
use codex_app_server_protocol::ClientNotification;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
@@ -63,7 +60,6 @@ use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::JSONRPCNotification;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::ServerRequestPayload;
|
||||
use codex_app_server_protocol::experimental_required_message;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_chatgpt::workspace_settings;
|
||||
@@ -74,12 +70,7 @@ use codex_feedback::CodexFeedback;
|
||||
use codex_goal_extension::GoalService;
|
||||
use codex_home::CodexHomeUserInstructionsProvider;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::auth::ExternalAuth;
|
||||
use codex_login::auth::ExternalAuthRefreshContext;
|
||||
use codex_login::auth::ExternalAuthRefreshReason;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::auth::AuthMode as LoginAuthMode;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_rollout::StateDbHandle;
|
||||
@@ -95,7 +86,6 @@ use tracing::Instrument;
|
||||
|
||||
use crate::models_refresh_worker::ModelsRefreshWorker;
|
||||
|
||||
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const CONNECTION_RPC_DRAIN_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 30);
|
||||
|
||||
fn deserialize_client_request(
|
||||
@@ -109,77 +99,6 @@ fn deserialize_client_request(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ExternalAuthRefreshBridge {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
}
|
||||
|
||||
impl ExternalAuthRefreshBridge {
|
||||
fn map_reason(reason: ExternalAuthRefreshReason) -> ChatgptAuthTokensRefreshReason {
|
||||
match reason {
|
||||
ExternalAuthRefreshReason::Unauthorized => ChatgptAuthTokensRefreshReason::Unauthorized,
|
||||
}
|
||||
}
|
||||
|
||||
async fn refresh(&self, context: ExternalAuthRefreshContext) -> std::io::Result<CodexAuth> {
|
||||
let params = ChatgptAuthTokensRefreshParams {
|
||||
reason: Self::map_reason(context.reason),
|
||||
previous_account_id: context.previous_account_id,
|
||||
};
|
||||
|
||||
let (request_id, rx) = self
|
||||
.outgoing
|
||||
.send_request(ServerRequestPayload::ChatgptAuthTokensRefresh(params))
|
||||
.await;
|
||||
|
||||
let result = match timeout(EXTERNAL_AUTH_REFRESH_TIMEOUT, rx).await {
|
||||
Ok(result) => {
|
||||
// Two failure scenarios:
|
||||
// 1) `oneshot::Receiver` failed (sender dropped) => request canceled/channel closed.
|
||||
// 2) client answered with JSON-RPC error payload => propagate code/message.
|
||||
let result = result.map_err(|err| {
|
||||
std::io::Error::other(format!("auth refresh request canceled: {err}"))
|
||||
})?;
|
||||
result.map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"auth refresh request failed: code={} message={}",
|
||||
err.code, err.message
|
||||
))
|
||||
})?
|
||||
}
|
||||
Err(_) => {
|
||||
let _canceled = self.outgoing.cancel_request(&request_id).await;
|
||||
return Err(std::io::Error::other(format!(
|
||||
"auth refresh request timed out after {}s",
|
||||
EXTERNAL_AUTH_REFRESH_TIMEOUT.as_secs()
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let response: ChatgptAuthTokensRefreshResponse =
|
||||
serde_json::from_value(result).map_err(std::io::Error::other)?;
|
||||
|
||||
CodexAuth::from_external_chatgpt_tokens(
|
||||
response.access_token.as_str(),
|
||||
response.chatgpt_account_id.as_str(),
|
||||
response.chatgpt_plan_type.as_deref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalAuth for ExternalAuthRefreshBridge {
|
||||
fn auth_mode(&self) -> LoginAuthMode {
|
||||
LoginAuthMode::Chatgpt
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&self,
|
||||
context: ExternalAuthRefreshContext,
|
||||
) -> codex_login::ExternalAuthFuture<'_, CodexAuth> {
|
||||
Box::pin(ExternalAuthRefreshBridge::refresh(self, context))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MessageProcessor {
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
models_refresh_worker: ModelsRefreshWorker,
|
||||
@@ -324,9 +243,6 @@ impl MessageProcessor {
|
||||
remote_control_handle,
|
||||
plugin_startup_tasks,
|
||||
} = args;
|
||||
auth_manager.set_external_auth(Arc::new(ExternalAuthRefreshBridge {
|
||||
outgoing: outgoing.clone(),
|
||||
}));
|
||||
let thread_state_manager = ThreadStateManager::new();
|
||||
// The thread store is intentionally process-scoped. Config reloads can
|
||||
// affect per-thread behavior, but they must not move newly started,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::bespoke_event_handling::apply_bespoke_event_handling;
|
||||
use crate::bespoke_event_handling::maybe_emit_hook_prompt_item_completed;
|
||||
use crate::command_exec::CommandExecManager;
|
||||
use crate::command_exec::StartCommandExecParams;
|
||||
use crate::config_manager::ConfigManager;
|
||||
@@ -94,6 +93,7 @@ use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
use codex_app_server_protocol::LoginAccountParams;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::LoginApiKeyParams;
|
||||
use codex_app_server_protocol::LoginAppBrand;
|
||||
use codex_app_server_protocol::LogoutAccountResponse;
|
||||
use codex_app_server_protocol::MarketplaceAddParams;
|
||||
use codex_app_server_protocol::MarketplaceAddResponse;
|
||||
@@ -379,10 +379,12 @@ use codex_feedback::FeedbackUploadOptions;
|
||||
use codex_git_utils::git_diff_to_remote;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CODEX_OPEN_APP_URL;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::LoginSuccessPage;
|
||||
use codex_login::LoginSuccessPageBrand;
|
||||
use codex_login::ServerOptions as LoginServerOptions;
|
||||
use codex_login::ShutdownHandle;
|
||||
use codex_login::auth::login_with_chatgpt_auth_tokens;
|
||||
use codex_login::complete_device_code_login;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_login::oauth_client_id;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::auth_mode::auth_mode_to_api;
|
||||
use crate::external_auth::ExternalAuthBridge;
|
||||
use chrono::DateTime;
|
||||
|
||||
mod rate_limit_resets;
|
||||
@@ -9,9 +10,11 @@ const LOGIN_CHATGPT_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
||||
const ACCOUNT_TOKEN_USAGE_FETCH_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 10);
|
||||
const ACCOUNT_WORKSPACE_MESSAGES_FETCH_TIMEOUT: Duration =
|
||||
Duration::from_millis(/*millis*/ 1000);
|
||||
// The override is intentionally available only in debug builds, matching the login path below.
|
||||
// Login overrides are intentionally available only in debug builds.
|
||||
#[cfg(debug_assertions)]
|
||||
const LOGIN_ISSUER_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_LOGIN_ISSUER";
|
||||
#[cfg(debug_assertions)]
|
||||
const LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR: &str = "CODEX_APP_SERVER_DEV_OPEN_APP_URL";
|
||||
|
||||
enum ActiveLogin {
|
||||
Browser {
|
||||
@@ -253,9 +256,25 @@ impl AccountRequestProcessor {
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::Chatgpt {
|
||||
app_brand,
|
||||
codex_streamlined_login,
|
||||
use_hosted_login_success_page,
|
||||
} => {
|
||||
self.login_chatgpt_v2(request_id, codex_streamlined_login)
|
||||
let login_success_page = if use_hosted_login_success_page {
|
||||
let app_brand = match app_brand.unwrap_or_default() {
|
||||
LoginAppBrand::Codex => LoginSuccessPageBrand::Codex,
|
||||
LoginAppBrand::Chatgpt => LoginSuccessPageBrand::Chatgpt,
|
||||
};
|
||||
LoginSuccessPage::Hosted {
|
||||
url: CODEX_OPEN_APP_URL.parse().map_err(|err| {
|
||||
internal_error(format!("invalid Codex open app URL: {err}"))
|
||||
})?,
|
||||
app_brand,
|
||||
}
|
||||
} else {
|
||||
LoginSuccessPage::default()
|
||||
};
|
||||
self.login_chatgpt_v2(request_id, codex_streamlined_login, login_success_page)
|
||||
.await;
|
||||
}
|
||||
LoginAccountParams::ChatgptDeviceCode => {
|
||||
@@ -341,6 +360,7 @@ impl AccountRequestProcessor {
|
||||
async fn login_chatgpt_common(
|
||||
&self,
|
||||
codex_streamlined_login: bool,
|
||||
login_success_page: LoginSuccessPage,
|
||||
) -> std::result::Result<LoginServerOptions, JSONRPCErrorError> {
|
||||
let config = self.config.as_ref();
|
||||
|
||||
@@ -357,6 +377,7 @@ impl AccountRequestProcessor {
|
||||
let opts = LoginServerOptions {
|
||||
open_browser: false,
|
||||
codex_streamlined_login,
|
||||
login_success_page,
|
||||
..LoginServerOptions::new(
|
||||
config.codex_home.to_path_buf(),
|
||||
oauth_client_id(),
|
||||
@@ -374,6 +395,14 @@ impl AccountRequestProcessor {
|
||||
{
|
||||
opts.issuer = issuer;
|
||||
}
|
||||
if let LoginSuccessPage::Hosted { url, .. } = &mut opts.login_success_page
|
||||
&& let Ok(open_app_url) = std::env::var(LOGIN_OPEN_APP_URL_OVERRIDE_ENV_VAR)
|
||||
&& !open_app_url.trim().is_empty()
|
||||
{
|
||||
*url = open_app_url
|
||||
.parse()
|
||||
.map_err(|err| internal_error(format!("invalid Codex open app URL: {err}")))?;
|
||||
}
|
||||
opts
|
||||
};
|
||||
|
||||
@@ -393,16 +422,22 @@ impl AccountRequestProcessor {
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
codex_streamlined_login: bool,
|
||||
login_success_page: LoginSuccessPage,
|
||||
) {
|
||||
let result = self.login_chatgpt_response(codex_streamlined_login).await;
|
||||
let result = self
|
||||
.login_chatgpt_response(codex_streamlined_login, login_success_page)
|
||||
.await;
|
||||
self.outgoing.send_result(request_id, result).await;
|
||||
}
|
||||
|
||||
async fn login_chatgpt_response(
|
||||
&self,
|
||||
codex_streamlined_login: bool,
|
||||
login_success_page: LoginSuccessPage,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self.login_chatgpt_common(codex_streamlined_login).await?;
|
||||
let opts = self
|
||||
.login_chatgpt_common(codex_streamlined_login, login_success_page)
|
||||
.await?;
|
||||
let server = run_login_server(opts)
|
||||
.map_err(|err| internal_error(format!("failed to start login server: {err}")))?;
|
||||
let login_id = Uuid::new_v4();
|
||||
@@ -474,7 +509,10 @@ impl AccountRequestProcessor {
|
||||
&self,
|
||||
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
|
||||
let opts = self
|
||||
.login_chatgpt_common(/*codex_streamlined_login*/ false)
|
||||
.login_chatgpt_common(
|
||||
/*codex_streamlined_login*/ false,
|
||||
LoginSuccessPage::default(),
|
||||
)
|
||||
.await?;
|
||||
let device_code = request_device_code(&opts)
|
||||
.await
|
||||
@@ -617,14 +655,19 @@ impl AccountRequestProcessor {
|
||||
)));
|
||||
}
|
||||
|
||||
login_with_chatgpt_auth_tokens(
|
||||
&self.config.codex_home,
|
||||
let auth = CodexAuth::from_external_chatgpt_tokens(
|
||||
&access_token,
|
||||
&chatgpt_account_id,
|
||||
chatgpt_plan_type.as_deref(),
|
||||
)
|
||||
.map_err(|err| internal_error(format!("failed to set external auth: {err}")))?;
|
||||
self.auth_manager.reload().await;
|
||||
self.auth_manager
|
||||
.set_external_auth(Arc::new(ExternalAuthBridge::new(
|
||||
Arc::clone(&self.outgoing),
|
||||
auth,
|
||||
)))
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to set external auth: {err}")))?;
|
||||
self.config_manager.replace_cloud_config_bundle_loader(
|
||||
self.auth_manager.clone(),
|
||||
self.config.chatgpt_base_url.clone(),
|
||||
@@ -812,7 +855,9 @@ impl AccountRequestProcessor {
|
||||
let auth_mode = auth_mode_to_api(auth.api_auth_mode());
|
||||
let (reported_auth_method, token_opt) = if matches!(
|
||||
auth,
|
||||
CodexAuth::AgentIdentity(_) | CodexAuth::PersonalAccessToken(_)
|
||||
CodexAuth::Headers(_)
|
||||
| CodexAuth::AgentIdentity(_)
|
||||
| CodexAuth::PersonalAccessToken(_)
|
||||
) || include_token
|
||||
&& permanent_refresh_failure
|
||||
{
|
||||
|
||||
@@ -1548,6 +1548,7 @@ impl PluginRequestProcessor {
|
||||
&remote_marketplace_name,
|
||||
/*plugin_id*/ None,
|
||||
error_type,
|
||||
/*sub_error_type*/ None,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_catalog_error_to_jsonrpc(
|
||||
@@ -1591,11 +1592,13 @@ impl PluginRequestProcessor {
|
||||
)
|
||||
.map_err(|err| {
|
||||
let error_type = remote_plugin_bundle_install_error_type(&err);
|
||||
let sub_error_type = err.sub_error_type();
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
sub_error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_bundle_install_error_to_jsonrpc(err)
|
||||
@@ -1608,11 +1611,13 @@ impl PluginRequestProcessor {
|
||||
.await
|
||||
.map_err(|err| {
|
||||
let error_type = remote_plugin_bundle_install_error_type(&err);
|
||||
let sub_error_type = err.sub_error_type();
|
||||
self.track_plugin_install_failed_for_remote_plugin(
|
||||
&remote_plugin_id,
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&resolved_plugin_id),
|
||||
error_type,
|
||||
sub_error_type,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_bundle_install_error_to_jsonrpc(err)
|
||||
@@ -1635,6 +1640,7 @@ impl PluginRequestProcessor {
|
||||
&actual_remote_marketplace_name,
|
||||
Some(&result.plugin_id),
|
||||
error_type,
|
||||
/*sub_error_type*/ None,
|
||||
err.to_string(),
|
||||
);
|
||||
remote_plugin_catalog_error_to_jsonrpc(err, "install remote plugin")
|
||||
@@ -1732,12 +1738,14 @@ impl PluginRequestProcessor {
|
||||
marketplace_name: &str,
|
||||
plugin_id: Option<&PluginId>,
|
||||
error_type: &'static str,
|
||||
sub_error_type: Option<String>,
|
||||
error_message: String,
|
||||
) {
|
||||
tracing::warn!(
|
||||
remote_plugin_id = %remote_plugin_id,
|
||||
marketplace_name = %marketplace_name,
|
||||
error_type = %error_type,
|
||||
sub_error_type = sub_error_type.as_deref(),
|
||||
error = %error_message,
|
||||
"remote plugin install failed"
|
||||
);
|
||||
|
||||
@@ -316,6 +316,9 @@ pub(super) async fn ensure_listener_task_running(
|
||||
thread_state.track_current_turn_event(&event.id, &event.msg);
|
||||
thread_state.experimental_raw_events
|
||||
};
|
||||
if matches!(&event.msg, EventMsg::RawResponseItem(_)) && !raw_events_enabled {
|
||||
continue;
|
||||
}
|
||||
let subscribed_connection_ids = thread_state_manager
|
||||
.subscribed_connection_ids(conversation_id)
|
||||
.await;
|
||||
@@ -325,19 +328,6 @@ pub(super) async fn ensure_listener_task_running(
|
||||
conversation_id,
|
||||
);
|
||||
|
||||
if let EventMsg::RawResponseItem(raw_response_item_event) = &event.msg
|
||||
&& !raw_events_enabled
|
||||
{
|
||||
maybe_emit_hook_prompt_item_completed(
|
||||
conversation_id,
|
||||
&event.id,
|
||||
&raw_response_item_event.item,
|
||||
&thread_outgoing,
|
||||
)
|
||||
.await;
|
||||
continue;
|
||||
}
|
||||
|
||||
apply_bespoke_event_handling(
|
||||
event.clone(),
|
||||
conversation_id,
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(super) fn redact_thread_resume_payloads(turns: &mut [Turn]) {
|
||||
}
|
||||
true
|
||||
}
|
||||
ThreadItem::ImageGeneration { .. } => false,
|
||||
ThreadItem::ImageGeneration(_) => false,
|
||||
_ => true,
|
||||
});
|
||||
}
|
||||
@@ -52,6 +52,7 @@ fn redacted_mcp_tool_call_result() -> McpToolCallResult {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::McpToolCallAppContext;
|
||||
use codex_app_server_protocol::McpToolCallError;
|
||||
use codex_app_server_protocol::McpToolCallStatus;
|
||||
@@ -100,13 +101,13 @@ mod tests {
|
||||
error: None,
|
||||
duration_ms: Some(8),
|
||||
},
|
||||
ThreadItem::ImageGeneration {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "ig-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("revised".to_string()),
|
||||
result: "base64-result".to_string(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
redact_thread_resume_payloads(&mut thread.turns);
|
||||
|
||||
@@ -27,6 +27,7 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
|
||||
@@ -49,6 +49,7 @@ use serde::de::DeserializeOwned;
|
||||
pub use test_app_server::DEFAULT_CLIENT_NAME;
|
||||
pub use test_app_server::DISABLE_PLUGIN_STARTUP_TASKS_ARG;
|
||||
pub use test_app_server::TestAppServer;
|
||||
pub use test_app_server::TestAppServerBuilder;
|
||||
|
||||
pub fn to_response<T: DeserializeOwned>(response: JSONRPCResponse) -> anyhow::Result<T> {
|
||||
let value = serde_json::to_value(response.result)?;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitStatus;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
@@ -122,6 +123,7 @@ use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
||||
use codex_login::default_client::CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR;
|
||||
use core_test_support::test_codex::TestEnv;
|
||||
use core_test_support::test_codex::test_env;
|
||||
use tempfile::TempDir;
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::json_logging::JsonLogCapture;
|
||||
@@ -138,98 +140,45 @@ pub struct TestAppServer {
|
||||
pending_messages: VecDeque<JSONRPCMessage>,
|
||||
auto_env: Option<TestEnv>,
|
||||
json_logs: JsonLogCapture,
|
||||
codex_home: PathBuf,
|
||||
_owned_codex_home: Option<TempDir>,
|
||||
}
|
||||
|
||||
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
|
||||
pub const DISABLE_PLUGIN_STARTUP_TASKS_ARG: &str = "--disable-plugin-startup-tasks-for-tests";
|
||||
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
|
||||
const CODE_MODE_HOST_PATH_ENV_VAR: &str = "CODEX_CODE_MODE_HOST_PATH";
|
||||
|
||||
impl TestAppServer {
|
||||
/// Starts building a server with a temporary CODEX_HOME and the standard
|
||||
/// automatic test environment.
|
||||
pub fn builder() -> TestAppServerBuilder {
|
||||
TestAppServerBuilder {
|
||||
codex_home: None,
|
||||
environment: TestAppServerEnvironment::Auto,
|
||||
program: None,
|
||||
env_overrides: Vec::new(),
|
||||
args: vec![DISABLE_PLUGIN_STARTUP_TASKS_ARG.to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait_for_exit(&mut self) -> std::io::Result<ExitStatus> {
|
||||
self.process.wait().await
|
||||
}
|
||||
|
||||
pub async fn new(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(codex_home, &[], &[DISABLE_PLUGIN_STARTUP_TASKS_ARG]).await
|
||||
}
|
||||
|
||||
/// Starts an app server with the standard test environment and retains it
|
||||
/// for the server's lifetime.
|
||||
///
|
||||
/// Local test runs explicitly remove `CODEX_EXEC_SERVER_URL`; Docker- and
|
||||
/// Wine-backed runs set it to the remote fixture URL. Use
|
||||
/// [`Self::auto_env_params`] or
|
||||
/// [`Self::send_thread_start_request_with_auto_env`] to select the matching
|
||||
/// target-native cwd in a thread. Because `environments.toml` overrides the
|
||||
/// URL-based configuration, this helper rejects a `codex_home` containing
|
||||
/// that file.
|
||||
pub async fn new_with_auto_env(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_auto_env_and_env(codex_home, &[]).await
|
||||
}
|
||||
|
||||
/// Starts an auto-environment app server that emits JSON logs.
|
||||
///
|
||||
/// `rust_log` is the value to use for the `RUST_LOG` environment variable.
|
||||
pub async fn new_with_auto_env_and_json_logging(
|
||||
codex_home: &Path,
|
||||
rust_log: impl Into<String>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let rust_log = rust_log.into();
|
||||
Self::new_with_auto_env_and_env(
|
||||
codex_home,
|
||||
&[
|
||||
("LOG_FORMAT", Some("json")),
|
||||
("RUST_LOG", Some(rust_log.as_str())),
|
||||
],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_auto_env_and_env(
|
||||
codex_home: &Path,
|
||||
extra_env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
let environments_toml = codex_home.join("environments.toml");
|
||||
ensure!(
|
||||
!environments_toml
|
||||
.try_exists()
|
||||
.with_context(|| format!("check whether {} exists", environments_toml.display()))?,
|
||||
"new_with_auto_env cannot be used when {} exists",
|
||||
environments_toml.display()
|
||||
);
|
||||
|
||||
let auto_env = test_env().await?;
|
||||
// Noise registry configuration takes precedence over the URL-based
|
||||
// provider, so clear inherited values to keep the selection hermetic.
|
||||
let mut env_overrides = vec![
|
||||
(
|
||||
CODEX_EXEC_SERVER_URL_ENV_VAR,
|
||||
auto_env.environment().exec_server_url(),
|
||||
),
|
||||
(CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR, None),
|
||||
(CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR, None),
|
||||
(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR, None),
|
||||
(CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR, None),
|
||||
];
|
||||
env_overrides.extend_from_slice(extra_env_overrides);
|
||||
let mut app_server = Self::new_with_env(codex_home, &env_overrides).await?;
|
||||
app_server.auto_env = Some(auto_env);
|
||||
Ok(app_server)
|
||||
}
|
||||
|
||||
/// Returns the automatically selected test environment retained by this server.
|
||||
///
|
||||
/// Tests can use the environment to arrange target-native filesystem fixtures before starting
|
||||
/// a thread. Returns an error unless this server was created with [`Self::new_with_auto_env`].
|
||||
/// a thread. Returns an error unless the builder's automatic environment is enabled.
|
||||
pub fn auto_env(&self) -> anyhow::Result<&TestEnv> {
|
||||
self.auto_env
|
||||
.as_ref()
|
||||
.context("auto environment is unavailable; use TestAppServer::new_with_auto_env")
|
||||
.context("auto environment is unavailable; enable it on TestAppServer::builder")
|
||||
}
|
||||
|
||||
/// Returns app-server protocol parameters for the automatically selected
|
||||
/// test environment. Returns an error unless this server was created with
|
||||
/// [`Self::new_with_auto_env`].
|
||||
/// test environment. Returns an error unless the builder's automatic
|
||||
/// environment is enabled.
|
||||
pub fn auto_env_params(&self) -> anyhow::Result<TurnEnvironmentParams> {
|
||||
let selection = self.auto_env()?.selection();
|
||||
Ok(TurnEnvironmentParams {
|
||||
@@ -238,6 +187,11 @@ impl TestAppServer {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the effective CODEX_HOME used by the child app-server.
|
||||
pub fn codex_home(&self) -> &Path {
|
||||
&self.codex_home
|
||||
}
|
||||
|
||||
/// Waits for a JSON stderr event whose structured `event.name` field matches.
|
||||
pub async fn wait_for_json_log_event(
|
||||
&self,
|
||||
@@ -260,77 +214,6 @@ impl TestAppServer {
|
||||
self.json_logs.events()
|
||||
}
|
||||
|
||||
pub async fn new_without_managed_config(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env(codex_home, &[(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))]).await
|
||||
}
|
||||
|
||||
pub async fn new_without_managed_config_with_env(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut all_env_overrides = vec![(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))];
|
||||
all_env_overrides.extend_from_slice(env_overrides);
|
||||
Self::new_with_env(codex_home, &all_env_overrides).await
|
||||
}
|
||||
|
||||
pub async fn new_with_plugin_startup_tasks(codex_home: &Path) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(codex_home, &[], &[]).await
|
||||
}
|
||||
|
||||
pub async fn new_with_env_and_plugin_startup_tasks(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(codex_home, env_overrides, &[]).await
|
||||
}
|
||||
|
||||
pub async fn new_with_args(codex_home: &Path, args: &[&str]) -> anyhow::Result<Self> {
|
||||
let mut all_args = vec![DISABLE_PLUGIN_STARTUP_TASKS_ARG];
|
||||
all_args.extend_from_slice(args);
|
||||
Self::new_with_env_and_args(codex_home, &[], &all_args).await
|
||||
}
|
||||
|
||||
/// Creates a new MCP process, allowing tests to override or remove
|
||||
/// specific environment variables for the child process only.
|
||||
///
|
||||
/// Pass a tuple of (key, Some(value)) to set/override, or (key, None) to
|
||||
/// remove a variable from the child's environment.
|
||||
pub async fn new_with_env(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::new_with_env_and_args(
|
||||
codex_home,
|
||||
env_overrides,
|
||||
&[DISABLE_PLUGIN_STARTUP_TASKS_ARG],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_with_program_and_env(
|
||||
codex_home: &Path,
|
||||
program: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::new_with_program_env_and_args(
|
||||
codex_home,
|
||||
program,
|
||||
env_overrides,
|
||||
&[DISABLE_PLUGIN_STARTUP_TASKS_ARG],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_with_env_and_args(
|
||||
codex_home: &Path,
|
||||
env_overrides: &[(&str, Option<&str>)],
|
||||
args: &[&str],
|
||||
) -> anyhow::Result<Self> {
|
||||
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find binary for codex-app-server")?;
|
||||
Self::new_with_program_env_and_args(codex_home, &program, env_overrides, args).await
|
||||
}
|
||||
|
||||
async fn new_with_program_env_and_args(
|
||||
codex_home: &Path,
|
||||
program: &Path,
|
||||
@@ -399,6 +282,8 @@ impl TestAppServer {
|
||||
pending_messages: VecDeque::new(),
|
||||
auto_env: None,
|
||||
json_logs,
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
_owned_codex_home: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -553,8 +438,8 @@ impl TestAppServer {
|
||||
chatgpt_account_id,
|
||||
chatgpt_plan_type,
|
||||
};
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("account/login/start", params).await
|
||||
self.send_login_account_request(serde_json::to_value(params)?)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `feedback/upload` JSON-RPC request.
|
||||
@@ -575,9 +460,9 @@ impl TestAppServer {
|
||||
self.send_request("thread/start", params).await
|
||||
}
|
||||
|
||||
/// Sends a `thread/start` request selecting the environment provisioned by
|
||||
/// [`Self::new_with_auto_env`]. Returns an error if `params` already select
|
||||
/// environments so the caller cannot accidentally override the fixture.
|
||||
/// Sends a `thread/start` request selecting the builder's automatic
|
||||
/// environment. Returns an error if `params` already select environments
|
||||
/// so the caller cannot accidentally override the fixture.
|
||||
pub async fn send_thread_start_request_with_auto_env(
|
||||
&mut self,
|
||||
mut params: ThreadStartParams,
|
||||
@@ -1420,6 +1305,14 @@ impl TestAppServer {
|
||||
self.send_request("account/logout", /*params*/ None).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request.
|
||||
pub async fn send_login_account_request(
|
||||
&mut self,
|
||||
params: serde_json::Value,
|
||||
) -> anyhow::Result<i64> {
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for API key login.
|
||||
pub async fn send_login_account_api_key_request(
|
||||
&mut self,
|
||||
@@ -1429,7 +1322,7 @@ impl TestAppServer {
|
||||
"type": "apiKey",
|
||||
"apiKey": api_key,
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for ChatGPT login.
|
||||
@@ -1437,7 +1330,7 @@ impl TestAppServer {
|
||||
let params = serde_json::json!({
|
||||
"type": "chatgpt"
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/start` JSON-RPC request for ChatGPT device code login.
|
||||
@@ -1445,7 +1338,7 @@ impl TestAppServer {
|
||||
let params = serde_json::json!({
|
||||
"type": "chatgptDeviceCode"
|
||||
});
|
||||
self.send_request("account/login/start", Some(params)).await
|
||||
self.send_login_account_request(params).await
|
||||
}
|
||||
|
||||
/// Send an `account/login/cancel` JSON-RPC request.
|
||||
@@ -1792,6 +1685,178 @@ impl TestAppServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for TestAppServer.
|
||||
pub struct TestAppServerBuilder {
|
||||
codex_home: Option<PathBuf>,
|
||||
environment: TestAppServerEnvironment,
|
||||
program: Option<PathBuf>,
|
||||
env_overrides: Vec<(String, Option<String>)>,
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
enum TestAppServerEnvironment {
|
||||
Auto,
|
||||
None,
|
||||
}
|
||||
|
||||
impl TestAppServerBuilder {
|
||||
/// Uses this existing CODEX_HOME instead of a temporary one.
|
||||
pub fn with_codex_home(mut self, codex_home: &Path) -> Self {
|
||||
self.codex_home = Some(codex_home.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Starts app-server without the standard automatic test environment.
|
||||
pub fn without_auto_env(mut self) -> Self {
|
||||
self.environment = TestAppServerEnvironment::None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Uses this app-server binary instead of the standard test binary.
|
||||
pub fn with_program(mut self, program: &Path) -> Self {
|
||||
self.program = Some(program.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds command-line arguments after the default test arguments.
|
||||
pub fn with_args(mut self, args: &[&str]) -> Self {
|
||||
self.args
|
||||
.extend(args.iter().map(|argument| (*argument).to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables startup tasks that the default test arguments disable.
|
||||
pub fn with_plugin_startup_tasks(mut self) -> Self {
|
||||
self.args
|
||||
.retain(|argument| argument != DISABLE_PLUGIN_STARTUP_TASKS_ARG);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds child-process environment overrides.
|
||||
///
|
||||
/// Some values set variables and None values remove inherited variables.
|
||||
pub fn with_env_overrides(mut self, env_overrides: &[(&str, Option<&str>)]) -> Self {
|
||||
self.env_overrides
|
||||
.extend(env_overrides.iter().map(|(key, value)| {
|
||||
(
|
||||
(*key).to_string(),
|
||||
value.map(std::string::ToString::to_string),
|
||||
)
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Prevents the child from loading managed configuration.
|
||||
pub fn without_managed_config(self) -> Self {
|
||||
self.with_env_overrides(&[(DISABLE_MANAGED_CONFIG_ENV_VAR, Some("1"))])
|
||||
}
|
||||
|
||||
/// Configures the child to emit JSON logs at the requested Rust log level.
|
||||
pub fn with_json_logging(self, rust_log: impl Into<String>) -> Self {
|
||||
let rust_log = rust_log.into();
|
||||
let mut builder = self.with_env_overrides(&[("LOG_FORMAT", Some("json"))]);
|
||||
builder
|
||||
.env_overrides
|
||||
.push(("RUST_LOG".to_string(), Some(rust_log)));
|
||||
builder
|
||||
}
|
||||
|
||||
/// Builds a server with a temporary CODEX_HOME and automatic environment
|
||||
/// by default.
|
||||
pub async fn build(self) -> anyhow::Result<TestAppServer> {
|
||||
let Self {
|
||||
codex_home,
|
||||
environment,
|
||||
program,
|
||||
mut env_overrides,
|
||||
args,
|
||||
} = self;
|
||||
let (codex_home, owned_codex_home) = match codex_home {
|
||||
Some(codex_home) => (codex_home, None),
|
||||
None => {
|
||||
let owned_codex_home = TempDir::new()?;
|
||||
(
|
||||
owned_codex_home.path().to_path_buf(),
|
||||
Some(owned_codex_home),
|
||||
)
|
||||
}
|
||||
};
|
||||
let auto_env = match environment {
|
||||
TestAppServerEnvironment::Auto => {
|
||||
let environments_toml = codex_home.join("environments.toml");
|
||||
ensure!(
|
||||
!environments_toml.try_exists().with_context(|| format!(
|
||||
"check whether {} exists",
|
||||
environments_toml.display()
|
||||
))?,
|
||||
"automatic environment cannot be used when {} exists",
|
||||
environments_toml.display()
|
||||
);
|
||||
let auto_env = test_env().await?;
|
||||
// Noise registry configuration takes precedence over the URL-based
|
||||
// provider, so clear inherited values to keep the selection hermetic.
|
||||
let mut auto_env_overrides = vec![
|
||||
(
|
||||
CODEX_EXEC_SERVER_URL_ENV_VAR.to_string(),
|
||||
auto_env.environment().exec_server_url().map(str::to_string),
|
||||
),
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR.to_string(),
|
||||
None,
|
||||
),
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR.to_string(),
|
||||
None,
|
||||
),
|
||||
(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_string(), None),
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR.to_string(),
|
||||
None,
|
||||
),
|
||||
];
|
||||
auto_env_overrides.append(&mut env_overrides);
|
||||
env_overrides = auto_env_overrides;
|
||||
Some(auto_env)
|
||||
}
|
||||
TestAppServerEnvironment::None => None,
|
||||
};
|
||||
if !env_overrides
|
||||
.iter()
|
||||
.any(|(key, _)| key == CODE_MODE_HOST_PATH_ENV_VAR)
|
||||
&& let Ok(code_mode_host_program) =
|
||||
codex_utils_cargo_bin::cargo_bin("codex-code-mode-host")
|
||||
{
|
||||
env_overrides.insert(
|
||||
0,
|
||||
(
|
||||
CODE_MODE_HOST_PATH_ENV_VAR.to_string(),
|
||||
Some(code_mode_host_program.to_string_lossy().into_owned()),
|
||||
),
|
||||
);
|
||||
}
|
||||
let program = match program {
|
||||
Some(program) => program,
|
||||
None => codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find binary for codex-app-server")?,
|
||||
};
|
||||
let env_overrides = env_overrides
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_deref()))
|
||||
.collect::<Vec<_>>();
|
||||
let args = args.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let mut app_server = TestAppServer::new_with_program_env_and_args(
|
||||
&codex_home,
|
||||
&program,
|
||||
&env_overrides,
|
||||
&args,
|
||||
)
|
||||
.await?;
|
||||
app_server.auto_env = auto_env;
|
||||
app_server._owned_codex_home = owned_codex_home;
|
||||
Ok(app_server)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestAppServer {
|
||||
fn drop(&mut self) {
|
||||
// These tests spawn a `codex-app-server` child process.
|
||||
|
||||
@@ -115,8 +115,12 @@ async fn get_auth_status_no_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -142,7 +146,11 @@ async fn get_auth_status_with_api_key() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -186,15 +194,16 @@ async fn personal_access_token_without_email_supports_auth_status_and_account_re
|
||||
.await;
|
||||
|
||||
let authapi_base_url = server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
("CODEX_ACCESS_TOKEN", Some("at-test-token")),
|
||||
("CODEX_AUTHAPI_BASE_URL", Some(authapi_base_url.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -256,7 +265,11 @@ 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(), /*requires_openai_auth*/ false)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -289,7 +302,11 @@ async fn get_auth_status_with_api_key_no_include_token() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -317,7 +334,11 @@ async fn get_auth_status_with_api_key_refresh_requested() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
login_with_api_key_via_request(&mut mcp, "sk-test-key").await?;
|
||||
@@ -373,17 +394,18 @@ async fn get_auth_status_omits_token_after_permanent_refresh_failure() -> Result
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -455,17 +477,18 @@ async fn get_auth_status_omits_token_after_proactive_refresh_failure() -> Result
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -522,17 +545,18 @@ async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Res
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let failed_request_id = mcp
|
||||
@@ -599,7 +623,11 @@ async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_forced_login(codex_home.path(), "chatgpt")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -92,7 +92,11 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> {
|
||||
))?,
|
||||
);
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -217,7 +221,11 @@ async fn get_conversation_summary_by_relative_rollout_path_resolves_from_codex_h
|
||||
let relative_path = rollout_path.strip_prefix(codex_home.path())?.to_path_buf();
|
||||
let expected = expected_summary(thread_id, normalized_canonical_path(rollout_path)?);
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -46,7 +46,11 @@ shell_snapshot = false
|
||||
|
||||
async fn initialized_mcp(codex_home: &TempDir) -> Result<TestAppServer> {
|
||||
create_config_toml(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
Ok(mcp)
|
||||
}
|
||||
@@ -236,7 +240,11 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
||||
.to_string();
|
||||
|
||||
// Start MCP server and initialize.
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let root_path = root.path().to_string_lossy().to_string();
|
||||
@@ -302,7 +310,11 @@ async fn test_fuzzy_file_search_accepts_cancellation_token() -> Result<()> {
|
||||
|
||||
std::fs::write(root.path().join("alpha.txt"), "contents")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let root_path = root.path().to_string_lossy().to_string();
|
||||
|
||||
@@ -68,11 +68,11 @@ async fn app_server_emits_structured_tool_call_timing_event() -> Result<()> {
|
||||
"compact",
|
||||
)?;
|
||||
|
||||
let mut app_server = TestAppServer::new_with_auto_env_and_json_logging(
|
||||
codex_home.path(),
|
||||
"warn,codex_core::tools::parallel=info",
|
||||
)
|
||||
.await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_json_logging("warn,codex_core::tools::parallel=info")
|
||||
.build()
|
||||
.await?;
|
||||
timeout(READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let thread_start_id = app_server
|
||||
|
||||
@@ -57,6 +57,7 @@ use wiremock::matchers::path;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
const LOGIN_ISSUER_ENV_VAR: &str = "CODEX_APP_SERVER_LOGIN_ISSUER";
|
||||
const LOGIN_OPEN_APP_URL_ENV_VAR: &str = "CODEX_APP_SERVER_DEV_OPEN_APP_URL";
|
||||
const WORKSPACE_ID_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174000";
|
||||
const WORKSPACE_ID_SECOND_ALLOWED: &str = "123e4567-e89b-42d3-a456-426614174001";
|
||||
const WORKSPACE_ID_DISALLOWED: &str = "123e4567-e89b-42d3-a456-426614174002";
|
||||
@@ -181,7 +182,7 @@ async fn mock_device_code_token_failure(server: &MockServer, status: u16) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn mock_device_code_oauth_token(server: &MockServer, id_token: &str) {
|
||||
async fn mock_oauth_token(server: &MockServer, id_token: &str) {
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/oauth/token"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
@@ -206,8 +207,12 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
|
||||
)?;
|
||||
assert!(codex_home.path().join("auth.json").exists());
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let id = mcp.send_logout_account_request().await?;
|
||||
@@ -274,8 +279,12 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -327,6 +336,27 @@ async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
|
||||
}
|
||||
);
|
||||
|
||||
let logout_id = mcp.send_logout_account_request().await?;
|
||||
let logout_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(logout_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: LogoutAccountResponse = to_response(logout_resp)?;
|
||||
|
||||
let get_id = mcp
|
||||
.send_get_account_request(GetAccountParams {
|
||||
refresh_token: false,
|
||||
})
|
||||
.await?;
|
||||
let get_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(get_id)),
|
||||
)
|
||||
.await??;
|
||||
let account: GetAccountResponse = to_response(get_resp)?;
|
||||
assert_eq!(account.account, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -349,8 +379,12 @@ async fn account_read_refresh_token_is_noop_in_external_mode() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -475,8 +509,11 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_REFRESHED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -500,7 +537,7 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(codex_app_server_protocol::ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(codex_app_server_protocol::ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -583,8 +620,11 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_INITIAL),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -608,7 +648,7 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(codex_app_server_protocol::ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(codex_app_server_protocol::ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -707,8 +747,11 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_DISALLOWED),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -732,7 +775,7 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(codex_app_server_protocol::ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(codex_app_server_protocol::ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -824,8 +867,11 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
|
||||
.chatgpt_account_id(WORKSPACE_ID_INITIAL),
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let set_id = mcp
|
||||
@@ -849,7 +895,7 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
|
||||
.await??;
|
||||
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(codex_app_server_protocol::ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(codex_app_server_protocol::ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -918,7 +964,11 @@ async fn login_account_api_key_succeeds_and_notifies() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
@@ -972,7 +1022,11 @@ async fn login_account_api_key_rejected_when_forced_chatgpt() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1002,7 +1056,11 @@ async fn login_account_chatgpt_rejected_when_forced_api() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1035,14 +1093,15 @@ async fn login_account_chatgpt_device_code_returns_error_when_disabled() -> Resu
|
||||
mock_device_code_usercode_failure(&mock_server, /*status*/ 404).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1097,17 +1156,18 @@ async fn login_account_chatgpt_device_code_succeeds_and_notifies() -> Result<()>
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id(WORKSPACE_ID_DEVICE),
|
||||
)?;
|
||||
mock_device_code_oauth_token(&mock_server, &id_token).await;
|
||||
mock_oauth_token(&mock_server, &id_token).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1177,14 +1237,15 @@ async fn login_account_chatgpt_device_code_failure_notifies_without_account_upda
|
||||
mock_device_code_token_failure(&mock_server, /*status*/ 500).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1252,14 +1313,15 @@ async fn login_account_chatgpt_device_code_can_be_cancelled() -> Result<()> {
|
||||
mock_device_code_token_failure(&mock_server, /*status*/ 404).await;
|
||||
|
||||
let issuer = mock_server.uri();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_device_code_request().await?;
|
||||
@@ -1325,7 +1387,11 @@ async fn login_account_chatgpt_start_can_be_cancelled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1391,14 +1457,15 @@ async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
(CLIENT_ID_OVERRIDE_ENV_VAR, Some("staging-client")),
|
||||
(LOGIN_ISSUER_ENV_VAR, Some("https://auth.example.com")),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1436,6 +1503,78 @@ async fn login_account_chatgpt_uses_debug_oauth_overrides() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Serialize tests that launch the login server since it binds to a fixed port.
|
||||
#[serial(login_port)]
|
||||
async fn login_account_chatgpt_redirects_to_hosted_success_page() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
let mock_server = MockServer::start().await;
|
||||
let id_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("hosted@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id(WORKSPACE_ID_EMBEDDED),
|
||||
)?;
|
||||
mock_oauth_token(&mock_server, &id_token).await;
|
||||
let issuer = mock_server.uri();
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
(LOGIN_ISSUER_ENV_VAR, Some(issuer.as_str())),
|
||||
(
|
||||
LOGIN_OPEN_APP_URL_ENV_VAR,
|
||||
Some("http://localhost:3000/codex/open-app"),
|
||||
),
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_login_account_request(json!({
|
||||
"type": "chatgpt",
|
||||
"appBrand": "chatgpt",
|
||||
"useHostedLoginSuccessPage": true,
|
||||
}))
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let login: LoginAccountResponse = to_response(resp)?;
|
||||
let LoginAccountResponse::Chatgpt { auth_url, .. } = login else {
|
||||
bail!("unexpected login response: {login:?}");
|
||||
};
|
||||
let auth_url = Url::parse(&auth_url)?;
|
||||
let callback_url = auth_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "redirect_uri").then(|| value.into_owned()))
|
||||
.ok_or_else(|| anyhow::anyhow!("missing redirect_uri"))?;
|
||||
let state = auth_url
|
||||
.query_pairs()
|
||||
.find_map(|(key, value)| (key == "state").then(|| value.into_owned()))
|
||||
.ok_or_else(|| anyhow::anyhow!("missing state"))?;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
|
||||
let response = client
|
||||
.get(format!("{callback_url}?code=test-code&state={state}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
assert_eq!(response.status(), 302);
|
||||
assert_eq!(
|
||||
response.headers()["location"].to_str()?,
|
||||
"http://localhost:3000/codex/open-app?source=login&app_brand=chatgpt"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// Serialize tests that launch the login server since it binds to a fixed port.
|
||||
#[serial(login_port)]
|
||||
@@ -1443,7 +1582,11 @@ async fn set_auth_token_cancels_active_chatgpt_login() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
// Initiate the ChatGPT login flow
|
||||
@@ -1518,7 +1661,11 @@ async fn login_account_chatgpt_includes_forced_workspace_query_param() -> Result
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1555,7 +1702,11 @@ async fn login_account_chatgpt_includes_forced_workspace_allowlist_query_param()
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_login_account_chatgpt_request().await?;
|
||||
@@ -1594,8 +1745,12 @@ async fn get_account_no_auth() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1626,7 +1781,11 @@ async fn get_account_with_api_key() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let req_id = mcp
|
||||
@@ -1670,7 +1829,11 @@ async fn get_account_when_auth_not_required() -> Result<()> {
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1711,7 +1874,11 @@ region = "us-west-2"
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1754,7 +1921,11 @@ async fn get_account_with_managed_bedrock_provider() -> Result<()> {
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1799,8 +1970,12 @@ async fn get_account_with_chatgpt() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
@@ -1842,8 +2017,12 @@ async fn get_account_with_chatgpt_without_email() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1905,17 +2084,18 @@ async fn get_account_omits_chatgpt_after_permanent_refresh_failure() -> Result<(
|
||||
.await;
|
||||
|
||||
let refresh_url = format!("{}/oauth/token", server.uri());
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("OPENAI_API_KEY", None),
|
||||
(
|
||||
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
||||
Some(refresh_url.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let auth_status_request_id = mcp
|
||||
@@ -1971,8 +2151,12 @@ async fn get_account_with_chatgpt_missing_plan_claim_returns_unknown() -> Result
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let params = GetAccountParams {
|
||||
|
||||
@@ -8,7 +8,9 @@ use std::time::Duration;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::ChatGptIdTokenClaims;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::encode_id_token;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use axum::Json;
|
||||
@@ -29,6 +31,7 @@ use codex_app_server_protocol::AppsListParams;
|
||||
use codex_app_server_protocol::AppsListResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
@@ -63,7 +66,11 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
#[tokio::test]
|
||||
async fn list_apps_returns_empty_when_connectors_disabled() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -129,7 +136,11 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -156,6 +167,93 @@ async fn list_apps_returns_empty_with_api_key_auth() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_apps_uses_external_chatgpt_auth() -> Result<()> {
|
||||
let access_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("external@example.com")
|
||||
.plan_type("pro")
|
||||
.chatgpt_account_id("account-123"),
|
||||
)?;
|
||||
let connectors = vec![AppInfo {
|
||||
id: "beta".to_string(),
|
||||
name: "Beta".to_string(),
|
||||
description: Some("Beta connector".to_string()),
|
||||
logo_url: None,
|
||||
logo_url_dark: None,
|
||||
icon_assets: None,
|
||||
icon_dark_assets: None,
|
||||
distribution_channel: None,
|
||||
branding: None,
|
||||
app_metadata: None,
|
||||
labels: None,
|
||||
install_url: None,
|
||||
is_accessible: false,
|
||||
is_enabled: true,
|
||||
plugin_display_names: Vec::new(),
|
||||
}];
|
||||
let tools = vec![connector_tool("beta", "Beta App")?];
|
||||
let (server_url, server_handle, _) = start_apps_server_with_delays_and_control_inner(
|
||||
connectors,
|
||||
tools,
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
/*workspace_plugins_enabled*/ true,
|
||||
&access_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
write_connectors_config(codex_home.path(), &server_url)?;
|
||||
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let login_id = mcp
|
||||
.send_chatgpt_auth_tokens_login_request(
|
||||
access_token,
|
||||
"account-123".to_string(),
|
||||
Some("pro".to_string()),
|
||||
)
|
||||
.await?;
|
||||
let login_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(login_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<LoginAccountResponse>(login_response)?,
|
||||
LoginAccountResponse::ChatgptAuthTokens {}
|
||||
);
|
||||
|
||||
let request_id = mcp
|
||||
.send_apps_list_request(AppsListParams {
|
||||
limit: None,
|
||||
cursor: None,
|
||||
thread_id: None,
|
||||
force_refetch: true,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let AppsListResponse { data, next_cursor } = to_response(response)?;
|
||||
|
||||
assert_eq!(data.len(), 1);
|
||||
assert_eq!(data[0].id, "beta");
|
||||
assert!(data[0].is_accessible);
|
||||
assert!(next_cursor.is_none());
|
||||
|
||||
server_handle.abort();
|
||||
let _ = server_handle.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Result<()> {
|
||||
let connectors = vec![AppInfo {
|
||||
@@ -193,7 +291,12 @@ async fn list_apps_returns_empty_when_workspace_codex_plugins_disabled() -> Resu
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.without_managed_config()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -238,7 +341,11 @@ async fn list_apps_includes_plugin_apps_for_chatgpt_auth() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -298,7 +405,10 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_request = mcp
|
||||
@@ -411,7 +521,11 @@ async fn list_apps_keeps_apps_with_app_only_tools_accessible() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -486,7 +600,11 @@ enabled = false
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -609,7 +727,11 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<(
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -759,7 +881,11 @@ async fn list_apps_waits_for_accessible_data_before_emitting_directory_updates()
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -871,7 +997,11 @@ async fn list_apps_does_not_emit_empty_interim_updates() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -986,7 +1116,11 @@ async fn list_apps_paginates_results() -> Result<()> {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let first_request = mcp
|
||||
@@ -1112,7 +1246,11 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let initial_request = mcp
|
||||
@@ -1242,7 +1380,11 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let warm_request = mcp
|
||||
@@ -1557,6 +1699,7 @@ async fn start_apps_server_with_workspace_plugins_enabled(
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
workspace_plugins_enabled,
|
||||
"chatgpt-token",
|
||||
)
|
||||
.await?;
|
||||
Ok((server_url, server_handle))
|
||||
@@ -1574,6 +1717,7 @@ async fn start_apps_server_with_delays_and_control(
|
||||
directory_delay,
|
||||
tools_delay,
|
||||
/*workspace_plugins_enabled*/ true,
|
||||
"chatgpt-token",
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1584,13 +1728,14 @@ async fn start_apps_server_with_delays_and_control_inner(
|
||||
directory_delay: Duration,
|
||||
tools_delay: Duration,
|
||||
workspace_plugins_enabled: bool,
|
||||
expected_bearer: &str,
|
||||
) -> Result<(String, JoinHandle<()>, AppsServerControl)> {
|
||||
let response = Arc::new(StdMutex::new(
|
||||
json!({ "apps": connectors, "next_token": null }),
|
||||
));
|
||||
let tools = Arc::new(StdMutex::new(tools));
|
||||
let state = AppsServerState {
|
||||
expected_bearer: "Bearer chatgpt-token".to_string(),
|
||||
expected_bearer: format!("Bearer {expected_bearer}"),
|
||||
expected_account_id: "account-123".to_string(),
|
||||
response: response.clone(),
|
||||
directory_delay,
|
||||
|
||||
@@ -67,8 +67,11 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
let initialized = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_capabilities(
|
||||
@@ -91,7 +94,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
|
||||
};
|
||||
|
||||
let thread_request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
|
||||
.await?;
|
||||
let thread_response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
|
||||
@@ -41,7 +41,10 @@ async fn thread_start_with_auto_env_exposes_fixture_cwd_to_model() -> Result<()>
|
||||
"compact",
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let expected_environment = mcp.auto_env_params()?;
|
||||
|
||||
@@ -110,14 +113,17 @@ async fn auto_env_rejects_explicit_environment_config() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join("environments.toml"), "")?;
|
||||
|
||||
let result = TestAppServer::new_with_auto_env(codex_home.path()).await;
|
||||
let result = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await;
|
||||
let Err(err) = result else {
|
||||
anyhow::bail!("auto-env construction unexpectedly succeeded");
|
||||
};
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!(
|
||||
"new_with_auto_env cannot be used when {} exists",
|
||||
"automatic environment cannot be used when {} exists",
|
||||
codex_home.path().join("environments.toml").display()
|
||||
)
|
||||
);
|
||||
|
||||
@@ -59,7 +59,10 @@ async fn turn_start_forwards_client_metadata_to_responses_request_v2() -> Result
|
||||
/*supports_websockets*/ false,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
@@ -155,7 +158,11 @@ async fn turn_start_sends_fork_lineage_in_turn_metadata_for_thread_fork_v2() ->
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let ThreadForkResponse { thread, .. } =
|
||||
@@ -239,7 +246,11 @@ async fn review_start_sends_parent_lineage_in_turn_metadata_for_thread_fork_v2()
|
||||
/*git_info*/ None,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let ThreadForkResponse { thread, .. } =
|
||||
@@ -339,7 +350,11 @@ async fn turn_start_sends_nested_subagent_lineage_after_cold_thread_resume_v2()
|
||||
parent_thread_id,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let resume_req = mcp
|
||||
@@ -434,7 +449,10 @@ async fn turn_steer_updates_client_metadata_on_follow_up_responses_request_v2()
|
||||
/*supports_websockets*/ false,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
@@ -559,7 +577,10 @@ async fn turn_start_forwards_client_metadata_to_responses_websocket_request_body
|
||||
/*supports_websockets*/ true,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
|
||||
@@ -28,7 +28,11 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
#[tokio::test]
|
||||
async fn list_collaboration_modes_returns_presets() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
|
||||
@@ -42,7 +42,11 @@ async fn command_exec_without_streams_can_be_terminated() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let process_id = "sleep-1".to_string();
|
||||
@@ -92,7 +96,11 @@ async fn command_exec_without_process_id_keeps_buffered_compatibility() -> Resul
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -140,11 +148,12 @@ async fn command_exec_env_overrides_merge_with_server_environment_and_support_un
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[("COMMAND_EXEC_BASELINE", Some("server"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("COMMAND_EXEC_BASELINE", Some("server"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -198,7 +207,11 @@ async fn command_exec_accepts_permission_profile() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -249,7 +262,11 @@ async fn command_exec_permission_profile_starts_selected_network_proxy() -> Resu
|
||||
codex_home.path(),
|
||||
/*default_permissions*/ None,
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -297,7 +314,11 @@ async fn command_exec_permission_profile_does_not_reuse_default_network_proxy()
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
insert_networked_permission_profile_config(codex_home.path(), Some("networked"))?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -355,7 +376,11 @@ async fn command_exec_permission_profile_project_roots_use_command_cwd() -> Resu
|
||||
":workspace_roots" = "write"
|
||||
"#,
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -406,11 +431,12 @@ async fn command_exec_returns_error_when_local_environment_is_disabled() -> Resu
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -445,7 +471,11 @@ async fn command_exec_rejects_sandbox_policy_with_permission_profile() -> Result
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -483,7 +513,11 @@ async fn command_exec_rejects_disable_timeout_with_timeout_ms() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -521,7 +555,11 @@ async fn command_exec_rejects_disable_output_cap_with_output_bytes_cap() -> Resu
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -559,7 +597,11 @@ async fn command_exec_rejects_negative_timeout_ms() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -597,7 +639,11 @@ async fn command_exec_without_process_id_rejects_streaming() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -635,7 +681,11 @@ async fn command_exec_non_streaming_respects_output_cap() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let command_request_id = mcp
|
||||
@@ -682,7 +732,11 @@ async fn command_exec_streaming_does_not_buffer_output() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let process_id = "stream-cap-1".to_string();
|
||||
@@ -746,7 +800,11 @@ async fn command_exec_pipe_streams_output_and_accepts_write() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let process_id = "pipe-1".to_string();
|
||||
@@ -822,7 +880,11 @@ async fn command_exec_tty_implies_streaming_and_reports_pty_output() -> Result<(
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let process_id = "tty-1".to_string();
|
||||
@@ -893,7 +955,11 @@ async fn command_exec_tty_supports_initial_size_and_resize() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let process_id = "tty-size-1".to_string();
|
||||
|
||||
@@ -81,7 +81,10 @@ async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()>
|
||||
COMPACT_PROMPT,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_id = start_thread(&mut mcp).await?;
|
||||
@@ -164,8 +167,11 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_id = start_thread(&mut mcp).await?;
|
||||
@@ -271,7 +277,10 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
|
||||
COMPACT_PROMPT,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_id = start_thread(&mut mcp).await?;
|
||||
@@ -321,7 +330,10 @@ async fn thread_compact_start_rejects_invalid_thread_id() -> Result<()> {
|
||||
COMPACT_PROMPT,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -357,7 +369,10 @@ async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()> {
|
||||
COMPACT_PROMPT,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -379,7 +394,7 @@ async fn thread_compact_start_rejects_unknown_thread_id() -> Result<()> {
|
||||
|
||||
async fn start_thread(mcp: &mut TestAppServer) -> Result<String> {
|
||||
let thread_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
|
||||
@@ -55,7 +55,11 @@ async fn config_requirements_read_includes_allow_remote_control() -> Result<()>
|
||||
codex_home.path().join("requirements.toml"),
|
||||
"allow_remote_control = false\n",
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_config_requirements_read_request().await?;
|
||||
@@ -87,7 +91,11 @@ model_reasoning_effort = "medium"
|
||||
service_tier = "fast"
|
||||
"#,
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_config_requirements_read_request().await?;
|
||||
@@ -125,7 +133,11 @@ sandbox_mode = "workspace-write"
|
||||
let codex_home_path = codex_home.path().canonicalize()?;
|
||||
let user_file = AbsolutePathBuf::try_from(codex_home_path.join("config.toml"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -175,7 +187,11 @@ allowed_domains = ["example.com"]
|
||||
let codex_home_path = codex_home.path().canonicalize()?;
|
||||
let user_file = AbsolutePathBuf::try_from(codex_home_path.join("config.toml"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -246,7 +262,11 @@ forced_chatgpt_workspace_id = "{WORKSPACE_ID}"
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -285,7 +305,11 @@ forced_chatgpt_workspace_id = ["{WORKSPACE_ID_A}", "{WORKSPACE_ID_B}"]
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -327,7 +351,11 @@ location = { country = "US", city = "New York", timezone = "America/New_York" }
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -371,7 +399,11 @@ web_search = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -412,7 +444,11 @@ default_tools_approval_mode = "prompt"
|
||||
let codex_home_path = codex_home.path().canonicalize()?;
|
||||
let user_file = AbsolutePathBuf::try_from(codex_home_path.join("config.toml"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -536,7 +572,11 @@ width = 320
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -583,7 +623,11 @@ model_reasoning_effort = "high"
|
||||
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
||||
let project_config = AbsolutePathBuf::try_from(project_config_dir)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -653,14 +697,15 @@ writable_roots = [{}]
|
||||
|
||||
let managed_path_str = managed_path.display().to_string();
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(
|
||||
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
|
||||
Some(&managed_path_str),
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -749,7 +794,11 @@ model = "gpt-old"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(&codex_home).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(&codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let read_id = mcp
|
||||
@@ -810,7 +859,11 @@ async fn config_value_write_updates_desktop_settings() -> Result<()> {
|
||||
let codex_home = temp_dir.path().canonicalize()?;
|
||||
write_config(&temp_dir, "")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(&codex_home).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(&codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let write_id = mcp
|
||||
@@ -859,7 +912,11 @@ model = "gpt-old"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(&codex_home).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(&codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let write_id = mcp
|
||||
@@ -907,7 +964,11 @@ model = "gpt-old"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let write_id = mcp
|
||||
@@ -942,7 +1003,11 @@ async fn config_batch_write_applies_multiple_edits() -> Result<()> {
|
||||
let codex_home = tmp_dir.path().canonicalize()?;
|
||||
write_config(&tmp_dir, "")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(&codex_home).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(&codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let writable_root = test_tmp_path_buf();
|
||||
@@ -1014,7 +1079,11 @@ model = "gpt-5.3-spark"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(&codex_home).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(&codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let batch_id = mcp
|
||||
@@ -1070,7 +1139,11 @@ async fn config_batch_write_updates_multiple_desktop_settings() -> Result<()> {
|
||||
let codex_home = tmp_dir.path().canonicalize()?;
|
||||
write_config(&tmp_dir, "")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(&codex_home).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(&codex_home)
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let batch_id = mcp
|
||||
|
||||
@@ -40,7 +40,10 @@ async fn current_time_read_round_trip_adds_reminder_to_model_input() -> Result<(
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut app_server = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let thread_request_id = app_server
|
||||
|
||||
@@ -57,7 +57,11 @@ async fn thread_start_normalizes_legacy_dynamic_tools_into_model_request() -> Re
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let visible_schema = json!({
|
||||
@@ -171,7 +175,10 @@ async fn thread_start_rejects_hidden_dynamic_tools_without_namespace() -> Result
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec {
|
||||
@@ -210,7 +217,11 @@ async fn thread_start_rejects_invalid_dynamic_tool_inputs() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
for (dynamic_tools, expected_error) in [
|
||||
@@ -352,7 +363,10 @@ async fn dynamic_tool_call_round_trip_sends_text_content_items_to_model() -> Res
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let input_schema = json!({
|
||||
@@ -579,7 +593,10 @@ async fn start_function_dynamic_tool_call(call_id: &str) -> Result<PendingDynami
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let dynamic_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec {
|
||||
|
||||
@@ -27,7 +27,11 @@ async fn environment_add_applies_connect_timeout() -> Result<()> {
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(RPC_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let request_id = app_server
|
||||
|
||||
@@ -40,7 +40,11 @@ async fn environment_info_returns_remote_environment_info() -> Result<()> {
|
||||
});
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(RPC_TIMEOUT, app_server.initialize()).await??;
|
||||
add_environment(
|
||||
&mut app_server,
|
||||
@@ -88,7 +92,11 @@ async fn environment_info_accepts_missing_cwd() -> Result<()> {
|
||||
});
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(RPC_TIMEOUT, app_server.initialize()).await??;
|
||||
add_environment(
|
||||
&mut app_server,
|
||||
@@ -125,7 +133,11 @@ async fn environment_info_accepts_missing_cwd() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn environment_info_rejects_unknown_environment() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(RPC_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let request_id = app_server
|
||||
@@ -158,7 +170,11 @@ async fn environment_info_reports_connection_failure() -> Result<()> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let exec_server_url = format!("ws://{}", listener.local_addr()?);
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(RPC_TIMEOUT, app_server.initialize()).await??;
|
||||
add_environment(&mut app_server, &exec_server_url, Some(50)).await?;
|
||||
|
||||
|
||||
@@ -176,7 +176,12 @@ HTTP_PROXY = {http_proxy}
|
||||
}))?,
|
||||
)?;
|
||||
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
// This suite owns environments.toml to exercise explicit executor selection.
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let selected_thread = start_thread(
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_remote;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -23,6 +24,12 @@ const LOCAL_SKILL_MARKER: &str = "LOCAL_SKILL_BODY_MARKER";
|
||||
|
||||
#[tokio::test]
|
||||
async fn selected_executor_root_exposes_plugin_skill() -> Result<()> {
|
||||
// TODO(anp): Remove after selected capability-root fixtures can be materialized in remote exec.
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"selected capability root fixture is only materialized on the host"
|
||||
);
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
@@ -81,11 +88,14 @@ stream_max_retries = 0
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut app_server = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(READ_TIMEOUT, app_server.initialize()).await??;
|
||||
|
||||
let request_id = app_server
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
selected_capability_roots: Some(vec![SelectedCapabilityRoot {
|
||||
id: "demo-plugin@1".to_string(),
|
||||
|
||||
@@ -30,7 +30,11 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
#[tokio::test]
|
||||
async fn mock_experimental_method_requires_experimental_api_capability() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
@@ -62,7 +66,11 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu
|
||||
#[tokio::test]
|
||||
async fn realtime_conversation_start_requires_experimental_api_capability() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
@@ -109,7 +117,11 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
|
||||
#[tokio::test]
|
||||
async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
@@ -144,7 +156,11 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result
|
||||
#[tokio::test]
|
||||
async fn thread_settings_update_requires_experimental_api_capability() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
@@ -179,7 +195,11 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result
|
||||
#[tokio::test]
|
||||
async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
@@ -231,7 +251,10 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
@@ -270,7 +293,10 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
@@ -308,7 +334,10 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
let init = mcp
|
||||
.initialize_with_capabilities(
|
||||
default_client_info(),
|
||||
|
||||
@@ -50,7 +50,11 @@ async fn experimental_feature_list_returns_feature_metadata_with_stage() -> Resu
|
||||
))
|
||||
.build()
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -134,7 +138,12 @@ async fn experimental_feature_list_marks_apps_and_plugins_disabled_by_workspace_
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.without_managed_config()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -195,11 +204,15 @@ memories = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_managed_config()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
cwd: Some(workspace.path().display().to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -229,7 +242,11 @@ memories = true
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_list_rejects_unknown_thread_id() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -264,7 +281,11 @@ async fn experimental_feature_enablement_set_applies_to_global_and_thread_config
|
||||
let project_cwd = codex_home.path().join("project");
|
||||
std::fs::create_dir_all(&project_cwd)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let actual = set_experimental_feature_enablement(
|
||||
@@ -301,7 +322,11 @@ async fn experimental_feature_enablement_set_does_not_override_user_config() ->
|
||||
codex_home.path().join("config.toml"),
|
||||
"[features]\nmemories = false\n",
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let actual = set_experimental_feature_enablement(
|
||||
@@ -332,7 +357,11 @@ async fn experimental_feature_enablement_set_does_not_override_user_config() ->
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_enablement_set_only_updates_named_features() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
set_experimental_feature_enablement(
|
||||
@@ -407,7 +436,11 @@ async fn experimental_feature_enablement_set_only_updates_named_features() -> Re
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_enablement_set_allows_remote_control() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let remote_control_enabled = false;
|
||||
let enablement = BTreeMap::from([("remote_control".to_string(), remote_control_enabled)]);
|
||||
@@ -425,7 +458,11 @@ async fn experimental_feature_enablement_set_allows_remote_control() -> Result<(
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_enablement_set_empty_map_is_no_op() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
set_experimental_feature_enablement(
|
||||
@@ -458,7 +495,11 @@ async fn experimental_feature_enablement_set_empty_map_is_no_op() -> Result<()>
|
||||
#[tokio::test]
|
||||
async fn experimental_feature_enablement_set_ignores_invalid_features() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let actual = set_experimental_feature_enablement(
|
||||
|
||||
@@ -58,14 +58,15 @@ async fn external_agent_config_import_sends_completion_notification_for_sync_onl
|
||||
let sqlite_home = TempDir::new()?;
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let sqlite_home_dir = sqlite_home.path().display().to_string();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("HOME", Some(home_dir.as_str())),
|
||||
("CODEX_SQLITE_HOME", Some(sqlite_home_dir.as_str())),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -189,17 +190,18 @@ async fn external_agent_config_import_reports_failed_sync_import_in_completion()
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let analytics_capture_file = codex_home.path().join("analytics-events.jsonl");
|
||||
let analytics_capture_file = analytics_capture_file.display().to_string();
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[
|
||||
("HOME", Some(home_dir.as_str())),
|
||||
(
|
||||
"CODEX_ANALYTICS_EVENTS_CAPTURE_FILE",
|
||||
Some(analytics_capture_file.as_str()),
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -335,9 +337,12 @@ async fn external_agent_config_import_completed_tracks_analytics_event() -> Resu
|
||||
external_agent_home(codex_home.path()).join("projects/repo/missing.jsonl");
|
||||
let project_root = codex_home.path().join("repo");
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -458,9 +463,12 @@ async fn external_agent_config_import_sends_completion_notification_for_local_pl
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -551,9 +559,12 @@ async fn external_agent_config_import_sends_completion_notification_after_pendin
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -604,6 +615,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
let recent_timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
|
||||
let session_dir = external_agent_home(codex_home.path()).join("projects/repo");
|
||||
let session_path = session_dir.join("session.jsonl");
|
||||
let control_request = "<ide_selection>src/auth.rs:1-5</ide_selection>";
|
||||
let first_request = "Fix auth flow";
|
||||
std::fs::create_dir_all(&project_root)?;
|
||||
std::fs::create_dir_all(&session_dir)?;
|
||||
std::fs::write(
|
||||
@@ -613,7 +626,14 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
"type": "user",
|
||||
"cwd": &project_root,
|
||||
"timestamp": &recent_timestamp,
|
||||
"message": { "content": "first request" },
|
||||
"message": { "content": control_request },
|
||||
})
|
||||
.to_string(),
|
||||
serde_json::json!({
|
||||
"type": "user",
|
||||
"cwd": &project_root,
|
||||
"timestamp": &recent_timestamp,
|
||||
"message": { "content": first_request },
|
||||
})
|
||||
.to_string(),
|
||||
serde_json::json!({
|
||||
@@ -623,19 +643,17 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
"message": { "content": "first answer" },
|
||||
})
|
||||
.to_string(),
|
||||
serde_json::json!({
|
||||
"type": "custom-title",
|
||||
"customTitle": "source session title",
|
||||
})
|
||||
.to_string(),
|
||||
]
|
||||
.join("\n"),
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -653,6 +671,14 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
.await??;
|
||||
let detected: ExternalAgentConfigDetectResponse = to_response(response)?;
|
||||
assert_eq!(detected.items.len(), 1);
|
||||
assert_eq!(
|
||||
detected.items[0]
|
||||
.details
|
||||
.as_ref()
|
||||
.and_then(|details| details.sessions.first())
|
||||
.and_then(|session| session.title.as_deref()),
|
||||
Some("Fix auth flow")
|
||||
);
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
@@ -729,8 +755,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
.expect("expected imported thread")
|
||||
.clone();
|
||||
assert_eq!(imported_thread_id, thread.id.to_string());
|
||||
assert_eq!(thread.preview, "first request");
|
||||
assert_eq!(thread.name.as_deref(), Some("source session title"));
|
||||
assert_eq!(thread.preview, control_request);
|
||||
assert_eq!(thread.name.as_deref(), Some("Fix auth flow"));
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
@@ -744,13 +770,39 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
)
|
||||
.await??;
|
||||
let response: ThreadReadResponse = to_response(response)?;
|
||||
assert_eq!(response.thread.turns.len(), 1);
|
||||
let items = &response.thread.turns[0].items;
|
||||
assert_eq!(items.len(), 3);
|
||||
assert_eq!(response.thread.turns.len(), 2);
|
||||
let control_items = &response.thread.turns[0].items;
|
||||
assert_eq!(control_items.len(), 1);
|
||||
match &control_items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
text: control_request.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
let imported_items = &response.thread.turns[1].items;
|
||||
assert_eq!(imported_items.len(), 3);
|
||||
match &imported_items[0] {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![UserInput::Text {
|
||||
text: first_request.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
other => panic!("expected user message item, got {other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
items.last(),
|
||||
imported_items.last(),
|
||||
Some(&ThreadItem::AgentMessage {
|
||||
id: "item-3".into(),
|
||||
id: "item-4".into(),
|
||||
text: "<EXTERNAL SESSION IMPORTED>".into(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
@@ -804,8 +856,8 @@ async fn external_agent_config_import_creates_session_rollouts() -> Result<()> {
|
||||
)
|
||||
.await??;
|
||||
let response: ThreadReadResponse = to_response(response)?;
|
||||
assert_eq!(response.thread.turns.len(), 2);
|
||||
match &response.thread.turns[1].items[1] {
|
||||
assert_eq!(response.thread.turns.len(), 3);
|
||||
match &response.thread.turns[2].items[1] {
|
||||
ThreadItem::AgentMessage { text, .. } => assert_eq!(text, "follow-up answer"),
|
||||
other => panic!("expected agent message item, got {other:?}"),
|
||||
}
|
||||
@@ -845,9 +897,12 @@ required = true
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -931,9 +986,12 @@ async fn external_agent_config_import_accepts_detected_session_payload_after_res
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1022,9 +1080,12 @@ async fn external_agent_config_import_skips_already_imported_session_versions()
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1115,9 +1176,12 @@ async fn external_agent_config_import_returns_before_background_session_import_f
|
||||
std::fs::write(&session_path, &session_contents)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1291,9 +1355,12 @@ async fn external_agent_config_import_compacts_huge_session_before_first_follow_
|
||||
)?;
|
||||
|
||||
let home_dir = codex_home.path().display().to_string();
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("HOME", Some(home_dir.as_str()))])
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("HOME", Some(home_dir.as_str()))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -37,7 +37,11 @@ const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const OPTIONAL_FS_CHANGE_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
async fn initialized_mcp(codex_home: &TempDir) -> Result<TestAppServer> {
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
Ok(mcp)
|
||||
}
|
||||
@@ -124,11 +128,12 @@ async fn fs_methods_return_error_when_local_environment_is_disabled() -> Result<
|
||||
let codex_home = TempDir::new()?;
|
||||
let absolute_file = codex_home.path().join("absolute.txt");
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(CODEX_EXEC_SERVER_URL_ENV_VAR, Some("none"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let read_id = mcp
|
||||
|
||||
@@ -26,6 +26,7 @@ use codex_core::config::set_project_trust_level;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use core_test_support::skip_if_host_windows;
|
||||
use core_test_support::skip_if_remote;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde::Serialize;
|
||||
use tempfile::TempDir;
|
||||
@@ -135,7 +136,10 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
|
||||
let cwd = TempDir::new()?;
|
||||
write_user_hook_config(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -211,7 +215,11 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -299,7 +307,10 @@ async fn hooks_list_warms_plugin_capabilities_for_thread_start() -> Result<()> {
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let hooks_list_id = mcp
|
||||
@@ -316,7 +327,7 @@ async fn hooks_list_warms_plugin_capabilities_for_thread_start() -> Result<()> {
|
||||
std::fs::remove_file(plugin_mcp_path)?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
|
||||
.await?;
|
||||
let _: ThreadStartResponse = to_response(
|
||||
timeout(
|
||||
@@ -348,7 +359,11 @@ async fn hooks_list_shows_plugin_hook_load_warnings() -> Result<()> {
|
||||
let cwd = TempDir::new()?;
|
||||
write_plugin_hook_config(codex_home.path(), "{ not-json")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -404,7 +419,11 @@ timeout = 5
|
||||
)?;
|
||||
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -486,7 +505,11 @@ async fn hooks_list_uses_root_repo_hooks_for_linked_worktrees() -> Result<()> {
|
||||
write_project_hook_config(&worktree_root.join(".codex"), "echo worktree hook")?;
|
||||
set_project_trust_level(codex_home.path(), &repo_root, TrustLevel::Trusted)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let list_id = mcp
|
||||
@@ -556,7 +579,11 @@ async fn config_batch_write_toggles_user_hook() -> Result<()> {
|
||||
let cwd = TempDir::new()?;
|
||||
write_user_hook_config(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -652,6 +679,8 @@ async fn config_batch_write_toggles_user_hook() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn config_batch_write_updates_hook_trust_for_loaded_session() -> Result<()> {
|
||||
skip_if_host_windows!(Ok(()));
|
||||
// TODO(anp): Teach command-hook fixtures to run in selected remote environments.
|
||||
skip_if_remote!(Ok(()), "command hooks use host-local script and log paths");
|
||||
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("Warmup")?,
|
||||
@@ -707,7 +736,10 @@ command = "python3 {hook_script_path}"
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let hook_list_id = mcp
|
||||
@@ -725,7 +757,7 @@ command = "python3 {hook_script_path}"
|
||||
assert_eq!(hook.trust_status, HookTrustStatus::Untrusted);
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -903,6 +935,8 @@ command = "python3 {hook_script_path}"
|
||||
#[tokio::test]
|
||||
async fn config_batch_write_disables_hook_for_loaded_session() -> Result<()> {
|
||||
skip_if_host_windows!(Ok(()));
|
||||
// TODO(anp): Teach command-hook fixtures to run in selected remote environments.
|
||||
skip_if_remote!(Ok(()), "command hooks use host-local script and log paths");
|
||||
|
||||
let responses = vec![
|
||||
create_final_assistant_message_sse_response("Warmup")?,
|
||||
@@ -957,7 +991,10 @@ command = "python3 {hook_script_path}"
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let hook_list_id = mcp
|
||||
@@ -998,7 +1035,7 @@ command = "python3 {hook_script_path}"
|
||||
let _: codex_app_server_protocol::ConfigWriteResponse = to_response(response)?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
@@ -18,6 +19,7 @@ use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_remote;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
@@ -87,8 +89,11 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
start_image_generation_turn(&mut mcp).await?;
|
||||
|
||||
@@ -103,13 +108,13 @@ async fn standalone_image_generation_returns_saved_path_hint_to_model() -> Resul
|
||||
)
|
||||
.await??;
|
||||
|
||||
let ThreadItem::ImageGeneration {
|
||||
let ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
status,
|
||||
revised_prompt,
|
||||
result,
|
||||
saved_path: Some(saved_path),
|
||||
..
|
||||
} = completed.item
|
||||
}) = completed.item
|
||||
else {
|
||||
panic!("expected completed image generation item with saved path");
|
||||
};
|
||||
@@ -185,8 +190,11 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>
|
||||
ChatGptAuthFixture::new("access-chatgpt"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
start_image_generation_turn(&mut mcp).await?;
|
||||
|
||||
@@ -197,13 +205,13 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>
|
||||
.await??;
|
||||
assert_eq!(
|
||||
completed.item,
|
||||
ThreadItem::ImageGeneration {
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: call_id.to_string(),
|
||||
status: "failed".to_string(),
|
||||
revised_prompt: Some("paint a blue whale".to_string()),
|
||||
result: String::new(),
|
||||
saved_path: None,
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
timeout(
|
||||
@@ -227,6 +235,11 @@ async fn standalone_image_generation_failure_emits_terminal_item() -> Result<()>
|
||||
|
||||
#[tokio::test]
|
||||
async fn standalone_image_edit_uses_attached_model_visible_image() -> Result<()> {
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"remote executors use different imagegen storage approaches, so host-local image paths are unavailable"
|
||||
);
|
||||
|
||||
let edit_request = run_image_edit_test(|codex_home| {
|
||||
let image_path = codex_home.join("attached.png");
|
||||
std::fs::write(&image_path, TINY_PNG_BYTES)?;
|
||||
@@ -306,8 +319,11 @@ async fn standalone_image_generation_is_exposed_in_code_mode_only() -> Result<()
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
start_image_generation_turn(&mut mcp).await?;
|
||||
timeout(
|
||||
@@ -369,8 +385,11 @@ generatedImage(result);
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
start_image_generation_turn(&mut mcp).await?;
|
||||
timeout(
|
||||
@@ -449,8 +468,11 @@ async fn run_image_edit_test(
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
start_turn(&mut mcp, input).await?;
|
||||
timeout(
|
||||
@@ -478,7 +500,7 @@ async fn run_image_edit_test(
|
||||
|
||||
async fn start_turn(mcp: &mut TestAppServer, input: Vec<V2UserInput>) -> Result<()> {
|
||||
let thread_req = mcp
|
||||
.send_thread_start_request(ThreadStartParams::default())
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams::default())
|
||||
.await?;
|
||||
let thread_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -517,7 +539,7 @@ async fn wait_for_image_generation_completed(
|
||||
.params
|
||||
.context("item/completed notification should include params")?,
|
||||
)?;
|
||||
if matches!(&completed.item, ThreadItem::ImageGeneration { .. }) {
|
||||
if matches!(&completed.item, ThreadItem::ImageGeneration(_)) {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ async fn initialize_uses_client_info_name_as_originator() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let expected_codex_home = AbsolutePathBuf::try_from(codex_home.path().canonicalize()?)?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -68,7 +72,11 @@ async fn initialize_probe_does_not_override_originator() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -95,7 +103,11 @@ async fn initialize_codex_backend_does_not_override_originator() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -123,14 +135,15 @@ async fn initialize_respects_originator_override_env_var() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let expected_codex_home = AbsolutePathBuf::try_from(codex_home.path().canonicalize()?)?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(
|
||||
"CODEX_INTERNAL_ORIGINATOR_OVERRIDE",
|
||||
Some("codex_originator_via_env_var"),
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
)])
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -165,11 +178,12 @@ async fn initialize_rejects_invalid_client_name() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", None)],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("CODEX_INTERNAL_ORIGINATOR_OVERRIDE", None)])
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -200,7 +214,10 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu
|
||||
let server = create_mock_responses_server_sequence_unchecked(responses).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let message = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
@@ -283,7 +300,10 @@ async fn turn_start_notify_payload_includes_initialize_client_name() -> Result<(
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_client_info(ClientInfo {
|
||||
|
||||
@@ -28,7 +28,11 @@ async fn marketplace_add_local_directory_source() -> Result<()> {
|
||||
r#"{"name":"sample"}"#,
|
||||
)?;
|
||||
std::fs::write(source.join("plugins/sample/marker.txt"), "local ref")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -53,7 +53,11 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
|
||||
write_installed_marketplace(codex_home.path(), "debug")?;
|
||||
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -91,7 +95,11 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
|
||||
async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -169,7 +169,11 @@ async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> {
|
||||
)?;
|
||||
disable_plugin_startup_tasks(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let debug_root = expected_installed_root(codex_home.path(), "debug")?;
|
||||
@@ -223,7 +227,11 @@ async fn marketplace_upgrade_named_marketplace_only() -> Result<()> {
|
||||
)?;
|
||||
disable_plugin_startup_tasks(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let tools_root = expected_installed_root(codex_home.path(), "tools")?;
|
||||
@@ -264,7 +272,11 @@ async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Re
|
||||
)?;
|
||||
disable_plugin_startup_tasks(codex_home.path())?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let first_response = send_marketplace_upgrade(&mut mcp, Some("debug")).await?;
|
||||
assert!(first_response.errors.is_empty());
|
||||
@@ -292,7 +304,11 @@ async fn marketplace_upgrade_rejects_unknown_or_non_git_marketplace() -> Result<
|
||||
&configured_local_marketplace_update(&local_source.path().display().to_string()),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
for marketplace_name in ["missing", "local-only"] {
|
||||
|
||||
@@ -89,11 +89,15 @@ async fn mcp_resource_read_returns_resource_contents() -> Result<()> {
|
||||
let (apps_server_url, _apps_server_calls, apps_server_handle) =
|
||||
start_resource_apps_mcp_server().await?;
|
||||
let responses_server_uri = responses_server.uri();
|
||||
let (_codex_home, mut mcp) =
|
||||
start_resource_test_app_server(&apps_server_url, &responses_server_uri).await?;
|
||||
let (_codex_home, mut mcp) = start_resource_test_app_server(
|
||||
&apps_server_url,
|
||||
&responses_server_uri,
|
||||
ResourceTestEnvironment::Auto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -133,8 +137,12 @@ async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -
|
||||
let (apps_server_url, apps_server_calls, apps_server_handle) =
|
||||
start_resource_apps_mcp_server().await?;
|
||||
let responses_server_uri = responses_server.uri();
|
||||
let (_codex_home, mut mcp) =
|
||||
start_resource_test_app_server(&apps_server_url, &responses_server_uri).await?;
|
||||
let (_codex_home, mut mcp) = start_resource_test_app_server(
|
||||
&apps_server_url,
|
||||
&responses_server_uri,
|
||||
ResourceTestEnvironment::Auto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
@@ -369,8 +377,13 @@ async fn local_executor_does_not_expose_orchestrator_skills() -> Result<()> {
|
||||
let (apps_server_url, _apps_server_calls, apps_server_handle) =
|
||||
start_resource_apps_mcp_server().await?;
|
||||
let responses_server_uri = responses_server.uri();
|
||||
let (_codex_home, mut mcp) =
|
||||
start_resource_test_app_server(&apps_server_url, &responses_server_uri).await?;
|
||||
let (_codex_home, mut mcp) = start_resource_test_app_server(
|
||||
&apps_server_url,
|
||||
&responses_server_uri,
|
||||
// This test exercises the implicit local executor.
|
||||
ResourceTestEnvironment::Local,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
@@ -449,13 +462,13 @@ async fn disabled_orchestrator_skills_do_not_expose_skills_namespace() -> Result
|
||||
[orchestrator.skills]
|
||||
enabled = false
|
||||
"#,
|
||||
ResourceTestEnvironment::Auto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
environments: Some(Vec::new()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -552,7 +565,11 @@ apps = true
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let read_request_id = mcp
|
||||
@@ -644,15 +661,22 @@ async fn mcp_resource_read_returns_error_for_unknown_thread() -> Result<()> {
|
||||
async fn start_resource_test_app_server(
|
||||
apps_server_url: &str,
|
||||
responses_server_uri: &str,
|
||||
environment: ResourceTestEnvironment,
|
||||
) -> Result<(TempDir, TestAppServer)> {
|
||||
start_resource_test_app_server_with_extra_config(apps_server_url, responses_server_uri, "")
|
||||
.await
|
||||
start_resource_test_app_server_with_extra_config(
|
||||
apps_server_url,
|
||||
responses_server_uri,
|
||||
"",
|
||||
environment,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn start_resource_test_app_server_with_extra_config(
|
||||
apps_server_url: &str,
|
||||
responses_server_uri: &str,
|
||||
extra_config: &str,
|
||||
environment: ResourceTestEnvironment,
|
||||
) -> Result<(TempDir, TestAppServer)> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
@@ -692,11 +716,22 @@ stream_max_retries = 0
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let builder = TestAppServer::builder().with_codex_home(codex_home.path());
|
||||
let builder = match environment {
|
||||
ResourceTestEnvironment::Auto => builder,
|
||||
// The Local caller explicitly exercises the implicit local executor.
|
||||
ResourceTestEnvironment::Local => builder.without_auto_env(),
|
||||
};
|
||||
let mut mcp = builder.build().await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
Ok((codex_home, mcp))
|
||||
}
|
||||
|
||||
enum ResourceTestEnvironment {
|
||||
Auto,
|
||||
Local,
|
||||
}
|
||||
|
||||
async fn start_resource_apps_mcp_server()
|
||||
-> Result<(String, Arc<ResourceAppsMcpCalls>, JoinHandle<()>)> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
|
||||
@@ -422,7 +422,10 @@ impl ElicitationRoundTripFixture {
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.initialize_with_capabilities(
|
||||
|
||||
@@ -67,7 +67,11 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -132,11 +136,14 @@ async fn mcp_server_status_list_uses_thread_project_local_config() -> Result<()>
|
||||
std::fs::create_dir_all(workspace.path().join(".git"))?;
|
||||
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
cwd: Some(workspace.path().to_string_lossy().into_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
@@ -336,7 +343,11 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -401,7 +412,11 @@ url = "{underscore_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -107,7 +107,10 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
@@ -174,7 +177,11 @@ url = "{mcp_server_url}/mcp"
|
||||
#[tokio::test]
|
||||
async fn mcp_server_tool_call_returns_error_for_unknown_thread() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -225,7 +232,10 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
@@ -336,7 +346,12 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
// This test adds and refreshes an explicitly selected runtime environment.
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
let add_environment_id = mcp
|
||||
.send_raw_request(
|
||||
@@ -474,7 +489,10 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
@@ -594,7 +612,10 @@ url = "{mcp_server_url}/mcp"
|
||||
));
|
||||
std::fs::write(config_path, config_toml)?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
|
||||
@@ -36,7 +36,11 @@ async fn memory_reset_clears_memory_files_and_rows_preserves_threads() -> Result
|
||||
|
||||
let thread_id = seed_stage1_output(&state_db, codex_home.path()).await?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -96,7 +96,11 @@ fn expected_visible_models() -> Vec<Model> {
|
||||
async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -130,7 +134,11 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
||||
async fn list_models_includes_hidden_models() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -216,8 +224,12 @@ openai_base_url = "{server_uri}/v1"
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -273,7 +285,11 @@ openai_base_url = "{server_uri}/v1"
|
||||
async fn list_models_pagination_works() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -322,7 +338,11 @@ async fn list_models_pagination_works() -> Result<()> {
|
||||
async fn list_models_rejects_invalid_cursor() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
#[tokio::test]
|
||||
async fn read_default_provider_capabilities() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -46,7 +50,11 @@ async fn read_amazon_bedrock_provider_capabilities() -> Result<()> {
|
||||
r#"model_provider = "amazon-bedrock"
|
||||
"#,
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -32,7 +32,10 @@ async fn turn_start_accepts_output_schema_v2() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
@@ -115,7 +118,10 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_req = mcp
|
||||
|
||||
@@ -41,7 +41,11 @@ description = "Inspect without writes."
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -113,7 +117,11 @@ description = "Project-scoped profile."
|
||||
)?;
|
||||
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let first_request_id = mcp
|
||||
@@ -190,7 +198,11 @@ description = "Project-scoped profile."
|
||||
)?;
|
||||
set_project_trust_level(codex_home.path(), workspace.path(), TrustLevel::Trusted)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
@@ -53,7 +53,10 @@ async fn plan_mode_uses_proposed_plan_block_for_plan_item() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let turn = start_plan_mode_turn(&mut mcp).await?;
|
||||
@@ -111,7 +114,10 @@ async fn plan_mode_without_proposed_plan_does_not_emit_plan_item() -> Result<()>
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_auto_env(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let _turn = start_plan_mode_turn(&mut mcp).await?;
|
||||
|
||||
@@ -72,7 +72,11 @@ const TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS: &str =
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -99,7 +103,11 @@ async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_missing_install_source() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -128,7 +136,11 @@ async fn plugin_install_rejects_missing_install_source() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn plugin_install_rejects_multiple_install_sources() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -165,7 +177,11 @@ async fn plugin_install_rejects_remote_marketplace_when_plugins_are_disabled() -
|
||||
plugins = false
|
||||
"#,
|
||||
)?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -231,11 +247,12 @@ async fn plugin_install_writes_remote_plugin_to_cloud_and_cache() -> Result<()>
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -318,11 +335,12 @@ async fn plugin_install_uses_remote_apps_needing_auth_response() -> Result<()> {
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
mount_remote_plugin_install_with_apps_needing_auth(&server, REMOTE_PLUGIN_ID, &["alpha"]).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -370,7 +388,11 @@ async fn plugin_install_rejects_missing_remote_bundle_url() -> Result<()> {
|
||||
.await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -411,7 +433,11 @@ async fn plugin_install_rejects_plain_http_remote_bundle_url() -> Result<()> {
|
||||
mount_remote_plugin_detail(&server, REMOTE_PLUGIN_ID, "1.2.3", Some(&bundle_url)).await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -457,7 +483,11 @@ async fn plugin_install_rejects_invalid_remote_release_version() -> Result<()> {
|
||||
.await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -489,7 +519,11 @@ async fn plugin_install_rejects_invalid_remote_release_version() -> Result<()> {
|
||||
async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_remote_plugin_catalog_config(codex_home.path(), "https://example.invalid/backend-api/")?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -519,7 +553,11 @@ async fn plugin_install_tracks_analytics_when_remote_detail_fetch_fails() -> Res
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
mount_backend_analytics_events(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -570,11 +608,12 @@ async fn plugin_install_rejects_remote_plugin_disabled_by_admin_before_download(
|
||||
.await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -623,7 +662,11 @@ async fn plugin_install_rejects_remote_plugin_not_available() -> Result<()> {
|
||||
.await;
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -686,7 +729,11 @@ async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Resul
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -715,7 +762,11 @@ async fn plugin_install_rejects_when_workspace_codex_plugins_disabled() -> Resul
|
||||
#[tokio::test]
|
||||
async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -756,7 +807,11 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -805,8 +860,12 @@ async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin()
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp =
|
||||
TestAppServer::new_with_args(codex_home.path(), &["--session-source", "atlas"]).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_args(&["--session-source", "atlas"])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -855,7 +914,11 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> {
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -921,7 +984,11 @@ async fn plugin_install_failure_tracks_analytics_event() -> Result<()> {
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -972,11 +1039,12 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> {
|
||||
mount_remote_plugin_install(&server, REMOTE_PLUGIN_ID).await;
|
||||
mount_backend_analytics_events(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -1023,11 +1091,12 @@ async fn plugin_install_preserves_status_when_remote_bundle_error_body_is_too_la
|
||||
mount_remote_plugin_install(&server, REMOTE_PLUGIN_ID).await;
|
||||
mount_backend_analytics_events(&server).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -1151,7 +1220,11 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> {
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let directory_requests_before_install = server_control.directory_request_count();
|
||||
|
||||
@@ -1238,7 +1311,11 @@ async fn plugin_install_skips_mcp_oauth_for_chatgpt_dual_surface_plugin() -> Res
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1298,7 +1375,11 @@ async fn plugin_install_starts_mcp_oauth_with_formerly_disallowed_plugin_app() -
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1395,7 +1476,11 @@ async fn plugin_install_starts_mcp_oauth_through_protected_resource_metadata() -
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1458,11 +1543,12 @@ connectors = true
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[("OPENAI_API_KEY", Some("test-api-key"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", Some("test-api-key"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
@@ -1500,11 +1586,12 @@ async fn plugin_install_starts_remote_mcp_oauth_for_install_response_only_app()
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
mount_remote_plugin_install_with_apps_needing_auth(&server, REMOTE_PLUGIN_ID, &["alpha"]).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -1552,11 +1639,12 @@ async fn plugin_install_skips_remote_mcp_oauth_for_bundled_same_name_app() -> Re
|
||||
mount_empty_remote_installed_plugins(&server).await;
|
||||
mount_remote_plugin_install_with_apps_needing_auth(&server, REMOTE_PLUGIN_ID, &["alpha"]).await;
|
||||
|
||||
let mut mcp = TestAppServer::new_with_env(
|
||||
codex_home.path(),
|
||||
&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))],
|
||||
)
|
||||
.await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.with_env_overrides(&[(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1"))])
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = send_remote_plugin_install_request(&mut mcp, REMOTE_PLUGIN_ID).await?;
|
||||
@@ -1634,7 +1722,11 @@ async fn plugin_install_includes_formerly_disallowed_apps_needing_auth() -> Resu
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
let directory_requests_before_install =
|
||||
warm_app_directory_cache(&mut mcp, &server_control, "Alpha").await?;
|
||||
@@ -1717,7 +1809,11 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let mut mcp = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.without_auto_env()
|
||||
.build()
|
||||
.await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user