mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
104 lines
4.4 KiB
Rust
104 lines
4.4 KiB
Rust
//! End-to-end test of the Claude Code stream-json pipeline.
|
|
//!
|
|
//! Feeds a captured representative CC 2.x stream-json transcript through
|
|
//! `StreamJsonParser` → `EventMapper` and asserts that:
|
|
//! - text deltas arrive in order and aggregate into the final response
|
|
//! - tool-use blocks emit ToolCallStart + ToolCallArgsDelta + a final
|
|
//! ToolCall with parsed JSON arguments
|
|
//! - the `result` event finalizes usage tokens (incl. cache_read)
|
|
//! - session_id is captured from the first `system` event
|
|
//!
|
|
//! This is a parser-level E2E; the real driver / process spawn is mocked
|
|
//! in `tests/claude_code_driver_smoke.rs`.
|
|
|
|
use openhuman_core::openhuman::inference::provider::claude_code::{
|
|
event_mapper::EventMapper, stream_parser::StreamJsonParser,
|
|
};
|
|
use openhuman_core::openhuman::inference::provider::traits::ProviderDelta;
|
|
|
|
const TRANSCRIPT: &str = r#"{"type":"system","subtype":"init","session_id":"f47ac10b-58cc-4372-a567-0e02b2c3d479","schema_version":"2.0"}
|
|
{"type":"stream_event","event":{"type":"content_block_start","index":0,"content_block":{"type":"text"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_stop","index":0}}
|
|
{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"call_42","name":"memory_search"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"que"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"ry\":\"foo\"}"}}}
|
|
{"type":"stream_event","event":{"type":"content_block_stop","index":1}}
|
|
{"type":"assistant","message":{"type":"message","role":"assistant","content":[]}}
|
|
{"type":"result","subtype":"success","usage":{"input_tokens":120,"output_tokens":42,"cache_read_input_tokens":80,"cache_creation_input_tokens":0},"total_cost_usd":0.0012}
|
|
"#;
|
|
|
|
#[test]
|
|
fn captures_text_tool_call_and_usage() {
|
|
let mut parser = StreamJsonParser::new();
|
|
let mut mapper = EventMapper::new();
|
|
let mut deltas: Vec<ProviderDelta> = Vec::new();
|
|
|
|
// Feed in chunks to exercise the chunk-boundary buffering as well.
|
|
let mid = TRANSCRIPT.len() / 2;
|
|
for chunk in [&TRANSCRIPT[..mid], &TRANSCRIPT[mid..]] {
|
|
for evt in parser.feed(chunk) {
|
|
for d in mapper.handle(evt) {
|
|
deltas.push(d);
|
|
}
|
|
}
|
|
}
|
|
for evt in parser.end() {
|
|
for d in mapper.handle(evt) {
|
|
deltas.push(d);
|
|
}
|
|
}
|
|
|
|
// Schema version was captured by the parser.
|
|
assert_eq!(parser.schema_version.as_deref(), Some("2.0"));
|
|
|
|
// Session id was captured by the mapper from the first system event.
|
|
assert_eq!(
|
|
mapper.session_id.as_deref(),
|
|
Some("f47ac10b-58cc-4372-a567-0e02b2c3d479")
|
|
);
|
|
|
|
// Text deltas arrived in order.
|
|
let text_chunks: Vec<&str> = deltas
|
|
.iter()
|
|
.filter_map(|d| match d {
|
|
ProviderDelta::TextDelta { delta } => Some(delta.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(text_chunks, vec!["Hello", " world"]);
|
|
|
|
// Tool call lifecycle.
|
|
assert!(deltas.iter().any(|d| matches!(
|
|
d,
|
|
ProviderDelta::ToolCallStart { tool_name, call_id }
|
|
if tool_name == "memory_search" && call_id == "call_42"
|
|
)));
|
|
let args_concat: String = deltas
|
|
.iter()
|
|
.filter_map(|d| match d {
|
|
ProviderDelta::ToolCallArgsDelta { call_id, delta } if call_id == "call_42" => {
|
|
Some(delta.as_str())
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("");
|
|
assert_eq!(args_concat, r#"{"query":"foo"}"#);
|
|
|
|
// Aggregated response.
|
|
assert_eq!(mapper.final_text, "Hello world");
|
|
assert_eq!(mapper.tool_calls.len(), 1);
|
|
assert_eq!(mapper.tool_calls[0].name, "memory_search");
|
|
assert_eq!(mapper.tool_calls[0].id, "call_42");
|
|
assert_eq!(mapper.tool_calls[0].arguments, r#"{"query":"foo"}"#);
|
|
|
|
// Usage from the `result` event.
|
|
assert!(mapper.finished);
|
|
let u = mapper.usage.as_ref().expect("usage should be populated");
|
|
assert_eq!(u.input_tokens, 120);
|
|
assert_eq!(u.output_tokens, 42);
|
|
assert_eq!(u.cached_input_tokens, 80);
|
|
}
|