mirror of
https://github.com/katanemo/plano.git
synced 2026-06-20 15:28:07 +02:00
- main.rs: rebuild claude_cli_config_from_env on top of
SessionManagerConfig::default() and only override fields that have a
parsed env var, so the defaults live in exactly one place.
- hermesllm/apis/claude_cli.rs: delete the dead
`_touch_messages_message_type` stub and its unused MessagesMessage
import; apply pedantic-clippy fixes that touch the new code
(clone_from over `= x.clone()`, Map::default() over Default::default(),
map_or_else over .map(...).unwrap_or_else(...), str::to_string method
reference, collapsed identical match arms).
- hermesllm/providers/id.rs: collapse the two match arms that mapped
"claude-cli" and "claude_cli" to ProviderId::ClaudeCli.
- hermesllm/tests/claude_cli_fixtures.rs: collect text deltas straight
into a String instead of `.collect::<Vec<_>>().join("")`.
- brightstaff/tests/claude_cli_bridge.rs: add a Drop impl on
BridgeFixture so a panicking test still releases the listener task.
113 lines
4 KiB
Rust
113 lines
4 KiB
Rust
//! End-to-end fixture tests for `apis::claude_cli`. Each NDJSON file under
|
|
//! `tests/fixtures/claude_cli/` represents one canned subprocess output. We
|
|
//! parse it line-by-line and feed it through the same translation entry points
|
|
//! the brightstaff bridge uses at runtime.
|
|
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
|
|
use hermesllm::apis::anthropic::{
|
|
MessagesContentBlock, MessagesContentDelta, MessagesStopReason, MessagesStreamEvent,
|
|
};
|
|
use hermesllm::apis::claude_cli::{
|
|
cli_event_to_messages_stream_event, collect_to_messages_response, parse_ndjson_line,
|
|
ClaudeCliEvent, ClaudeCliTranslationError,
|
|
};
|
|
|
|
fn fixture_path(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("fixtures")
|
|
.join("claude_cli")
|
|
.join(name)
|
|
}
|
|
|
|
fn load_events(name: &str) -> Vec<ClaudeCliEvent> {
|
|
let body = fs::read_to_string(fixture_path(name))
|
|
.unwrap_or_else(|e| panic!("read fixture {name}: {e}"));
|
|
body.lines()
|
|
.filter_map(|line| parse_ndjson_line(line).map(|r| r.unwrap_or_else(|e| panic!("{e}"))))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn text_response_aggregates_into_messages_response() {
|
|
let events = load_events("text_response.ndjson");
|
|
let resp = collect_to_messages_response("claude-cli/sonnet", events.clone()).unwrap();
|
|
assert_eq!(resp.id, "msg_01ABC");
|
|
assert_eq!(resp.model, "claude-sonnet-4-6");
|
|
assert_eq!(resp.usage.input_tokens, 12);
|
|
assert_eq!(resp.usage.output_tokens, 4);
|
|
assert!(matches!(resp.stop_reason, MessagesStopReason::EndTurn));
|
|
match &resp.content[..] {
|
|
[MessagesContentBlock::Text { text, .. }] => assert_eq!(text, "Hello, world!"),
|
|
other => panic!("expected single Text, got {other:?}"),
|
|
}
|
|
|
|
// Verify the streaming projection emits exactly the events the Anthropic
|
|
// SSE wire protocol expects, in order.
|
|
let stream: Vec<MessagesStreamEvent> = events
|
|
.iter()
|
|
.filter_map(cli_event_to_messages_stream_event)
|
|
.collect();
|
|
assert!(matches!(
|
|
stream[0],
|
|
MessagesStreamEvent::MessageStart { .. }
|
|
));
|
|
let final_event = stream.last().unwrap();
|
|
assert!(matches!(final_event, MessagesStreamEvent::MessageStop));
|
|
let text_deltas: String = stream
|
|
.iter()
|
|
.filter_map(|ev| match ev {
|
|
MessagesStreamEvent::ContentBlockDelta {
|
|
delta: MessagesContentDelta::TextDelta { text },
|
|
..
|
|
} => Some(text.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(text_deltas, "Hello, world!");
|
|
}
|
|
|
|
#[test]
|
|
fn tool_use_response_assembles_partial_json() {
|
|
let events = load_events("tool_use_response.ndjson");
|
|
let resp = collect_to_messages_response("sonnet", events).unwrap();
|
|
assert!(matches!(resp.stop_reason, MessagesStopReason::ToolUse));
|
|
match &resp.content[..] {
|
|
[MessagesContentBlock::ToolUse {
|
|
id, name, input, ..
|
|
}] => {
|
|
assert_eq!(id, "toolu_W");
|
|
assert_eq!(name, "get_weather");
|
|
assert_eq!(input["city"], "Seattle");
|
|
}
|
|
other => panic!("expected single ToolUse block, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn error_response_returns_cli_error() {
|
|
let events = load_events("error_response.ndjson");
|
|
let err = collect_to_messages_response("sonnet", events).unwrap_err();
|
|
match err {
|
|
ClaudeCliTranslationError::CliError { message } => {
|
|
assert!(
|
|
message.contains("529"),
|
|
"expected 529 in error message, got: {message}"
|
|
);
|
|
}
|
|
other => panic!("expected CliError, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn retry_then_success_is_treated_as_success() {
|
|
let events = load_events("retry_then_success.ndjson");
|
|
let resp = collect_to_messages_response("sonnet", events).unwrap();
|
|
assert!(matches!(resp.stop_reason, MessagesStopReason::EndTurn));
|
|
match &resp.content[..] {
|
|
[MessagesContentBlock::Text { text, .. }] => assert_eq!(text, "ok"),
|
|
other => panic!("expected Text block, got {other:?}"),
|
|
}
|
|
}
|