use super::*;
#[test]
fn timeline_entry_from_thread_item_classifies_command_execution_semantics_from_actions() {
let cases = vec![
(
"read",
"cat bridge/src/state/timeline.rs",
json!([{ "type": "read", "path": "bridge/src/state/timeline.rs" }]),
"explored",
Some("read"),
1_i64,
Some("bridge/src/state/timeline.rs"),
None,
),
(
"list",
"ls bridge/src/state",
json!([{ "type": "listFiles", "path": "bridge/src/state" }]),
"explored",
Some("list"),
1_i64,
Some("bridge/src/state"),
None,
),
(
"search",
"rg semantic bridge/src/state",
json!([{ "type": "search", "path": "bridge/src/state", "query": "semantic" }]),
"explored",
Some("search"),
1_i64,
Some("bridge/src/state"),
Some("semantic"),
),
(
"mixed",
"bash -lc 'cat bridge/src/state/timeline.rs && rg semantic bridge/src/state'",
json!([
{ "type": "read", "path": "bridge/src/state/timeline.rs" },
{ "type": "search", "path": "bridge/src/state", "query": "semantic" }
]),
"explored",
Some("mixed"),
2_i64,
Some("bridge/src/state/timeline.rs"),
Some("semantic"),
),
(
"unknown",
"printf 'done'",
json!([{ "type": "write", "path": "bridge/src/state/timeline.rs" }]),
"ran",
Some("run"),
0_i64,
None,
None,
),
];
for (
index,
(
_label,
command,
actions,
expected_kind,
expected_detail,
expected_target_count,
expected_primary_target,
expected_query,
),
) in cases.into_iter().enumerate()
{
let entry = timeline_entry_from_thread_item(
PRIMARY_RUNTIME_ID,
"thread-1",
Some("turn-1"),
&json!({
"id": format!("item-command-{index}"),
"type": "commandExecution",
"command": command,
"aggregatedOutput": "ok",
"commandActions": actions,
}),
"thread_item",
false,
true,
)
.expect("应创建 commandExecution 时间线条目");
assert_eq!(
Some(expected_kind),
entry
.metadata
.get("semantic")
.and_then(|value| value.get("kind"))
.and_then(Value::as_str),
);
assert_eq!(
expected_detail,
entry
.metadata
.get("semantic")
.and_then(|value| value.get("detail"))
.and_then(Value::as_str),
);
assert_eq!(
Some("high"),
entry
.metadata
.get("semantic")
.and_then(|value| value.get("confidence"))
.and_then(Value::as_str),
);
assert_eq!(
Some(command),
entry
.metadata
.get("summary")
.and_then(|value| value.get("command"))
.and_then(Value::as_str),
);
assert_eq!(
Some(expected_target_count),
entry
.metadata
.get("summary")
.and_then(|value| value.get("targets"))
.and_then(Value::as_array)
.map(|items| items.len() as i64),
);
assert_eq!(
expected_primary_target,
entry
.metadata
.get("summary")
.and_then(|value| value.get("targets"))
.and_then(Value::as_array)
.and_then(|items| items.first())
.and_then(Value::as_str),
);
assert_eq!(
expected_query,
entry
.metadata
.get("summary")
.and_then(|value| value.get("query"))
.and_then(Value::as_str),
);
}
}
#[test]
fn timeline_entry_from_thread_item_summarizes_edited_and_waited_semantics() {
let file_change_entry = timeline_entry_from_thread_item(
PRIMARY_RUNTIME_ID,
"thread-1",
Some("turn-2"),
&json!({
"id": "item-file-change",
"type": "fileChange",
"changes": [
{
"path": "bridge/src/state/timeline.rs",
"kind": "modified",
"diff": "diff --git a/bridge/src/state/timeline.rs b/bridge/src/state/timeline.rs\n--- a/bridge/src/state/timeline.rs\n+++ b/bridge/src/state/timeline.rs\n@@ -1 +1,2 @@\n-old\n+new\n+extra\n"
},
{
"path": "docs/timeline.md",
"kind": "deleted",
"content": "line-1\nline-2"
}
]
}),
"thread_item",
false,
true,
)
.expect("应创建 fileChange 时间线条目");
assert_eq!(
Some("edited"),
file_change_entry
.metadata
.get("semantic")
.and_then(|value| value.get("kind"))
.and_then(Value::as_str),
);
assert_eq!(
Some("bridge/src/state/timeline.rs"),
file_change_entry
.metadata
.get("summary")
.and_then(|value| value.get("primaryPath"))
.and_then(Value::as_str),
);
assert_eq!(
Some(2),
file_change_entry
.metadata
.get("summary")
.and_then(|value| value.get("fileCount"))
.and_then(Value::as_i64),
);
assert_eq!(
Some(2),
file_change_entry
.metadata
.get("summary")
.and_then(|value| value.get("addLines"))
.and_then(Value::as_i64),
);
assert_eq!(
Some(3),
file_change_entry
.metadata
.get("summary")
.and_then(|value| value.get("removeLines"))
.and_then(Value::as_i64),
);
let waited_entry = timeline_entry_from_thread_item(
PRIMARY_RUNTIME_ID,
"thread-1",
Some("turn-2"),
&json!({
"id": "item-wait",
"type": "collabAgentToolCall",
"tool": "Wait",
"receiverThreadIds": ["thread-a", "thread-b"]
}),
"thread_item",
false,
true,
)
.expect("应创建 collabAgentToolCall 时间线条目");
assert_eq!(
Some("waited"),
waited_entry
.metadata
.get("semantic")
.and_then(|value| value.get("kind"))
.and_then(Value::as_str),
);
assert_eq!(
Some("wait_agents"),
waited_entry
.metadata
.get("semantic")
.and_then(|value| value.get("detail"))
.and_then(Value::as_str),
);
assert_eq!(
Some(2),
waited_entry
.metadata
.get("summary")
.and_then(|value| value.get("waitCount"))
.and_then(Value::as_i64),
);
}