@celestia-island/plana-types 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/Cargo.toml +38 -0
  2. package/bindings/FileAnchor.ts +3 -0
  3. package/bindings/engine.ts +277 -0
  4. package/bindings/enums.ts +31 -0
  5. package/bindings/httpTypes.ts +197 -0
  6. package/bindings/index.ts +47 -0
  7. package/bindings/mcp/aporia.ts +61 -0
  8. package/bindings/mcp/eleos.ts +18 -0
  9. package/bindings/mcp/epieikeia.ts +48 -0
  10. package/bindings/mcp/haplotes.ts +47 -0
  11. package/bindings/mcp/hubris.ts +41 -0
  12. package/bindings/mcp/index.ts +13 -0
  13. package/bindings/mcp/kalos.ts +47 -0
  14. package/bindings/mcp/neikos.ts +76 -0
  15. package/bindings/mcp/orexis.ts +64 -0
  16. package/bindings/mcp/philia.ts +57 -0
  17. package/bindings/mcp/polemos.ts +55 -0
  18. package/bindings/mcp/skemma.ts +58 -0
  19. package/bindings/mcp/skopeo.ts +56 -0
  20. package/bindings/mcp/webAutomation.ts +31 -0
  21. package/bindings/model.ts +240 -0
  22. package/bindings/package.json +16 -0
  23. package/bindings/region.ts +3 -0
  24. package/bindings/serde_json/JsonValue.ts +3 -0
  25. package/bindings/ws/agentLifecycle.ts +41 -0
  26. package/bindings/ws/auth.ts +15 -0
  27. package/bindings/ws/baseMessages.ts +7 -0
  28. package/bindings/ws/bridgeNetwork.ts +71 -0
  29. package/bindings/ws/core.ts +51 -0
  30. package/bindings/ws/fileBrowsing.ts +57 -0
  31. package/bindings/ws/handshake.ts +23 -0
  32. package/bindings/ws/industrial.ts +72 -0
  33. package/bindings/ws/knowledgeBase.ts +10 -0
  34. package/bindings/ws/layer2.ts +25 -0
  35. package/bindings/ws/llmProvider.ts +74 -0
  36. package/bindings/ws/logs.ts +11 -0
  37. package/bindings/ws/malkuth.ts +62 -0
  38. package/bindings/ws/noa.ts +17 -0
  39. package/bindings/ws/stateSync.ts +19 -0
  40. package/bindings/ws/systemUi.ts +5 -0
  41. package/bindings/ws/tasks.ts +6 -0
  42. package/bindings/ws/views.ts +163 -0
  43. package/bindings/ws/workspace.ts +11 -0
  44. package/bindings/ws/yolo.ts +32 -0
  45. package/examples/schema_dump.rs +51 -0
  46. package/package.json +6 -0
  47. package/pnpm-workspace.yaml +2 -0
  48. package/src/engine.rs +602 -0
  49. package/src/enums.rs +334 -0
  50. package/src/external_mcp.rs +132 -0
  51. package/src/http.rs +1077 -0
  52. package/src/identity.rs +160 -0
  53. package/src/lib.rs +1215 -0
  54. package/src/malkuth.rs +145 -0
  55. package/src/mcp/aporia.rs +272 -0
  56. package/src/mcp/eleos.rs +210 -0
  57. package/src/mcp/epieikeia.rs +194 -0
  58. package/src/mcp/haplotes.rs +310 -0
  59. package/src/mcp/hubris.rs +377 -0
  60. package/src/mcp/kalos.rs +251 -0
  61. package/src/mcp/mod.rs +23 -0
  62. package/src/mcp/neikos.rs +533 -0
  63. package/src/mcp/orexis.rs +493 -0
  64. package/src/mcp/philia.rs +267 -0
  65. package/src/mcp/polemos.rs +241 -0
  66. package/src/mcp/skemma.rs +442 -0
  67. package/src/mcp/skopeo.rs +282 -0
  68. package/src/mcp/web_automation.rs +122 -0
  69. package/src/model.rs +421 -0
  70. package/src/protocol/base_messages.rs +125 -0
  71. package/src/protocol/handshake.rs +364 -0
  72. package/src/protocol/jsonrpc.rs +888 -0
  73. package/src/protocol/mod.rs +10 -0
  74. package/src/rbac.rs +786 -0
  75. package/src/region.rs +362 -0
  76. package/src/tracing_helpers.rs +9 -0
  77. package/src/ws/agent/agent_lifecycle.rs +221 -0
  78. package/src/ws/agent/layer2.rs +126 -0
  79. package/src/ws/agent/mod.rs +9 -0
  80. package/src/ws/agent/state_sync.rs +111 -0
  81. package/src/ws/agent/tasks.rs +43 -0
  82. package/src/ws/agent/yolo.rs +161 -0
  83. package/src/ws/mod.rs +10 -0
  84. package/src/ws/services/auth.rs +99 -0
  85. package/src/ws/services/industrial.rs +647 -0
  86. package/src/ws/services/knowledge_base.rs +59 -0
  87. package/src/ws/services/llm_provider.rs +371 -0
  88. package/src/ws/services/mod.rs +7 -0
  89. package/src/ws/ui/bridge_network.rs +96 -0
  90. package/src/ws/ui/file_browsing.rs +88 -0
  91. package/src/ws/ui/logs.rs +55 -0
  92. package/src/ws/ui/mod.rs +11 -0
  93. package/src/ws/ui/noa.rs +105 -0
  94. package/src/ws/ui/system_ui.rs +27 -0
  95. package/src/ws/ui/views.rs +159 -0
  96. package/src/ws/ui/workspace.rs +73 -0
