79 lines
3.3 KiB
Rust
79 lines
3.3 KiB
Rust
//! Robustness against a hostile or broken agent: malformed lines, oversized
|
|
//! lines, unknown update shapes and unsupported agent requests.
|
|
|
|
mod common;
|
|
|
|
use std::time::Duration;
|
|
|
|
use common::{collect_turn, collect_until, handshake, spawn_stub};
|
|
use notescraft_acp::{Event, PromptBlock, SessionUpdate};
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(15);
|
|
|
|
#[test]
|
|
fn malformed_lines_become_diagnostics_and_the_turn_still_finishes() {
|
|
let process = spawn_stub("garbage");
|
|
let session_id = handshake(&process);
|
|
process.prompt(&session_id, &[PromptBlock::Text("hello".to_string())]).unwrap_or_else(|err| panic!("prompt failed: {err}"));
|
|
let events = collect_turn(&process, Some("allow"), TIMEOUT);
|
|
|
|
let diagnostics: Vec<&str> = events
|
|
.iter()
|
|
.filter_map(|event| match event {
|
|
Event::Error(message) => Some(message.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(diagnostics.len(), 2, "expected one diagnostic per bad line: {events:?}");
|
|
assert!(diagnostics.iter().all(|message| !message.is_empty()));
|
|
assert!(matches!(events.last(), Some(Event::PromptDone { stop_reason }) if stop_reason == "end_turn"), "the turn must still end: {events:?}");
|
|
let chunks: String = events
|
|
.iter()
|
|
.filter_map(|event| match event {
|
|
Event::Update(SessionUpdate::AgentMessageChunk(text)) => Some(text.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(chunks, "Hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn oversized_line_is_dropped_with_a_diagnostic_and_the_stream_continues() {
|
|
let process = spawn_stub("giant");
|
|
let session_id = handshake(&process);
|
|
process.prompt(&session_id, &[PromptBlock::Text("hello".to_string())]).unwrap_or_else(|err| panic!("prompt failed: {err}"));
|
|
let events = collect_turn(&process, Some("allow"), TIMEOUT);
|
|
|
|
let dropped = events.iter().any(|event| matches!(event, Event::Error(message) if message.contains("longer than")));
|
|
assert!(dropped, "expected an oversized-line diagnostic: {events:?}");
|
|
let chunks: String = events
|
|
.iter()
|
|
.filter_map(|event| match event {
|
|
Event::Update(SessionUpdate::AgentMessageChunk(text)) => Some(text.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
assert_eq!(chunks, "Hello world");
|
|
assert!(matches!(events.last(), Some(Event::PromptDone { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn unsupported_agent_requests_get_method_not_found() {
|
|
let process = spawn_stub("unsupported");
|
|
let session_id = handshake(&process);
|
|
process.prompt(&session_id, &[PromptBlock::Text("write a note".to_string())]).unwrap_or_else(|err| panic!("prompt failed: {err}"));
|
|
|
|
let events = collect_until(process.events(), TIMEOUT, |event| {
|
|
matches!(
|
|
event,
|
|
Event::Update(SessionUpdate::Other(value))
|
|
if value.get("sessionUpdate").and_then(serde_json::Value::as_str) == Some("ack")
|
|
)
|
|
});
|
|
let Some(Event::Update(SessionUpdate::Other(ack))) = events.iter().find(|event| matches!(event, Event::Update(SessionUpdate::Other(_)))) else {
|
|
panic!("no ack update: {events:?}");
|
|
};
|
|
assert_eq!(ack.get("code").and_then(serde_json::Value::as_i64), Some(-32_601), "expected -32601 method not found: {ack:?}");
|
|
assert_eq!(ack.get("optionId").and_then(serde_json::Value::as_str), Some("cancelled"));
|
|
}
|