Add the channel messages and the usage record to proto
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
//! 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user