cloud-tasks: split the mock client out of cloud-tasks-client (#16456)

## Why

`codex-cloud-tasks-client` was mixing two different roles: the real HTTP
client and the mock implementation used by tests and local mock mode.
Keeping both in the same crate forced Cargo feature toggles and Bazel
`crate_features` just to pick an implementation.

This change keeps `codex-cloud-tasks-client` focused on the shared API
surface and real backend client, and moves the mock implementation into
its own crate so we can remove those feature permutations cleanly.

## What changed

- add a new `codex-cloud-tasks-mock-client` crate that owns `MockClient`
- remove the `mock` and `online` features from
`codex-cloud-tasks-client`
- make `codex-cloud-tasks-client` unconditionally depend on
`codex-backend-client` and export `HttpClient` directly
- gate the mock-mode path in `codex-cloud-tasks` behind
`#[cfg(debug_assertions)]`, so release builds always initialize the real
HTTP client
- update `codex-cloud-tasks` and its tests to use
`codex-cloud-tasks-mock-client::MockClient` wherever mock behavior is
needed
- remove the matching Bazel `crate_features` override and shrink the
manifest verifier allowlist accordingly

## How tested

- `cargo test -p codex-cloud-tasks-client`
- `cargo test -p codex-cloud-tasks-mock-client`
- `cargo test -p codex-cloud-tasks`

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16456).
* #16457
* __->__ #16456
This commit is contained in:
Michael Bolin
2026-04-01 12:09:14 -07:00
committed by GitHub
Unverified
parent dc263f5926
commit 9f0be146db
13 changed files with 93 additions and 85 deletions
@@ -27,11 +27,6 @@ UTILITY_NAME_EXCEPTIONS = {
"path-utils": "codex-utils-path",
}
MANIFEST_FEATURE_EXCEPTIONS = {
"codex-rs/cloud-tasks-client/Cargo.toml": {
"default": ("online",),
"online": ("dep:codex-backend-client",),
"mock": (),
},
"codex-rs/otel/Cargo.toml": {
"disable-default-metrics-exporter": (),
},
@@ -43,11 +38,6 @@ MANIFEST_FEATURE_EXCEPTIONS = {
},
}
OPTIONAL_DEPENDENCY_EXCEPTIONS = {
(
"codex-rs/cloud-tasks-client/Cargo.toml",
"dependencies",
"codex-backend-client",
),
(
"codex-rs/tui/Cargo.toml",
'target.cfg(not(target_os = "linux")).dependencies',
@@ -55,11 +45,6 @@ OPTIONAL_DEPENDENCY_EXCEPTIONS = {
),
}
INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS = {
(
"codex-rs/cloud-tasks/Cargo.toml",
"dependencies",
"codex-cloud-tasks-client",
): ("mock", "online"),
(
"codex-rs/core/Cargo.toml",
"dev-dependencies",
+11 -1
View File
@@ -1721,6 +1721,7 @@ dependencies = [
"clap",
"codex-client",
"codex-cloud-tasks-client",
"codex-cloud-tasks-mock-client",
"codex-core",
"codex-git-utils",
"codex-login",
@@ -1750,12 +1751,21 @@ dependencies = [
"chrono",
"codex-backend-client",
"codex-git-utils",
"diffy",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "codex-cloud-tasks-mock-client"
version = "0.0.0"
dependencies = [
"async-trait",
"chrono",
"codex-cloud-tasks-client",
"diffy",
]
[[package]]
name = "codex-code-mode"
version = "0.0.0"
+12 -9
View File
@@ -18,6 +18,7 @@ members = [
"cloud-requirements",
"cloud-tasks",
"cloud-tasks-client",
"cloud-tasks-mock-client",
"cli",
"connectors",
"config",
@@ -96,10 +97,9 @@ license = "Apache-2.0"
[workspace.dependencies]
# Internal
app_test_support = { path = "app-server/tests/common" }
codex-ansi-escape = { path = "ansi-escape" }
codex-analytics = { path = "analytics" }
codex-ansi-escape = { path = "ansi-escape" }
codex-api = { path = "codex-api" }
codex-code-mode = { path = "code-mode" }
codex-app-server = { path = "app-server" }
codex-app-server-client = { path = "app-server-client" }
codex-app-server-protocol = { path = "app-server-protocol" }
@@ -112,16 +112,19 @@ codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli" }
codex-client = { path = "codex-client" }
codex-cloud-requirements = { path = "cloud-requirements" }
codex-connectors = { path = "connectors" }
codex-cloud-tasks-client = { path = "cloud-tasks-client" }
codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" }
codex-code-mode = { path = "code-mode" }
codex-config = { path = "config" }
codex-connectors = { path = "connectors" }
codex-core = { path = "core" }
codex-core-skills = { path = "core-skills" }
codex-exec = { path = "exec" }
codex-exec-server = { path = "exec-server" }
codex-execpolicy = { path = "execpolicy" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
codex-feedback = { path = "feedback" }
codex-features = { path = "features" }
codex-feedback = { path = "feedback" }
codex-file-search = { path = "file-search" }
codex-git-utils = { path = "git-utils" }
codex-hooks = { path = "hooks" }
@@ -137,9 +140,9 @@ codex-otel = { path = "otel" }
codex-plugin = { path = "plugin" }
codex-process-hardening = { path = "process-hardening" }
codex-protocol = { path = "protocol" }
codex-rollout = { path = "rollout" }
codex-responses-api-proxy = { path = "responses-api-proxy" }
codex-rmcp-client = { path = "rmcp-client" }
codex-rollout = { path = "rollout" }
codex-sandboxing = { path = "sandboxing" }
codex-secrets = { path = "secrets" }
codex-shell-command = { path = "shell-command" }
@@ -150,7 +153,6 @@ codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-terminal-detection = { path = "terminal-detection" }
codex-tools = { path = "tools" }
codex-tui = { path = "tui" }
codex-v8-poc = { path = "v8-poc" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
codex-utils-approval-presets = { path = "utils/approval-presets" }
codex-utils-cache = { path = "utils/cache" }
@@ -171,8 +173,9 @@ codex-utils-rustls-provider = { path = "utils/rustls-provider" }
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
codex-utils-stream-parser = { path = "utils/stream-parser" }
codex-utils-template = { path = "utils/template" }
codex-utils-string = { path = "utils/string" }
codex-utils-template = { path = "utils/template" }
codex-v8-poc = { path = "v8-poc" }
codex-windows-sandbox = { path = "windows-sandbox-rs" }
core_test_support = { path = "core/tests/common" }
mcp_test_support = { path = "mcp-server/tests/common" }
@@ -217,12 +220,12 @@ gethostname = "1.1.0"
globset = "0.4"
hmac = "0.12.1"
http = "1.3.1"
iana-time-zone = "0.1.64"
icu_decimal = "2.1"
icu_locale_core = "2.1"
icu_provider = { version = "2.1", features = ["sync"] }
ignore = "0.4.23"
image = { version = "^0.25.9", default-features = false }
iana-time-zone = "0.1.64"
include_dir = "0.7.4"
indexmap = "2.12.0"
insta = "1.46.3"
@@ -264,7 +267,6 @@ regex-lite = "0.1.8"
reqwest = "0.12"
rmcp = { version = "0.15.0", default-features = false }
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
v8 = "=146.4.0"
rustls = { version = "0.23", default-features = false, features = [
"ring",
"std",
@@ -333,6 +335,7 @@ unicode-width = "0.2"
url = "2"
urlencoding = "2.1"
uuid = "1"
v8 = "=146.4.0"
vt100 = "0.16.2"
walkdir = "2.5.0"
webbrowser = "1.0"
-4
View File
@@ -3,8 +3,4 @@ load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "cloud-tasks-client",
crate_name = "codex_cloud_tasks_client",
crate_features = [
"mock",
"online",
],
)
+9 -15
View File
@@ -1,8 +1,8 @@
[package]
name = "codex-cloud-tasks-client"
version.workspace = true
edition.workspace = true
license.workspace = true
name = "codex-cloud-tasks-client"
version.workspace = true
[lib]
name = "codex_cloud_tasks_client"
@@ -11,18 +11,12 @@ path = "src/lib.rs"
[lints]
workspace = true
[features]
default = ["online"]
online = ["dep:codex-backend-client"]
mock = []
[dependencies]
anyhow = "1"
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
diffy = "0.4.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2.0.17"
codex-backend-client = { path = "../backend-client", optional = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codex-backend-client = { workspace = true }
codex-git-utils = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
-11
View File
@@ -15,16 +15,5 @@ pub use api::TaskSummary;
pub use api::TaskText;
pub use api::TurnAttempt;
#[cfg(feature = "mock")]
mod mock;
#[cfg(feature = "online")]
mod http;
#[cfg(feature = "mock")]
pub use mock::MockClient;
#[cfg(feature = "online")]
pub use http::HttpClient;
// Reusable apply engine now lives in the shared crate `codex-git-utils`.
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "cloud-tasks-mock-client",
crate_name = "codex_cloud_tasks_mock_client",
)
@@ -0,0 +1,19 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-cloud-tasks-mock-client"
version.workspace = true
[lib]
name = "codex_cloud_tasks_mock_client"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
async-trait = { workspace = true }
chrono = { workspace = true }
codex-cloud-tasks-client = { workspace = true }
diffy = { workspace = true }
@@ -0,0 +1,3 @@
mod mock;
pub use mock::MockClient;
@@ -1,15 +1,18 @@
use crate::ApplyOutcome;
use crate::AttemptStatus;
use crate::CloudBackend;
use crate::CloudTaskError;
use crate::DiffSummary;
use crate::Result;
use crate::TaskId;
use crate::TaskStatus;
use crate::TaskSummary;
use crate::TurnAttempt;
use crate::api::TaskText;
use chrono::Utc;
use codex_cloud_tasks_client::ApplyOutcome;
use codex_cloud_tasks_client::ApplyStatus;
use codex_cloud_tasks_client::AttemptStatus;
use codex_cloud_tasks_client::CloudBackend;
use codex_cloud_tasks_client::CloudTaskError;
use codex_cloud_tasks_client::CreatedTask;
use codex_cloud_tasks_client::DiffSummary;
use codex_cloud_tasks_client::Result;
use codex_cloud_tasks_client::TaskId;
use codex_cloud_tasks_client::TaskListPage;
use codex_cloud_tasks_client::TaskStatus;
use codex_cloud_tasks_client::TaskSummary;
use codex_cloud_tasks_client::TaskText;
use codex_cloud_tasks_client::TurnAttempt;
#[derive(Clone, Default)]
pub struct MockClient;
@@ -21,7 +24,7 @@ impl CloudBackend for MockClient {
_env: Option<&str>,
_limit: Option<i64>,
_cursor: Option<&str>,
) -> Result<crate::TaskListPage> {
) -> Result<TaskListPage> {
// Slightly vary content by env to aid tests that rely on the mock
let rows = match _env {
Some("env-A") => vec![("T-2000", "A: First", TaskStatus::Ready)],
@@ -63,7 +66,7 @@ impl CloudBackend for MockClient {
attempt_total: Some(if id_str == "T-1000" { 2 } else { 1 }),
});
}
Ok(crate::TaskListPage {
Ok(TaskListPage {
tasks: out,
cursor: None,
})
@@ -104,7 +107,7 @@ impl CloudBackend for MockClient {
async fn apply_task(&self, id: TaskId, _diff_override: Option<String>) -> Result<ApplyOutcome> {
Ok(ApplyOutcome {
applied: true,
status: crate::ApplyStatus::Success,
status: ApplyStatus::Success,
message: format!("Applied task {} locally (mock)", id.0),
skipped_paths: Vec::new(),
conflict_paths: Vec::new(),
@@ -118,7 +121,7 @@ impl CloudBackend for MockClient {
) -> Result<ApplyOutcome> {
Ok(ApplyOutcome {
applied: false,
status: crate::ApplyStatus::Success,
status: ApplyStatus::Success,
message: format!("Preflight passed for task {} (mock)", id.0),
skipped_paths: Vec::new(),
conflict_paths: Vec::new(),
@@ -150,10 +153,10 @@ impl CloudBackend for MockClient {
git_ref: &str,
qa_mode: bool,
best_of_n: usize,
) -> Result<crate::CreatedTask> {
) -> Result<CreatedTask> {
let _ = (env_id, prompt, git_ref, qa_mode, best_of_n);
let id = format!("task_local_{}", chrono::Utc::now().timestamp_millis());
Ok(crate::CreatedTask { id: TaskId(id) })
Ok(CreatedTask { id: TaskId(id) })
}
}
+8 -10
View File
@@ -1,8 +1,8 @@
[package]
name = "codex-cloud-tasks"
version.workspace = true
edition.workspace = true
license.workspace = true
name = "codex-cloud-tasks"
version.workspace = true
[lib]
name = "codex_cloud_tasks"
@@ -16,32 +16,30 @@ anyhow = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] }
codex-cloud-tasks-client = { path = "../cloud-tasks-client", features = [
"mock",
"online",
] }
codex-client = { workspace = true }
codex-core = { path = "../core" }
codex-cloud-tasks-client = { workspace = true }
# TODO: codex-cloud-tasks-mock-client should be in dev-dependencies.
codex-cloud-tasks-mock-client = { workspace = true }
codex-core = { workspace = true }
codex-git-utils = { workspace = true }
codex-login = { path = "../login" }
codex-tui = { workspace = true }
codex-utils-cli = { workspace = true }
crossterm = { workspace = true, features = ["event-stream"] }
owo-colors = { workspace = true, features = ["supports-colors"] }
ratatui = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
supports-color = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
tokio-stream = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
unicode-width = { workspace = true }
owo-colors = { workspace = true, features = ["supports-colors"] }
supports-color = { workspace = true }
[dependencies.async-trait]
workspace = true
[dev-dependencies]
async-trait = { workspace = true }
pretty_assertions = { workspace = true }
+4 -2
View File
@@ -40,6 +40,7 @@ struct BackendContext {
}
async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext> {
#[cfg(debug_assertions)]
let use_mock = matches!(
std::env::var("CODEX_CLOUD_TASKS_MODE").ok().as_deref(),
Some("mock") | Some("MOCK")
@@ -49,9 +50,10 @@ async fn init_backend(user_agent_suffix: &str) -> anyhow::Result<BackendContext>
set_user_agent_suffix(user_agent_suffix);
#[cfg(debug_assertions)]
if use_mock {
return Ok(BackendContext {
backend: Arc::new(codex_cloud_tasks_client::MockClient),
backend: Arc::new(codex_cloud_tasks_mock_client::MockClient),
base_url,
});
}
@@ -2132,10 +2134,10 @@ mod tests {
use super::*;
use crate::resolve_git_ref_with_git_info;
use codex_cloud_tasks_client::DiffSummary;
use codex_cloud_tasks_client::MockClient;
use codex_cloud_tasks_client::TaskId;
use codex_cloud_tasks_client::TaskStatus;
use codex_cloud_tasks_client::TaskSummary;
use codex_cloud_tasks_mock_client::MockClient;
use codex_tui::ComposerAction;
use codex_tui::ComposerInput;
use crossterm::event::KeyCode;
+1 -1
View File
@@ -1,5 +1,5 @@
use codex_cloud_tasks_client::CloudBackend;
use codex_cloud_tasks_client::MockClient;
use codex_cloud_tasks_mock_client::MockClient;
#[tokio::test]
async fn mock_backend_varies_by_env() {