use codex_mobile_contracts::{ExecCommandLine, ThreadRenderNode, ThreadRenderSnapshot};
use serde_json::json;
#[test]
fn thread_render_snapshot_serializes_exec_group_fields_in_camel_case() {
let snapshot = ThreadRenderSnapshot {
runtime_id: "primary".to_string(),
thread_id: "thread-1".to_string(),
revision: 7,
status_surface: None,
nodes: vec![ThreadRenderNode::ExecGroup {
id: "exec-1".to_string(),
turn_id: Some("turn-1".to_string()),
item_id: Some("item-1".to_string()),
title: "Ran".to_string(),
kind: "ran".to_string(),
state: "completed".to_string(),
commands: vec![ExecCommandLine {
label: "Run".to_string(),
text: "uname -a".to_string(),
}],
output_text: Some("Linux\n".to_string()),
exit_code: Some(0),
}],
};
let value = serde_json::to_value(&snapshot).expect("snapshot 应可序列化");
let node = &value["nodes"][0];
assert_eq!(node["turnId"], json!("turn-1"));
assert_eq!(node["itemId"], json!("item-1"));
assert_eq!(node["outputText"], json!("Linux\n"));
assert_eq!(node["exitCode"], json!(0));
assert!(node.get("turn_id").is_none());
assert!(node.get("item_id").is_none());
assert!(node.get("output_text").is_none());
assert!(node.get("exit_code").is_none());
}
#[test]
fn thread_render_node_rejects_legacy_snake_case_exec_fields() {
let error = serde_json::from_value::<ThreadRenderNode>(json!({
"type": "exec_group",
"id": "exec-legacy",
"turn_id": "turn-legacy",
"item_id": "item-legacy",
"title": "Ran",
"kind": "ran",
"state": "completed",
"commands": [
{
"label": "Run",
"text": "pwd"
}
],
"output_text": "/tmp/work\n",
"exit_code": 0
}))
.expect_err("旧版 exec_group JSON 应被拒绝");
assert!(error.to_string().contains("unknown field"));
}
#[test]
fn thread_render_nodes_serialize_and_deserialize_detail_lines_and_saved_path() {
let collab_value = serde_json::to_value(ThreadRenderNode::CollabEvent {
id: "collab-1".to_string(),
turn_id: Some("turn-2".to_string()),
item_id: Some("item-2".to_string()),
title: "Subagent".to_string(),
detail_lines: vec!["step 1".to_string(), "step 2".to_string()],
})
.expect("collab event 应可序列化");
let image_value = serde_json::to_value(ThreadRenderNode::ImageGeneration {
id: "image-1".to_string(),
turn_id: Some("turn-3".to_string()),
item_id: Some("item-3".to_string()),
title: "Image generation".to_string(),
state: "completed".to_string(),
prompt: Some("draw it".to_string()),
result: "ok".to_string(),
saved_path: Some("/tmp/image.png".to_string()),
})
.expect("image generation 应可序列化");
assert_eq!(collab_value["detailLines"], json!(["step 1", "step 2"]));
assert!(collab_value.get("detail_lines").is_none());
assert_eq!(image_value["savedPath"], json!("/tmp/image.png"));
assert!(image_value.get("saved_path").is_none());
let collab_error = serde_json::from_value::<ThreadRenderNode>(json!({
"type": "collab_event",
"id": "collab-legacy",
"turn_id": "turn-legacy",
"item_id": "item-legacy",
"title": "Subagent",
"detail_lines": ["legacy line"]
}))
.expect_err("旧版 collab_event JSON 应被拒绝");
let image_error = serde_json::from_value::<ThreadRenderNode>(json!({
"type": "image_generation",
"id": "image-legacy",
"turn_id": "turn-legacy",
"item_id": "item-legacy",
"title": "Image generation",
"state": "completed",
"result": "ok",
"saved_path": "/tmp/legacy.png"
}))
.expect_err("旧版 image_generation JSON 应被拒绝");
assert!(collab_error.to_string().contains("unknown field"));
assert!(image_error.to_string().contains("unknown field"));
}