Merge branch 'etraut/next-turn-state-remove-override-context' into etraut/next-turn-state-core

This commit is contained in:
Eric Traut
2026-05-16 17:27:17 -07:00
5 changed files with 280 additions and 54 deletions

View File

@@ -403,10 +403,47 @@ fn parse_key_path(path: &str) -> Result<Vec<String>, String> {
if path.trim().is_empty() {
return Err("keyPath must not be empty".to_string());
}
Ok(path
.split('.')
.map(std::string::ToString::to_string)
.collect())
let mut segments = Vec::new();
let mut segment = String::new();
let mut chars = path.chars();
let mut quoted = false;
// Split on dots unless they appear inside a quoted segment. Bare segments
// intentionally stay permissive so existing paths like `sample@catalog`
// remain valid.
while let Some(ch) = chars.next() {
match ch {
'"' if segment.is_empty() && !quoted => quoted = true,
'"' if quoted => quoted = false,
'\\' if quoted => {
// Quoted segments may escape punctuation that would otherwise
// participate in parsing, such as `.` or `"`.
let Some(escaped) = chars.next() else {
return Err("unterminated escape in keyPath".to_string());
};
segment.push(escaped);
}
'.' if !quoted => {
if segment.is_empty() {
return Err("keyPath segments must not be empty".to_string());
}
segments.push(std::mem::take(&mut segment));
}
'"' => return Err("invalid quoted keyPath segment".to_string()),
_ => segment.push(ch),
}
}
if quoted {
return Err("unterminated quoted keyPath segment".to_string());
}
if segment.is_empty() {
return Err("keyPath segments must not be empty".to_string());
}
segments.push(segment);
Ok(segments)
}
#[derive(Debug)]

View File

@@ -893,6 +893,71 @@ async fn config_batch_write_applies_multiple_edits() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_batch_write_preserves_dotted_profile_names() -> Result<()> {
let tmp_dir = TempDir::new()?;
let codex_home = tmp_dir.path().canonicalize()?;
write_config(
&tmp_dir,
r#"
profile = "team.prod"
[profiles."team.prod"]
model = "gpt-5.3-spark"
[profiles.team.prod]
model = "should-stay-put"
"#,
)?;
let mut mcp = McpProcess::new(&codex_home).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let batch_id = mcp
.send_config_batch_write_request(ConfigBatchWriteParams {
file_path: Some(codex_home.join("config.toml").display().to_string()),
edits: vec![
ConfigEdit {
key_path: "profiles.\"team.prod\".model".to_string(),
value: json!("gpt-5.5"),
merge_strategy: MergeStrategy::Replace,
},
ConfigEdit {
key_path: "items.sample@catalog.enabled".to_string(),
value: json!(true),
merge_strategy: MergeStrategy::Replace,
},
],
expected_version: None,
reload_user_config: false,
})
.await?;
let batch_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(batch_id)),
)
.await??;
let batch_write: ConfigWriteResponse = to_response(batch_resp)?;
assert_eq!(batch_write.status, WriteStatus::Ok);
let config: toml::Value =
toml::from_str(&std::fs::read_to_string(codex_home.join("config.toml"))?)?;
assert_eq!(
config["profiles"]["team.prod"]["model"].as_str(),
Some("gpt-5.5")
);
assert_eq!(
config["profiles"]["team"]["prod"]["model"].as_str(),
Some("should-stay-put")
);
assert_eq!(
config["items"]["sample@catalog"]["enabled"].as_bool(),
Some(true)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_batch_write_updates_multiple_desktop_settings() -> Result<()> {
let tmp_dir = TempDir::new()?;