use super::*;
#[test]
fn timeline_entries_from_events_merge_reasoning_deltas_and_replace_plan_placeholder() {
let authoritative_plan = serde_json::to_value(TimelineEntry {
id: "turn-1:plan-1".to_string(),
runtime_id: PRIMARY_RUNTIME_ID.to_string(),
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
item_id: Some("plan-1".to_string()),
entry_type: "plan".to_string(),
title: Some("执行计划".to_string()),
text: "[进行中] 读取线程".to_string(),
status: Some("inProgress".to_string()),
metadata: json!({
"schemaVersion": 1,
"sourceKind": "stream_event",
"rawType": "plan",
"renderKind": "plan",
"collapseHint": {
"enabled": true,
"preferCollapsed": false,
"lineThreshold": 8,
"charThreshold": 320
},
"stream": {
"isStreaming": false,
"authoritative": true
},
"payload": {
"explanation": "先确认协议,再更新 UI。",
"plan": [
{
"step": "读取线程",
"status": "inProgress"
}
]
}
}),
})
.expect("authoritative plan 应可序列化");
let events = vec![
PersistedEvent {
seq: 1,
event_type: "plan_delta".to_string(),
runtime_id: Some(PRIMARY_RUNTIME_ID.to_string()),
thread_id: Some("thread-1".to_string()),
payload: normalize_delta_payload(
PRIMARY_RUNTIME_ID,
json!({
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "turn-plan:turn-1",
"delta": "[进行中] 读取线程"
}),
"plan",
Some("执行计划".to_string()),
Some("inProgress".to_string()),
"item/plan/delta",
json!({
"explanation": null,
"plan": [],
}),
None,
None,
),
created_at_ms: 1,
},
PersistedEvent {
seq: 2,
event_type: "turn_plan_updated".to_string(),
runtime_id: Some(PRIMARY_RUNTIME_ID.to_string()),
thread_id: Some("thread-1".to_string()),
payload: authoritative_plan,
created_at_ms: 2,
},
PersistedEvent {
seq: 3,
event_type: "reasoning_summary_part_added".to_string(),
runtime_id: Some(PRIMARY_RUNTIME_ID.to_string()),
thread_id: Some("thread-1".to_string()),
payload: normalize_delta_payload(
PRIMARY_RUNTIME_ID,
json!({
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "reason-1",
"delta": ""
}),
"reasoning",
Some("思考过程".to_string()),
Some("inProgress".to_string()),
"item/reasoning/summaryPartAdded",
json!({
"summary": [],
"content": [],
}),
Some(0),
None,
),
created_at_ms: 3,
},
PersistedEvent {
seq: 4,
event_type: "reasoning_summary_text_delta".to_string(),
runtime_id: Some(PRIMARY_RUNTIME_ID.to_string()),
thread_id: Some("thread-1".to_string()),
payload: normalize_delta_payload(
PRIMARY_RUNTIME_ID,
json!({
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "reason-1",
"delta": "先分析"
}),
"reasoning",
Some("思考过程".to_string()),
Some("inProgress".to_string()),
"item/reasoning/summaryTextDelta",
json!({
"summary": [],
"content": [],
}),
Some(0),
None,
),
created_at_ms: 4,
},
PersistedEvent {
seq: 5,
event_type: "reasoning_text_delta".to_string(),
runtime_id: Some(PRIMARY_RUNTIME_ID.to_string()),
thread_id: Some("thread-1".to_string()),
payload: normalize_delta_payload(
PRIMARY_RUNTIME_ID,
json!({
"threadId": "thread-1",
"turnId": "turn-1",
"itemId": "reason-1",
"delta": "检查流式合并"
}),
"reasoning",
Some("思考过程".to_string()),
Some("inProgress".to_string()),
"item/reasoning/textDelta",
json!({
"summary": [],
"content": [],
}),
None,
Some(0),
),
created_at_ms: 5,
},
];
let entries = timeline_entries_from_events(&events);
let plan_entries = entries
.iter()
.filter(|entry| entry.entry_type == "plan")
.collect::<Vec<_>>();
assert_eq!(1, plan_entries.len());
assert_eq!(Some("plan-1"), plan_entries[0].item_id.as_deref());
let reasoning_entry = entries
.iter()
.find(|entry| entry.item_id.as_deref() == Some("reason-1"))
.expect("应合并 reasoning 条目");
assert!(reasoning_entry.text.contains("思考摘要\n先分析"));
assert!(reasoning_entry.text.contains("思考内容\n检查流式合并"));
assert_eq!(
Some("thinking"),
reasoning_entry
.metadata
.get("renderKind")
.and_then(Value::as_str),
);
}