Files
Kigi-CLI/crates/common/kigi-tool-protocol/tests/jsonrpc_envelope.rs
T
ZacharyZhang-NY 6f31415ed6 §9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed
The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
2026-07-18 02:48:46 -04:00

266 lines
8.9 KiB
Rust

//! Envelope-shape tests for the JSON-RPC 2.0 wrappers.
use kigi_tool_protocol::{
FrameSeq, JsonRpcError, JsonRpcId, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse,
JsonRpcVersion, RequestId, ResponseOutcome, SessionId,
};
use serde_json::{Value, json};
fn session() -> SessionId {
SessionId::new("sess_abc").unwrap()
}
#[test]
fn request_with_no_session_id_omits_envelope_field() {
let req: JsonRpcRequest<Value> = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: JsonRpcId::new_string("req-1"),
session_id: None,
method: "tool.call".to_owned(),
params: json!({}),
};
let v = serde_json::to_value(&req).unwrap();
let obj = v.as_object().unwrap();
assert!(
!obj.contains_key("session_id"),
"session_id=None must be omitted: {v}"
);
assert_eq!(v["jsonrpc"], json!("2.0"));
assert_eq!(v["id"], json!("req-1"));
assert_eq!(v["method"], json!("tool.call"));
}
#[test]
fn request_with_session_id_includes_envelope_field() {
let req: JsonRpcRequest<Value> = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: JsonRpcId::new_string("req-2"),
session_id: Some(session()),
method: "tool.call".to_owned(),
params: json!({"x": 1}),
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["session_id"], json!("sess_abc"));
assert_eq!(v["params"]["x"], json!(1));
let parsed: JsonRpcRequest<Value> = serde_json::from_value(v).unwrap();
assert_eq!(parsed.id, JsonRpcId::new_string("req-2"));
assert_eq!(
parsed.session_id.as_ref().map(|s| s.as_str()),
Some("sess_abc")
);
}
#[test]
fn notification_with_no_seq_omits_envelope_field() {
let n: JsonRpcNotification<Value> = JsonRpcNotification {
jsonrpc: JsonRpcVersion,
session_id: None,
seq: None,
method: "tool.notification".to_owned(),
params: json!({}),
};
let v = serde_json::to_value(&n).unwrap();
let obj = v.as_object().unwrap();
assert!(!obj.contains_key("seq"));
assert!(!obj.contains_key("session_id"));
assert!(!obj.contains_key("id"));
}
#[test]
fn notification_with_seq_includes_envelope_field() {
let n: JsonRpcNotification<Value> = JsonRpcNotification {
jsonrpc: JsonRpcVersion,
session_id: Some(session()),
seq: Some(FrameSeq::new(42)),
method: "tool.notification".to_owned(),
params: json!({}),
};
let v = serde_json::to_value(&n).unwrap();
assert_eq!(v["seq"], json!(42));
}
#[test]
fn response_ok_serialises_with_result_only() {
let resp: JsonRpcResponse<Value> =
JsonRpcResponse::ok(JsonRpcId::new_string("r"), json!({"y": 2}));
let v = serde_json::to_value(&resp).unwrap();
let obj = v.as_object().unwrap();
assert_eq!(v["jsonrpc"], json!("2.0"));
assert_eq!(v["id"], json!("r"));
assert_eq!(v["result"], json!({"y": 2}));
assert!(!obj.contains_key("error"), "ok must omit `error`: {v}");
}
#[test]
fn response_err_serialises_with_error_only() {
let resp: JsonRpcResponse<Value> = JsonRpcResponse::err(
JsonRpcId::new_string("r"),
JsonRpcError {
code: -32011,
message: "tool not found".to_owned(),
data: Some(json!({"code": "tool_not_found"})),
},
);
let v = serde_json::to_value(&resp).unwrap();
let obj = v.as_object().unwrap();
assert_eq!(v["error"]["code"], json!(-32011));
assert_eq!(v["error"]["data"]["code"], json!("tool_not_found"));
assert!(!obj.contains_key("result"), "err must omit `result`: {v}");
}
#[test]
fn response_round_trips_with_session_envelope() {
let resp: JsonRpcResponse<Value> =
JsonRpcResponse::ok(JsonRpcId::Number(7), json!({})).with_session(session());
let v = serde_json::to_value(&resp).unwrap();
assert_eq!(v["session_id"], json!("sess_abc"));
assert_eq!(v["id"], json!(7));
let parsed: JsonRpcResponse<Value> = serde_json::from_value(v).unwrap();
assert_eq!(parsed.id, JsonRpcId::Number(7));
match parsed.outcome {
ResponseOutcome::Result(_) => {}
ResponseOutcome::Error(e) => panic!("expected Result, got Error({e:?})"),
}
}
#[test]
fn response_with_both_result_and_error_fails_to_deserialize() {
let bad = json!({
"jsonrpc": "2.0",
"id": "r",
"result": {"x": 1},
"error": {"code": -32000, "message": "no"},
});
let err = serde_json::from_value::<JsonRpcResponse<Value>>(bad).unwrap_err();
assert!(
err.to_string().contains("XOR"),
"expected XOR-violation message, got: {err}"
);
}
#[test]
fn response_with_neither_result_nor_error_fails_to_deserialize() {
let bad = json!({"jsonrpc": "2.0", "id": "r"});
let err = serde_json::from_value::<JsonRpcResponse<Value>>(bad).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("`result` or `error`"),
"expected exactly-one message, got: {msg}"
);
}
#[test]
fn jsonrpc_error_round_trips_with_and_without_data() {
let e_no_data = JsonRpcError {
code: -32603,
message: "internal".to_owned(),
data: None,
};
let v = serde_json::to_value(&e_no_data).unwrap();
assert!(
!v.as_object().unwrap().contains_key("data"),
"data=None must be omitted: {v}"
);
let back: JsonRpcError = serde_json::from_value(v).unwrap();
assert_eq!(back, e_no_data);
let e_with_data = JsonRpcError {
code: -32011,
message: "tool not found".to_owned(),
data: Some(json!({"code": "tool_not_found", "tool_id": "echo"})),
};
let v = serde_json::to_value(&e_with_data).unwrap();
assert_eq!(v["data"]["tool_id"], json!("echo"));
let back: JsonRpcError = serde_json::from_value(v).unwrap();
assert_eq!(back, e_with_data);
}
#[test]
fn jsonrpc_id_accepts_string_and_number_on_request() {
let v_str = json!({
"jsonrpc": "2.0",
"id": "req-9c4f",
"method": "tool.call",
"params": {},
});
let req: JsonRpcRequest<Value> = serde_json::from_value(v_str).unwrap();
assert_eq!(req.id, JsonRpcId::new_string("req-9c4f"));
let v_num = json!({
"jsonrpc": "2.0",
"id": 99,
"method": "tool.call",
"params": {},
});
let req: JsonRpcRequest<Value> = serde_json::from_value(v_num).unwrap();
assert_eq!(req.id, JsonRpcId::Number(99));
}
#[test]
fn jsonrpc_id_round_trips_to_request_id_correlator() {
let original = RequestId::new("req-42").unwrap();
let envelope_id = JsonRpcId::from_request_id(&original);
assert_eq!(envelope_id.as_request_id().unwrap(), original);
// Numeric ids are stringified.
let nid = JsonRpcId::Number(7);
assert_eq!(nid.as_request_id().unwrap().as_str(), "7");
}
#[test]
fn full_call_envelope_serialises_to_expected_shape() {
use kigi_tool_protocol::{ToolCallId, ToolCallParams, ToolId};
let req = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: JsonRpcId::new_string("req-9c4f"),
session_id: Some(session()),
method: "tool.call".to_owned(),
params: ToolCallParams {
tool_call_id: ToolCallId::new("call_xyz").unwrap(),
tool_id: ToolId::new("Kigi:read_file").unwrap(),
arguments: json!({"path": "/etc/hosts"}),
deadline_ms: None,
behavior_version: None,
cwd: None,
trace_context: None,
},
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["jsonrpc"], json!("2.0"));
assert_eq!(v["id"], json!("req-9c4f"));
assert_eq!(v["session_id"], json!("sess_abc"));
assert_eq!(v["method"], json!("tool.call"));
assert_eq!(v["params"]["tool_id"], json!("Kigi:read_file"));
assert_eq!(v["params"]["tool_call_id"], json!("call_xyz"));
}
/// The envelope-level `session_id` and an inner `params.session_id` (e.g.
/// on `ToolsListParams`) are independent keys in the wire JSON tree.
/// This test pins that invariant so a refactor that accidentally
/// collapses the two layers (e.g. via `#[serde(flatten)]`) fails loudly.
#[test]
fn envelope_session_id_and_inner_params_session_id_are_distinct_layers() {
use kigi_tool_protocol::{ToolDefinitionMode, ToolsListParams};
let req = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: JsonRpcId::new_string("req-mix"),
session_id: Some(session()),
method: "tools.list".to_owned(),
params: ToolsListParams {
session_id: session(),
mode: ToolDefinitionMode::Full,
},
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["session_id"], json!("sess_abc"));
assert_eq!(v["params"]["session_id"], json!("sess_abc"));
let top_keys: std::collections::BTreeSet<&str> =
v.as_object().unwrap().keys().map(String::as_str).collect();
assert_eq!(
top_keys,
["id", "jsonrpc", "method", "params", "session_id"]
.into_iter()
.collect::<std::collections::BTreeSet<_>>(),
);
}