use codex_codes::io::items::{
CommandExecutionStatus, FileChangeItem, PatchApplyStatus, PatchChangeKind, ThreadItem,
};
use codex_codes::protocol::{
ConfigReadResponse, FeedbackUploadResponse, GetAccountRateLimitsParams,
GetAccountRateLimitsResponse, McpServerStatus, Thread, ThreadListParams,
};
use codex_codes::{
JsonRpcMessage, JsonRpcNotification, McpServerElicitationRequestParams, Notification,
ParseError, ThreadEvent, ThreadItemsListResponse, ThreadRevertResponse,
ThreadTurnsListResponse, TurnSteerResponse,
};
#[test]
fn config_read_preserves_additional_properties() {
let original = serde_json::json!({
"config": {
"analytics": {
"enabled": true,
"unknownAnalyticsSetting": "kept"
},
"sandbox_workspace_write": {
"network_access": true,
"writable_roots": ["/tmp/cache"]
},
"model_providers": {
"local": {
"base_url": "http://localhost:11434/v1"
}
}
},
"origins": {}
});
let response: ConfigReadResponse =
serde_json::from_value(original.clone()).expect("deserialize config/read response");
assert_eq!(
response.config.additional.get("model_providers"),
original["config"].get("model_providers")
);
assert_eq!(
response
.config
.analytics
.as_ref()
.and_then(|analytics| analytics.additional.get("unknownAnalyticsSetting")),
Some(&serde_json::json!("kept"))
);
assert_eq!(
response
.config
.sandbox_workspace_write
.as_ref()
.and_then(|sandbox| sandbox.writable_roots.as_deref()),
Some(["/tmp/cache".to_string()].as_slice())
);
assert_eq!(
serde_json::to_value(response).expect("serialize config/read response"),
original
);
}
#[test]
fn account_rate_limits_include_account_and_upsell() {
let original = serde_json::json!({
"accountId": "workspace-123",
"rateLimits": {},
"rateLimitUpsell": {
"message": "Upgrade for more usage"
}
});
let response: GetAccountRateLimitsResponse =
serde_json::from_value(original.clone()).expect("deserialize rate limits response");
assert_eq!(response.account_id.as_deref(), Some("workspace-123"));
assert_eq!(
response.rate_limit_upsell.as_ref(),
original.get("rateLimitUpsell")
);
assert_eq!(
serde_json::to_value(response).expect("serialize rate limits response"),
original
);
}
#[test]
fn account_rate_limits_params_omit_false_capabilities() {
assert_eq!(
serde_json::to_value(GetAccountRateLimitsParams::default()).unwrap(),
serde_json::json!({})
);
let params = GetAccountRateLimitsParams {
supports_luna_reserve: true,
exclude_reset_credit_details: true,
};
let wire = serde_json::to_value(¶ms).unwrap();
assert_eq!(
wire,
serde_json::json!({
"supportsLunaReserve": true,
"excludeResetCreditDetails": true
})
);
let back: GetAccountRateLimitsParams = serde_json::from_value(wire).unwrap();
assert_eq!(back, params);
}
#[test]
fn account_rate_limits_decode_usage_permission_and_model_slug() {
let original = serde_json::json!({
"ordinaryUsageAllowed": false,
"rateLimits": {
"limitId": "codex",
"normalModelSlug": "gpt-5-codex"
}
});
let response: GetAccountRateLimitsResponse = serde_json::from_value(original.clone()).unwrap();
assert_eq!(response.ordinary_usage_allowed, Some(false));
assert_eq!(
serde_json::to_value(&response).unwrap()["rateLimits"]["normalModelSlug"],
"gpt-5-codex"
);
assert_eq!(serde_json::to_value(response).unwrap(), original);
}
#[test]
fn thread_originator_fields_round_trip() {
let thread: Thread = serde_json::from_value(serde_json::json!({
"id": "thr-1",
"originator": "codex_cli_rs"
}))
.unwrap();
assert_eq!(thread.originator.as_deref(), Some("codex_cli_rs"));
let bare: Thread = serde_json::from_value(serde_json::json!({"id": "thr-2"})).unwrap();
assert!(serde_json::to_value(bare)
.unwrap()
.get("originator")
.is_none());
let params = ThreadListParams {
originators: Some(vec!["codex_vscode".to_string()]),
..ThreadListParams::default()
};
assert_eq!(
serde_json::to_value(params).unwrap(),
serde_json::json!({"originators": ["codex_vscode"]})
);
}
#[test]
fn mcp_server_status_decodes_tools_error() {
let status: McpServerStatus = serde_json::from_value(serde_json::json!({
"authStatus": "unsupported",
"name": "broken",
"toolsError": "transport closed during tools/list"
}))
.unwrap();
assert_eq!(
status.tools_error.as_deref(),
Some("transport closed during tools/list")
);
let healthy: McpServerStatus = serde_json::from_value(serde_json::json!({
"authStatus": "unsupported",
"name": "ok"
}))
.unwrap();
assert!(serde_json::to_value(healthy)
.unwrap()
.get("toolsError")
.is_none());
}
#[test]
fn feedback_upload_response_decodes_prompt_hash() {
let response: FeedbackUploadResponse = serde_json::from_value(serde_json::json!({
"threadId": "thr_123",
"promptHash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}))
.unwrap();
assert_eq!(
response.prompt_hash.as_deref(),
Some("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")
);
let bare: FeedbackUploadResponse =
serde_json::from_value(serde_json::json!({"threadId": "thr_123"})).unwrap();
assert_eq!(bare.prompt_hash, None);
assert!(serde_json::to_value(bare)
.unwrap()
.get("promptHash")
.is_none());
}
#[test]
fn mcp_elicitation_accepts_current_openai_form_mode() {
let request: McpServerElicitationRequestParams = serde_json::from_value(serde_json::json!({
"mode": "openaiForm",
"message": "Choose an account",
"requestedSchema": {"type": "object"}
}))
.expect("deserialize openaiForm elicitation");
assert!(matches!(
request,
McpServerElicitationRequestParams::OpenAiElicitationForm { .. }
));
}
#[test]
fn sync_helper_response_types_decode_current_wire_shapes() {
let steer: TurnSteerResponse =
serde_json::from_value(serde_json::json!({"turnId": "turn-1"})).unwrap();
let items: ThreadItemsListResponse =
serde_json::from_value(serde_json::json!({"data": []})).unwrap();
let turns: ThreadTurnsListResponse =
serde_json::from_value(serde_json::json!({"data": []})).unwrap();
let revert: ThreadRevertResponse =
serde_json::from_value(serde_json::json!({"thread": {}})).unwrap();
assert_eq!(steer.turn_id, "turn-1");
assert!(items.data.is_empty());
assert!(turns.data.is_empty());
assert_eq!(revert.thread, codex_codes::Thread::default());
}
fn parse_capture(jsonl: &str) -> Vec<ThreadEvent> {
jsonl
.lines()
.filter(|line| !line.trim().is_empty())
.enumerate()
.map(|(i, line)| {
serde_json::from_str::<ThreadEvent>(line)
.unwrap_or_else(|e| panic!("Failed to parse line {}: {}\n JSON: {}", i, e, line))
})
.collect()
}
fn completed_items(events: &[ThreadEvent]) -> Vec<&ThreadItem> {
events
.iter()
.filter_map(|e| match e {
ThreadEvent::ItemCompleted(ic) => Some(&ic.item),
_ => None,
})
.collect()
}
fn assert_standard_envelope(events: &[ThreadEvent]) {
assert!(
events.len() >= 3,
"Expected at least 3 events, got {}",
events.len()
);
assert_eq!(events[0].event_type(), "thread.started");
assert_eq!(events[1].event_type(), "turn.started");
assert_eq!(
events.last().unwrap().event_type(),
"turn.completed",
"Last event should be turn.completed"
);
if let ThreadEvent::ThreadStarted(e) = &events[0] {
assert!(!e.thread_id.is_empty(), "thread_id must not be empty");
}
if let ThreadEvent::TurnCompleted(e) = events.last().unwrap() {
assert!(e.usage.input_tokens > 0, "input_tokens should be > 0");
assert!(e.usage.output_tokens > 0, "output_tokens should be > 0");
}
}
#[test]
fn test_hello_world_parses_all_lines() {
let events = parse_capture(include_str!("../test_cases/captures/hello_world.jsonl"));
assert_standard_envelope(&events);
assert_eq!(events.len(), 5);
}
#[test]
fn test_hello_world_contains_reasoning_and_message() {
let events = parse_capture(include_str!("../test_cases/captures/hello_world.jsonl"));
let items = completed_items(&events);
let reasoning_count = items
.iter()
.filter(|i| matches!(i, ThreadItem::Reasoning(_)))
.count();
let message_count = items
.iter()
.filter(|i| matches!(i, ThreadItem::AgentMessage(_)))
.count();
assert!(reasoning_count >= 1, "Expected at least one reasoning item");
assert!(message_count >= 1, "Expected at least one agent message");
let last_msg = items
.iter()
.rev()
.find_map(|i| match i {
ThreadItem::AgentMessage(m) => Some(&m.text),
_ => None,
})
.expect("Should have an agent message");
assert_eq!(last_msg, "hello world");
}
#[test]
fn test_list_files_parses_all_lines() {
let events = parse_capture(include_str!("../test_cases/captures/list_files.jsonl"));
assert_standard_envelope(&events);
assert_eq!(events.len(), 8);
}
#[test]
fn test_list_files_command_lifecycle() {
let events = parse_capture(include_str!("../test_cases/captures/list_files.jsonl"));
let started_cmd = events.iter().find_map(|e| match e {
ThreadEvent::ItemStarted(is) => match &is.item {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
},
_ => None,
});
let completed_cmd = completed_items(&events).into_iter().find_map(|i| match i {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
});
let started = started_cmd.expect("Should have an item.started command");
let completed = completed_cmd.expect("Should have an item.completed command");
assert_eq!(started.id, completed.id);
assert_eq!(started.status, CommandExecutionStatus::InProgress);
assert_eq!(started.exit_code, None);
assert!(started
.aggregated_output
.as_deref()
.unwrap_or("")
.is_empty());
assert_eq!(completed.status, CommandExecutionStatus::Completed);
assert_eq!(completed.exit_code, Some(0));
assert!(!completed
.aggregated_output
.as_deref()
.unwrap_or("")
.is_empty());
assert!(completed.command.contains("ls"));
}
#[test]
fn test_file_create_parses_all_lines() {
let events = parse_capture(include_str!("../test_cases/captures/file_create.jsonl"));
assert_standard_envelope(&events);
assert_eq!(events.len(), 8);
}
#[test]
fn test_file_create_command_output() {
let events = parse_capture(include_str!("../test_cases/captures/file_create.jsonl"));
let cmd = completed_items(&events)
.into_iter()
.find_map(|i| match i {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
})
.expect("Should have a completed command");
assert_eq!(cmd.exit_code, Some(0));
assert_eq!(cmd.aggregated_output.as_deref(), Some("hello from codex"));
}
#[test]
fn test_failed_command_parses_all_lines() {
let events = parse_capture(include_str!("../test_cases/captures/failed_command.jsonl"));
assert_standard_envelope(&events);
assert_eq!(events.len(), 8);
}
#[test]
fn test_failed_command_status_and_exit_code() {
let events = parse_capture(include_str!("../test_cases/captures/failed_command.jsonl"));
let started_cmd = events.iter().find_map(|e| match e {
ThreadEvent::ItemStarted(is) => match &is.item {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
},
_ => None,
});
let completed_cmd = completed_items(&events).into_iter().find_map(|i| match i {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
});
let started = started_cmd.expect("Should have started command");
let completed = completed_cmd.expect("Should have completed command");
assert_eq!(started.status, CommandExecutionStatus::InProgress);
assert_eq!(started.exit_code, None);
assert_eq!(completed.status, CommandExecutionStatus::Failed);
assert_eq!(completed.exit_code, Some(42));
assert!(completed
.aggregated_output
.as_deref()
.unwrap_or("")
.is_empty());
}
#[test]
fn test_file_change_parses_all_lines() {
let events = parse_capture(include_str!("../test_cases/captures/file_change.jsonl"));
assert_standard_envelope(&events);
assert_eq!(events.len(), 12);
}
#[test]
fn test_file_change_item_fields() {
let events = parse_capture(include_str!("../test_cases/captures/file_change.jsonl"));
let fc: &FileChangeItem = completed_items(&events)
.into_iter()
.find_map(|i| match i {
ThreadItem::FileChange(f) => Some(f),
_ => None,
})
.expect("Should have a file_change item");
assert_eq!(fc.status, PatchApplyStatus::Completed);
assert_eq!(fc.changes.len(), 1);
assert!(matches!(fc.changes[0].kind, PatchChangeKind::Update { .. }));
assert!(fc.changes[0].path.contains("test.txt"));
}
#[test]
fn test_file_change_also_has_command_verification() {
let events = parse_capture(include_str!("../test_cases/captures/file_change.jsonl"));
let cmds: Vec<_> = completed_items(&events)
.into_iter()
.filter_map(|i| match i {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
})
.collect();
assert!(!cmds.is_empty());
assert!(cmds.iter().any(|c| c.command.contains("cat")));
assert!(cmds.iter().any(|c| c
.aggregated_output
.as_deref()
.unwrap_or("")
.contains("new content")));
}
#[test]
fn test_multi_command_parses_all_lines() {
let events = parse_capture(include_str!("../test_cases/captures/multi_command.jsonl"));
assert_standard_envelope(&events);
assert_eq!(events.len(), 12);
}
#[test]
fn test_multi_command_three_commands_executed() {
let events = parse_capture(include_str!("../test_cases/captures/multi_command.jsonl"));
let cmds: Vec<_> = completed_items(&events)
.into_iter()
.filter_map(|i| match i {
ThreadItem::CommandExecution(c) => Some(c),
_ => None,
})
.collect();
assert_eq!(cmds.len(), 3, "Expected exactly 3 completed commands");
for (i, cmd) in cmds.iter().enumerate() {
let step = format!("step{}", i + 1);
assert!(
cmd.command.contains(&step),
"Command {} should contain '{}'",
i,
step
);
assert_eq!(cmd.exit_code, Some(0));
assert_eq!(cmd.status, CommandExecutionStatus::Completed);
assert!(
cmd.aggregated_output
.as_deref()
.unwrap_or("")
.contains(&step),
"Output of command {} should contain '{}'",
i,
step
);
}
}
#[test]
fn test_multi_command_started_events_match_completed() {
let events = parse_capture(include_str!("../test_cases/captures/multi_command.jsonl"));
let started_ids: Vec<_> = events
.iter()
.filter_map(|e| match e {
ThreadEvent::ItemStarted(is) => match &is.item {
ThreadItem::CommandExecution(c) => Some(c.id.clone()),
_ => None,
},
_ => None,
})
.collect();
let completed_ids: Vec<_> = completed_items(&events)
.into_iter()
.filter_map(|i| match i {
ThreadItem::CommandExecution(c) => Some(c.id.clone()),
_ => None,
})
.collect();
assert_eq!(started_ids.len(), 3);
assert_eq!(completed_ids.len(), 3);
assert_eq!(
started_ids, completed_ids,
"Every started command should have a matching completed event"
);
}
#[test]
fn test_all_captures_have_unique_thread_ids() {
let captures = [
include_str!("../test_cases/captures/hello_world.jsonl"),
include_str!("../test_cases/captures/list_files.jsonl"),
include_str!("../test_cases/captures/file_create.jsonl"),
include_str!("../test_cases/captures/failed_command.jsonl"),
include_str!("../test_cases/captures/file_change.jsonl"),
include_str!("../test_cases/captures/multi_command.jsonl"),
];
let thread_ids: Vec<String> = captures
.iter()
.map(|c| {
let events = parse_capture(c);
match &events[0] {
ThreadEvent::ThreadStarted(e) => e.thread_id.clone(),
_ => panic!("First event should be ThreadStarted"),
}
})
.collect();
let mut deduped = thread_ids.clone();
deduped.sort();
deduped.dedup();
assert_eq!(
thread_ids.len(),
deduped.len(),
"All captures should have unique thread IDs"
);
}
#[test]
fn test_all_captures_have_cached_tokens() {
let captures = [
include_str!("../test_cases/captures/hello_world.jsonl"),
include_str!("../test_cases/captures/list_files.jsonl"),
include_str!("../test_cases/captures/file_create.jsonl"),
include_str!("../test_cases/captures/failed_command.jsonl"),
include_str!("../test_cases/captures/file_change.jsonl"),
include_str!("../test_cases/captures/multi_command.jsonl"),
];
for (i, capture) in captures.iter().enumerate() {
let events = parse_capture(capture);
if let ThreadEvent::TurnCompleted(tc) = events.last().unwrap() {
assert!(
tc.usage.cached_input_tokens > 0,
"Capture {} should have cached_input_tokens > 0",
i
);
}
}
}
#[test]
fn test_all_item_ids_are_sequential_within_capture() {
let captures = [
include_str!("../test_cases/captures/hello_world.jsonl"),
include_str!("../test_cases/captures/list_files.jsonl"),
include_str!("../test_cases/captures/file_create.jsonl"),
include_str!("../test_cases/captures/failed_command.jsonl"),
include_str!("../test_cases/captures/file_change.jsonl"),
include_str!("../test_cases/captures/multi_command.jsonl"),
];
for capture in &captures {
let events = parse_capture(capture);
let ids: Vec<String> = completed_items(&events)
.into_iter()
.map(|item| match item {
ThreadItem::UserMessage(u) => u.id.clone(),
ThreadItem::AgentMessage(m) => m.id.clone(),
ThreadItem::Reasoning(r) => r.id.clone(),
ThreadItem::CommandExecution(c) => c.id.clone(),
ThreadItem::FileChange(f) => f.id.clone(),
ThreadItem::McpToolCall(m) => m.id.clone(),
ThreadItem::WebSearch(w) => w.id.clone(),
ThreadItem::TodoList(t) => t.id.clone(),
ThreadItem::Error(e) => e.id.clone(),
})
.collect();
let mut seen: Vec<usize> = Vec::new();
for id in &ids {
assert!(
id.starts_with("item_"),
"ID '{}' should start with 'item_'",
id
);
let n: usize = id[5..]
.parse()
.unwrap_or_else(|_| panic!("ID '{}' should have a numeric suffix", id));
if !seen.contains(&n) {
seen.push(n);
}
}
for window in seen.windows(2) {
assert!(
window[1] > window[0],
"Item IDs should be monotonically increasing, got {} after {}",
window[1],
window[0]
);
}
}
}
#[test]
fn parse_error_carries_method_and_params_for_unmodeled_notification_variant() {
let line = r#"{"method":"item/completed","params":{"item":42}}"#;
let envelope: JsonRpcMessage = serde_json::from_str(line).expect("envelope parses");
let JsonRpcMessage::Notification(JsonRpcNotification { method, params }) = envelope else {
panic!("expected Notification, got: {:?}", envelope);
};
let err = Notification::from_envelope(&method, params.clone())
.expect_err("malformed params must fail typed decode");
let pe = ParseError::from_envelope(method.clone(), params.clone(), err);
assert_eq!(pe.method.as_deref(), Some("item/completed"));
assert_eq!(pe.raw_json, params);
assert!(
!pe.error_message.is_empty(),
"error_message should be populated"
);
let echoed: JsonRpcMessage = serde_json::from_str(&pe.raw_line)
.unwrap_or_else(|e| panic!("raw_line should re-parse as JsonRpcMessage: {}", e));
if let JsonRpcMessage::Notification(n) = echoed {
assert_eq!(n.method, "item/completed");
assert_eq!(n.params.unwrap()["item"], 42);
} else {
panic!("raw_line should re-parse as a Notification");
}
}
#[test]
fn parse_error_from_invalid_envelope_keeps_raw_line_without_method() {
let line = r#"{"completely":"unexpected"}"#;
let err = serde_json::from_str::<JsonRpcMessage>(line).expect_err("should not parse");
let pe = ParseError::from_line(line, err);
assert!(pe.method.is_none());
assert_eq!(pe.raw_line, line);
assert_eq!(pe.raw_json.as_ref().unwrap()["completely"], "unexpected");
}
#[test]
fn user_input_image_round_trips_url_and_file_id() {
use codex_codes::protocol::{ImageDetail, UserInput, UserInputImageReference};
let inline = UserInput::Image {
detail: Some(ImageDetail::High),
image: UserInputImageReference::Inline {
url: "data:image/png;base64,AAAA".into(),
},
};
let wire = serde_json::to_value(&inline).unwrap();
assert_eq!(
wire,
serde_json::json!({"type": "image", "detail": "high", "url": "data:image/png;base64,AAAA"})
);
assert_eq!(serde_json::from_value::<UserInput>(wire).unwrap(), inline);
let uploaded: UserInput =
serde_json::from_value(serde_json::json!({"type": "image", "fileId": "file_123"})).unwrap();
assert_eq!(
uploaded,
UserInput::Image {
detail: None,
image: UserInputImageReference::File {
file_id: "file_123".into()
},
}
);
let wire = serde_json::to_value(&uploaded).unwrap();
assert_eq!(wire["fileId"], "file_123");
assert!(wire.get("url").is_none());
assert!(wire.get("detail").is_none());
}
#[test]
fn function_call_output_input_image_round_trips_image_url_and_file_id() {
use codex_codes::protocol::{FunctionCallOutputContentItem, ImageReference};
let inline: FunctionCallOutputContentItem = serde_json::from_value(serde_json::json!({
"type": "input_image",
"image_url": "https://example.test/a.png",
"detail": "low"
}))
.unwrap();
let FunctionCallOutputContentItem::InputImage { image, detail } = &inline else {
panic!("expected InputImage");
};
assert_eq!(
*image,
ImageReference::Inline {
image_url: "https://example.test/a.png".into()
}
);
assert!(detail.is_some());
let wire = serde_json::to_value(&inline).unwrap();
assert_eq!(wire["image_url"], "https://example.test/a.png");
assert!(wire.get("file_id").is_none());
let uploaded: FunctionCallOutputContentItem =
serde_json::from_value(serde_json::json!({"type": "input_image", "file_id": "file_9"}))
.unwrap();
assert_eq!(
uploaded,
FunctionCallOutputContentItem::InputImage {
detail: None,
image: ImageReference::File {
file_id: "file_9".into()
},
}
);
}
#[test]
fn mcp_tool_call_item_carries_mcp_app_ui() {
use codex_codes::protocol::{McpAppDisplayMode, McpAppUi, ThreadItem as ProtocolThreadItem};
let item: ProtocolThreadItem = serde_json::from_value(serde_json::json!({
"type": "mcpToolCall",
"id": "item_1",
"server": "widgets",
"tool": "lookup",
"arguments": {"q": "x"},
"status": "completed",
"mcpAppResourceUri": "ui://widget/lookup.html",
"mcpAppUi": {
"resourceUri": "ui://widget/lookup.html",
"preferredModelDisplayMode": "fullscreen"
}
}))
.unwrap();
let ProtocolThreadItem::McpToolCall {
mcp_app_ui,
mcp_app_resource_uri,
..
} = &item
else {
panic!("expected McpToolCall");
};
assert_eq!(
*mcp_app_ui,
Some(McpAppUi {
resource_uri: "ui://widget/lookup.html".into(),
preferred_model_display_mode: McpAppDisplayMode::Fullscreen,
})
);
assert_eq!(
mcp_app_resource_uri.as_deref(),
Some("ui://widget/lookup.html")
);
let wire = serde_json::to_value(&item).unwrap();
assert_eq!(wire["mcpAppUi"]["preferredModelDisplayMode"], "fullscreen");
let legacy: ProtocolThreadItem = serde_json::from_value(serde_json::json!({
"type": "mcpToolCall",
"id": "item_1",
"server": "widgets",
"tool": "lookup",
"arguments": {},
"status": "inProgress"
}))
.unwrap();
let ProtocolThreadItem::McpToolCall { mcp_app_ui, .. } = &legacy else {
panic!("expected McpToolCall");
};
assert!(mcp_app_ui.is_none());
assert!(serde_json::to_value(&legacy)
.unwrap()
.get("mcpAppUi")
.is_none());
}
#[test]
fn config_requirements_reads_login_methods_and_sandbox_implementations() {
use codex_codes::protocol::{
ConfigRequirements, ForcedLoginMethod, WindowsSandboxImplementation,
};
let reqs: ConfigRequirements = serde_json::from_value(serde_json::json!({
"allowedLoginMethods": ["chatgpt"],
"allowedWindowsSandboxImplementations": ["elevated", "mxc"]
}))
.unwrap();
assert_eq!(
reqs.allowed_login_methods,
Some(vec![ForcedLoginMethod::Chatgpt])
);
assert_eq!(
reqs.allowed_windows_sandbox_implementations,
Some(vec![
WindowsSandboxImplementation::Elevated,
WindowsSandboxImplementation::Mxc
])
);
let none: ConfigRequirements = serde_json::from_value(serde_json::json!({
"allowedLoginMethods": []
}))
.unwrap();
assert_eq!(none.allowed_login_methods, Some(vec![]));
let wire = serde_json::to_value(ConfigRequirements::default()).unwrap();
assert!(wire.get("allowedLoginMethods").is_none());
}
#[test]
fn thread_resume_response_reads_collaboration_mode() {
use codex_codes::protocol::ThreadResumeResponse;
let base = serde_json::json!({
"thread": {
"id": "thr_1",
"preview": "",
"modelProvider": "openai",
"createdAt": 0,
"updatedAt": 0,
"path": "/tmp/thr_1.jsonl",
"cwd": "/tmp",
"cliVersion": "0.154.0",
"source": "cli",
"agentNickname": null,
"agentRole": null,
"gitInfo": null,
"name": null,
"turns": []
},
"model": "gpt-5",
"modelProvider": "openai",
"cwd": "/tmp",
"approvalPolicy": "never",
"approvalsReviewer": "user",
"sandbox": {"type": "readOnly"},
"serviceTier": null,
"reasoningEffort": null
});
let mut with_mode = base.clone();
with_mode["collaborationMode"] = serde_json::json!({
"mode": "default",
"settings": {"model": "gpt-5", "reasoning_effort": null, "developer_instructions": null}
});
let resumed: ThreadResumeResponse = serde_json::from_value(with_mode).unwrap();
let mode = resumed.collaboration_mode.expect("collaborationMode");
assert_eq!(serde_json::to_value(&mode.mode).unwrap(), "default");
let older: ThreadResumeResponse = serde_json::from_value(base).unwrap();
assert!(older.collaboration_mode.is_none());
let wire = serde_json::to_value(&older).unwrap();
assert!(wire.get("collaborationMode").is_none());
}