252 lines
7.2 KiB
Rust
252 lines
7.2 KiB
Rust
//! Tests for the channel messages and the usage record, against byte-exact fixtures. Do not edit.
|
|
|
|
use proto::{
|
|
CallId, DataClass, Envelope, Epoch, ErrorCode, Hash32, LogRecord, Message, SessionId,
|
|
Timestamp, ToolCall, Turn, TurnDone, TurnEvent, Usage, WireError,
|
|
};
|
|
|
|
fn fixture(kind: &str, name: &str) -> String {
|
|
let path = format!(
|
|
"{}/tests/fixtures/{kind}/{name}",
|
|
env!("CARGO_MANIFEST_DIR")
|
|
);
|
|
std::fs::read_to_string(&path)
|
|
.unwrap_or_else(|e| panic!("{path}: {e}"))
|
|
.trim_end_matches('\n')
|
|
.to_string()
|
|
}
|
|
|
|
fn check(name: &str, want: Envelope) {
|
|
let text = fixture("wire", name);
|
|
let got: Envelope = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{name}: {e}"));
|
|
assert_eq!(got, want, "{name}: decoded value");
|
|
assert_eq!(
|
|
serde_json::to_string(&want).unwrap(),
|
|
text,
|
|
"{name}: encoded bytes"
|
|
);
|
|
}
|
|
|
|
fn env(id: u64, r#final: bool, msg: Message) -> Envelope {
|
|
Envelope {
|
|
v: 1,
|
|
id,
|
|
r#final,
|
|
msg,
|
|
}
|
|
}
|
|
|
|
fn usage() -> Usage {
|
|
Usage {
|
|
cache_n: 539,
|
|
prompt_n: 27,
|
|
predicted_n: 24,
|
|
reasoning_tokens: 8,
|
|
thinking_capped: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn turn() {
|
|
let body = Turn {
|
|
session: SessionId::new("chat-1789700000-42").unwrap(),
|
|
content: "What time is it?".to_string(),
|
|
resume: false,
|
|
};
|
|
check("turn.json", env(3, true, Message::Turn(body)));
|
|
}
|
|
|
|
#[test]
|
|
fn turn_events() {
|
|
check(
|
|
"turn_event_tool_result.json",
|
|
env(
|
|
3,
|
|
false,
|
|
Message::TurnEvent(TurnEvent::ToolResult {
|
|
name: "clock".to_string(),
|
|
class: DataClass::Public,
|
|
truncated: false,
|
|
}),
|
|
),
|
|
);
|
|
check(
|
|
"turn_event_content.json",
|
|
env(
|
|
3,
|
|
false,
|
|
Message::TurnEvent(TurnEvent::Content {
|
|
text: "It is ".to_string(),
|
|
}),
|
|
),
|
|
);
|
|
check(
|
|
"turn_event_retrying.json",
|
|
env(
|
|
3,
|
|
false,
|
|
Message::TurnEvent(TurnEvent::Retrying {
|
|
attempt: 2,
|
|
after_ms: 1500,
|
|
error: "the server went silent".to_string(),
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn turn_done_and_the_new_error_codes() {
|
|
check(
|
|
"turn_done.json",
|
|
env(
|
|
3,
|
|
true,
|
|
Message::TurnDone(TurnDone {
|
|
content: "It is noon.".to_string(),
|
|
usage: usage(),
|
|
}),
|
|
),
|
|
);
|
|
let body = WireError {
|
|
code: ErrorCode::SessionFull,
|
|
detail: "this conversation is full; start a new one".to_string(),
|
|
};
|
|
check(
|
|
"error_session_full.json",
|
|
env(3, true, Message::Error(body)),
|
|
);
|
|
let codes = [
|
|
(ErrorCode::SessionFull, "session_full"),
|
|
(ErrorCode::TurnLimit, "turn_limit"),
|
|
(ErrorCode::SessionBusy, "session_busy"),
|
|
(ErrorCode::NoSuchSession, "no_such_session"),
|
|
(ErrorCode::SessionExists, "session_exists"),
|
|
(ErrorCode::Inference, "inference"),
|
|
];
|
|
for (value, text) in codes {
|
|
assert_eq!(
|
|
serde_json::to_string(&value).unwrap(),
|
|
format!("\"{text}\"")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_turn_event_kind_round_trips() {
|
|
let all = vec![
|
|
TurnEvent::Queued { ahead: 1 },
|
|
TurnEvent::Waiting { slot_busy: true },
|
|
TurnEvent::Progress {
|
|
total: 100,
|
|
cache: 50,
|
|
processed: 75,
|
|
},
|
|
TurnEvent::Reasoning {
|
|
text: "hm".to_string(),
|
|
},
|
|
TurnEvent::Content {
|
|
text: "hi".to_string(),
|
|
},
|
|
TurnEvent::ToolCallStarted {
|
|
name: "clock".to_string(),
|
|
},
|
|
TurnEvent::ToolResult {
|
|
name: "clock".to_string(),
|
|
class: DataClass::Secret,
|
|
truncated: true,
|
|
},
|
|
TurnEvent::ThinkingCapped { tokens: 4096 },
|
|
TurnEvent::Retrying {
|
|
attempt: 2,
|
|
after_ms: 10,
|
|
error: "x".to_string(),
|
|
},
|
|
TurnEvent::CacheLoss {
|
|
expected: 500,
|
|
got: 20,
|
|
},
|
|
];
|
|
for event in all {
|
|
let text = serde_json::to_string(&event).unwrap();
|
|
assert!(text.starts_with("{\"event\":\""), "{text}");
|
|
assert_eq!(serde_json::from_str::<TurnEvent>(&text).unwrap(), event);
|
|
}
|
|
assert!(serde_json::from_str::<TurnEvent>(r#"{"event":"content","text":"x","zz":1}"#).is_err());
|
|
assert!(serde_json::from_str::<TurnEvent>(r#"{"event":"dance"}"#).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn usage_records() {
|
|
let text = fixture("records", "session_usage.jsonl");
|
|
let lines: Vec<&str> = text.lines().collect();
|
|
let want = [
|
|
LogRecord::SessionStart {
|
|
time: Timestamp::parse("2026-09-18T08:05:00.000Z").unwrap(),
|
|
session: SessionId::new("chat-1789700000-42").unwrap(),
|
|
epoch: Epoch(0),
|
|
slot: 0,
|
|
baseline: Hash32::from_hex(
|
|
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
|
)
|
|
.unwrap(),
|
|
},
|
|
LogRecord::User {
|
|
time: Timestamp::parse("2026-09-18T08:05:01.000Z").unwrap(),
|
|
content: "What time is it?".to_string(),
|
|
},
|
|
LogRecord::Assistant {
|
|
time: Timestamp::parse("2026-09-18T08:05:03.000Z").unwrap(),
|
|
content: None,
|
|
reasoning_content: None,
|
|
tool_calls: vec![ToolCall {
|
|
id: "call_a1".to_string(),
|
|
name: "clock".to_string(),
|
|
arguments: "{}".to_string(),
|
|
}],
|
|
},
|
|
LogRecord::Usage {
|
|
time: Timestamp::parse("2026-09-18T08:05:03.000Z").unwrap(),
|
|
cache_n: 539,
|
|
prompt_n: 27,
|
|
predicted_n: 24,
|
|
reasoning_tokens: 8,
|
|
thinking_capped: false,
|
|
},
|
|
LogRecord::ToolResult {
|
|
time: Timestamp::parse("2026-09-18T08:05:03.100Z").unwrap(),
|
|
call: CallId(1),
|
|
tool_call_id: "call_a1".to_string(),
|
|
content: "2026-09-18T08:05:03.000Z".to_string(),
|
|
class: DataClass::Public,
|
|
untrusted: false,
|
|
truncated: false,
|
|
},
|
|
LogRecord::Assistant {
|
|
time: Timestamp::parse("2026-09-18T08:05:05.000Z").unwrap(),
|
|
content: Some("It is five past eight.".to_string()),
|
|
reasoning_content: None,
|
|
tool_calls: vec![],
|
|
},
|
|
LogRecord::Usage {
|
|
time: Timestamp::parse("2026-09-18T08:05:05.000Z").unwrap(),
|
|
cache_n: 589,
|
|
prompt_n: 40,
|
|
predicted_n: 64,
|
|
reasoning_tokens: 27,
|
|
thinking_capped: true,
|
|
},
|
|
];
|
|
assert_eq!(lines.len(), want.len());
|
|
for (i, (line, want)) in lines.iter().zip(&want).enumerate() {
|
|
let got: LogRecord =
|
|
serde_json::from_str(line).unwrap_or_else(|e| panic!("line {}: {e}", i + 1));
|
|
assert_eq!(&got, want, "line {}", i + 1);
|
|
assert_eq!(
|
|
&serde_json::to_string(want).unwrap(),
|
|
line,
|
|
"line {}: bytes",
|
|
i + 1
|
|
);
|
|
}
|
|
}
|