fix: project model and reasoning_effort into Codex CLI args, stop rewriting .codex/config.toml
- Add model_reasoning_effort field to AgentProfile (domain layer) - Remove model parameter from codex_config_toml, stop rewriting model in TOML - Pass model and model_reasoning_effort via Codex CLI -c overrides on every exec - Update CodexExecSession with new_with_policy_and_overrides factory - Cover new conversation and resume flows with tests
This commit is contained in:
@ -2598,7 +2598,6 @@ impl LaunchAgent {
|
||||
&declaration,
|
||||
run_dir.as_str(),
|
||||
project_root.as_str(),
|
||||
profile.model.as_deref(),
|
||||
);
|
||||
let _ = self.fs.write(&path, rendered.as_bytes()).await;
|
||||
}
|
||||
@ -2610,7 +2609,6 @@ impl LaunchAgent {
|
||||
&declaration,
|
||||
run_dir.as_str(),
|
||||
project_root.as_str(),
|
||||
profile.model.as_deref(),
|
||||
);
|
||||
let _ = self.fs.write(&path, rendered.as_bytes()).await;
|
||||
}
|
||||
@ -3215,11 +3213,6 @@ fn toml_string(s: &str) -> String {
|
||||
format!("\"{}\"", json_escape(s))
|
||||
}
|
||||
|
||||
fn set_top_level_toml_value(input: &str, key: &str, value: &str) -> String {
|
||||
let line = format!("{key} = {}", toml_string(value));
|
||||
set_top_level_toml_line(input, key, &line)
|
||||
}
|
||||
|
||||
/// Renders Codex's `config.toml` **MCP part only** (lot LP3-3 decoupling): merges
|
||||
/// the `[mcp_servers.idea]` table and ensures the run-dir + project-root trust
|
||||
/// entries. The permission part (`sandbox_mode` / `approval_policy` + the
|
||||
@ -3231,12 +3224,8 @@ fn codex_config_toml(
|
||||
mcp_declaration: &str,
|
||||
run_dir: &str,
|
||||
project_root: &str,
|
||||
model: Option<&str>,
|
||||
) -> String {
|
||||
let mut text = existing.unwrap_or_default().to_owned();
|
||||
if let Some(model) = model {
|
||||
text = set_top_level_toml_value(&text, "model", model);
|
||||
}
|
||||
text = replace_toml_table(&text, "mcp_servers.idea", mcp_declaration.trim_end());
|
||||
text = replace_toml_table(
|
||||
&text,
|
||||
@ -4750,7 +4739,6 @@ command = "other"
|
||||
"[mcp_servers.idea]\ncommand = \"new\"",
|
||||
"/home/me/proj/.ideai/run/a",
|
||||
"/home/me/proj",
|
||||
Some("gpt-5-codex"),
|
||||
);
|
||||
|
||||
// MCP table + trust entries are the only things this function touches.
|
||||
@ -4759,7 +4747,7 @@ command = "other"
|
||||
assert!(rendered.contains("approval_policy = \"nested\""));
|
||||
assert!(rendered.contains("[mcp_servers.idea]\ncommand = \"new\""));
|
||||
assert!(rendered.contains("[mcp_servers.other]\ncommand = \"other\""));
|
||||
assert!(rendered.contains("model = \"gpt-5-codex\""));
|
||||
assert!(!rendered.contains("model = \"gpt-5-codex\""));
|
||||
assert!(!rendered.contains("command = \"old\""));
|
||||
assert_eq!(rendered.matches("[mcp_servers.idea]").count(), 1);
|
||||
assert_eq!(rendered.matches("[projects.\"/home/me/proj\"]").count(), 1);
|
||||
@ -4774,7 +4762,6 @@ command = "other"
|
||||
"[mcp_servers.idea]\ncommand = \"idea-mcp\"",
|
||||
"/home/me/proj/.ideai/run/a",
|
||||
"/home/me/proj",
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!rendered.contains("model ="));
|
||||
|
||||
@ -851,7 +851,7 @@ pub struct ProjectionContext<'a> {
|
||||
/// Absolute isolated run dir of the agent (`.ideai/run/<agent-id>/`).
|
||||
pub run_dir: &'a str,
|
||||
/// Optional model selected by the agent profile. Orthogonal to permissions:
|
||||
/// projectors may still materialise it even when `eff == None`.
|
||||
/// projectors may still materialise or forward it when appropriate.
|
||||
pub model: Option<&'a str>,
|
||||
}
|
||||
|
||||
|
||||
@ -910,6 +910,10 @@ pub struct AgentProfile {
|
||||
/// CLI. OpenCode garde ses champs dédiés.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
/// Effort de raisonnement Codex explicitement configuré par le profil. `None`
|
||||
/// conserve le défaut natif de la CLI ; seules les sessions Codex le consomment.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model_reasoning_effort: Option<String>,
|
||||
/// Capacité **MCP** (ARCHITECTURE §14.3, orchestration v3, Décision 1).
|
||||
/// `None` ⇒ repli fichier `.ideai/requests` + prose (comportement actuel).
|
||||
/// `Some(_)` ⇒ IdeA matérialise la config MCP de cette CLI au lancement et
|
||||
@ -1117,6 +1121,7 @@ impl AgentProfile {
|
||||
opencode: None,
|
||||
opencode_provider: None,
|
||||
model: None,
|
||||
model_reasoning_effort: None,
|
||||
mcp: None,
|
||||
liveness: None,
|
||||
rate_limit_pattern: None,
|
||||
@ -1174,6 +1179,13 @@ impl AgentProfile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe l'effort de raisonnement Codex direct (ticket #99 follow-up).
|
||||
#[must_use]
|
||||
pub fn with_model_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
|
||||
self.model_reasoning_effort = Some(effort.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder : fixe la [`McpCapability`] (§14.3, orchestration v3) et renvoie le
|
||||
/// profil. Laisse [`AgentProfile::new`] stable (zéro régression d'appel) : les
|
||||
/// profils sans MCP ne l'appellent simplement pas.
|
||||
@ -1488,10 +1500,15 @@ mod mcp_tests {
|
||||
fn profile_model_round_trips_without_codex_or_claude_provider_config() {
|
||||
let profile = profile_without_mcp()
|
||||
.with_structured_adapter(StructuredAdapter::Codex)
|
||||
.with_model("gpt-5-codex");
|
||||
.with_model("gpt-5-codex")
|
||||
.with_model_reasoning_effort("medium");
|
||||
|
||||
let json = serde_json::to_string(&profile).expect("serialise");
|
||||
assert!(json.contains("\"model\":\"gpt-5-codex\""), "got: {json}");
|
||||
assert!(
|
||||
json.contains("\"modelReasoningEffort\":\"medium\""),
|
||||
"got: {json}"
|
||||
);
|
||||
assert!(!json.contains("codexProvider"), "got: {json}");
|
||||
assert!(!json.contains("claudeProvider"), "got: {json}");
|
||||
assert!(!json.contains("apiKeyRef"), "got: {json}");
|
||||
@ -1499,6 +1516,7 @@ mod mcp_tests {
|
||||
|
||||
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
|
||||
assert_eq!(back.model.as_deref(), Some("gpt-5-codex"));
|
||||
assert_eq!(back.model_reasoning_effort.as_deref(), Some("medium"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -239,7 +239,6 @@ impl TicketAssistantEnvironmentPreparer {
|
||||
&declaration,
|
||||
cwd.as_str(),
|
||||
project.root.as_str(),
|
||||
profile.model.as_deref(),
|
||||
);
|
||||
self.write_file(&path, rendered.as_bytes()).await?;
|
||||
env.push((home_env.clone(), parent_dir(cwd, target)));
|
||||
@ -518,12 +517,8 @@ fn codex_config_toml(
|
||||
mcp_declaration: &str,
|
||||
run_dir: &str,
|
||||
project_root: &str,
|
||||
model: Option<&str>,
|
||||
) -> String {
|
||||
let mut text = existing.unwrap_or_default().to_owned();
|
||||
if let Some(model) = model {
|
||||
text = set_top_level_toml_value(&text, "model", model);
|
||||
}
|
||||
text = replace_toml_table_block(&text, "mcp_servers.idea", mcp_declaration.trim_end());
|
||||
text = replace_toml_table_block(
|
||||
&text,
|
||||
@ -538,33 +533,6 @@ fn codex_config_toml(
|
||||
text
|
||||
}
|
||||
|
||||
fn set_top_level_toml_value(input: &str, key: &str, value: &str) -> String {
|
||||
let line = format!("{key} = {}", toml_quoted(value));
|
||||
set_top_level_toml_line(input, key, &line)
|
||||
}
|
||||
|
||||
fn set_top_level_toml_line(input: &str, key: &str, replacement: &str) -> String {
|
||||
let needle = format!("{key} =");
|
||||
let mut out = Vec::new();
|
||||
let mut replaced = false;
|
||||
for line in input.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if !replaced && trimmed.starts_with(&needle) {
|
||||
out.push(replacement.to_owned());
|
||||
replaced = true;
|
||||
} else {
|
||||
out.push(line.to_owned());
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
if !out.is_empty() && !out.first().is_some_and(|line| line.trim().starts_with('[')) {
|
||||
out.push(String::new());
|
||||
}
|
||||
out.insert(0, replacement.to_owned());
|
||||
}
|
||||
out.join("\n")
|
||||
}
|
||||
|
||||
fn replace_toml_table_block(existing: &str, table: &str, replacement: &str) -> String {
|
||||
let header = format!("[{table}]");
|
||||
let mut out = Vec::new();
|
||||
@ -669,13 +637,7 @@ mod codex_config_toml_tests {
|
||||
.to_config_toml();
|
||||
let existing = "user_key = \"keep\"\n\n[features.code_mode]\ndirect_only_tool_namespaces = [\"old\"]\n\n[features.preview]\nenabled = true\n\n[user.table]\nvalue = 1\n";
|
||||
|
||||
let rendered = codex_config_toml(
|
||||
Some(existing),
|
||||
&declaration,
|
||||
"/run/assistant",
|
||||
"/proj",
|
||||
None,
|
||||
);
|
||||
let rendered = codex_config_toml(Some(existing), &declaration, "/run/assistant", "/proj");
|
||||
|
||||
assert!(
|
||||
rendered.contains("user_key = \"keep\""),
|
||||
|
||||
@ -58,7 +58,7 @@ impl PermissionProjector for CodexPermissionProjector {
|
||||
// orthogonal and still gets an explicit env override to avoid stale inheritance.
|
||||
let Some(permissions) = eff else {
|
||||
return PermissionProjection {
|
||||
files: vec![codex_model_and_network_file(ctx, network)],
|
||||
files: vec![codex_network_file(network)],
|
||||
env: codex_network_env(network),
|
||||
..PermissionProjection::empty()
|
||||
};
|
||||
@ -67,15 +67,11 @@ impl PermissionProjector for CodexPermissionProjector {
|
||||
let approval = codex_approval_policy(permissions);
|
||||
let network_access = codex_network_access(network);
|
||||
|
||||
// Permission-only TOML fragment (escaped exactly like the former
|
||||
// `set_top_level_toml_value`). The mcp_servers/trust tables are NOT a
|
||||
// permission concern and stay with the MCP wiring (LP3-3).
|
||||
let mut contents = format!(
|
||||
"sandbox_mode = {}\napproval_policy = {}\n",
|
||||
toml_string(sandbox),
|
||||
toml_string(approval),
|
||||
);
|
||||
append_codex_model_config(&mut contents, ctx);
|
||||
contents.push_str(&format!(
|
||||
"\n[{}]\nnetwork_access = {}\n\n{}",
|
||||
SANDBOX_WORKSPACE_WRITE_TABLE, network_access, CODEX_CODE_MODE_FEATURES_TOML,
|
||||
@ -95,8 +91,8 @@ impl PermissionProjector for CodexPermissionProjector {
|
||||
PermissionProjection {
|
||||
files: vec![ProjectedFile::MergeToml {
|
||||
rel_path: CONFIG_REL_PATH.to_owned(),
|
||||
managed_tables: codex_managed_tables(ctx),
|
||||
managed_keys: codex_managed_keys(ctx, true),
|
||||
managed_tables: codex_managed_tables(),
|
||||
managed_keys: codex_managed_keys(true),
|
||||
contents,
|
||||
}],
|
||||
args,
|
||||
@ -110,53 +106,36 @@ impl PermissionProjector for CodexPermissionProjector {
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_model_and_network_file(
|
||||
ctx: &ProjectionContext,
|
||||
network: Option<NetworkPolicy>,
|
||||
) -> ProjectedFile {
|
||||
let mut contents = String::new();
|
||||
append_codex_model_config(&mut contents, ctx);
|
||||
if !contents.is_empty() {
|
||||
contents.push('\n');
|
||||
}
|
||||
contents.push_str(&format!(
|
||||
fn codex_network_file(network: Option<NetworkPolicy>) -> ProjectedFile {
|
||||
let contents = format!(
|
||||
"[{}]\nnetwork_access = {}\n\n{}",
|
||||
SANDBOX_WORKSPACE_WRITE_TABLE,
|
||||
codex_network_access(network),
|
||||
CODEX_CODE_MODE_FEATURES_TOML,
|
||||
));
|
||||
);
|
||||
ProjectedFile::MergeToml {
|
||||
rel_path: CONFIG_REL_PATH.to_owned(),
|
||||
managed_tables: codex_managed_tables(ctx),
|
||||
managed_keys: codex_managed_keys(ctx, false),
|
||||
managed_tables: codex_managed_tables(),
|
||||
managed_keys: codex_managed_keys(false),
|
||||
contents,
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_managed_keys(ctx: &ProjectionContext, include_permissions: bool) -> Vec<String> {
|
||||
fn codex_managed_keys(include_permissions: bool) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
if include_permissions {
|
||||
keys.extend(PERMISSION_MANAGED_KEYS.iter().map(|k| (*k).to_owned()));
|
||||
}
|
||||
if ctx.model.is_some() {
|
||||
keys.push("model".to_owned());
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn codex_managed_tables(_ctx: &ProjectionContext) -> Vec<String> {
|
||||
fn codex_managed_tables() -> Vec<String> {
|
||||
vec![
|
||||
SANDBOX_WORKSPACE_WRITE_TABLE.to_owned(),
|
||||
CODEX_CODE_MODE_FEATURES_TABLE.to_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
fn append_codex_model_config(contents: &mut String, ctx: &ProjectionContext) {
|
||||
if let Some(model) = ctx.model {
|
||||
contents.push_str(&format!("model = {}\n", toml_string(model)));
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_network_env(network: Option<NetworkPolicy>) -> Vec<(String, String)> {
|
||||
// Codex inherits the parent environment by default. Always set the variable for
|
||||
// Codex launches so a stale `CODEX_SANDBOX_NETWORK_DISABLED=1` in IdeA's own
|
||||
@ -259,7 +238,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_projection_without_permissions_writes_only_model() {
|
||||
fn projection_without_permissions_writes_network_only_and_leaves_model_to_exec_args() {
|
||||
let ctx = ProjectionContext {
|
||||
project_root: "/proj",
|
||||
run_dir: "/run/agent",
|
||||
@ -274,10 +253,11 @@ mod tests {
|
||||
contents,
|
||||
..
|
||||
} => {
|
||||
assert!(managed_keys.contains(&"model".to_owned()));
|
||||
assert!(!managed_keys.contains(&"model".to_owned()));
|
||||
assert!(!managed_keys.contains(&"model_provider".to_owned()));
|
||||
assert_eq!(managed_tables, &expected_managed_tables());
|
||||
assert!(contents.contains("model = \"gpt-5\""), "{contents}");
|
||||
assert!(!contents.contains("model ="), "{contents}");
|
||||
assert!(contents.contains("[sandbox_workspace_write]"), "{contents}");
|
||||
assert!(!contents.contains("model_provider"), "{contents}");
|
||||
assert!(!contents.contains("model_providers"), "{contents}");
|
||||
assert!(!contents.contains("base_url"), "{contents}");
|
||||
|
||||
@ -160,6 +160,10 @@ pub struct CodexExecSession {
|
||||
writable_roots: Vec<String>,
|
||||
/// Structured policy projection of Codex workspace-write sandbox network access.
|
||||
network_access: Option<bool>,
|
||||
/// Profile-selected model forwarded as a Codex config override on every exec turn.
|
||||
model: Option<String>,
|
||||
/// Profile-selected reasoning effort forwarded as a Codex config override.
|
||||
model_reasoning_effort: Option<String>,
|
||||
/// Variables d'environnement préparées au lancement (ex. `CODEX_HOME` isolé).
|
||||
env: Vec<(String, String)>,
|
||||
/// Id de conversation **du moteur** Codex, capté au premier tour, `None` avant.
|
||||
@ -188,7 +192,7 @@ impl CodexExecSession {
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
) -> Self {
|
||||
Self::new_with_policy(
|
||||
Self::new_with_policy_and_overrides(
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
@ -196,6 +200,8 @@ impl CodexExecSession {
|
||||
"workspace-write",
|
||||
writable_roots,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
env,
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
@ -217,6 +223,39 @@ impl CodexExecSession {
|
||||
env: Vec<(String, String)>,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
) -> Self {
|
||||
Self::new_with_policy_and_overrides(
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
seed_conversation_id,
|
||||
sandbox_mode,
|
||||
writable_roots,
|
||||
network_access,
|
||||
None,
|
||||
None,
|
||||
env,
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
)
|
||||
}
|
||||
|
||||
/// Construit l'adapter avec politique + overrides de config issus du profil IdeA.
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new_with_policy_and_overrides(
|
||||
id: SessionId,
|
||||
command: impl Into<String>,
|
||||
cwd: impl Into<String>,
|
||||
seed_conversation_id: Option<String>,
|
||||
sandbox_mode: impl Into<String>,
|
||||
writable_roots: Vec<String>,
|
||||
network_access: Option<bool>,
|
||||
model: Option<String>,
|
||||
model_reasoning_effort: Option<String>,
|
||||
env: Vec<(String, String)>,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
@ -225,6 +264,8 @@ impl CodexExecSession {
|
||||
sandbox_mode: sandbox_mode.into(),
|
||||
writable_roots,
|
||||
network_access,
|
||||
model,
|
||||
model_reasoning_effort,
|
||||
env,
|
||||
conversation_id: Mutex::new(seed_conversation_id),
|
||||
sandbox,
|
||||
@ -269,6 +310,25 @@ impl CodexExecSession {
|
||||
"sandbox_workspace_write.network_access={network_access}"
|
||||
));
|
||||
}
|
||||
if let Some(model) = self
|
||||
.model
|
||||
.as_deref()
|
||||
.filter(|model| !model.trim().is_empty())
|
||||
{
|
||||
args.push("-c".to_owned());
|
||||
args.push(format!("model={}", codex_toml_string(model)));
|
||||
}
|
||||
if let Some(effort) = self
|
||||
.model_reasoning_effort
|
||||
.as_deref()
|
||||
.filter(|effort| !effort.trim().is_empty())
|
||||
{
|
||||
args.push("-c".to_owned());
|
||||
args.push(format!(
|
||||
"model_reasoning_effort={}",
|
||||
codex_toml_string(effort)
|
||||
));
|
||||
}
|
||||
if let Some(id) = conversation_id {
|
||||
args.push("resume".to_owned());
|
||||
args.push(id);
|
||||
@ -293,6 +353,10 @@ impl CodexExecSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn codex_toml_string(value: &str) -> String {
|
||||
serde_json::to_string(value).expect("string serialization cannot fail")
|
||||
}
|
||||
|
||||
fn upsert_env(env: &mut Vec<(String, String)>, key: &str, value: &str) {
|
||||
if let Some((_, existing)) = env.iter_mut().find(|(k, _)| k == key) {
|
||||
*existing = value.to_owned();
|
||||
|
||||
@ -195,7 +195,7 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
None,
|
||||
),
|
||||
};
|
||||
Arc::new(CodexExecSession::new_with_policy(
|
||||
Arc::new(CodexExecSession::new_with_policy_and_overrides(
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
@ -203,6 +203,8 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
policy.0,
|
||||
policy.1,
|
||||
policy.2,
|
||||
profile.model.clone(),
|
||||
profile.model_reasoning_effort.clone(),
|
||||
env.to_vec(),
|
||||
plan,
|
||||
enforcer,
|
||||
|
||||
@ -625,6 +625,54 @@ mod tests {
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factory_passes_codex_profile_model_overrides_to_exec_session() {
|
||||
let factory = StructuredSessionFactory::new();
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let codex = structured_profile(StructuredAdapter::Codex, &cmd)
|
||||
.with_model("gpt-5.4")
|
||||
.with_model_reasoning_effort("medium");
|
||||
let ctx = PreparedContext {
|
||||
content: MarkdownDoc::new("# ctx"),
|
||||
relative_path: "AGENTS.md".to_owned(),
|
||||
project_root: "/project/root".to_owned(),
|
||||
};
|
||||
|
||||
let session = factory
|
||||
.start(
|
||||
&codex,
|
||||
&ctx,
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
None,
|
||||
&[],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("start Codex ok");
|
||||
let content = drain_final(session.as_ref()).await;
|
||||
assert_eq!(content, "ok");
|
||||
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
assert!(
|
||||
args.windows(2).any(|w| w == ["-c", "model=\"gpt-5.4\""]),
|
||||
"factory must relay profile.model to Codex exec overrides, got: {args:?}"
|
||||
);
|
||||
assert!(
|
||||
args.windows(2)
|
||||
.any(|w| w == ["-c", "model_reasoning_effort=\"medium\""]),
|
||||
"factory must relay profile.model_reasoning_effort to Codex exec overrides, got: {args:?}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn factory_resume_seeds_conversation_id() {
|
||||
let factory = StructuredSessionFactory::new();
|
||||
@ -1774,6 +1822,101 @@ mod tests {
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_new_conversation_command_carries_profile_model_overrides() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let session = CodexExecSession::new_with_policy_and_overrides(
|
||||
SessionId::new_random(),
|
||||
cmd.clone(),
|
||||
"/",
|
||||
None,
|
||||
"workspace-write",
|
||||
vec!["/project/root".to_owned()],
|
||||
Some(true),
|
||||
Some("gpt-5.4".to_owned()),
|
||||
Some("medium".to_owned()),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let _ = session.send("salut").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"exec",
|
||||
"--json",
|
||||
"--skip-git-repo-check",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--add-dir",
|
||||
"/project/root",
|
||||
"-c",
|
||||
"sandbox_workspace_write.network_access=true",
|
||||
"-c",
|
||||
"model=\"gpt-5.4\"",
|
||||
"-c",
|
||||
"model_reasoning_effort=\"medium\"",
|
||||
"salut",
|
||||
],
|
||||
"profile model overrides must be passed before the prompt, got: {args:?}"
|
||||
);
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_resume_command_carries_profile_model_overrides_before_resume() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let session = CodexExecSession::new_with_policy_and_overrides(
|
||||
SessionId::new_random(),
|
||||
cmd.clone(),
|
||||
"/",
|
||||
Some("cx-id".to_owned()),
|
||||
"workspace-write",
|
||||
vec!["/project/root".to_owned()],
|
||||
None,
|
||||
Some("gpt-5.4".to_owned()),
|
||||
Some("medium".to_owned()),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let _ = session.send("vas-y").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"exec",
|
||||
"--json",
|
||||
"--skip-git-repo-check",
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--add-dir",
|
||||
"/project/root",
|
||||
"-c",
|
||||
"model=\"gpt-5.4\"",
|
||||
"-c",
|
||||
"model_reasoning_effort=\"medium\"",
|
||||
"resume",
|
||||
"cx-id",
|
||||
"vas-y",
|
||||
],
|
||||
"profile model overrides must precede resume, got: {args:?}"
|
||||
);
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn codex_read_only_policy_omits_add_dir_and_approval_flag() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
|
||||
Reference in New Issue
Block a user