1use std::fmt;
2
3#[path = "adapter/describe.rs"]
4mod describe;
5
6#[path = "adapter/history.rs"]
7mod history;
8
9#[path = "adapter/read_support.rs"]
10mod read_support;
11
12#[path = "adapter/review.rs"]
13mod review;
14
15#[path = "adapter/review_lines.rs"]
16mod review_lines;
17
18#[path = "adapter/writes.rs"]
19mod writes;
20
21#[cfg(test)]
22#[path = "adapter/describe_tests.rs"]
23mod describe_tests;
24
25#[cfg(test)]
26#[path = "adapter/history_tests.rs"]
27mod history_tests;
28
29#[cfg(test)]
30#[path = "adapter/review_tests.rs"]
31mod review_tests;
32
33#[cfg(test)]
34#[path = "adapter/writes_tests.rs"]
35mod writes_tests;
36
37use serde::{Deserialize, Deserializer, Serialize};
38use serde_json::value::RawValue;
39
40pub use writes::ASSISTANT_RECORD;
41
42use super::{rpc, types};
43use crate::serve::protocol;
44
45pub trait Sender {
48 fn send(&mut self, request: protocol::Operation) -> Result<Box<RawValue>, ServiceRefusal>;
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56pub struct ServiceRefusal {
57 pub code: String,
58 pub message: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub receipt: Option<Box<RawValue>>,
61}
62
63#[derive(Debug, Serialize)]
64#[serde(tag = "type", rename_all = "lowercase")]
65pub enum TextContent {
66 Text { text: String },
67}
68
69#[derive(Debug)]
70pub struct ToolResult {
71 pub content: [TextContent; 1],
72 pub structured_content: Box<RawValue>,
73 pub is_error: bool,
74}
75
76impl Serialize for ToolResult {
77 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
78 #[derive(Serialize)]
79 #[serde(rename_all = "camelCase")]
80 struct Encoded<'a> {
81 content: &'a [TextContent; 1],
82 #[serde(skip_serializing_if = "Option::is_none")]
83 structured_content: Option<&'a RawValue>,
84 is_error: bool,
85 }
86 Encoded {
87 content: &self.content,
88 structured_content: Some(self.structured_content.as_ref()),
89 is_error: self.is_error,
90 }
91 .serialize(serializer)
92 }
93}
94
95#[derive(Debug)]
96pub struct EncodeError {
97 cause: serde_json::Error,
98 receipt: Option<Box<RawValue>>,
99}
100
101impl fmt::Display for EncodeError {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(
104 formatter,
105 "mcp_result_encoding_failed: {}; retained receipt {}",
106 self.cause,
107 self.receipt.is_some()
108 )
109 }
110}
111
112impl std::error::Error for EncodeError {
113 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
114 Some(&self.cause)
115 }
116}
117
118pub fn dispatch(sender: &mut impl Sender, call: &rpc::ToolCall) -> Result<ToolResult, EncodeError> {
126 dispatch_as(sender, writes::Writer::Public, call)
127}
128
129pub fn dispatch_assistant_record(
135 sender: &mut impl Sender,
136 call: &rpc::ToolCall,
137) -> Result<ToolResult, EncodeError> {
138 dispatch_as(sender, writes::Writer::AssistantRecorder, call)
139}
140
141fn dispatch_as(
142 sender: &mut impl Sender,
143 writer: writes::Writer,
144 call: &rpc::ToolCall,
145) -> Result<ToolResult, EncodeError> {
146 match call.name.as_str() {
147 "describe_store" => return describe::dispatch(sender, call),
148 "read_history" => return history::dispatch(sender, call),
149 "review_branch" => return review::dispatch(sender, call),
150 "define_collection" => return writes::define_collection(sender, writer, call),
151 "put_records" => return writes::put_records(sender, writer, call),
152 "delete_record" => return writes::delete_record(sender, writer, call),
153 "link_records" => return writes::link_records(sender, writer, call),
154 "unlink_records" => return writes::unlink_records(sender, writer, call),
155 "create_branch" => return writes::create_branch(sender, writer, call),
156 "merge_branch" => return writes::merge_branch(sender, writer, call),
157 _ => {}
158 }
159 if call.name != "run_query" {
160 return refused(&ServiceRefusal {
161 code: "mcp_tool_unknown".into(),
162 message: format!("{} is not a registered tool", call.name),
163 receipt: None,
164 });
165 }
166 let input: types::RunQueryInput = match serde_json::from_str(call.arguments.get()) {
167 Ok(input) => input,
168 Err(error) => {
169 return refused(&ServiceRefusal {
170 code: "mcp_arguments_invalid".into(),
171 message: error.to_string(),
172 receipt: None,
173 });
174 }
175 };
176 let requested = input.selection;
177 let bytes = input.page.bytes.get();
178 let request = protocol::Operation::QueryRun(protocol::QueryRequest {
179 selection: protocol::Selection {
180 branch: requested.branch.clone(),
181 version: requested.version.map(|version| version.0),
182 },
183 query: input.query,
184 parameters: input.parameters,
185 page: protocol::QueryPage {
186 rows: input.page.rows.into(),
187 bytes: input.page.bytes,
188 },
189 continuation: input.after,
190 rows: crate::serve::query::Rows::Values,
191 });
192 let raw = match sender.send(request) {
193 Ok(raw) => raw,
194 Err(refusal) => return refused(&refusal),
195 };
196 let answer: QueryAnswer = match serde_json::from_str(raw.get()) {
197 Ok(answer) => answer,
198 Err(error) => {
199 return refused(&ServiceRefusal {
200 code: "mcp_service_answer_invalid".into(),
201 message: error.to_string(),
202 receipt: None,
203 });
204 }
205 };
206 if let Err(error) = answer.selection.requested(&requested) {
207 return refused(&error);
208 }
209 let output = types::RunQueryOutput {
210 selection: answer.selection.output(),
211 rows: types::Page {
212 entries: answer
213 .rows
214 .into_iter()
215 .map(|row| types::QueryRow {
216 value: row.value,
217 origin: row.origin.map(|origin| types::RecordRef {
218 collection: origin.collection,
219 document_id: origin.id,
220 }),
221 })
222 .collect(),
223 next_after: answer.next,
224 },
225 };
226 match read_support::bounded(&output, bytes) {
227 Ok(output) => Ok(output),
228 Err(error) => refused(&error),
229 }
230}
231
232fn refused(refusal: &ServiceRefusal) -> Result<ToolResult, EncodeError> {
233 let structured_content =
234 serde_json::value::to_raw_value(refusal).map_err(|cause| EncodeError {
235 cause,
236 receipt: refusal.receipt.clone(),
237 })?;
238 let text = structured_content.get().to_owned();
239 Ok(ToolResult {
240 content: [TextContent::Text { text }],
241 structured_content,
242 is_error: true,
243 })
244}
245
246fn result(body: &impl Serialize, is_error: bool) -> Result<ToolResult, EncodeError> {
247 let structured_content =
248 serde_json::value::to_raw_value(body).map_err(|cause| EncodeError {
249 cause,
250 receipt: None,
251 })?;
252 let text = structured_content.get().to_owned();
253 Ok(ToolResult {
254 content: [TextContent::Text { text }],
255 structured_content,
256 is_error,
257 })
258}
259
260#[derive(Deserialize)]
263struct QueryAnswer {
264 selection: read_support::Snapshot,
265 rows: Vec<QueryRow>,
266 #[serde(deserialize_with = "present_next")]
267 next: Option<String>,
268}
269
270#[derive(Deserialize)]
271struct QueryRow {
272 value: Box<RawValue>,
273 #[serde(deserialize_with = "present_origin")]
274 origin: Option<QueryOrigin>,
275}
276
277#[derive(Deserialize)]
278struct QueryOrigin {
279 collection: String,
280 id: String,
281}
282
283fn present_next<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
284 Option::deserialize(deserializer)
285}
286
287fn present_origin<'de, D: Deserializer<'de>>(
288 deserializer: D,
289) -> Result<Option<QueryOrigin>, D::Error> {
290 Option::deserialize(deserializer)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 type TestResult = Result<(), Box<dyn std::error::Error>>;
298
299 const ARGUMENTS: &str = r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"query":".amount","parameters":{"amount":1e2,"identity":123456789012345678},"page":{"rows":17,"bytes":4096},"after":"opaque+/=cursor"}"#;
300 const ANSWER: &str = r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"},"roots":[]},"rows":[{"value":{"amount":1e2,"identity":123456789012345678},"origin":{"collection":"accounts","id":"a","version":11,"score":null}}],"next":"next+/=cursor","engine":"jq","records_read":1}"#;
301
302 struct RecordingSender {
303 requests: Vec<protocol::Operation>,
304 answer: Option<Result<Box<RawValue>, ServiceRefusal>>,
305 }
306
307 impl RecordingSender {
308 fn answered(text: &str) -> Result<Self, serde_json::Error> {
309 Ok(Self {
310 requests: Vec::new(),
311 answer: Some(Ok(serde_json::from_str(text)?)),
312 })
313 }
314 }
315
316 impl Sender for RecordingSender {
317 fn send(&mut self, request: protocol::Operation) -> Result<Box<RawValue>, ServiceRefusal> {
318 self.requests.push(request);
319 self.answer.take().ok_or_else(|| ServiceRefusal {
320 code: "test_unexpected_request".into(),
321 message: "no further answer was supplied".into(),
322 receipt: None,
323 })?
324 }
325 }
326
327 fn call(name: &str, arguments: &str) -> Result<rpc::ToolCall, serde_json::Error> {
328 Ok(rpc::ToolCall {
329 name: name.into(),
330 arguments: serde_json::from_str(arguments)?,
331 metadata: None,
332 })
333 }
334
335 fn assert_refusal(result: &ToolResult, code: &str) -> TestResult {
336 let refusal: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
337 assert!(result.is_error);
338 assert_eq!(refusal.code, code);
339 let TextContent::Text { text } = &result.content[0];
340 assert_eq!(text, result.structured_content.get());
341 let encoded = serde_json::to_string(result)?;
342 assert!(!encoded.contains("\"jsonrpc\""));
343 assert!(!encoded.contains("\"error\""));
344 let wire: serde_json::Value = serde_json::to_value(result)?;
345 assert_eq!(wire["isError"], true);
346 assert_eq!(
347 wire["structuredContent"],
348 serde_json::from_str::<serde_json::Value>(result.structured_content.get())?
349 );
350 let text = wire["content"][0]["text"]
351 .as_str()
352 .ok_or("refusal text missing")?;
353 let refusal: ServiceRefusal = serde_json::from_str(text)?;
354 assert_eq!(refusal.code, code);
355 Ok(())
356 }
357
358 #[test]
359 fn query_sends_one_request_with_exact_inputs() -> TestResult {
360 let mut sender = RecordingSender::answered(ANSWER)?;
361 dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
362 assert_eq!(sender.requests.len(), 1);
363 let Some(protocol::Operation::QueryRun(request)) = sender.requests.first() else {
364 return Err(std::io::Error::other("expected query.run").into());
365 };
366 assert_eq!(request.selection.branch, "main");
367 assert_eq!(
368 request.selection.version,
369 Some(protocol::Version {
370 generation: 7,
371 sequence: 11
372 })
373 );
374 assert_eq!(request.query, ".amount");
375 assert_eq!(
376 request.parameters.get("amount").map(|raw| raw.get()),
377 Some("1e2")
378 );
379 assert_eq!(
380 request.parameters.get("identity").map(|raw| raw.get()),
381 Some("123456789012345678")
382 );
383 assert_eq!(request.page.rows.get(), 17);
384 assert_eq!(request.page.bytes.get(), 4096);
385 assert_eq!(request.continuation.as_deref(), Some("opaque+/=cursor"));
386 assert_eq!(serde_json::to_string(&request.rows)?, "\"values\"");
387 Ok(())
388 }
389
390 #[test]
391 fn raw_row_numbers_survive_both_result_representations() -> TestResult {
392 #[derive(serde::Deserialize)]
393 struct EncodedResult {
394 #[serde(rename = "structuredContent")]
395 structured_content: Box<RawValue>,
396 }
397
398 let mut sender = RecordingSender::answered(ANSWER)?;
399 let result = dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
400 assert!(!result.is_error);
401 assert_eq!(result.content.len(), 1);
402 let TextContent::Text { text } = &result.content[0];
403 assert_eq!(text, result.structured_content.get());
404 assert!(text.contains(r#""amount":1e2"#));
405 assert!(text.contains(r#""identity":123456789012345678"#));
406 let output: types::RunQueryOutput = serde_json::from_str(result.structured_content.get())?;
407 assert_eq!(output.selection.branch, "main");
408 assert_eq!(
409 output.selection.version.0,
410 protocol::Version {
411 generation: 7,
412 sequence: 11
413 }
414 );
415 assert_eq!(output.rows.next_after.as_deref(), Some("next+/=cursor"));
416 let row = output
417 .rows
418 .entries
419 .first()
420 .ok_or_else(|| std::io::Error::other("expected one row"))?;
421 assert_eq!(
422 row.value.get(),
423 r#"{"amount":1e2,"identity":123456789012345678}"#
424 );
425 let origin = row
426 .origin
427 .as_ref()
428 .ok_or_else(|| std::io::Error::other("expected origin"))?;
429 assert_eq!(origin.collection, "accounts");
430 assert_eq!(origin.document_id, "a");
431 let encoded: EncodedResult = serde_json::from_str(&serde_json::to_string(&result)?)?;
432 assert_eq!(
433 encoded.structured_content.get(),
434 result.structured_content.get()
435 );
436 Ok(())
437 }
438
439 #[test]
440 fn fresh_capture_and_scalar_rows_are_preserved() -> TestResult {
441 let mut sender = RecordingSender::answered(
442 r#"{"selection":{"branch":"main","version":{"generation":"8","sequence":"12"}},"rows":[{"value":1e2,"origin":null}],"next":null}"#,
443 )?;
444 let args = ARGUMENTS.replace(r#"{"generation":"7","sequence":"11"}"#, "null");
445 let result = dispatch(&mut sender, &call("run_query", &args)?)?;
446 let Some(protocol::Operation::QueryRun(request)) = sender.requests.first() else {
447 return Err(std::io::Error::other("expected query.run").into());
448 };
449 assert_eq!(request.selection.version, None);
450 let output: types::RunQueryOutput = serde_json::from_str(result.structured_content.get())?;
451 assert_eq!(
452 output.selection.version.0,
453 protocol::Version {
454 generation: 8,
455 sequence: 12
456 }
457 );
458 assert_eq!(
459 output.rows.entries.first().map(|row| row.value.get()),
460 Some("1e2")
461 );
462 assert!(
463 output
464 .rows
465 .entries
466 .first()
467 .is_some_and(|row| row.origin.is_none())
468 );
469 assert_eq!(output.rows.next_after, None);
470 Ok(())
471 }
472
473 #[test]
474 fn service_refusals_keep_their_codes_and_messages() -> TestResult {
475 for code in ["collection_missing", "version_conflict"] {
476 let mut sender = RecordingSender {
477 requests: Vec::new(),
478 answer: Some(Err(ServiceRefusal {
479 code: code.into(),
480 message: "original refusal".into(),
481 receipt: None,
482 })),
483 };
484 let result = dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
485 assert_refusal(&result, code)?;
486 let refusal: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
487 assert_eq!(refusal.message, "original refusal");
488 assert_eq!(sender.requests.len(), 1);
489 let encoded = serde_json::to_string(&result)?;
490 assert!(!encoded.contains("\"jsonrpc\""));
491 assert!(!encoded.contains("\"error\""));
492 }
493 Ok(())
494 }
495
496 #[test]
497 fn unknown_argument_member_makes_no_request() -> TestResult {
498 let mut sender = RecordingSender::answered(ANSWER)?;
499 let args = ARGUMENTS.replacen('{', r#"{"caller":{},"#, 1);
500 let result = dispatch(&mut sender, &call("run_query", &args)?)?;
501 assert_refusal(&result, "mcp_arguments_invalid")?;
502 assert!(sender.requests.is_empty());
503 Ok(())
504 }
505
506 #[test]
507 fn invalid_limits_and_nested_members_make_no_request() -> TestResult {
508 for args in [
509 ARGUMENTS.replace("\"rows\":17", "\"rows\":0"),
510 ARGUMENTS.replace("\"bytes\":4096", "\"bytes\":0"),
511 ARGUMENTS.replace("\"rows\":17", "\"rows\":17,\"extra\":true"),
512 "{}".into(),
513 "[]".into(),
514 ] {
515 let mut sender = RecordingSender::answered(ANSWER)?;
516 let result = dispatch(&mut sender, &call("run_query", &args)?)?;
517 assert_refusal(&result, "mcp_arguments_invalid")?;
518 assert!(sender.requests.is_empty());
519 }
520 Ok(())
521 }
522
523 #[test]
524 fn merge_branch_checks_its_arguments_before_any_request() -> TestResult {
525 let mut sender = RecordingSender::answered(ANSWER)?;
526 let result = dispatch(&mut sender, &call("merge_branch", "{}")?)?;
527 assert_refusal(&result, "mcp_arguments_invalid")?;
528 assert!(sender.requests.is_empty());
529 Ok(())
530 }
531
532 #[test]
533 fn malformed_write_receipts_leave_the_mutation_outcome_unknown() -> TestResult {
534 let arguments = r#"{"request_id":"new","branch":"main","expected":{"generation":"7","sequence":"11"},"collection":"records","schema":null}"#;
535 for answer in [
536 "{}",
537 r#"{"branch":"main","version":{"generation":"7","sequence":"12"},"request_id":"different"}"#,
538 r#"{"branch":"other","version":{"generation":"7","sequence":"12"},"request_id":"new"}"#,
539 ] {
540 let mut sender = RecordingSender::answered(answer)?;
541 let result = dispatch(&mut sender, &call("define_collection", arguments)?)?;
542 assert_refusal(&result, "mcp_client_outcome_unknown")?;
543 let refusal: ServiceRefusal = serde_json::from_str(result.structured_content.get())?;
544 assert!(refusal.message.contains("mcp_service_answer_invalid"));
545 assert!(refusal.message.contains("request_id new"));
546 assert!(refusal.message.contains("mutation.receipt"));
547 assert_eq!(sender.requests.len(), 1);
548 }
549 Ok(())
550 }
551
552 #[test]
553 fn adapted_tools_reject_missing_input_before_a_service_request() -> TestResult {
554 for tool in crate::mcp::schema::tools() {
555 let mut sender = RecordingSender::answered(ANSWER)?;
556 let result = dispatch(&mut sender, &call(tool.name, "{}")?)?;
557 assert_refusal(&result, "mcp_arguments_invalid")?;
558 assert!(sender.requests.is_empty());
559 }
560 Ok(())
561 }
562
563 #[test]
564 fn malformed_service_answers_are_named_tool_refusals() -> TestResult {
565 for answer in [
566 "{}",
567 r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[{"origin":null}],"next":null}"#,
568 r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[{"value":1}] ,"next":null}"#,
569 r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[]}"#,
570 r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[],"next":42}"#,
571 ] {
572 let mut sender = RecordingSender::answered(answer)?;
573 let result = dispatch(&mut sender, &call("run_query", ARGUMENTS)?)?;
574 assert_refusal(&result, "mcp_service_answer_invalid")?;
575 assert_eq!(sender.requests.len(), 1);
576 }
577 Ok(())
578 }
579
580 #[test]
581 fn read_tools_use_after_and_pages_with_entries_and_next_after() -> TestResult {
582 for tool in crate::mcp::schema::tools() {
583 if [
584 "describe_store",
585 "run_query",
586 "read_history",
587 "review_branch",
588 ]
589 .contains(&tool.name)
590 {
591 let properties = tool.input_schema.as_value()["properties"]
592 .as_object()
593 .ok_or("input properties missing")?;
594 assert!(
595 properties.contains_key("after"),
596 "{} has no after field",
597 tool.name
598 );
599 assert!(
600 !properties.contains_key("cursors"),
601 "{} still has cursors",
602 tool.name
603 );
604 assert!(
605 !properties.contains_key("continuation"),
606 "{} still has continuation",
607 tool.name
608 );
609 }
610 }
611 let mut arguments: serde_json::Value = serde_json::from_str(ARGUMENTS)?;
612 let arguments = arguments
613 .as_object_mut()
614 .ok_or("arguments are not an object")?;
615 arguments.remove("continuation");
616 arguments.insert("after".to_owned(), serde_json::json!("opaque+/=cursor"));
617 let mut sender = RecordingSender::answered(ANSWER)?;
618 let result = dispatch(
619 &mut sender,
620 &call("run_query", &serde_json::to_string(arguments)?)?,
621 )?;
622 assert!(!result.is_error, "{}", result.structured_content.get());
623 assert_eq!(sender.requests.len(), 1);
624 let Some(protocol::Operation::QueryRun(request)) = sender.requests.first() else {
625 return Err(std::io::Error::other("expected query.run").into());
626 };
627 assert_eq!(request.continuation.as_deref(), Some("opaque+/=cursor"));
628 let output: serde_json::Value = serde_json::from_str(result.structured_content.get())?;
629 assert_eq!(
630 output["rows"]["entries"]
631 .as_array()
632 .ok_or("entries missing")?
633 .len(),
634 1
635 );
636 assert_eq!(output["rows"]["next_after"], "next+/=cursor");
637 assert!(output.get("next").is_none());
638 Ok(())
639 }
640
641 #[test]
642 fn query_and_fresh_description_refuse_unrequested_snapshots_before_more_calls() -> TestResult {
643 for (answer, arguments) in [
644 (
645 ANSWER.replace("\"branch\":\"main\"", "\"branch\":\"other\""),
646 ARGUMENTS.to_owned(),
647 ),
648 (
649 ANSWER.replace("\"sequence\":\"11\"", "\"sequence\":\"12\""),
650 ARGUMENTS.to_owned(),
651 ),
652 (
653 ANSWER.replace("\"branch\":\"main\"", "\"branch\":\"other\""),
654 ARGUMENTS.replace(r#"{"generation":"7","sequence":"11"}"#, "null"),
655 ),
656 ] {
657 let mut sender = RecordingSender::answered(&answer)?;
658 let result = dispatch(&mut sender, &call("run_query", &arguments)?)?;
659 assert_refusal(&result, "mcp_service_snapshot_mismatch")?;
660 assert_eq!(sender.requests.len(), 1);
661 }
662 let mut arguments: serde_json::Value = serde_json::from_str(&types::tests::inputs()[0].1)?;
663 arguments["selection"] = serde_json::json!({"branch":"main","version":null});
664 let mut sender = RecordingSender::answered(
665 r#"{"selection":{"branch":"other","version":{"generation":"7","sequence":"11"}},"entries":[],"next_after":null}"#,
666 )?;
667 let result = dispatch(
668 &mut sender,
669 &call("describe_store", &arguments.to_string())?,
670 )?;
671 assert_refusal(&result, "mcp_service_snapshot_mismatch")?;
672 assert_eq!(sender.requests.len(), 1);
673 Ok(())
674 }
675
676 #[test]
677 fn query_refuses_a_projected_page_that_exceeds_the_requested_bytes() -> TestResult {
678 let answer = r#"{"selection":{"branch":"main","version":{"generation":"7","sequence":"11"}},"rows":[{"value":1,"origin":{"collection":"records","id":"a"}}],"next":null}"#;
679 let mut arguments: serde_json::Value = serde_json::from_str(ARGUMENTS)?;
680 arguments["page"]["bytes"] = serde_json::json!(answer.len());
681 let mut sender = RecordingSender::answered(answer)?;
682 let result = dispatch(&mut sender, &call("run_query", &arguments.to_string())?)?;
683 assert_refusal(&result, "mcp_page_bytes_exceeded")?;
684 assert_eq!(sender.requests.len(), 1);
685 Ok(())
686 }
687
688 #[test]
689 fn diff_and_conflict_values_distinguish_absence_null_and_raw_json() -> TestResult {
690 let difference: review_lines::Difference = serde_json::from_value(serde_json::json!({
691 "collection":"records","document_id":"a","what":"document",
692 "base":"null","here":"\"\""
693 }))?;
694 let difference = review_lines::difference(difference).map_err(|refusal| {
695 std::io::Error::other(format!("{}: {}", refusal.code, refusal.message))
696 })?;
697 let difference = serde_json::to_value(difference)?;
698 assert_eq!(
699 difference["before"],
700 serde_json::json!({"kind":"value","value":null})
701 );
702 assert_eq!(
703 difference["after"],
704 serde_json::json!({"kind":"value","value":""})
705 );
706 let conflict: review_lines::Conflict = serde_json::from_value(serde_json::json!({
707 "collection":"records","document_id":"a","what":"document",
708 "ancestor":"","target":"null","source":r#"{"n":1e2,"id":123456789012345678}"#,
709 "state":"conflict","choice_token":"chosen"
710 }))?;
711 let conflict = review_lines::conflict(conflict).map_err(|refusal| {
712 std::io::Error::other(format!("{}: {}", refusal.code, refusal.message))
713 })?;
714 let encoded = serde_json::to_string(&conflict)?;
715 assert!(encoded.contains(r#""n":1e2"#));
716 assert!(encoded.contains(r#""id":123456789012345678"#));
717 let conflict: serde_json::Value = serde_json::from_str(&encoded)?;
718 assert_eq!(conflict["ancestor"], serde_json::json!({"kind":"absent"}));
719 assert_eq!(
720 conflict["target"],
721 serde_json::json!({"kind":"value","value":null})
722 );
723 assert_eq!(conflict["source"]["kind"], "value");
724 assert_eq!(conflict["state"], "conflict");
725 assert_eq!(conflict["choice_token"], "chosen");
726 let malformed: review_lines::Conflict = serde_json::from_value(serde_json::json!({
727 "collection":"records","document_id":"a","what":"document",
728 "ancestor":"","target":"not-json","source":"null",
729 "state":"conflict","choice_token":"chosen"
730 }))?;
731 let refusal = review_lines::conflict(malformed)
732 .err()
733 .ok_or("invalid JSON accepted")?;
734 assert_eq!(refusal.code, "mcp_service_answer_invalid");
735 Ok(())
736 }
737}
738
739#[cfg(test)]
740mod typed_error_retention_tests {
741 use super::*;
742 #[test]
743 fn is_error_keeps_the_full_structured_error_data() -> Result<(), Box<dyn std::error::Error>> {
744 let refusal = ServiceRefusal {
745 code: "partial_commit".into(),
746 message: "original complete cause".into(),
747 receipt: None,
748 };
749 let result = refused(&refusal)?;
750 let encoded = serde_json::to_value(&result)?;
751 assert_eq!(encoded["isError"], true);
752 assert_eq!(encoded["structuredContent"]["code"], "partial_commit");
753 assert_eq!(
754 encoded["structuredContent"]["message"],
755 "original complete cause"
756 );
757 Ok(())
758 }
759}