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:
2026-09-18 17:30:51 -07:00
parent e156975649
commit 06298d6a8e
13 changed files with 376 additions and 6 deletions
+251
View File
@@ -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
);
}
}