use std::collections::BTreeSet;
use std::process::Command;
use codex_wrapper::{
ApprovalPolicy, ApprovalPolicyConfig, CodexCommand, Color, ExecCommand, ExecResumeCommand,
ForkCommand, ResumeCommand, ReviewCommand, SandboxMode, WebSearchMode,
};
const SENTINEL: &str = "__codex_wrapper_contract_probe__";
const ENUM_CONFIG_KEYS: &[&str] = &["approval_policy", "web_search", "sandbox_mode"];
struct Emitted {
subcommand: Vec<String>,
flags: BTreeSet<String>,
config_keys: Vec<(String, String)>,
}
fn split(args: &[String], subcommand_len: usize) -> Emitted {
let subcommand: Vec<String> = args[..subcommand_len].to_vec();
let mut flags = BTreeSet::new();
let mut config_keys = Vec::new();
let mut rest = args[subcommand_len..].iter().peekable();
while let Some(arg) = rest.next() {
if !arg.starts_with('-') {
continue;
}
flags.insert(arg.clone());
if (arg == "-c" || arg == "--config")
&& let Some(payload) = rest.next()
&& let Some((key, value)) = payload.split_once('=')
{
config_keys.push((key.to_string(), value.trim_matches('"').to_string()));
}
}
Emitted {
subcommand,
flags,
config_keys,
}
}
fn run_codex(args: &[String]) -> String {
let output = Command::new("codex")
.args(args)
.stdin(std::process::Stdio::null())
.output()
.expect("codex binary must be in PATH; run with --ignored only when it is");
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn cli_version() -> String {
run_codex(&["--version".to_string()]).trim().to_string()
}
fn help_flags(subcommand: &[String]) -> BTreeSet<String> {
let mut args = subcommand.to_vec();
args.push("--help".into());
let help = run_codex(&args);
let mut flags = BTreeSet::new();
for line in help.lines() {
let indent = line.len() - line.trim_start().len();
if !(indent == 2 || indent == 6) {
continue;
}
let trimmed = line.trim_start();
if !trimmed.starts_with('-') {
continue;
}
let head = trimmed.split('<').next().unwrap_or(trimmed);
for token in head.split([',', ' ']) {
let token = token.trim();
if token.starts_with('-') && token.len() > 1 {
flags.insert(token.to_string());
}
}
}
assert!(
!flags.is_empty(),
"no option lines parsed from `codex {} --help`; the help format changed \
and this harness needs updating",
subcommand.join(" ")
);
flags
}
fn probe_config_key(subcommand: &[String], key: &str) -> Result<BTreeSet<String>, String> {
let mut args = subcommand.to_vec();
args.push("--strict-config".into());
args.push("-c".into());
args.push(format!("{key}=\"{SENTINEL}\""));
let output = run_codex(&args);
if output.contains(&format!("unknown configuration field `{key}`")) {
return Err(format!(
"config key `{key}` no longer exists: {}",
output.lines().next().unwrap_or("").trim()
));
}
let marker = format!("unknown variant `{SENTINEL}`, expected one of ");
let Some(rest) = output.split(&marker).nth(1) else {
return Err(format!(
"probe of `{key}` produced no recognizable error; the CLI's config \
diagnostics changed and this harness needs updating. Output:\n{output}"
));
};
Ok(rest
.lines()
.next()
.unwrap_or("")
.split(',')
.map(|v| v.trim().trim_matches('`').to_string())
.filter(|v| !v.is_empty())
.collect())
}
fn assert_contract(label: &str, args: Vec<String>, subcommand_len: usize) {
let emitted = split(&args, subcommand_len);
let accepted = help_flags(&emitted.subcommand);
let mut drift = Vec::new();
for flag in &emitted.flags {
if !accepted.contains(flag) {
drift.push(format!(
"flag `{flag}` is not listed by `codex {} --help`",
emitted.subcommand.join(" ")
));
}
}
for (key, value) in &emitted.config_keys {
if !ENUM_CONFIG_KEYS.contains(&key.as_str()) {
drift.push(format!(
"config key `{key}` has no probe policy; add it to ENUM_CONFIG_KEYS \
if its value set is closed, or teach the harness how to check it \
without starting a session"
));
continue;
}
match probe_config_key(&emitted.subcommand, key) {
Err(message) => drift.push(message),
Ok(valid) => {
if !valid.contains(value) {
drift.push(format!(
"config key `{key}` no longer accepts `{value}`; valid values are {}",
valid
.iter()
.map(|v| format!("`{v}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
}
}
}
assert!(
drift.is_empty(),
"{label} has drifted from the installed CLI ({}):\n - {}\n\nEmitted argv: {args:?}",
cli_version(),
drift.join("\n - ")
);
}
#[test]
#[ignore]
fn exec_contract() {
let args = ExecCommand::new("probe")
.approval_policy(ApprovalPolicyConfig::Granular)
.search_mode(WebSearchMode::Live)
.enable("feature")
.disable("other")
.image("/tmp/a.png")
.model("o3")
.oss()
.local_provider("ollama")
.sandbox(SandboxMode::WorkspaceWrite)
.strict_config()
.ignore_user_config()
.ignore_rules()
.profile("default")
.cd("/tmp")
.skip_git_repo_check()
.add_dir("/tmp/extra")
.ephemeral()
.output_schema("/tmp/schema.json")
.color(Color::Never)
.json()
.output_last_message("/tmp/last.txt")
.args();
assert_contract("ExecCommand", args, 1);
}
#[test]
#[ignore]
fn exec_full_auto_contract() {
let args = ExecCommand::new("probe").full_auto().args();
assert_contract("ExecCommand::full_auto", args, 1);
}
#[test]
#[ignore]
fn exec_resume_contract() {
let args = ExecResumeCommand::new()
.last()
.prompt("probe")
.approval_policy(ApprovalPolicyConfig::OnFailure)
.search_mode(WebSearchMode::Cached)
.enable("feature")
.disable("other")
.image("/tmp/a.png")
.model("o3")
.strict_config()
.ignore_user_config()
.ignore_rules()
.output_schema("/tmp/schema.json")
.skip_git_repo_check()
.ephemeral()
.json()
.output_last_message("/tmp/last.txt")
.args();
assert_contract("ExecResumeCommand", args, 2);
}
#[test]
#[ignore]
fn exec_resume_full_auto_contract() {
let args = ExecResumeCommand::new().last().full_auto().args();
assert_contract("ExecResumeCommand::full_auto", args, 2);
}
#[test]
#[ignore]
fn review_contract() {
let args = ReviewCommand::new()
.prompt("probe")
.approval_policy(ApprovalPolicyConfig::Never)
.search_mode(WebSearchMode::Indexed)
.enable("feature")
.disable("other")
.uncommitted()
.model("o3")
.title("probe")
.strict_config()
.ignore_user_config()
.ignore_rules()
.output_schema("/tmp/schema.json")
.skip_git_repo_check()
.ephemeral()
.json()
.output_last_message("/tmp/last.txt")
.args();
assert_contract("ReviewCommand", args, 2);
}
#[test]
#[ignore]
fn review_full_auto_contract() {
let args = ReviewCommand::new().uncommitted().full_auto().args();
assert_contract("ReviewCommand::full_auto", args, 2);
}
#[test]
#[ignore]
fn fork_contract() {
let args = ForkCommand::new()
.last()
.prompt("probe")
.enable("feature")
.disable("other")
.image("/tmp/a.png")
.model("o3")
.oss()
.local_provider("ollama")
.profile("default")
.sandbox(SandboxMode::WorkspaceWrite)
.approval_policy(ApprovalPolicy::Never)
.strict_config()
.no_alt_screen()
.remote("ws://127.0.0.1:9000")
.remote_auth_token_env("CODEX_TOKEN")
.cd("/tmp")
.search()
.add_dir("/tmp/extra")
.args();
assert_contract("ForkCommand", args, 1);
}
#[test]
#[ignore]
fn fork_full_auto_contract() {
let args = ForkCommand::new().last().full_auto().args();
assert_contract("ForkCommand::full_auto", args, 1);
}
#[test]
#[ignore]
fn resume_contract() {
let args = ResumeCommand::new()
.last()
.prompt("probe")
.enable("feature")
.disable("other")
.image("/tmp/a.png")
.model("o3")
.oss()
.local_provider("ollama")
.profile("default")
.sandbox(SandboxMode::WorkspaceWrite)
.approval_policy(ApprovalPolicy::Never)
.strict_config()
.no_alt_screen()
.include_non_interactive()
.remote("ws://127.0.0.1:9000")
.remote_auth_token_env("CODEX_TOKEN")
.cd("/tmp")
.search()
.add_dir("/tmp/extra")
.args();
assert_contract("ResumeCommand", args, 1);
}
#[test]
#[ignore]
fn resume_full_auto_contract() {
let args = ResumeCommand::new().last().full_auto().args();
assert_contract("ResumeCommand::full_auto", args, 1);
}
#[test]
#[ignore]
fn removed_exec_flags_are_still_absent() {
let accepted = help_flags(&["exec".to_string()]);
for flag in ["--ask-for-approval", "--search", "--progress-cursor"] {
assert!(
!accepted.contains(flag),
"`{flag}` is listed by `codex exec --help` again; the config-key \
workaround added for #53 may no longer be needed"
);
}
}
#[test]
#[ignore]
fn no_builder_emits_full_auto() {
let commands: Vec<(&str, Vec<String>)> = vec![
("ExecCommand", ExecCommand::new("probe").full_auto().args()),
(
"ExecResumeCommand",
ExecResumeCommand::new().last().full_auto().args(),
),
(
"ReviewCommand",
ReviewCommand::new().uncommitted().full_auto().args(),
),
("ForkCommand", ForkCommand::new().last().full_auto().args()),
(
"ResumeCommand",
ResumeCommand::new().last().full_auto().args(),
),
];
for (label, args) in commands {
assert!(
!args.iter().any(|a| a == "--full-auto"),
"{label} emits --full-auto: {args:?}"
);
}
}
#[test]
#[ignore]
fn harness_detects_a_removed_flag() {
let args = vec![
"exec".to_string(),
"--codex-wrapper-not-a-real-flag".to_string(),
];
let result = std::panic::catch_unwind(|| assert_contract("synthetic", args, 1));
assert!(
result.is_err(),
"harness accepted a flag the CLI does not have"
);
}
#[test]
#[ignore]
fn harness_detects_a_removed_config_key() {
let error = probe_config_key(&["exec".to_string()], "codex_wrapper_not_a_real_key")
.expect_err("harness accepted a config key the CLI does not have");
assert!(
error.contains("no longer exists"),
"expected a removed-key diagnosis, got: {error}"
);
}
#[test]
#[ignore]
fn harness_detects_a_removed_config_value() {
let valid = probe_config_key(&["exec".to_string()], "approval_policy")
.expect("approval_policy should still exist");
assert!(
!valid.contains("not-a-real-policy"),
"probe returned a value set that accepts anything: {valid:?}"
);
assert!(
valid.contains("never") && valid.contains("granular"),
"value set parsed as {valid:?}, which does not look like approval_policy"
);
}
#[test]
#[ignore]
fn harness_refuses_to_probe_free_form_config_keys() {
assert!(
!ENUM_CONFIG_KEYS.contains(&"model"),
"`model` is free-form; probing it would start a real session"
);
let args = vec![
"exec".to_string(),
"-c".to_string(),
"model=\"o3\"".to_string(),
];
let result = std::panic::catch_unwind(|| assert_contract("synthetic", args, 1));
assert!(
result.is_err(),
"harness probed a free-form config key instead of refusing"
);
}
#[test]
#[ignore]
fn top_level_review_remains_a_subset_of_exec_review() {
let top_level = help_flags(&["review".to_string()]);
let exec_review = help_flags(&["exec".to_string(), "review".to_string()]);
let only_on_top_level: Vec<&String> = top_level.difference(&exec_review).collect();
assert!(
only_on_top_level.is_empty(),
"`codex review` ({}) now accepts flags `codex exec review` does not: {:?}\n\n\
#56 chose to wrap only `codex exec review` because the top-level path was a \
strict subset. Re-evaluate that decision, and update the rationale on \
`ReviewCommand`.",
cli_version(),
only_on_top_level
);
assert!(
exec_review.difference(&top_level).next().is_some(),
"`codex review` and `codex exec review` now accept the same flags ({}); \
the rationale on `ReviewCommand` says the top-level path is strictly \
narrower and needs updating",
cli_version()
);
}
#[test]
#[ignore]
fn dangerous_flags_contract() {
use codex_wrapper::dangerous::{ALLOW_DANGEROUS_ENV, Dangerous, DangerousClient};
let allow = DangerousClient::new().unwrap_or_else(|_| {
panic!(
"set {ALLOW_DANGEROUS_ENV}=1 to run this check; without it the two \
dangerous flags are not verified against the CLI at all"
)
});
let args = ExecCommand::new("probe")
.bypass_approvals_and_sandbox(&allow)
.unwrap()
.bypass_hook_trust(&allow)
.unwrap()
.args();
assert!(
args.iter()
.any(|a| a == "--dangerously-bypass-approvals-and-sandbox"),
"{args:?}"
);
assert_contract("ExecCommand::dangerous", args, 1);
let args = ReviewCommand::new()
.uncommitted()
.bypass_approvals_and_sandbox(&allow)
.unwrap()
.bypass_hook_trust(&allow)
.unwrap()
.args();
assert_contract("ReviewCommand::dangerous", args, 2);
}
#[test]
#[ignore]
fn mcp_config_overrides_contract() {
use codex_wrapper::{McpConfigBuilder, McpServerConfig};
let mcp = McpConfigBuilder::new()
.server(
"files",
McpServerConfig::stdio("npx")
.arg("-y")
.arg("server")
.env("API_KEY", "x"),
)
.server(
"docs",
McpServerConfig::http("https://example.com/mcp")
.bearer_token_env_var("TOKEN")
.env_http_header("X-Identity", "IDENTITY_TOKEN")
.required(),
);
let mut args = vec!["exec".to_string(), "--strict-config".to_string()];
for override_ in mcp.config_overrides() {
args.push("-c".into());
args.push(override_);
}
args.push("probe".into());
let output = run_codex(&args);
assert!(
!output.contains("unknown configuration field"),
"the CLI rejected a generated MCP override:\n{output}"
);
assert!(
!output.contains("invalid transport"),
"the CLI could not read a generated MCP server:\n{output}"
);
assert!(
!output.contains("Error loading config.toml"),
"config load failed on the generated overrides:\n{output}"
);
}
#[test]
#[ignore]
fn approve_for_me_contract() {
use codex_wrapper::CliVersion;
let introduced = CliVersion::new(0, 147, 0);
let installed = CliVersion::parse_version_output(&cli_version())
.expect("the CLI should report a parsable version");
for subcommand in [
vec!["exec".to_string()],
vec!["fork".to_string()],
vec!["resume".to_string()],
] {
let accepted = help_flags(&subcommand);
let name = subcommand.join(" ");
if installed >= introduced {
assert!(
accepted.contains("--approve-for-me"),
"`codex {name}` no longer accepts --approve-for-me on {installed}"
);
} else {
assert!(
!accepted.contains("--approve-for-me"),
"`codex {name}` accepts --approve-for-me on {installed}, earlier than \
the 0.147.0 floor documented on the builder methods"
);
}
}
if installed < introduced {
return;
}
assert_contract(
"ExecCommand::approve_for_me",
ExecCommand::new("probe").approve_for_me().args(),
1,
);
assert_contract(
"ForkCommand::approve_for_me",
ForkCommand::new().last().approve_for_me().args(),
1,
);
assert_contract(
"ResumeCommand::approve_for_me",
ResumeCommand::new().last().approve_for_me().args(),
1,
);
}