@@ -0,0 +1,888 @@
1
+ //! JSON-RPC 2.0 envelope types (TS-exporting copy).
2
+ //!
3
+ //! NOTE: This is the TS-bindings copy for the `arona` crate. The canonical
4
+ //! wire-layer copy lives in `_jsonrpc::types`. The two are kept in sync
5
+ //! manually — they define the same types but the arona copy additionally
6
+ //! exports TypeScript bindings. A future refactor could extract the envelope
7
+ //! into a shared leaf crate to eliminate this duplication, but that would
8
+ //! require breaking the arona → _state_sync → arona dependency cycle.
9
+
10
+ use serde::{Deserialize, Serialize};
11
+
12
+ pub const JSONRPC_VERSION: &str = "2.0";
13
+
14
+ pub mod error_codes {
15
+ pub const PARSE_ERROR: i64 = -32700;
16
+ pub const INVALID_REQUEST: i64 = -32600;
17
+ pub const METHOD_NOT_FOUND: i64 = -32601;
18
+ pub const INVALID_PARAMS: i64 = -32602;
19
+ pub const INTERNAL_ERROR: i64 = -32603;
20
+ pub const SNAPSHOT_FAILED: i64 = -32001;
21
+ pub const AGENT_UNAVAILABLE: i64 = -32002;
22
+ pub const CONTAINER_ERROR: i64 = -32003;
23
+ pub const REPL_ERROR: i64 = -32004;
24
+ pub const AUTH_ERROR: i64 = -32005;
25
+ }
26
+
27
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28
+ pub struct JsonRpcError {
29
+ pub code: i64,
30
+ pub message: String,
31
+ #[serde(skip_serializing_if = "Option::is_none")]
32
+ pub data: Option<serde_json::Value>,
33
+ }
34
+
35
+ impl JsonRpcError {
36
+ pub fn new(code: i64, message: impl Into<String>) -> Self {
37
+ Self {
38
+ code,
39
+ message: message.into(),
40
+ data: None,
41
+ }
42
+ }
43
+
44
+ pub fn with_data(mut self, data: serde_json::Value) -> Self {
45
+ self.data = Some(data);
46
+ self
47
+ }
48
+
49
+ pub fn parse_error() -> Self {
50
+ Self::new(error_codes::PARSE_ERROR, "Parse error")
51
+ }
52
+
53
+ pub fn invalid_request() -> Self {
54
+ Self::new(error_codes::INVALID_REQUEST, "Invalid Request")
55
+ }
56
+
57
+ pub fn method_not_found(method: &str) -> Self {
58
+ Self::new(
59
+ error_codes::METHOD_NOT_FOUND,
60
+ format!("Method not found: {}", method),
61
+ )
62
+ }
63
+
64
+ pub fn invalid_params(msg: &str) -> Self {
65
+ Self::new(
66
+ error_codes::INVALID_PARAMS,
67
+ format!("Invalid params: {}", msg),
68
+ )
69
+ }
70
+
71
+ pub fn internal_error(msg: &str) -> Self {
72
+ Self::new(error_codes::INTERNAL_ERROR, msg)
73
+ }
74
+ }
75
+
76
+ #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
77
+ #[serde(untagged)]
78
+ pub enum Id {
79
+ #[default]
80
+ Null,
81
+ String(String),
82
+ Number(i64),
83
+ }
84
+
85
+ impl Id {
86
+ /// Creates a new unique request `Id` using UUID v7 (time-ordered, sortable).
87
+ ///
88
+ /// UUID v7 is the canonical wire format for the celestia-island platform.
89
+ /// Both entelecheia (Rust) and shittim-chest (TypeScript) MUST agree on
90
+ /// this format — TypeScript consumers should parse the string value with a
91
+ /// UUID v7 library or treat it as an opaque string that sorts
92
+ /// lexicographically.
93
+ pub fn new_uuid() -> Self {
94
+ Self::String(uuid::Uuid::now_v7().to_string())
95
+ }
96
+
97
+ /// Creates a new unique request `Id` using UUID v4 (random).
98
+ ///
99
+ /// Provided for consumers that prefer random UUIDs over time-ordered v7.
100
+ /// Default preference in this platform is [`Id::new_uuid`] (v7).
101
+ pub fn new_uuid_v4() -> Self {
102
+ Self::String(uuid::Uuid::new_v4().to_string())
103
+ }
104
+ }
105
+
106
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107
+ pub struct JsonRpcRequest {
108
+ pub jsonrpc: String,
109
+ #[serde(skip_serializing_if = "Option::is_none")]
110
+ pub id: Option<Id>,
111
+ pub method: String,
112
+ #[serde(skip_serializing_if = "Option::is_none")]
113
+ pub params: Option<serde_json::Value>,
114
+ }
115
+
116
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117
+ pub struct JsonRpcNotification {
118
+ pub jsonrpc: String,
119
+ pub method: String,
120
+ #[serde(skip_serializing_if = "Option::is_none")]
121
+ pub params: Option<serde_json::Value>,
122
+ }
123
+
124
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125
+ pub struct JsonRpcResponse {
126
+ pub jsonrpc: String,
127
+ pub id: Id,
128
+ #[serde(skip_serializing_if = "Option::is_none")]
129
+ pub result: Option<serde_json::Value>,
130
+ #[serde(skip_serializing_if = "Option::is_none")]
131
+ pub error: Option<JsonRpcError>,
132
+ }
133
+
134
+ #[derive(Debug, Clone, Serialize)]
135
+ #[serde(untagged)]
136
+ pub enum JsonRpcMessage {
137
+ Request(JsonRpcRequest),
138
+ Notification(JsonRpcNotification),
139
+ Response(JsonRpcResponse),
140
+ }
141
+
142
+ impl<'de> serde::Deserialize<'de> for JsonRpcMessage {
143
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144
+ where
145
+ D: serde::Deserializer<'de>,
146
+ {
147
+ let value = serde_json::Value::deserialize(deserializer)?;
148
+
149
+ let has_id = value.get("id").map(|v| !v.is_null()).unwrap_or(false);
150
+ let has_method = value.get("method").is_some();
151
+ let has_result = value.get("result").is_some();
152
+ let has_error = value.get("error").is_some();
153
+
154
+ // A Response is identified by `result`/`error`. Its `id` may be null:
155
+ // JSON-RPC 2.0 mandates `"id": null` on error responses to requests
156
+ // whose id could not be detected (Parse error / Invalid Request).
157
+ // R9 redefined null id as "no id" only for *request/notification*
158
+ // discrimination below; responses must still accept it.
159
+ if has_result || has_error {
160
+ let resp: JsonRpcResponse = serde_json::from_value(value)
161
+ .map_err(|e| serde::de::Error::custom(format!("invalid response: {}", e)))?;
162
+ Ok(JsonRpcMessage::Response(resp))
163
+ } else if has_method && has_id {
164
+ let req: JsonRpcRequest = serde_json::from_value(value)
165
+ .map_err(|e| serde::de::Error::custom(format!("invalid request: {}", e)))?;
166
+ Ok(JsonRpcMessage::Request(req))
167
+ } else if has_method && !has_id {
168
+ let notif: JsonRpcNotification = serde_json::from_value(value)
169
+ .map_err(|e| serde::de::Error::custom(format!("invalid notification: {}", e)))?;
170
+ Ok(JsonRpcMessage::Notification(notif))
171
+ } else {
172
+ Err(serde::de::Error::custom(
173
+ "cannot classify JSON-RPC message: must have (method + id) for request, (method) for notification, or (id + result/error) for response",
174
+ ))
175
+ }
176
+ }
177
+ }
178
+
179
+ impl JsonRpcRequest {
180
+ pub fn new(method: impl Into<String>, params: Option<serde_json::Value>) -> Self {
181
+ Self {
182
+ jsonrpc: JSONRPC_VERSION.to_string(),
183
+ id: Some(Id::new_uuid()),
184
+ method: method.into(),
185
+ params,
186
+ }
187
+ }
188
+
189
+ pub fn with_id(mut self, id: Id) -> Self {
190
+ self.id = Some(id);
191
+ self
192
+ }
193
+ }
194
+
195
+ impl JsonRpcNotification {
196
+ pub fn new(method: impl Into<String>, params: Option<serde_json::Value>) -> Self {
197
+ Self {
198
+ jsonrpc: JSONRPC_VERSION.to_string(),
199
+ method: method.into(),
200
+ params,
201
+ }
202
+ }
203
+ }
204
+
205
+ impl JsonRpcResponse {
206
+ pub fn success(id: Id, result: serde_json::Value) -> Self {
207
+ Self {
208
+ jsonrpc: JSONRPC_VERSION.to_string(),
209
+ id,
210
+ result: Some(result),
211
+ error: None,
212
+ }
213
+ }
214
+
215
+ pub fn error(id: Id, error: JsonRpcError) -> Self {
216
+ Self {
217
+ jsonrpc: JSONRPC_VERSION.to_string(),
218
+ id,
219
+ result: None,
220
+ error: Some(error),
221
+ }
222
+ }
223
+ }
224
+
225
+ pub fn build_notification(method: &str, params: impl serde::Serialize) -> String {
226
+ let method = if method.trim().is_empty() {
227
+ "internal.fallback"
228
+ } else {
229
+ method
230
+ };
231
+ let params = serde_json::to_value(&params)
232
+ .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
233
+ let notif = JsonRpcNotification::new(method, Some(params));
234
+ serde_json::to_string(&notif).unwrap_or_else(|_| {
235
+ let fallback = serde_json::json!({
236
+ "jsonrpc": "2.0",
237
+ "method": "internal.fallback",
238
+ "params": {}
239
+ });
240
+ serde_json::to_string(&fallback).unwrap_or_else(|_| {
241
+ String::from(r#"{"jsonrpc":"2.0","method":"internal.fallback","params":{}}"#)
242
+ })
243
+ })
244
+ }
245
+
246
+ pub fn build_notification_value(method: &str, params: impl serde::Serialize) -> serde_json::Value {
247
+ let method = if method.trim().is_empty() {
248
+ "internal.fallback"
249
+ } else {
250
+ method
251
+ };
252
+ let params = serde_json::to_value(&params)
253
+ .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new()));
254
+ let notif = JsonRpcNotification::new(method, Some(params));
255
+ serde_json::to_value(notif).unwrap_or_else(|_| {
256
+ serde_json::json!({
257
+ "jsonrpc": "2.0",
258
+ "method": "internal.fallback",
259
+ "params": {}
260
+ })
261
+ })
262
+ }
263
+
264
+ // Helper trait only used in tests above — keeps the assertion readable without
265
+ // adding a public method to JsonRpcNotification.
266
+ #[cfg(test)]
267
+ impl JsonRpcNotification {
268
+ fn id_param_check(&self) -> bool {
269
+ // Notifications never carry an id by construction; this is a no-op
270
+ // placeholder to document intent in the test.
271
+ true
272
+ }
273
+ }
274
+
275
+ #[cfg(test)]
276
+ mod tests {
277
+ use super::*;
278
+ use serde_json::json;
279
+
280
+ // ── Id handling ──────────────────────────────────────────
281
+
282
+ #[test]
283
+ fn id_numeric_round_trip() {
284
+ let id = Id::Number(42);
285
+ let s = serde_json::to_string(&id).unwrap();
286
+ assert_eq!(s, "42");
287
+ let back: Id = serde_json::from_str(&s).unwrap();
288
+ assert_eq!(id, back);
289
+ }
290
+
291
+ #[test]
292
+ fn id_string_round_trip() {
293
+ let id = Id::String("abc-123".to_string());
294
+ let s = serde_json::to_string(&id).unwrap();
295
+ assert_eq!(s, r#""abc-123""#);
296
+ let back: Id = serde_json::from_str(&s).unwrap();
297
+ assert_eq!(id, back);
298
+ }
299
+
300
+ #[test]
301
+ fn id_null_default_round_trip() {
302
+ let id = Id::Null;
303
+ let s = serde_json::to_string(&id).unwrap();
304
+ assert_eq!(s, "null");
305
+ let back: Id = serde_json::from_str(&s).unwrap();
306
+ assert_eq!(id, back);
307
+ assert_eq!(Id::default(), Id::Null);
308
+ }
309
+
310
+ #[test]
311
+ fn id_new_uuid_is_v7_shaped_string() {
312
+ let id = Id::new_uuid();
313
+ let s = serde_json::to_string(&id).unwrap();
314
+ // new_uuid() produces Id::String(_); serialized as a JSON string.
315
+ assert!(s.starts_with('"') && s.ends_with('"'));
316
+ let inner = &s[1..s.len() - 1];
317
+ // UUID v7: version nibble at position 14 must be '7'.
318
+ let bytes = inner.as_bytes();
319
+ assert_eq!(bytes.len(), 36, "expected hyphenated UUID, got {inner}");
320
+ assert_eq!(bytes[14], b'7', "expected UUID v7, got {inner}");
321
+ let back: Id = serde_json::from_str(&s).unwrap();
322
+ assert_eq!(id, back);
323
+ }
324
+
325
+ // ── Request round-trip & exact JSON shape ────────────────
326
+
327
+ #[test]
328
+ fn request_round_trip_with_numeric_id() {
329
+ let req = JsonRpcRequest::new("agent.run", Some(json!({"agent": "haplotes"})))
330
+ .with_id(Id::Number(7));
331
+ let s = serde_json::to_string(&req).unwrap();
332
+ // Exact-shape assertion guards against protocol drift.
333
+ assert_eq!(
334
+ s,
335
+ r#"{"jsonrpc":"2.0","id":7,"method":"agent.run","params":{"agent":"haplotes"}}"#
336
+ );
337
+ let back: JsonRpcRequest = serde_json::from_str(&s).unwrap();
338
+ assert_eq!(back.jsonrpc, "2.0");
339
+ assert_eq!(back.id, Some(Id::Number(7)));
340
+ assert_eq!(back.method, "agent.run");
341
+ assert_eq!(back.params, Some(json!({"agent": "haplotes"})));
342
+ }
343
+
344
+ #[test]
345
+ fn request_without_params_omits_field() {
346
+ let req = JsonRpcRequest::new("ping", None).with_id(Id::String("s1".into()));
347
+ let v = serde_json::to_value(&req).unwrap();
348
+ // `params` must be absent (skip_serializing_if = "Option::is_none"),
349
+ // not serialized as null.
350
+ assert!(v.get("params").is_none());
351
+ assert_eq!(v["id"], "s1");
352
+ let back: JsonRpcRequest = serde_json::from_value(v).unwrap();
353
+ assert_eq!(back.params, None);
354
+ assert_eq!(back.id, Some(Id::String("s1".into())));
355
+ }
356
+
357
+ // ── Notification round-trip & discrimination ─────────────
358
+
359
+ #[test]
360
+ fn notification_has_no_id_field() {
361
+ let notif = JsonRpcNotification::new("agent.status", Some(json!({"status": "online"})));
362
+ let v = serde_json::to_value(&notif).unwrap();
363
+ assert!(v.get("id").is_none(), "notifications must not carry an id");
364
+ assert_eq!(v["jsonrpc"], "2.0");
365
+ assert_eq!(v["method"], "agent.status");
366
+ let back: JsonRpcNotification = serde_json::from_value(v).unwrap();
367
+ assert_eq!(back.method, "agent.status");
368
+ }
369
+
370
+ #[test]
371
+ fn message_classifies_request_vs_notification() {
372
+ // No `id` + `method` → notification.
373
+ let notif_json = r#"{"jsonrpc":"2.0","method":"ev.push","params":{"x":1}}"#;
374
+ let msg: JsonRpcMessage = serde_json::from_str(notif_json).unwrap();
375
+ assert!(matches!(msg, JsonRpcMessage::Notification(_)));
376
+
377
+ // `id` + `method` → request.
378
+ let req_json = r#"{"jsonrpc":"2.0","id":99,"method":"agent.run","params":{}}"#;
379
+ let msg: JsonRpcMessage = serde_json::from_str(req_json).unwrap();
380
+ match msg {
381
+ JsonRpcMessage::Request(r) => assert_eq!(r.id, Some(Id::Number(99))),
382
+ other => panic!("expected Request, got {other:?}"),
383
+ }
384
+ }
385
+
386
+ #[test]
387
+ fn message_classifies_response_success_and_error() {
388
+ let ok_json = r#"{"jsonrpc":"2.0","id":5,"result":{"ok":true}}"#;
389
+ let msg: JsonRpcMessage = serde_json::from_str(ok_json).unwrap();
390
+ match msg {
391
+ JsonRpcMessage::Response(r) => {
392
+ assert_eq!(r.id, Id::Number(5));
393
+ assert!(r.result.is_some());
394
+ assert!(r.error.is_none());
395
+ }
396
+ other => panic!("expected Response, got {other:?}"),
397
+ }
398
+
399
+ let err_json =
400
+ r#"{"jsonrpc":"2.0","id":"e1","error":{"code":-32601,"message":"not found"}}"#;
401
+ let msg: JsonRpcMessage = serde_json::from_str(err_json).unwrap();
402
+ match msg {
403
+ JsonRpcMessage::Response(r) => {
404
+ assert_eq!(r.id, Id::String("e1".into()));
405
+ let err = r.error.expect("error field");
406
+ assert_eq!(err.code, error_codes::METHOD_NOT_FOUND);
407
+ assert_eq!(err.message, "not found");
408
+ assert!(r.result.is_none());
409
+ }
410
+ other => panic!("expected Response, got {other:?}"),
411
+ }
412
+ }
413
+
414
+ #[test]
415
+ fn message_classifies_explicit_null_id_as_notification() {
416
+ // JSON-RPC 2.0 allows id to be null. A payload with `"id": null`
417
+ // semantically means "no response expected", i.e. notification.
418
+ let json = r#"{"jsonrpc":"2.0","method":"ev.push","id":null,"params":{"x":1}}"#;
419
+ let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
420
+ assert!(
421
+ matches!(msg, JsonRpcMessage::Notification(_)),
422
+ "explicit null id must be classified as Notification"
423
+ );
424
+ }
425
+
426
+ #[test]
427
+ fn message_classifies_null_id_error_response() {
428
+ // JSON-RPC 2.0: a response to a request whose id could not be
429
+ // detected (Parse error / Invalid Request) MUST carry `"id": null`.
430
+ // R9 made `"id": null` mean "no id" for *request/notification*
431
+ // discrimination, but a *response* (identified by result/error) must
432
+ // still deserialize with a null id.
433
+ let raw = r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"Parse error"}}"#;
434
+ let msg: JsonRpcMessage = serde_json::from_str(raw).unwrap();
435
+ match msg {
436
+ JsonRpcMessage::Response(r) => {
437
+ assert_eq!(r.id, Id::Null);
438
+ assert_eq!(r.error.unwrap().code, error_codes::PARSE_ERROR);
439
+ }
440
+ other => panic!("expected Response, got {other:?}"),
441
+ }
442
+ }
443
+
444
+ #[test]
445
+ fn message_rejects_unclassifiable_payload() {
446
+ // Neither method nor result/error.
447
+ let bad = r#"{"jsonrpc":"2.0","id":1}"#;
448
+ assert!(serde_json::from_str::<JsonRpcMessage>(bad).is_err());
449
+ }
450
+
451
+ #[test]
452
+ fn message_ambiguous_payload_classified_as_response() {
453
+ // A malformed payload carrying BOTH `method` and `result` must be
454
+ // classified as a Response (result/error takes priority per JSON-RPC
455
+ // spec), NOT a Request — otherwise `result` is silently discarded.
456
+ let ambig = r#"{"jsonrpc":"2.0","id":7,"method":"x","result":{"ok":true}}"#;
457
+ let msg: JsonRpcMessage = serde_json::from_str(ambig).unwrap();
458
+ match msg {
459
+ JsonRpcMessage::Response(r) => {
460
+ assert_eq!(r.id, Id::Number(7));
461
+ assert!(r.result.is_some());
462
+ }
463
+ other => panic!("expected Response for ambiguous payload, got {other:?}"),
464
+ }
465
+
466
+ // Same with `error` instead of `result`.
467
+ let ambig_err =
468
+ r#"{"jsonrpc":"2.0","id":9,"method":"x","error":{"code":-1,"message":"e"}}"#;
469
+ let msg: JsonRpcMessage = serde_json::from_str(ambig_err).unwrap();
470
+ assert!(matches!(msg, JsonRpcMessage::Response(_)));
471
+ }
472
+
473
+ // ── Response round-trip & error codes ────────────────────
474
+
475
+ #[test]
476
+ fn response_success_round_trip_preserves_shape() {
477
+ let resp = JsonRpcResponse::success(Id::Number(3), json!({"value": 42}));
478
+ let s = serde_json::to_string(&resp).unwrap();
479
+ assert_eq!(s, r#"{"jsonrpc":"2.0","id":3,"result":{"value":42}}"#);
480
+ // `error` is skipped when None.
481
+ assert!(!s.contains(r#""error""#));
482
+ let back: JsonRpcResponse = serde_json::from_str(&s).unwrap();
483
+ assert_eq!(back.id, Id::Number(3));
484
+ assert_eq!(back.result, Some(json!({"value": 42})));
485
+ assert!(back.error.is_none());
486
+ }
487
+
488
+ #[test]
489
+ fn response_error_round_trip_with_data() {
490
+ let err = JsonRpcError::new(error_codes::PARSE_ERROR, "Parse error")
491
+ .with_data(json!({"offset": 12}));
492
+ let resp = JsonRpcResponse::error(Id::String("p".into()), err);
493
+ let v = serde_json::to_value(&resp).unwrap();
494
+ // `result` skipped on error path; `error.data` present.
495
+ assert!(v.get("result").is_none());
496
+ assert_eq!(v["error"]["code"], -32700);
497
+ assert_eq!(v["error"]["message"], "Parse error");
498
+ assert_eq!(v["error"]["data"]["offset"], 12);
499
+ let back: JsonRpcResponse = serde_json::from_value(v).unwrap();
500
+ let err = back.error.unwrap();
501
+ assert_eq!(err.code, error_codes::PARSE_ERROR);
502
+ assert_eq!(err.data, Some(json!({"offset": 12})));
503
+ }
504
+
505
+ #[test]
506
+ fn response_error_without_data_omits_field() {
507
+ let err = JsonRpcError::new(error_codes::INTERNAL_ERROR, "boom");
508
+ let resp = JsonRpcResponse::error(Id::Number(0), err);
509
+ let s = serde_json::to_string(&resp).unwrap();
510
+ assert!(
511
+ !s.contains(r#""data""#),
512
+ "`data` must be absent when None, got: {s}"
513
+ );
514
+ let back: JsonRpcResponse = serde_json::from_str(&s).unwrap();
515
+ assert_eq!(back.error.unwrap().data, None);
516
+ }
517
+
518
+ #[test]
519
+ fn error_code_constants_unchanged() {
520
+ // Guard against accidental drift in well-known JSON-RPC error codes.
521
+ assert_eq!(error_codes::PARSE_ERROR, -32700);
522
+ assert_eq!(error_codes::INVALID_REQUEST, -32600);
523
+ assert_eq!(error_codes::METHOD_NOT_FOUND, -32601);
524
+ assert_eq!(error_codes::INVALID_PARAMS, -32602);
525
+ assert_eq!(error_codes::INTERNAL_ERROR, -32603);
526
+ }
527
+
528
+ // ── Notification helpers ─────────────────────────────────
529
+
530
+ #[test]
531
+ fn build_notification_value_shape() {
532
+ let v = build_notification_value("ev.tick", json!({"n": 1}));
533
+ assert_eq!(v["jsonrpc"], "2.0");
534
+ assert_eq!(v["method"], "ev.tick");
535
+ assert_eq!(v["params"]["n"], 1);
536
+ assert!(v.get("id").is_none());
537
+ }
538
+
539
+ #[test]
540
+ fn build_notification_string_is_valid_json() {
541
+ let s = build_notification("ev.tick", json!({"n": 1}));
542
+ let v: serde_json::Value = serde_json::from_str(&s).unwrap();
543
+ assert_eq!(v["method"], "ev.tick");
544
+ }
545
+
546
+ #[test]
547
+ fn build_notification_empty_method_produces_fallback() {
548
+ let s = build_notification("", json!({"n": 1}));
549
+ let v: serde_json::Value = serde_json::from_str(&s).unwrap();
550
+ assert_eq!(v["method"], "internal.fallback");
551
+ }
552
+
553
+ #[test]
554
+ fn build_notification_whitespace_method_produces_fallback() {
555
+ let s = build_notification(" \t", json!({"n": 1}));
556
+ let v: serde_json::Value = serde_json::from_str(&s).unwrap();
557
+ assert_eq!(v["method"], "internal.fallback");
558
+ }
559
+
560
+ #[test]
561
+ fn build_notification_value_empty_method_produces_fallback() {
562
+ let v = build_notification_value("", json!({"n": 1}));
563
+ assert_eq!(v["method"], "internal.fallback");
564
+ }
565
+
566
+ #[test]
567
+ fn build_notification_value_whitespace_method_produces_fallback() {
568
+ let v = build_notification_value(" \n ", json!({"n": 1}));
569
+ assert_eq!(v["method"], "internal.fallback");
570
+ }
571
+
572
+ // ── Additional edge-case tests (Phase 1.3) ─────────────────────
573
+
574
+ // ── Id edge cases ───────────────────────────────────────────────
575
+
576
+ #[test]
577
+ fn id_number_zero_round_trip() {
578
+ let id = Id::Number(0);
579
+ let s = serde_json::to_string(&id).unwrap();
580
+ assert_eq!(s, "0");
581
+ let back: Id = serde_json::from_str(&s).unwrap();
582
+ assert_eq!(id, back);
583
+ }
584
+
585
+ #[test]
586
+ fn id_number_negative_round_trip() {
587
+ let id = Id::Number(-42);
588
+ let s = serde_json::to_string(&id).unwrap();
589
+ assert_eq!(s, "-42");
590
+ let back: Id = serde_json::from_str(&s).unwrap();
591
+ assert_eq!(id, back);
592
+ }
593
+
594
+ #[test]
595
+ fn id_number_large_round_trip() {
596
+ let id = Id::Number(i64::MAX);
597
+ let s = serde_json::to_string(&id).unwrap();
598
+ let back: Id = serde_json::from_str(&s).unwrap();
599
+ assert_eq!(id, back);
600
+ }
601
+
602
+ #[test]
603
+ fn id_empty_string_round_trip() {
604
+ let id = Id::String(String::new());
605
+ let s = serde_json::to_string(&id).unwrap();
606
+ assert_eq!(s, r#""""#);
607
+ let back: Id = serde_json::from_str(&s).unwrap();
608
+ assert_eq!(id, back);
609
+ }
610
+
611
+ #[test]
612
+ fn id_unicode_string_round_trip() {
613
+ let id = Id::String("请求-42".into());
614
+ let s = serde_json::to_string(&id).unwrap();
615
+ let back: Id = serde_json::from_str(&s).unwrap();
616
+ assert_eq!(id, back);
617
+ }
618
+
619
+ #[test]
620
+ fn id_new_uuid_v4_round_trip() {
621
+ let id = Id::new_uuid_v4();
622
+ let s = serde_json::to_string(&id).unwrap();
623
+ let inner = &s[1..s.len() - 1];
624
+ let bytes = inner.as_bytes();
625
+ assert_eq!(bytes.len(), 36);
626
+ // UUID v4: version nibble at position 14 must be '4'.
627
+ assert_eq!(bytes[14], b'4', "expected UUID v4, got {inner}");
628
+ let back: Id = serde_json::from_str(&s).unwrap();
629
+ assert_eq!(id, back);
630
+ }
631
+
632
+ #[test]
633
+ fn id_ordering_null_lt_string_lt_number() {
634
+ // Id derives Ord — Null < String < Number (enum variant order).
635
+ let n = Id::Null;
636
+ let s = Id::String("z".into());
637
+ let num = Id::Number(0);
638
+ assert!(n < s);
639
+ assert!(s < num);
640
+ }
641
+
642
+ // ── JsonRpcError ───────────────────────────────────────────────
643
+
644
+ #[test]
645
+ fn error_new_without_data() {
646
+ let e = JsonRpcError::new(-32700, "Parse error");
647
+ assert_eq!(e.code, -32700);
648
+ assert_eq!(e.message, "Parse error");
649
+ assert!(e.data.is_none());
650
+ }
651
+
652
+ #[test]
653
+ fn error_with_data_builder() {
654
+ let e = JsonRpcError::new(-1, "x").with_data(json!({"k": "v"}));
655
+ assert_eq!(e.data, Some(json!({"k": "v"})));
656
+ }
657
+
658
+ #[test]
659
+ fn error_constructors_use_correct_codes() {
660
+ assert_eq!(JsonRpcError::parse_error().code, error_codes::PARSE_ERROR);
661
+ assert_eq!(
662
+ JsonRpcError::invalid_request().code,
663
+ error_codes::INVALID_REQUEST
664
+ );
665
+ assert_eq!(
666
+ JsonRpcError::method_not_found("test").code,
667
+ error_codes::METHOD_NOT_FOUND
668
+ );
669
+ assert_eq!(
670
+ JsonRpcError::invalid_params("bad").code,
671
+ error_codes::INVALID_PARAMS
672
+ );
673
+ assert_eq!(
674
+ JsonRpcError::internal_error("boom").code,
675
+ error_codes::INTERNAL_ERROR
676
+ );
677
+ }
678
+
679
+ #[test]
680
+ fn error_method_not_found_includes_method_name() {
681
+ let e = JsonRpcError::method_not_found("my.method");
682
+ assert!(e.message.contains("my.method"));
683
+ }
684
+
685
+ #[test]
686
+ fn error_invalid_params_includes_detail() {
687
+ let e = JsonRpcError::invalid_params("missing field `token`");
688
+ assert!(e.message.contains("missing field"));
689
+ }
690
+
691
+ #[test]
692
+ fn custom_error_codes_unchanged() {
693
+ // Platform-specific codes (-32000 range).
694
+ assert_eq!(error_codes::SNAPSHOT_FAILED, -32001);
695
+ assert_eq!(error_codes::AGENT_UNAVAILABLE, -32002);
696
+ assert_eq!(error_codes::CONTAINER_ERROR, -32003);
697
+ assert_eq!(error_codes::REPL_ERROR, -32004);
698
+ assert_eq!(error_codes::AUTH_ERROR, -32005);
699
+ }
700
+
701
+ #[test]
702
+ fn error_data_omitted_when_none() {
703
+ let e = JsonRpcError::new(-32603, "err");
704
+ let s = serde_json::to_string(&e).unwrap();
705
+ assert!(!s.contains(r#""data""#), "data must be omitted: {s}");
706
+ }
707
+
708
+ #[test]
709
+ fn error_data_present_when_some() {
710
+ let e = JsonRpcError::new(-32603, "err").with_data(json!(42));
711
+ let s = serde_json::to_string(&e).unwrap();
712
+ assert!(s.contains(r#""data":42"#));
713
+ }
714
+
715
+ // ── JsonRpcMessage serialization (forward direction) ───────────
716
+
717
+ #[test]
718
+ fn message_serialize_request_has_no_result_or_error() {
719
+ let req = JsonRpcRequest::new("m", Some(json!({"x": 1}))).with_id(Id::Number(1));
720
+ let msg = JsonRpcMessage::Request(req);
721
+ let v = serde_json::to_value(&msg).unwrap();
722
+ assert!(v.get("result").is_none(), "Request must not carry result");
723
+ assert!(v.get("error").is_none(), "Request must not carry error");
724
+ assert_eq!(v["method"], "m");
725
+ }
726
+
727
+ #[test]
728
+ fn message_serialize_notification_has_no_id() {
729
+ let notif = JsonRpcNotification::new("ev", None);
730
+ let msg = JsonRpcMessage::Notification(notif);
731
+ let v = serde_json::to_value(&msg).unwrap();
732
+ assert!(v.get("id").is_none(), "Notification must not carry id");
733
+ }
734
+
735
+ #[test]
736
+ fn message_serialize_response_has_no_method() {
737
+ let resp = JsonRpcResponse::success(Id::Number(1), json!({"ok": true}));
738
+ let msg = JsonRpcMessage::Response(resp);
739
+ let v = serde_json::to_value(&msg).unwrap();
740
+ assert!(
741
+ v.get("method").is_none(),
742
+ "Response must not carry method field"
743
+ );
744
+ }
745
+
746
+ // ── JsonRpcRequest edge cases ──────────────────────────────────
747
+
748
+ #[test]
749
+ fn request_with_none_id_serializes_without_id() {
750
+ let mut req = JsonRpcRequest::new("m", None);
751
+ req.id = None;
752
+ let v = serde_json::to_value(&req).unwrap();
753
+ assert!(v.get("id").is_none());
754
+ }
755
+
756
+ #[test]
757
+ fn request_with_complex_params_round_trip() {
758
+ let params = json!({
759
+ "nested": {"deep": [1, 2, {"x": true}]},
760
+ "str": "hello",
761
+ "null_val": null
762
+ });
763
+ let req = JsonRpcRequest::new("complex", Some(params.clone())).with_id(Id::Number(99));
764
+ let s = serde_json::to_string(&req).unwrap();
765
+ let back: JsonRpcRequest = serde_json::from_str(&s).unwrap();
766
+ assert_eq!(back.params, Some(params));
767
+ }
768
+
769
+ // ── JsonRpcResponse edge cases ─────────────────────────────────
770
+
771
+ #[test]
772
+ fn response_success_with_null_result() {
773
+ let resp = JsonRpcResponse::success(Id::Number(1), serde_json::Value::Null);
774
+ let v = serde_json::to_value(&resp).unwrap();
775
+ // result is Some(Null) — not skipped because skip_serializing_if only
776
+ // checks Option::is_none, not the inner value.
777
+ assert_eq!(v["result"], serde_json::Value::Null);
778
+ assert!(v.get("error").is_none());
779
+ }
780
+
781
+ #[test]
782
+ fn response_error_with_null_id() {
783
+ let resp = JsonRpcResponse::error(Id::Null, JsonRpcError::parse_error());
784
+ let v = serde_json::to_value(&resp).unwrap();
785
+ assert_eq!(v["id"], serde_json::Value::Null);
786
+ assert_eq!(v["error"]["code"], -32700);
787
+ assert!(v.get("result").is_none());
788
+ }
789
+
790
+ // ── Large payload stress test ──────────────────────────────────
791
+
792
+ #[test]
793
+ fn request_with_large_params_round_trip() {
794
+ // Deeply nested JSON to verify no stack overflow or truncation.
795
+ let mut params = serde_json::Value::Object(serde_json::Map::new());
796
+ for i in 0..100 {
797
+ params[&format!("key_{i}")] = json!({
798
+ "data": vec![i; 100],
799
+ "meta": {"index": i, "tag": format!("item-{i}")}
800
+ });
801
+ }
802
+ let req =
803
+ JsonRpcRequest::new("bulk", Some(params.clone())).with_id(Id::String("bulk-1".into()));
804
+ let s = serde_json::to_string(&req).unwrap();
805
+ let back: JsonRpcRequest = serde_json::from_str(&s).unwrap();
806
+ assert_eq!(back.params, Some(params));
807
+ }
808
+
809
+ // ── build_notification with unicode ────────────────────────────
810
+
811
+ #[test]
812
+ fn build_notification_unicode_method() {
813
+ let s = build_notification("智能体.运行", json!({"a": 1}));
814
+ let v: serde_json::Value = serde_json::from_str(&s).unwrap();
815
+ assert_eq!(v["method"], "智能体.运行");
816
+ assert_eq!(v["params"]["a"], 1);
817
+ }
818
+
819
+ #[test]
820
+ fn build_notification_value_unicode_method() {
821
+ let v = build_notification_value("通知.推送", json!({"x": 0}));
822
+ assert_eq!(v["method"], "通知.推送");
823
+ }
824
+
825
+ // ── JSONRPC_VERSION constant ───────────────────────────────────
826
+
827
+ #[test]
828
+ fn jsonrpc_version_is_two_point_zero() {
829
+ assert_eq!(JSONRPC_VERSION, "2.0");
830
+ }
831
+
832
+ // ── Id equality / hashing ──────────────────────────────────────
833
+
834
+ #[test]
835
+ fn id_equality_and_hash() {
836
+ use std::collections::HashSet;
837
+ let a = Id::String("x".into());
838
+ let b = Id::String("x".into());
839
+ assert_eq!(a, b);
840
+ let set: HashSet<Id> = [a, b, Id::Number(1)].into_iter().collect();
841
+ assert_eq!(set.len(), 2);
842
+ }
843
+
844
+ // ── Round-trip through JsonRpcMessage for all three variants ───
845
+
846
+ #[test]
847
+ fn message_round_trip_request() {
848
+ let req = JsonRpcRequest::new("agent.run", Some(json!({"k": "v"})))
849
+ .with_id(Id::String("rid".into()));
850
+ let s = serde_json::to_string(&JsonRpcMessage::Request(req.clone())).unwrap();
851
+ let msg: JsonRpcMessage = serde_json::from_str(&s).unwrap();
852
+ match msg {
853
+ JsonRpcMessage::Request(r) => {
854
+ assert_eq!(r.method, "agent.run");
855
+ assert_eq!(r.id, Some(Id::String("rid".into())));
856
+ }
857
+ other => panic!("expected Request, got {other:?}"),
858
+ }
859
+ }
860
+
861
+ #[test]
862
+ fn message_round_trip_notification() {
863
+ let notif = JsonRpcNotification::new("ev.push", Some(json!({"n": 1})));
864
+ let s = serde_json::to_string(&JsonRpcMessage::Notification(notif.clone())).unwrap();
865
+ let msg: JsonRpcMessage = serde_json::from_str(&s).unwrap();
866
+ match msg {
867
+ JsonRpcMessage::Notification(n) => {
868
+ assert_eq!(n.method, "ev.push");
869
+ assert!(n.id_param_check());
870
+ }
871
+ other => panic!("expected Notification, got {other:?}"),
872
+ }
873
+ }
874
+
875
+ #[test]
876
+ fn message_round_trip_response() {
877
+ let resp = JsonRpcResponse::success(Id::Number(42), json!({"done": true}));
878
+ let s = serde_json::to_string(&JsonRpcMessage::Response(resp.clone())).unwrap();
879
+ let msg: JsonRpcMessage = serde_json::from_str(&s).unwrap();
880
+ match msg {
881
+ JsonRpcMessage::Response(r) => {
882
+ assert_eq!(r.id, Id::Number(42));
883
+ assert!(r.result.is_some());
884
+ }
885
+ other => panic!("expected Response, got {other:?}"),
886
+ }
887
+ }
888
+ }