mirror of
https://github.com/openai/codex.git
synced 2026-04-25 23:24:55 +00:00
feat[app-serve]: config management (#7241)
This commit is contained in:
347
codex-rs/app-server/tests/suite/v2/config_rpc.rs
Normal file
347
codex-rs/app-server/tests/suite/v2/config_rpc.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigEdit;
|
||||
use codex_app_server_protocol::ConfigLayerName;
|
||||
use codex_app_server_protocol::ConfigReadParams;
|
||||
use codex_app_server_protocol::ConfigReadResponse;
|
||||
use codex_app_server_protocol::ConfigValueWriteParams;
|
||||
use codex_app_server_protocol::ConfigWriteResponse;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::MergeStrategy;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::WriteStatus;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
fn write_config(codex_home: &TempDir, contents: &str) -> Result<()> {
|
||||
Ok(std::fs::write(
|
||||
codex_home.path().join("config.toml"),
|
||||
contents,
|
||||
)?)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_returns_effective_and_layers() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(
|
||||
&codex_home,
|
||||
r#"
|
||||
model = "gpt-user"
|
||||
sandbox_mode = "workspace-write"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: true,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ConfigReadResponse {
|
||||
config,
|
||||
origins,
|
||||
layers,
|
||||
} = to_response(resp)?;
|
||||
|
||||
assert_eq!(config.get("model"), Some(&json!("gpt-user")));
|
||||
assert_eq!(
|
||||
origins.get("model").expect("origin").name,
|
||||
ConfigLayerName::User
|
||||
);
|
||||
let layers = layers.expect("layers present");
|
||||
assert_eq!(layers.len(), 2);
|
||||
assert_eq!(layers[0].name, ConfigLayerName::SessionFlags);
|
||||
assert_eq!(layers[1].name, ConfigLayerName::User);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_read_includes_system_layer_and_overrides() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(
|
||||
&codex_home,
|
||||
r#"
|
||||
model = "gpt-user"
|
||||
approval_policy = "on-request"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
writable_roots = ["/user"]
|
||||
network_access = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let managed_path = codex_home.path().join("managed_config.toml");
|
||||
std::fs::write(
|
||||
&managed_path,
|
||||
r#"
|
||||
model = "gpt-system"
|
||||
approval_policy = "never"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
writable_roots = ["/system"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let managed_path_str = managed_path.display().to_string();
|
||||
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[("CODEX_MANAGED_CONFIG_PATH", Some(&managed_path_str))],
|
||||
)
|
||||
.await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: true,
|
||||
})
|
||||
.await?;
|
||||
let resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ConfigReadResponse {
|
||||
config,
|
||||
origins,
|
||||
layers,
|
||||
} = to_response(resp)?;
|
||||
|
||||
assert_eq!(config.get("model"), Some(&json!("gpt-system")));
|
||||
assert_eq!(
|
||||
origins.get("model").expect("origin").name,
|
||||
ConfigLayerName::System
|
||||
);
|
||||
|
||||
assert_eq!(config.get("approval_policy"), Some(&json!("never")));
|
||||
assert_eq!(
|
||||
origins.get("approval_policy").expect("origin").name,
|
||||
ConfigLayerName::System
|
||||
);
|
||||
|
||||
assert_eq!(config.get("sandbox_mode"), Some(&json!("workspace-write")));
|
||||
assert_eq!(
|
||||
origins.get("sandbox_mode").expect("origin").name,
|
||||
ConfigLayerName::User
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.get("sandbox_workspace_write")
|
||||
.and_then(|v| v.get("writable_roots")),
|
||||
Some(&json!(["/system"]))
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("sandbox_workspace_write.writable_roots.0")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerName::System
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config
|
||||
.get("sandbox_workspace_write")
|
||||
.and_then(|v| v.get("network_access")),
|
||||
Some(&json!(true))
|
||||
);
|
||||
assert_eq!(
|
||||
origins
|
||||
.get("sandbox_workspace_write.network_access")
|
||||
.expect("origin")
|
||||
.name,
|
||||
ConfigLayerName::User
|
||||
);
|
||||
|
||||
let layers = layers.expect("layers present");
|
||||
assert_eq!(layers.len(), 3);
|
||||
assert_eq!(layers[0].name, ConfigLayerName::System);
|
||||
assert_eq!(layers[1].name, ConfigLayerName::SessionFlags);
|
||||
assert_eq!(layers[2].name, ConfigLayerName::User);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_value_write_replaces_value() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(
|
||||
&codex_home,
|
||||
r#"
|
||||
model = "gpt-old"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let read_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let read: ConfigReadResponse = to_response(read_resp)?;
|
||||
let expected_version = read.origins.get("model").map(|m| m.version.clone());
|
||||
|
||||
let write_id = mcp
|
||||
.send_config_value_write_request(ConfigValueWriteParams {
|
||||
file_path: codex_home.path().join("config.toml").display().to_string(),
|
||||
key_path: "model".to_string(),
|
||||
value: json!("gpt-new"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version,
|
||||
})
|
||||
.await?;
|
||||
let write_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
let write: ConfigWriteResponse = to_response(write_resp)?;
|
||||
|
||||
assert_eq!(write.status, WriteStatus::Ok);
|
||||
assert!(write.overridden_metadata.is_none());
|
||||
|
||||
let verify_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
})
|
||||
.await?;
|
||||
let verify_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(verify_id)),
|
||||
)
|
||||
.await??;
|
||||
let verify: ConfigReadResponse = to_response(verify_resp)?;
|
||||
assert_eq!(verify.config.get("model"), Some(&json!("gpt-new")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_value_write_rejects_version_conflict() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(
|
||||
&codex_home,
|
||||
r#"
|
||||
model = "gpt-old"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let write_id = mcp
|
||||
.send_config_value_write_request(ConfigValueWriteParams {
|
||||
file_path: codex_home.path().join("config.toml").display().to_string(),
|
||||
key_path: "model".to_string(),
|
||||
value: json!("gpt-new"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
expected_version: Some("sha256:stale".to_string()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let err: JSONRPCError = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(write_id)),
|
||||
)
|
||||
.await??;
|
||||
let code = err
|
||||
.error
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|d| d.get("config_write_error_code"))
|
||||
.and_then(|v| v.as_str());
|
||||
assert_eq!(code, Some("configVersionConflict"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn config_batch_write_applies_multiple_edits() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
write_config(&codex_home, "")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let batch_id = mcp
|
||||
.send_config_batch_write_request(ConfigBatchWriteParams {
|
||||
file_path: codex_home.path().join("config.toml").display().to_string(),
|
||||
edits: vec![
|
||||
ConfigEdit {
|
||||
key_path: "sandbox_mode".to_string(),
|
||||
value: json!("workspace-write"),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
},
|
||||
ConfigEdit {
|
||||
key_path: "sandbox_workspace_write".to_string(),
|
||||
value: json!({
|
||||
"writable_roots": ["/tmp"],
|
||||
"network_access": false
|
||||
}),
|
||||
merge_strategy: MergeStrategy::Replace,
|
||||
},
|
||||
],
|
||||
expected_version: None,
|
||||
})
|
||||
.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 read_id = mcp
|
||||
.send_config_read_request(ConfigReadParams {
|
||||
include_layers: false,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let read: ConfigReadResponse = to_response(read_resp)?;
|
||||
assert_eq!(
|
||||
read.config.get("sandbox_mode"),
|
||||
Some(&json!("workspace-write"))
|
||||
);
|
||||
assert_eq!(
|
||||
read.config
|
||||
.get("sandbox_workspace_write")
|
||||
.and_then(|v| v.get("writable_roots")),
|
||||
Some(&json!(["/tmp"]))
|
||||
);
|
||||
assert_eq!(
|
||||
read.config
|
||||
.get("sandbox_workspace_write")
|
||||
.and_then(|v| v.get("network_access")),
|
||||
Some(&json!(false))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user