//! Tests for IPC messages against byte-exact fixtures. Do not edit these or the fixtures. use proto::{ CallId, DataClass, DenyReason, Envelope, ErrorCode, Message, SessionId, Timestamp, ToolRequest, ToolResponse, WireError, }; fn fixture(name: &str) -> String { let path = format!("{}/tests/fixtures/wire/{name}", env!("CARGO_MANIFEST_DIR")); let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}")); text.trim_end_matches('\n').to_string() } /// The fixture must decode to `want`, and `want` must encode to exactly the fixture's bytes. fn check(name: &str, want: Envelope) { let text = fixture(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 response(id: u64, r#final: bool, body: ToolResponse) -> Envelope { Envelope { v: 1, id, r#final, msg: Message::ToolResponse(body), } } #[test] fn tool_request() { let body = ToolRequest { session: SessionId::new("mm-thread-42").unwrap(), call: CallId(3), tool: "read_file".to_string(), arguments: r#"{"path":"/etc/hosts"}"#.to_string(), }; check( "tool_request.json", Envelope { v: 1, id: 7, r#final: true, msg: Message::ToolRequest(body), }, ); } #[test] fn tool_response_pending() { let body = ToolResponse::PendingApproval { approval: "ap-0001".to_string(), expires: Timestamp::parse("2026-09-17T08:35:00.000Z").unwrap(), }; check("tool_response_pending.json", response(7, false, body)); } #[test] fn tool_response_result() { let body = ToolResponse::Result { content: "127.0.0.1 localhost\n".to_string(), class: DataClass::Private, untrusted: true, truncated: false, }; check("tool_response_result.json", response(7, true, body)); } #[test] fn tool_response_failed() { let body = ToolResponse::Failed { message: "exit status 2".to_string(), }; check("tool_response_failed.json", response(8, true, body)); } #[test] fn tool_response_denied() { let body = ToolResponse::Denied { reason: DenyReason::TaintTooHigh, }; check("tool_response_denied.json", response(9, true, body)); } #[test] fn error_message() { let body = WireError { code: ErrorCode::BadVersion, detail: "expected 1".to_string(), }; check( "error.json", Envelope { v: 1, id: 0, r#final: true, msg: Message::Error(body), }, ); } #[test] fn deny_reasons_and_error_codes_are_snake_case() { let reasons = [ (DenyReason::NoGrant, "no_grant"), (DenyReason::GrantExpired, "grant_expired"), (DenyReason::TaintTooHigh, "taint_too_high"), (DenyReason::DeniedByGrant, "denied_by_grant"), (DenyReason::ApprovalRefused, "approval_refused"), (DenyReason::ApprovalExpired, "approval_expired"), ]; for (value, text) in reasons { assert_eq!( serde_json::to_string(&value).unwrap(), format!("\"{text}\"") ); } let codes = [ (ErrorCode::BadFrame, "bad_frame"), (ErrorCode::BadVersion, "bad_version"), (ErrorCode::BadMessage, "bad_message"), (ErrorCode::Internal, "internal"), ]; for (value, text) in codes { assert_eq!( serde_json::to_string(&value).unwrap(), format!("\"{text}\"") ); } } #[test] fn unknown_and_missing_fields_are_rejected() { let good = fixture("tool_request.json"); assert!(serde_json::from_str::(&good).is_ok()); let bad = [ // extra field in the envelope good.replacen("{\"v\":1,", "{\"v\":1,\"extra\":0,", 1), // extra field beside kind and body good.replacen( "\"kind\":\"tool_request\",", "\"kind\":\"tool_request\",\"x\":1,", 1, ), // extra field in the body good.replacen("\"call\":3,", "\"call\":3,\"priority\":9,", 1), // missing field in the body good.replacen("\"call\":3,", "", 1), // missing `final` good.replacen("\"final\":true,", "", 1), // unknown kind good.replacen("tool_request", "tool_demand", 1), // invalid session id inside a message good.replacen("mm-thread-42", "../../etc", 1), ]; for text in bad { assert!( serde_json::from_str::(&text).is_err(), "accepted {text}" ); } } #[test] fn unknown_field_in_a_response_variant_is_rejected() { let good = fixture("tool_response_denied.json"); let bad = good.replacen("\"reason\":", "\"note\":\"x\",\"reason\":", 1); assert!(serde_json::from_str::(&good).is_ok()); assert!(serde_json::from_str::(&bad).is_err()); let bad_status = good.replacen("denied", "refused", 1); assert!(serde_json::from_str::(&bad_status).is_err()); }