use codeoid_protocol::{DaemonMessage, ErrorCode};
#[test]
fn auth_ok_with_protocol_version() {
let raw = r#"{
"type": "auth.ok",
"identity": { "sub": "spiffe://x/y", "name": "Alice", "type": "human" },
"scopes": ["sessions.read", "sessions.write"],
"protocolVersion": 1
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).expect("auth.ok parses");
match msg {
DaemonMessage::AuthOk(ref ok) => {
assert_eq!(ok.protocol_version, Some(1));
assert_eq!(ok.scopes.len(), 2);
}
_ => panic!("expected AuthOk"),
}
}
#[test]
fn auth_ok_without_protocol_version_accepted() {
let raw = r#"{
"type": "auth.ok",
"identity": { "sub": "s", "type": "human" },
"scopes": []
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).unwrap();
match msg {
DaemonMessage::AuthOk(ok) => assert!(ok.protocol_version.is_none()),
_ => panic!("expected AuthOk"),
}
}
#[test]
fn unknown_message_type_falls_through() {
let raw = r#"{ "type": "future.thing", "payload": { "anything": true } }"#;
let msg: DaemonMessage = serde_json::from_str(raw).expect("unknown parses");
assert!(matches!(msg, DaemonMessage::Unknown));
}
#[test]
fn response_error_parses_with_code() {
let raw = r#"{
"type": "response.error",
"requestId": "req-1",
"error": "permission denied",
"code": "forbidden"
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).unwrap();
match msg {
DaemonMessage::ResponseError { code, .. } => {
assert_eq!(code, ErrorCode::Forbidden);
}
_ => panic!("expected ResponseError"),
}
}
#[test]
fn session_message_with_tool_roundtrip() {
let raw = r#"{
"type": "session.message",
"sessionId": "s1",
"messageId": "m1",
"role": "tool_call",
"content": "",
"identity": { "sub": "s", "type": "agent" },
"tool": {
"toolId": "t1",
"name": "Bash",
"state": {
"phase": "executing",
"elapsedMs": 1200
}
},
"timestamp": "2026-04-22T00:00:00.000Z"
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).unwrap();
let DaemonMessage::SessionMessage(sm) = msg else {
panic!("expected SessionMessage");
};
let tool = sm.tool.clone().expect("tool present");
assert_eq!(tool.name, "Bash");
assert_eq!(tool.state.phase(), codeoid_protocol::ToolPhase::Executing);
let back = serde_json::to_string(&DaemonMessage::SessionMessage(sm)).unwrap();
assert!(back.contains("\"phase\":\"executing\""));
}
#[test]
fn session_message_delta_tool_replacement() {
let raw = r#"{
"type": "session.message.delta",
"sessionId": "s1",
"messageId": "m1",
"toolStateUpdate": {
"phase": "completed",
"success": true,
"output": "ok",
"elapsedMs": 1500
},
"timestamp": "2026-04-22T00:00:00.001Z"
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).unwrap();
let DaemonMessage::SessionMessageDelta(d) = msg else {
panic!("expected delta");
};
let state = d.tool_state_update.expect("tool state present");
assert_eq!(state.phase(), codeoid_protocol::ToolPhase::Completed);
}
#[test]
fn session_ui_request_parses_from_daemon_json() {
let raw = r#"{
"type": "session.ui_request",
"sessionId": "s1",
"requestId": "u1",
"method": "select",
"title": "Pick one",
"message": "The extension wants an answer.",
"options": ["Allow", "Block"],
"timeoutMs": 30000,
"timestamp": "2026-07-09T00:00:00Z"
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).expect("ui_request parses");
match msg {
DaemonMessage::SessionUiRequest(req) => {
assert_eq!(req.request_id, "u1");
assert_eq!(req.method, codeoid_protocol::UiRequestMethod::Select);
assert_eq!(
req.options.as_deref(),
Some(&["Allow".to_string(), "Block".to_string()][..])
);
assert_eq!(req.timeout_ms, Some(30_000));
}
_ => panic!("expected SessionUiRequest"),
}
}
#[test]
fn session_ui_resolved_unknown_reason_degrades_to_other() {
let raw = r#"{
"type": "session.ui_resolved",
"sessionId": "s1",
"requestId": "u1",
"reason": "superseded_by_something_new",
"timestamp": "t"
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).expect("ui_resolved parses");
match msg {
DaemonMessage::SessionUiResolved { reason, .. } => {
assert_eq!(reason, codeoid_protocol::UiResolvedReason::Other);
}
_ => panic!("expected SessionUiResolved"),
}
}
#[test]
fn session_commands_result_parses_with_optional_fields_absent() {
let raw = r#"{
"type": "session.commands.result",
"requestId": "r1",
"sessionId": "s1",
"providerId": "pi",
"commands": [
{ "name": "review", "description": "Review the diff", "source": "extension" },
{ "name": "fix-tests" }
]
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).expect("commands.result parses");
match msg {
DaemonMessage::SessionCommandsResult {
provider_id,
commands,
..
} => {
assert_eq!(provider_id, "pi");
assert_eq!(commands.len(), 2);
assert_eq!(commands[0].name, "review");
assert!(commands[1].description.is_none());
}
_ => panic!("expected SessionCommandsResult"),
}
}
#[test]
fn session_ui_response_serializes_camel_case() {
use codeoid_protocol::ClientMessage;
let msg = ClientMessage::SessionUiResponse {
id: "1".into(),
session_id: "s".into(),
request_id: "u".into(),
value: None,
confirmed: Some(true),
cancelled: None,
};
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["type"], "session.ui_response");
assert_eq!(json["sessionId"], "s");
assert_eq!(json["requestId"], "u");
assert_eq!(json["confirmed"], true);
assert!(json.get("value").is_none());
assert!(json.get("cancelled").is_none());
}
#[test]
fn new_client_verbs_expose_their_request_id() {
use codeoid_protocol::ClientMessage;
let msgs = [
ClientMessage::SessionUiResponse {
id: "r1".into(),
session_id: "s".into(),
request_id: "u".into(),
value: None,
confirmed: None,
cancelled: Some(true),
},
ClientMessage::SessionPartAction {
id: "r2".into(),
session_id: "s".into(),
message_id: "m".into(),
action: "a".into(),
data: None,
},
ClientMessage::SessionCommands {
id: "r3".into(),
session_id: "s".into(),
},
];
let ids: Vec<&str> = msgs.iter().map(ClientMessage::request_id).collect();
assert_eq!(ids, ["r1", "r2", "r3"]);
}
#[test]
fn auth_ok_advertises_providers_default_first() {
let raw = r#"{
"type": "auth.ok",
"identity": { "sub": "s", "type": "human" },
"scopes": [],
"protocolVersion": 1,
"providers": ["claude", "gemini", "openai", "pi"]
}"#;
let msg: DaemonMessage = serde_json::from_str(raw).unwrap();
match msg {
DaemonMessage::AuthOk(ok) => {
let providers = ok.providers.expect("providers present");
assert_eq!(providers[0], "claude");
assert!(providers.contains(&"pi".to_string()));
}
_ => panic!("expected AuthOk"),
}
}
#[test]
fn session_set_provider_serializes_camel_case() {
use codeoid_protocol::ClientMessage;
let msg = ClientMessage::SessionSetProvider {
id: "1".into(),
session_id: "s".into(),
provider_id: "pi".into(),
};
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["type"], "session.set_provider");
assert_eq!(json["sessionId"], "s");
assert_eq!(json["providerId"], "pi");
assert_eq!(msg.request_id(), "1");
}
#[test]
fn scrollback_replay_tail_fields_parse_and_stay_backwards_compatible() {
use codeoid_protocol::DaemonMessage;
let json =
r#"{"type":"scrollback.replay","sessionId":"s1","messages":[],"tail":true,"hasMore":true}"#;
let msg: DaemonMessage = serde_json::from_str(json).unwrap();
match msg {
DaemonMessage::ScrollbackReplay { tail, has_more, .. } => {
assert_eq!(tail, Some(true));
assert_eq!(has_more, Some(true));
}
other => panic!("wrong variant: {other:?}"),
}
let legacy = r#"{"type":"scrollback.replay","sessionId":"s1","messages":[]}"#;
let msg: DaemonMessage = serde_json::from_str(legacy).unwrap();
match msg {
DaemonMessage::ScrollbackReplay { tail, has_more, .. } => {
assert_eq!(tail, None);
assert_eq!(has_more, None);
}
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn scrollback_page_wire_shapes() {
use codeoid_protocol::{ClientMessage, DaemonMessage};
let req = ClientMessage::ScrollbackPage {
id: "r1".into(),
session_id: "s1".into(),
before_message_id: "m-oldest".into(),
max_bytes: None,
};
assert_eq!(req.request_id(), "r1");
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["type"], "scrollback.page");
assert_eq!(json["beforeMessageId"], "m-oldest");
assert!(json.get("maxBytes").is_none());
let res = r#"{"type":"scrollback.page.result","requestId":"r1","sessionId":"s1","messages":[],"hasMore":false,"source":"transcript"}"#;
let msg: DaemonMessage = serde_json::from_str(res).unwrap();
match msg {
DaemonMessage::ScrollbackPageResult {
has_more, source, ..
} => {
assert!(!has_more);
assert_eq!(source, "transcript");
}
other => panic!("wrong variant: {other:?}"),
}
}