1use serde_json::{Value, json};
2
3use crate::assertions::VerifyBuilder;
4use crate::error::TestError;
5use crate::visual::{VisualDiff, VisualOptions};
6
7pub struct VictauriClient {
12 http: reqwest::Client,
13 base_url: String,
14 session_id: String,
15 next_id: u64,
16 auth_token: Option<String>,
17}
18
19impl VictauriClient {
20 pub async fn connect(port: u16) -> Result<Self, TestError> {
29 Self::connect_with_token(port, None).await
30 }
31
32 pub async fn connect_with_token(port: u16, token: Option<&str>) -> Result<Self, TestError> {
42 let base_url = format!("http://127.0.0.1:{port}");
43 let http = reqwest::Client::builder()
44 .timeout(std::time::Duration::from_secs(60))
45 .connect_timeout(std::time::Duration::from_secs(10))
46 .build()
47 .map_err(|e| TestError::Connection(e.to_string()))?;
48
49 let init_body = json!({
50 "jsonrpc": "2.0",
51 "id": 1,
52 "method": "initialize",
53 "params": {
54 "protocolVersion": "2025-03-26",
55 "capabilities": {},
56 "clientInfo": {"name": "victauri-test", "version": env!("CARGO_PKG_VERSION")}
57 }
58 });
59
60 let mut init_resp = None;
61 for attempt in 0..4 {
62 let mut req = http
63 .post(format!("{base_url}/mcp"))
64 .header("Content-Type", "application/json")
65 .header("Accept", "application/json, text/event-stream")
66 .json(&init_body);
67 if let Some(t) = token {
68 req = req.header("Authorization", format!("Bearer {t}"));
69 }
70
71 let resp = req
72 .send()
73 .await
74 .map_err(|e| TestError::Connection(e.to_string()))?;
75
76 if resp.status() == 429 && attempt < 3 {
77 let delay = std::time::Duration::from_millis(100 * (1 << attempt));
78 tokio::time::sleep(delay).await;
79 continue;
80 }
81
82 init_resp = Some(resp);
83 break;
84 }
85
86 let init_resp = init_resp
87 .ok_or_else(|| TestError::Connection("initialize failed after retries".into()))?;
88
89 if !init_resp.status().is_success() {
90 return Err(TestError::Connection(format!(
91 "initialize returned {}",
92 init_resp.status()
93 )));
94 }
95
96 let session_id = init_resp
97 .headers()
98 .get("mcp-session-id")
99 .and_then(|v| v.to_str().ok())
100 .ok_or_else(|| TestError::Connection("no mcp-session-id header".into()))?
101 .to_string();
102
103 let mut notify_req = http
104 .post(format!("{base_url}/mcp"))
105 .header("Content-Type", "application/json")
106 .header("mcp-session-id", &session_id)
107 .json(&json!({
108 "jsonrpc": "2.0",
109 "method": "notifications/initialized"
110 }));
111
112 if let Some(t) = token {
113 notify_req = notify_req.header("Authorization", format!("Bearer {t}"));
114 }
115
116 notify_req.send().await?;
117
118 Ok(Self {
119 http,
120 base_url,
121 session_id,
122 next_id: 10,
123 auth_token: token.map(String::from),
124 })
125 }
126
127 pub async fn discover() -> Result<Self, TestError> {
139 let port = Self::discover_port();
140 let token = Self::discover_token();
141 Self::connect_with_token(port, token.as_deref()).await
142 }
143
144 fn discover_port() -> u16 {
145 if let Ok(p) = std::env::var("VICTAURI_PORT")
146 && let Ok(port) = p.parse::<u16>()
147 {
148 return port;
149 }
150 let path = std::env::temp_dir().join("victauri.port");
151 if let Ok(contents) = std::fs::read_to_string(&path)
152 && let Ok(port) = contents.trim().parse::<u16>()
153 {
154 return port;
155 }
156 7373
157 }
158
159 fn discover_token() -> Option<String> {
160 if let Ok(token) = std::env::var("VICTAURI_AUTH_TOKEN") {
161 return Some(token);
162 }
163 let path = std::env::temp_dir().join("victauri.token");
164 let token = std::fs::read_to_string(&path).ok()?;
165 let token = token.trim().to_string();
166 if token.is_empty() { None } else { Some(token) }
167 }
168
169 pub async fn call_tool(&mut self, name: &str, arguments: Value) -> Result<Value, TestError> {
179 let id = self.next_id;
180 self.next_id += 1;
181
182 let call_body = json!({
183 "jsonrpc": "2.0",
184 "id": id,
185 "method": "tools/call",
186 "params": {
187 "name": name,
188 "arguments": arguments
189 }
190 });
191
192 let mut resp = None;
193 for attempt in 0..4 {
194 let mut req = self
195 .http
196 .post(format!("{}/mcp", self.base_url))
197 .header("Content-Type", "application/json")
198 .header("Accept", "application/json, text/event-stream")
199 .header("mcp-session-id", &self.session_id)
200 .json(&call_body);
201 if let Some(ref t) = self.auth_token {
202 req = req.header("Authorization", format!("Bearer {t}"));
203 }
204 let r = req.send().await?;
205
206 if r.status() == 429 && attempt < 3 {
207 let delay = std::time::Duration::from_millis(100 * (1 << attempt));
208 tokio::time::sleep(delay).await;
209 continue;
210 }
211 resp = Some(r);
212 break;
213 }
214
215 let resp =
216 resp.ok_or_else(|| TestError::Connection("tool call failed after retries".into()))?;
217 let body = Self::parse_response(resp).await?;
218
219 if let Some(error) = body.get("error") {
220 return Err(TestError::Mcp {
221 code: error["code"].as_i64().unwrap_or(-1),
222 message: error["message"].as_str().unwrap_or("unknown").to_string(),
223 });
224 }
225
226 let content = &body["result"]["content"];
227 if let Some(arr) = content.as_array()
228 && let Some(first) = arr.first()
229 && let Some(text) = first["text"].as_str()
230 {
231 if let Ok(parsed) = serde_json::from_str::<Value>(text) {
232 return Ok(parsed);
233 }
234 return Ok(Value::String(text.to_string()));
235 }
236
237 Ok(body)
238 }
239
240 async fn parse_response(resp: reqwest::Response) -> Result<Value, TestError> {
245 let content_type = resp
246 .headers()
247 .get("content-type")
248 .and_then(|v| v.to_str().ok())
249 .unwrap_or("")
250 .to_string();
251
252 let text = resp.text().await?;
253
254 if content_type.contains("text/event-stream") {
255 for line in text.lines() {
256 let data = line
257 .strip_prefix("data: ")
258 .or_else(|| line.strip_prefix("data:"));
259 let Some(data) = data else { continue };
260 let trimmed = data.trim();
261 if trimmed.is_empty() {
262 continue;
263 }
264 if let Ok(parsed) = serde_json::from_str::<Value>(trimmed) {
265 return Ok(parsed);
266 }
267 }
268 Err(TestError::Connection(
269 "SSE stream contained no JSON-RPC data".into(),
270 ))
271 } else {
272 serde_json::from_str(&text).map_err(|e| {
273 TestError::Connection(format!(
274 "JSON parse error: {e}, body: {}",
275 &text[..200.min(text.len())]
276 ))
277 })
278 }
279 }
280
281 pub async fn eval_js(&mut self, code: &str) -> Result<Value, TestError> {
287 self.call_tool("eval_js", json!({"code": code})).await
288 }
289
290 pub async fn dom_snapshot(&mut self) -> Result<Value, TestError> {
296 self.call_tool("dom_snapshot", json!({})).await
297 }
298
299 pub async fn click(&mut self, ref_id: &str) -> Result<Value, TestError> {
305 self.call_tool("interact", json!({"action": "click", "ref_id": ref_id}))
306 .await
307 }
308
309 pub async fn fill(&mut self, ref_id: &str, value: &str) -> Result<Value, TestError> {
315 self.call_tool(
316 "input",
317 json!({"action": "fill", "ref_id": ref_id, "value": value}),
318 )
319 .await
320 }
321
322 pub async fn type_text(&mut self, ref_id: &str, text: &str) -> Result<Value, TestError> {
328 self.call_tool(
329 "input",
330 json!({"action": "type_text", "ref_id": ref_id, "text": text}),
331 )
332 .await
333 }
334
335 pub async fn list_windows(&mut self) -> Result<Value, TestError> {
341 self.call_tool("window", json!({"action": "list"})).await
342 }
343
344 pub async fn get_window_state(&mut self, label: Option<&str>) -> Result<Value, TestError> {
350 let mut args = json!({"action": "get_state"});
351 if let Some(l) = label {
352 args["label"] = json!(l);
353 }
354 self.call_tool("window", args).await
355 }
356
357 pub async fn screenshot(&mut self) -> Result<Value, TestError> {
363 self.call_tool("screenshot", json!({})).await
364 }
365
366 pub async fn screenshot_visual(
378 &mut self,
379 name: &str,
380 options: &VisualOptions,
381 ) -> Result<VisualDiff, TestError> {
382 let result = self.screenshot().await?;
383 let base64_data = extract_screenshot_base64(&result)?;
384 crate::visual::compare_screenshot(name, &base64_data, options)
385 }
386
387 pub async fn invoke_command(
393 &mut self,
394 command: &str,
395 args: Option<Value>,
396 ) -> Result<Value, TestError> {
397 let mut params = json!({"command": command});
398 if let Some(a) = args {
399 params["args"] = a;
400 }
401 self.call_tool("invoke_command", params).await
402 }
403
404 pub async fn get_ipc_log(&mut self, limit: Option<usize>) -> Result<Value, TestError> {
410 let mut args = json!({"action": "ipc"});
411 if let Some(n) = limit {
412 args["limit"] = json!(n);
413 }
414 self.call_tool("logs", args).await
415 }
416
417 pub async fn verify_state(
423 &mut self,
424 frontend_expr: &str,
425 backend_state: Value,
426 ) -> Result<Value, TestError> {
427 self.call_tool(
428 "verify_state",
429 json!({
430 "frontend_expr": frontend_expr,
431 "backend_state": backend_state,
432 }),
433 )
434 .await
435 }
436
437 pub async fn detect_ghost_commands(&mut self) -> Result<Value, TestError> {
443 self.call_tool("detect_ghost_commands", json!({})).await
444 }
445
446 pub async fn check_ipc_integrity(&mut self) -> Result<Value, TestError> {
452 self.call_tool("check_ipc_integrity", json!({})).await
453 }
454
455 pub async fn assert_semantic(
461 &mut self,
462 expression: &str,
463 label: &str,
464 condition: &str,
465 expected: Value,
466 ) -> Result<Value, TestError> {
467 self.call_tool(
468 "assert_semantic",
469 json!({
470 "expression": expression,
471 "label": label,
472 "condition": condition,
473 "expected": expected,
474 }),
475 )
476 .await
477 }
478
479 pub async fn audit_accessibility(&mut self) -> Result<Value, TestError> {
485 self.call_tool("inspect", json!({"action": "audit_accessibility"}))
486 .await
487 }
488
489 pub async fn get_performance_metrics(&mut self) -> Result<Value, TestError> {
495 self.call_tool("inspect", json!({"action": "get_performance"}))
496 .await
497 }
498
499 pub async fn get_registry(&mut self) -> Result<Value, TestError> {
505 self.call_tool("get_registry", json!({})).await
506 }
507
508 pub async fn get_memory_stats(&mut self) -> Result<Value, TestError> {
514 self.call_tool("get_memory_stats", json!({})).await
515 }
516
517 pub async fn get_plugin_info(&mut self) -> Result<Value, TestError> {
523 self.call_tool("get_plugin_info", json!({})).await
524 }
525
526 pub async fn wait_for(
535 &mut self,
536 condition: &str,
537 value: Option<&str>,
538 timeout_ms: Option<u64>,
539 poll_ms: Option<u64>,
540 ) -> Result<Value, TestError> {
541 let mut args = json!({"condition": condition});
542 if let Some(v) = value {
543 args["value"] = json!(v);
544 }
545 if let Some(t) = timeout_ms {
546 args["timeout_ms"] = json!(t);
547 }
548 if let Some(p) = poll_ms {
549 args["poll_ms"] = json!(p);
550 }
551 self.call_tool("wait_for", args).await
552 }
553
554 pub async fn start_recording(&mut self, session_id: Option<&str>) -> Result<Value, TestError> {
560 let mut args = json!({"action": "start"});
561 if let Some(id) = session_id {
562 args["session_id"] = json!(id);
563 }
564 self.call_tool("recording", args).await
565 }
566
567 pub async fn stop_recording(&mut self) -> Result<Value, TestError> {
573 self.call_tool("recording", json!({"action": "stop"})).await
574 }
575
576 pub async fn export_session(&mut self) -> Result<Value, TestError> {
582 self.call_tool("recording", json!({"action": "export"}))
583 .await
584 }
585
586 pub async fn find_elements(&mut self, query: Value) -> Result<Value, TestError> {
592 self.call_tool("find_elements", query).await
593 }
594
595 pub async fn hover(&mut self, ref_id: &str) -> Result<Value, TestError> {
601 self.call_tool("interact", json!({"action": "hover", "ref_id": ref_id}))
602 .await
603 }
604
605 pub async fn focus(&mut self, ref_id: &str) -> Result<Value, TestError> {
611 self.call_tool("interact", json!({"action": "focus", "ref_id": ref_id}))
612 .await
613 }
614
615 pub async fn press_key(&mut self, key: &str) -> Result<Value, TestError> {
621 self.call_tool("input", json!({"action": "press_key", "key": key}))
622 .await
623 }
624
625 pub async fn navigate(&mut self, url: &str) -> Result<Value, TestError> {
631 self.call_tool("navigate", json!({"action": "go_to", "url": url}))
632 .await
633 }
634
635 pub async fn logs(&mut self, action: &str, limit: Option<usize>) -> Result<Value, TestError> {
641 self.call_tool("logs", json!({"action": action, "limit": limit}))
642 .await
643 }
644
645 pub async fn scroll_to(&mut self, ref_id: &str) -> Result<Value, TestError> {
651 self.call_tool(
652 "interact",
653 json!({"action": "scroll_into_view", "ref_id": ref_id}),
654 )
655 .await
656 }
657
658 pub async fn select_option(
664 &mut self,
665 ref_id: &str,
666 values: &[&str],
667 ) -> Result<Value, TestError> {
668 self.call_tool(
669 "interact",
670 json!({"action": "select_option", "ref_id": ref_id, "values": values}),
671 )
672 .await
673 }
674
675 #[must_use]
677 pub fn base_url(&self) -> &str {
678 &self.base_url
679 }
680
681 #[must_use]
683 pub fn session_id(&self) -> &str {
684 &self.session_id
685 }
686
687 pub(crate) fn http_client(&self) -> &reqwest::Client {
688 &self.http
689 }
690
691 pub async fn get_ipc_calls(&mut self, command: &str) -> Result<Vec<Value>, TestError> {
701 let log = self.get_ipc_log(None).await?;
702 let entries = if let Some(arr) = log.as_array() {
703 arr.clone()
704 } else if let Some(entries) = log.get("entries").and_then(Value::as_array) {
705 entries.clone()
706 } else {
707 return Ok(Vec::new());
708 };
709 Ok(entries
710 .into_iter()
711 .filter(|e| {
712 e.get("command")
713 .and_then(Value::as_str)
714 .is_some_and(|c| c == command)
715 })
716 .collect())
717 }
718
719 pub async fn ipc_checkpoint(&mut self) -> Result<usize, TestError> {
725 let log = self.get_ipc_log(None).await?;
726 let len = if let Some(arr) = log.as_array() {
727 arr.len()
728 } else if let Some(entries) = log.get("entries").and_then(Value::as_array) {
729 entries.len()
730 } else {
731 0
732 };
733 Ok(len)
734 }
735
736 pub async fn ipc_calls_since(&mut self, checkpoint: usize) -> Result<Vec<Value>, TestError> {
742 let log = self.get_ipc_log(None).await?;
743 let entries = if let Some(arr) = log.as_array() {
744 arr.clone()
745 } else if let Some(entries) = log.get("entries").and_then(Value::as_array) {
746 entries.clone()
747 } else {
748 return Ok(Vec::new());
749 };
750 Ok(entries.into_iter().skip(checkpoint).collect())
751 }
752
753 pub fn verify(&mut self) -> VerifyBuilder<'_> {
774 VerifyBuilder::new(self)
775 }
776
777 pub async fn click_by_text(&mut self, text: &str) -> Result<Value, TestError> {
788 let ref_id = self.find_ref_by_text(text).await?;
789 self.click(&ref_id).await
790 }
791
792 pub async fn click_by_id(&mut self, id: &str) -> Result<Value, TestError> {
799 let ref_id = self.find_ref_by_id(id).await?;
800 self.click(&ref_id).await
801 }
802
803 pub async fn fill_by_id(&mut self, id: &str, value: &str) -> Result<Value, TestError> {
810 let ref_id = self.find_ref_by_id(id).await?;
811 self.fill(&ref_id, value).await
812 }
813
814 pub async fn type_by_id(&mut self, id: &str, text: &str) -> Result<Value, TestError> {
821 let ref_id = self.find_ref_by_id(id).await?;
822 self.type_text(&ref_id, text).await
823 }
824
825 pub async fn expect_text(&mut self, text: &str) -> Result<(), TestError> {
834 self.expect_text_with_timeout(text, 5000).await
835 }
836
837 pub async fn expect_text_with_timeout(
844 &mut self,
845 text: &str,
846 timeout_ms: u64,
847 ) -> Result<(), TestError> {
848 let result = self
849 .wait_for("text", Some(text), Some(timeout_ms), Some(200))
850 .await?;
851 if result.get("ok").and_then(Value::as_bool) == Some(true) {
852 Ok(())
853 } else {
854 Err(TestError::Timeout(format!(
855 "text \"{text}\" did not appear within {timeout_ms}ms"
856 )))
857 }
858 }
859
860 pub async fn expect_no_text(&mut self, text: &str) -> Result<(), TestError> {
869 let result = self
870 .wait_for("text_gone", Some(text), Some(3000), Some(200))
871 .await?;
872 if result.get("ok").and_then(Value::as_bool) == Some(true) {
873 Ok(())
874 } else {
875 Err(TestError::Timeout(format!(
876 "text \"{text}\" still present after 3000ms"
877 )))
878 }
879 }
880
881 pub async fn select_by_id(&mut self, id: &str, value: &str) -> Result<Value, TestError> {
888 let ref_id = self.find_ref_by_id(id).await?;
889 self.select_option(&ref_id, &[value]).await
890 }
891
892 pub async fn text_by_id(&mut self, id: &str) -> Result<String, TestError> {
899 let snap = self.snapshot_json().await?;
900 let tree = &snap["tree"];
901 find_text_by_attr_id(tree, id)
902 .ok_or_else(|| TestError::ElementNotFound(format!("id=\"{id}\"")))
903 }
904
905 async fn snapshot_json(&mut self) -> Result<Value, TestError> {
908 self.call_tool("dom_snapshot", json!({"format": "json"}))
909 .await
910 }
911
912 async fn find_ref_by_text(&mut self, text: &str) -> Result<String, TestError> {
913 let snap = self.snapshot_json().await?;
914 let tree = &snap["tree"];
915 find_in_tree_by_text(tree, text)
916 .ok_or_else(|| TestError::ElementNotFound(format!("text=\"{text}\"")))
917 }
918
919 async fn find_ref_by_id(&mut self, id: &str) -> Result<String, TestError> {
920 let snap = self.snapshot_json().await?;
921 let tree = &snap["tree"];
922 find_in_tree_by_attr_id(tree, id)
923 .ok_or_else(|| TestError::ElementNotFound(format!("id=\"{id}\"")))
924 }
925}
926
927fn extract_screenshot_base64(result: &Value) -> Result<String, TestError> {
928 if let Some(data) = result.get("base64").and_then(Value::as_str) {
930 return Ok(data.to_string());
931 }
932 if let Some(data) = result.get("data").and_then(Value::as_str) {
933 return Ok(data.to_string());
934 }
935 if let Some(data) = result.get("image").and_then(Value::as_str) {
936 return Ok(data.to_string());
937 }
938 if let Some(data) = result
939 .pointer("/result/content/0/data")
940 .and_then(Value::as_str)
941 {
942 return Ok(data.to_string());
943 }
944 Err(TestError::Other(
945 "screenshot result does not contain recognizable base64 image data".to_string(),
946 ))
947}
948
949fn find_in_tree_by_text(node: &Value, text: &str) -> Option<String> {
950 let node_text = node.get("text").and_then(Value::as_str).unwrap_or("");
951 let node_name = node.get("name").and_then(Value::as_str).unwrap_or("");
952 if (node_text.contains(text) || node_name.contains(text))
953 && let Some(ref_id) = node.get("ref_id").and_then(Value::as_str)
954 {
955 return Some(ref_id.to_string());
956 }
957 if let Some(children) = node.get("children").and_then(Value::as_array) {
958 for child in children {
959 if let Some(found) = find_in_tree_by_text(child, text) {
960 return Some(found);
961 }
962 }
963 }
964 None
965}
966
967fn find_in_tree_by_attr_id(node: &Value, id: &str) -> Option<String> {
968 if node
969 .get("attributes")
970 .and_then(|a| a.get("id"))
971 .and_then(Value::as_str)
972 == Some(id)
973 && let Some(ref_id) = node.get("ref_id").and_then(Value::as_str)
974 {
975 return Some(ref_id.to_string());
976 }
977 if let Some(children) = node.get("children").and_then(Value::as_array) {
978 for child in children {
979 if let Some(found) = find_in_tree_by_attr_id(child, id) {
980 return Some(found);
981 }
982 }
983 }
984 None
985}
986
987fn find_text_by_attr_id(node: &Value, id: &str) -> Option<String> {
988 if node
989 .get("attributes")
990 .and_then(|a| a.get("id"))
991 .and_then(Value::as_str)
992 == Some(id)
993 {
994 let text = node.get("text").and_then(Value::as_str).unwrap_or("");
995 return Some(text.to_string());
996 }
997 if let Some(children) = node.get("children").and_then(Value::as_array) {
998 for child in children {
999 if let Some(found) = find_text_by_attr_id(child, id) {
1000 return Some(found);
1001 }
1002 }
1003 }
1004 None
1005}
1006
1007pub fn assert_json_eq(value: &Value, pointer: &str, expected: &Value) {
1025 let actual = value.pointer(pointer);
1026 assert!(
1027 actual == Some(expected),
1028 "JSON pointer {pointer}: expected {expected}, got {}",
1029 actual.map_or("missing".to_string(), std::string::ToString::to_string)
1030 );
1031}
1032
1033pub fn assert_json_truthy(value: &Value, pointer: &str) {
1050 let actual = value.pointer(pointer);
1051 let is_truthy = match actual {
1052 None | Some(Value::Null) => false,
1053 Some(Value::Bool(b)) => *b,
1054 Some(Value::Number(n)) => n.as_f64().unwrap_or(0.0) != 0.0,
1055 Some(Value::String(s)) => !s.is_empty(),
1056 Some(Value::Array(a)) => !a.is_empty(),
1057 Some(Value::Object(_)) => true,
1058 };
1059 assert!(
1060 is_truthy,
1061 "JSON pointer {pointer}: expected truthy, got {}",
1062 actual.map_or("missing".to_string(), std::string::ToString::to_string)
1063 );
1064}
1065
1066pub fn assert_no_a11y_violations(audit: &Value) {
1081 let violations = audit
1082 .pointer("/summary/violations")
1083 .and_then(serde_json::Value::as_u64)
1084 .unwrap_or(u64::MAX);
1085 assert_eq!(
1086 violations, 0,
1087 "expected 0 accessibility violations, got {violations}"
1088 );
1089}
1090
1091pub fn assert_performance_budget(metrics: &Value, max_load_ms: f64, max_heap_mb: f64) {
1109 if let Some(load) = metrics
1110 .pointer("/navigation/load_event_ms")
1111 .and_then(serde_json::Value::as_f64)
1112 {
1113 assert!(
1114 load <= max_load_ms,
1115 "load event took {load}ms, budget is {max_load_ms}ms"
1116 );
1117 }
1118
1119 if let Some(heap) = metrics
1120 .pointer("/js_heap/used_mb")
1121 .and_then(serde_json::Value::as_f64)
1122 {
1123 assert!(
1124 heap <= max_heap_mb,
1125 "JS heap is {heap}MB, budget is {max_heap_mb}MB"
1126 );
1127 }
1128}
1129
1130pub fn assert_ipc_healthy(integrity: &Value) {
1145 let healthy = integrity
1146 .get("healthy")
1147 .and_then(serde_json::Value::as_bool)
1148 .unwrap_or(false);
1149 assert!(
1150 healthy,
1151 "IPC integrity check failed: {}",
1152 serde_json::to_string_pretty(integrity).unwrap_or_default()
1153 );
1154}
1155
1156pub fn assert_state_matches(verification: &Value) {
1171 let passed = verification
1172 .get("passed")
1173 .and_then(serde_json::Value::as_bool)
1174 .unwrap_or(false);
1175 assert!(
1176 passed,
1177 "state verification failed: {}",
1178 serde_json::to_string_pretty(verification).unwrap_or_default()
1179 );
1180}