1use std::cell::RefCell;
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::io::{Read, Write};
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex};
8use std::time::{Duration, Instant};
9
10use crate::ipc_binary::{self, BinaryFrame};
11use crate::runtime_protocol::{BridgeResponse, RuntimeEvent};
12use crate::session::RuntimeEventEnvelope;
13
14static SYNCRPC_LAT: std::sync::OnceLock<std::sync::Mutex<(u64, u64, u64)>> =
20 std::sync::OnceLock::new();
21
22fn syncrpc_lat_enabled() -> bool {
23 std::env::var("AGENTOS_SYNCRPC_LAT").as_deref() == Ok("1")
24}
25
26fn record_syncrpc_lat(ns: u64) {
27 let m = SYNCRPC_LAT.get_or_init(|| std::sync::Mutex::new((0, 0, 0)));
28 let Ok(mut a) = m.lock() else {
29 return;
30 };
31 a.0 += 1;
32 a.1 = a.1.wrapping_add(ns / 1000);
33 a.2 = a.2.max(ns / 1000);
34 if a.0 % 25 == 0 {
35 if let Ok(path) = std::env::var("AGENTOS_SYNCRPC_LAT_FILE") {
36 let _ = std::fs::write(
37 &path,
38 format!(
39 "calls={} total_us={} avg_us={} max_us={}\n",
40 a.0,
41 a.1,
42 a.1 / a.0,
43 a.2
44 ),
45 );
46 }
47 }
48}
49
50#[derive(Debug, Default, Clone)]
51struct SyncBridgeHostPhaseStats {
52 calls: u64,
53 total_us: u64,
54 max_us: u64,
55}
56
57static SYNC_BRIDGE_HOST_PHASES: std::sync::OnceLock<
58 std::sync::Mutex<BTreeMap<String, SyncBridgeHostPhaseStats>>,
59> = std::sync::OnceLock::new();
60static SYNC_BRIDGE_CALL_METHODS: std::sync::OnceLock<std::sync::Mutex<HashMap<u64, String>>> =
61 std::sync::OnceLock::new();
62static SYNC_BRIDGE_RESPONSE_CHANNEL_SENDS: std::sync::OnceLock<
63 std::sync::Mutex<HashMap<u64, (String, Instant)>>,
64> = std::sync::OnceLock::new();
65
66fn sync_bridge_host_phases_enabled() -> bool {
67 std::env::var("AGENTOS_SYNC_BRIDGE_HOST_PHASES").as_deref() == Ok("1")
68}
69
70pub(crate) fn record_sync_bridge_host_phase(method: &str, stage: &str, elapsed: Duration) {
71 if !sync_bridge_host_phases_enabled() {
72 return;
73 }
74 let stats = SYNC_BRIDGE_HOST_PHASES.get_or_init(|| std::sync::Mutex::new(BTreeMap::new()));
75 let Ok(mut stats) = stats.lock() else {
76 return;
77 };
78 let elapsed_us = elapsed.as_micros() as u64;
79 let key = format!("{method}:{stage}");
80 let entry = stats.entry(key).or_default();
81 entry.calls += 1;
82 entry.total_us = entry.total_us.wrapping_add(elapsed_us);
83 entry.max_us = entry.max_us.max(elapsed_us);
84
85 if let Ok(path) = std::env::var("AGENTOS_SYNC_BRIDGE_HOST_PHASES_FILE") {
86 let mut lines = String::new();
87 for (key, value) in stats.iter() {
88 let Some((method, stage)) = key.split_once(':') else {
89 continue;
90 };
91 let avg_us = value.total_us.checked_div(value.calls).unwrap_or(0);
92 lines.push_str(&format!(
93 "method={method} stage={stage} calls={} total_us={} avg_us={} max_us={}\n",
94 value.calls, value.total_us, avg_us, value.max_us
95 ));
96 }
97 let _ = std::fs::write(path, lines);
98 }
99}
100
101fn track_sync_bridge_call_method(call_id: u64, method: &str) {
102 if !sync_bridge_host_phases_enabled() {
103 return;
104 }
105 let methods = SYNC_BRIDGE_CALL_METHODS.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
106 let Ok(mut methods) = methods.lock() else {
107 return;
108 };
109 if methods.len() > 4096 {
110 methods.clear();
111 }
112 methods.insert(call_id, method.to_owned());
113}
114
115fn cleanup_sync_bridge_call_tracking(call_id: u64) {
116 if let Some(methods) = SYNC_BRIDGE_CALL_METHODS.get() {
117 if let Ok(mut methods) = methods.lock() {
118 methods.remove(&call_id);
119 }
120 }
121 if let Some(starts) = SYNC_BRIDGE_RESPONSE_CHANNEL_SENDS.get() {
122 if let Ok(mut starts) = starts.lock() {
123 starts.remove(&call_id);
124 }
125 }
126}
127
128pub(crate) fn record_sync_bridge_response_channel_send_start(call_id: u64) {
129 if !sync_bridge_host_phases_enabled() {
130 return;
131 }
132 let method = SYNC_BRIDGE_CALL_METHODS
133 .get()
134 .and_then(|methods| methods.lock().ok()?.get(&call_id).cloned())
135 .unwrap_or_else(|| String::from("sync_rpc_response"));
136 let starts =
137 SYNC_BRIDGE_RESPONSE_CHANNEL_SENDS.get_or_init(|| std::sync::Mutex::new(HashMap::new()));
138 let Ok(mut starts) = starts.lock() else {
139 return;
140 };
141 if starts.len() > 4096 {
142 starts.clear();
143 }
144 starts.insert(call_id, (method, Instant::now()));
145}
146
147pub(crate) fn record_sync_bridge_response_channel_received(call_id: u64) {
148 if !sync_bridge_host_phases_enabled() {
149 return;
150 }
151 let Some(starts) = SYNC_BRIDGE_RESPONSE_CHANNEL_SENDS.get() else {
152 return;
153 };
154 let Ok(mut starts) = starts.lock() else {
155 return;
156 };
157 let Some((method, started)) = starts.remove(&call_id) else {
158 return;
159 };
160 record_sync_bridge_host_phase(&method, "host_response_channel_recv", started.elapsed());
161}
162
163pub trait RuntimeEventSender: Send {
166 fn send_event(&self, event: RuntimeEvent) -> Result<(), String>;
167}
168
169pub struct ChannelRuntimeEventSender {
173 pub tx: crossbeam_channel::Sender<RuntimeEventEnvelope>,
174 output_generation: Option<u64>,
175 #[allow(dead_code)]
178 frame_buf: RefCell<Vec<u8>>,
179}
180
181impl ChannelRuntimeEventSender {
182 pub fn new(
183 tx: crossbeam_channel::Sender<RuntimeEventEnvelope>,
184 output_generation: Option<u64>,
185 ) -> Self {
186 ChannelRuntimeEventSender {
187 tx,
188 output_generation,
189 frame_buf: RefCell::new(Vec::with_capacity(256)),
190 }
191 }
192}
193
194impl RuntimeEventSender for ChannelRuntimeEventSender {
195 fn send_event(&self, event: RuntimeEvent) -> Result<(), String> {
196 self.tx
197 .send(RuntimeEventEnvelope {
198 output_generation: self.output_generation,
199 event,
200 })
201 .map_err(|e| format!("channel send failed: {}", e))
202 }
203}
204
205#[allow(dead_code)]
207pub struct WriterRuntimeEventSender {
208 writer: Mutex<Box<dyn Write + Send>>,
209}
210
211impl RuntimeEventSender for WriterRuntimeEventSender {
212 fn send_event(&self, event: RuntimeEvent) -> Result<(), String> {
213 let mut w = self.writer.lock().unwrap();
214 let frame: BinaryFrame = event.into();
215 ipc_binary::write_frame(&mut *w, &frame).map_err(|e| format!("write error: {}", e))
216 }
217}
218
219pub trait BridgeResponseReceiver: Send {
222 fn recv_response(&self, expected_call_id: u64) -> Result<BridgeResponse, String>;
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct SyncBridgeCallResponse {
227 pub status: u8,
228 pub payload: Vec<u8>,
229}
230
231#[allow(dead_code)]
234pub struct ReaderBridgeResponseReceiver {
235 reader: Mutex<Box<dyn Read + Send>>,
236}
237
238impl ReaderBridgeResponseReceiver {
239 #[allow(dead_code)]
240 pub fn new(reader: Box<dyn Read + Send>) -> Self {
241 ReaderBridgeResponseReceiver {
242 reader: Mutex::new(reader),
243 }
244 }
245}
246
247impl BridgeResponseReceiver for ReaderBridgeResponseReceiver {
248 fn recv_response(&self, expected_call_id: u64) -> Result<BridgeResponse, String> {
249 let mut reader = self.reader.lock().unwrap();
250 let frame = ipc_binary::read_frame(&mut *reader)
251 .map_err(|e| format!("failed to read BridgeResponse: {}", e))?;
252 match frame {
253 BinaryFrame::BridgeResponse {
254 call_id,
255 status,
256 payload,
257 ..
258 } => {
259 if call_id != expected_call_id {
260 return Err(format!(
261 "call_id mismatch: expected {}, got {}",
262 expected_call_id, call_id
263 ));
264 }
265 Ok(BridgeResponse {
266 call_id,
267 status,
268 payload,
269 })
270 }
271 _ => Err("expected BridgeResponse, got different message type".into()),
272 }
273 }
274}
275
276pub type CallIdRouter = Arc<Mutex<HashMap<u64, String>>>;
280
281pub type SharedCallIdCounter = Arc<AtomicU64>;
285
286pub struct BridgeCallContext {
292 sender: Box<dyn RuntimeEventSender>,
294 response_rx: Mutex<Box<dyn BridgeResponseReceiver>>,
296 pub session_id: String,
298 next_call_id: Arc<AtomicU64>,
301 pending_calls: Mutex<HashSet<u64>>,
303 track_pending_calls: bool,
307 call_id_router: Option<CallIdRouter>,
311}
312
313#[allow(dead_code)]
316struct StubRuntimeEventSender;
317
318impl RuntimeEventSender for StubRuntimeEventSender {
319 fn send_event(&self, _event: RuntimeEvent) -> Result<(), String> {
320 panic!(
321 "stub bridge function called during snapshot creation — bridge IIFE must not call bridge functions at setup time"
322 )
323 }
324}
325
326#[allow(dead_code)]
329struct StubBridgeResponseReceiver;
330
331impl BridgeResponseReceiver for StubBridgeResponseReceiver {
332 fn recv_response(&self, _expected_call_id: u64) -> Result<BridgeResponse, String> {
333 panic!(
334 "stub bridge function called during snapshot creation — bridge IIFE must not call bridge functions at setup time"
335 )
336 }
337}
338
339#[allow(dead_code)]
340impl BridgeCallContext {
341 pub fn stub() -> Self {
345 BridgeCallContext {
346 sender: Box::new(StubRuntimeEventSender),
347 response_rx: Mutex::new(Box::new(StubBridgeResponseReceiver)),
348 session_id: "stub".into(),
349 next_call_id: Arc::new(AtomicU64::new(1)),
350 pending_calls: Mutex::new(HashSet::new()),
351 track_pending_calls: should_track_pending_sync_calls(),
352 call_id_router: None,
353 }
354 }
355
356 pub fn new(
359 writer: Box<dyn Write + Send>,
360 reader: Box<dyn Read + Send>,
361 session_id: String,
362 ) -> Self {
363 BridgeCallContext {
364 sender: Box::new(WriterRuntimeEventSender {
365 writer: Mutex::new(writer),
366 }),
367 response_rx: Mutex::new(Box::new(ReaderBridgeResponseReceiver::new(reader))),
368 session_id,
369 next_call_id: Arc::new(AtomicU64::new(1)),
370 pending_calls: Mutex::new(HashSet::new()),
371 track_pending_calls: should_track_pending_sync_calls(),
372 call_id_router: None,
373 }
374 }
375
376 pub fn with_receiver(
380 sender: Box<dyn RuntimeEventSender>,
381 response_rx: Box<dyn BridgeResponseReceiver>,
382 session_id: String,
383 router: CallIdRouter,
384 shared_call_id: SharedCallIdCounter,
385 ) -> Self {
386 BridgeCallContext {
387 sender,
388 response_rx: Mutex::new(response_rx),
389 session_id,
390 next_call_id: shared_call_id,
391 pending_calls: Mutex::new(HashSet::new()),
392 track_pending_calls: should_track_pending_sync_calls(),
393 call_id_router: Some(router),
394 }
395 }
396
397 pub fn sync_call_response(
403 &self,
404 method: &str,
405 args: Vec<u8>,
406 ) -> Result<Option<SyncBridgeCallResponse>, String> {
407 let call_id = self.next_call_id.fetch_add(1, Ordering::Relaxed);
408 track_sync_bridge_call_method(call_id, method);
409
410 if self.track_pending_calls {
413 let mut pending = self.pending_calls.lock().unwrap();
414 if !pending.insert(call_id) {
415 return Err(format!("duplicate call_id: {}", call_id));
416 }
417 }
418
419 if let Some(ref router) = self.call_id_router {
421 let phase_start = Instant::now();
422 router
423 .lock()
424 .unwrap()
425 .insert(call_id, self.session_id.clone());
426 record_sync_bridge_host_phase(method, "host_register_route", phase_start.elapsed());
427 }
428
429 let bridge_call = RuntimeEvent::BridgeCall {
431 session_id: self.session_id.clone(),
432 call_id,
433 method: method.to_string(),
434 payload: args,
435 };
436
437 let __lat = syncrpc_lat_enabled().then(Instant::now);
438 let phase_start = Instant::now();
439 if let Err(e) = self.sender.send_event(bridge_call) {
440 self.remove_pending_call(call_id);
441 self.remove_call_route(call_id);
442 return Err(format!("failed to write BridgeCall: {}", e));
443 }
444 record_sync_bridge_host_phase(method, "host_send_event", phase_start.elapsed());
445
446 let response = {
448 let rx = self.response_rx.lock().unwrap();
449 let phase_start = Instant::now();
450 match rx.recv_response(call_id) {
451 Ok(frame) => {
452 record_sync_bridge_host_phase(
453 method,
454 "host_recv_response",
455 phase_start.elapsed(),
456 );
457 frame
458 }
459 Err(e) => {
460 self.remove_pending_call(call_id);
461 self.remove_call_route(call_id);
462 return Err(e);
463 }
464 }
465 };
466 if let Some(t) = __lat {
467 record_syncrpc_lat(t.elapsed().as_nanos() as u64);
468 }
469
470 let phase_start = Instant::now();
471 self.remove_pending_call(call_id);
472 self.remove_call_route(call_id);
473 record_sync_bridge_host_phase(method, "host_cleanup", phase_start.elapsed());
474
475 let phase_start = Instant::now();
477 if response.status == 1 {
478 let result = Err(String::from_utf8_lossy(&response.payload).to_string());
479 record_sync_bridge_host_phase(method, "host_extract_response", phase_start.elapsed());
480 result
481 } else if response.payload.is_empty() && response.status != 2 {
482 record_sync_bridge_host_phase(method, "host_extract_response", phase_start.elapsed());
483 Ok(None)
484 } else {
485 let result = Ok(Some(SyncBridgeCallResponse {
486 status: response.status,
487 payload: response.payload,
488 }));
489 record_sync_bridge_host_phase(method, "host_extract_response", phase_start.elapsed());
490 result
491 }
492 }
493
494 pub fn sync_call(&self, method: &str, args: Vec<u8>) -> Result<Option<Vec<u8>>, String> {
495 self.sync_call_response(method, args)
496 .map(|response| response.map(|response| response.payload))
497 }
498
499 pub fn async_send(&self, method: &str, args: Vec<u8>) -> Result<u64, String> {
503 let call_id = self.next_call_id.fetch_add(1, Ordering::Relaxed);
504
505 if let Some(ref router) = self.call_id_router {
507 router
508 .lock()
509 .unwrap()
510 .insert(call_id, self.session_id.clone());
511 }
512
513 let bridge_call = RuntimeEvent::BridgeCall {
514 session_id: self.session_id.clone(),
515 call_id,
516 method: method.to_string(),
517 payload: args,
518 };
519
520 if let Err(e) = self.sender.send_event(bridge_call) {
521 self.remove_call_route(call_id);
522 return Err(format!("failed to write BridgeCall: {}", e));
523 }
524
525 Ok(call_id)
526 }
527
528 fn remove_call_route(&self, call_id: u64) {
529 if let Some(ref router) = self.call_id_router {
530 router.lock().unwrap().remove(&call_id);
531 }
532 }
533
534 fn remove_pending_call(&self, call_id: u64) {
535 cleanup_sync_bridge_call_tracking(call_id);
536 if self.track_pending_calls {
537 self.pending_calls.lock().unwrap().remove(&call_id);
538 }
539 }
540
541 pub fn is_call_pending(&self, call_id: u64) -> bool {
543 if !self.track_pending_calls {
544 return false;
545 }
546 self.pending_calls.lock().unwrap().contains(&call_id)
547 }
548
549 pub fn pending_count(&self) -> usize {
551 if !self.track_pending_calls {
552 return 0;
553 }
554 self.pending_calls.lock().unwrap().len()
555 }
556}
557
558fn should_track_pending_sync_calls() -> bool {
559 std::env::var("AGENTOS_TRACK_PENDING_SYNC_CALLS").as_deref() == Ok("1")
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use std::io::Cursor;
566 use std::sync::Arc;
567
568 struct SharedWriter(Arc<Mutex<Vec<u8>>>);
570
571 impl Write for SharedWriter {
572 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
573 self.0.lock().unwrap().write(buf)
574 }
575 fn flush(&mut self) -> std::io::Result<()> {
576 self.0.lock().unwrap().flush()
577 }
578 }
579
580 fn make_response_bytes(
582 call_id: u64,
583 result: Option<Vec<u8>>,
584 error: Option<String>,
585 ) -> Vec<u8> {
586 let mut buf = Vec::new();
587 let (status, payload) = if let Some(err) = error {
588 (1u8, err.into_bytes())
589 } else if let Some(res) = result {
590 (0u8, res)
591 } else {
592 (0u8, vec![])
593 };
594 ipc_binary::write_frame(
595 &mut buf,
596 &BinaryFrame::BridgeResponse {
597 session_id: String::new(),
598 call_id,
599 status,
600 payload,
601 },
602 )
603 .unwrap();
604 buf
605 }
606
607 #[test]
608 fn sync_call_success_with_result() {
609 let response_bytes = make_response_bytes(1, Some(vec![0x93, 0x01, 0x02, 0x03]), None);
610 let writer_buf = Arc::new(Mutex::new(Vec::new()));
611
612 let ctx = BridgeCallContext::new(
613 Box::new(SharedWriter(Arc::clone(&writer_buf))),
614 Box::new(Cursor::new(response_bytes)),
615 "test-session-abc".into(),
616 );
617
618 let result = ctx.sync_call("_fsReadFile", vec![0x91, 0xa3, 0x66, 0x6f, 0x6f]);
619 assert!(result.is_ok());
620 assert_eq!(result.unwrap(), Some(vec![0x93, 0x01, 0x02, 0x03]));
621
622 let written = writer_buf.lock().unwrap();
624 let call = ipc_binary::read_frame(&mut Cursor::new(&*written)).unwrap();
625 match call {
626 BinaryFrame::BridgeCall {
627 call_id,
628 session_id,
629 method,
630 payload,
631 ..
632 } => {
633 assert_eq!(call_id, 1);
634 assert_eq!(session_id, "test-session-abc");
635 assert_eq!(method, "_fsReadFile");
636 assert_eq!(payload, vec![0x91, 0xa3, 0x66, 0x6f, 0x6f]);
637 }
638 _ => panic!("expected BridgeCall"),
639 }
640 }
641
642 #[test]
643 fn sync_call_success_null_result() {
644 let response_bytes = make_response_bytes(1, None, None);
645 let ctx = BridgeCallContext::new(
646 Box::new(Vec::new()),
647 Box::new(Cursor::new(response_bytes)),
648 "session-1".into(),
649 );
650
651 let result = ctx.sync_call("_log", vec![0xc0]).unwrap();
652 assert_eq!(result, None);
653 }
654
655 #[test]
656 fn sync_call_error_response() {
657 let response_bytes = make_response_bytes(1, None, Some("ENOENT: no such file".into()));
658 let ctx = BridgeCallContext::new(
659 Box::new(Vec::new()),
660 Box::new(Cursor::new(response_bytes)),
661 "session-1".into(),
662 );
663
664 let result = ctx.sync_call("_fsReadFile", vec![0xc0]);
665 assert!(result.is_err());
666 assert_eq!(result.unwrap_err(), "ENOENT: no such file");
667 }
668
669 #[test]
670 fn sync_call_call_id_increments() {
671 let mut response_bytes = make_response_bytes(1, Some(vec![0xa1, 0x61]), None);
673 response_bytes.extend_from_slice(&make_response_bytes(2, Some(vec![0xa1, 0x62]), None));
674
675 let ctx = BridgeCallContext::new(
676 Box::new(Vec::new()),
677 Box::new(Cursor::new(response_bytes)),
678 "session-1".into(),
679 );
680
681 let r1 = ctx.sync_call("_fn1", vec![]).unwrap();
682 let r2 = ctx.sync_call("_fn2", vec![]).unwrap();
683 assert_eq!(r1, Some(vec![0xa1, 0x61]));
684 assert_eq!(r2, Some(vec![0xa1, 0x62]));
685 }
686
687 #[test]
688 fn sync_call_pending_cleanup_on_read_error() {
689 let ctx = BridgeCallContext::new(
691 Box::new(Vec::new()),
692 Box::new(Cursor::new(Vec::new())),
693 "session-1".into(),
694 );
695
696 assert_eq!(ctx.pending_count(), 0);
697 let _ = ctx.sync_call("_fn", vec![]);
698 assert_eq!(ctx.pending_count(), 0);
699 }
700
701 #[test]
702 fn sync_call_id_mismatch_rejected() {
703 let response_bytes = make_response_bytes(99, Some(vec![0xc0]), None);
705 let ctx = BridgeCallContext::new(
706 Box::new(Vec::new()),
707 Box::new(Cursor::new(response_bytes)),
708 "session-1".into(),
709 );
710
711 let result = ctx.sync_call("_fn", vec![]);
712 assert!(result.is_err());
713 assert!(result.unwrap_err().contains("call_id mismatch"));
714 }
715
716 #[test]
717 fn sync_call_unexpected_message_type_rejected() {
718 let mut response_bytes = Vec::new();
720 ipc_binary::write_frame(
721 &mut response_bytes,
722 &BinaryFrame::TerminateExecution {
723 session_id: "session-1".into(),
724 },
725 )
726 .unwrap();
727
728 let ctx = BridgeCallContext::new(
729 Box::new(Vec::new()),
730 Box::new(Cursor::new(response_bytes)),
731 "session-1".into(),
732 );
733
734 let result = ctx.sync_call("_fn", vec![]);
735 assert!(result.is_err());
736 assert!(result.unwrap_err().contains("expected BridgeResponse"));
737 }
738
739 #[test]
740 fn async_send_writes_bridge_call() {
741 let writer_buf = Arc::new(Mutex::new(Vec::new()));
742 let ctx = BridgeCallContext::new(
743 Box::new(SharedWriter(Arc::clone(&writer_buf))),
744 Box::new(Cursor::new(Vec::new())),
745 "test-session-abc".into(),
746 );
747
748 let call_id = ctx
749 .async_send("_asyncFn", vec![0x91, 0xa3, 0x66, 0x6f, 0x6f])
750 .unwrap();
751 assert_eq!(call_id, 1);
752
753 let written = writer_buf.lock().unwrap();
755 let call = ipc_binary::read_frame(&mut Cursor::new(&*written)).unwrap();
756 match call {
757 BinaryFrame::BridgeCall {
758 call_id,
759 session_id,
760 method,
761 payload,
762 ..
763 } => {
764 assert_eq!(call_id, 1);
765 assert_eq!(session_id, "test-session-abc");
766 assert_eq!(method, "_asyncFn");
767 assert_eq!(payload, vec![0x91, 0xa3, 0x66, 0x6f, 0x6f]);
768 }
769 _ => panic!("expected BridgeCall"),
770 }
771 }
772
773 #[test]
774 fn async_send_increments_call_id() {
775 let ctx = BridgeCallContext::new(
776 Box::new(Vec::new()),
777 Box::new(Cursor::new(Vec::new())),
778 "session-1".into(),
779 );
780
781 let id1 = ctx.async_send("_fn1", vec![]).unwrap();
782 let id2 = ctx.async_send("_fn2", vec![]).unwrap();
783 assert_eq!(id1, 1);
784 assert_eq!(id2, 2);
785 }
786
787 #[test]
788 fn async_send_shares_counter_with_sync() {
789 let response_bytes = make_response_bytes(1, Some(vec![0xc0]), None);
791 let ctx = BridgeCallContext::new(
792 Box::new(Vec::new()),
793 Box::new(Cursor::new(response_bytes)),
794 "session-1".into(),
795 );
796
797 let _ = ctx.sync_call("_sync", vec![]);
798 let id = ctx.async_send("_async", vec![]).unwrap();
799 assert_eq!(id, 2);
800 }
801
802 #[test]
803 fn channel_runtime_event_sender_delivers_frames() {
804 let (tx, rx) = crossbeam_channel::unbounded();
805 let sender = super::ChannelRuntimeEventSender::new(tx, None);
806
807 let event = RuntimeEvent::BridgeCall {
808 session_id: "sess-1".into(),
809 call_id: 42,
810 method: "_fsReadFile".into(),
811 payload: vec![0x01, 0x02],
812 };
813 sender.send_event(event.clone()).expect("send_event");
814
815 let received = rx.recv().expect("recv");
817 assert_eq!(received.output_generation, None);
818 assert_eq!(received.event, event);
819 }
820
821 #[test]
822 fn channel_runtime_event_sender_no_mutex_contention() {
823 let (tx, rx) = crossbeam_channel::unbounded();
825 let handles: Vec<_> = (0..4)
826 .map(|i| {
827 let sender = super::ChannelRuntimeEventSender::new(tx.clone(), None);
828 std::thread::spawn(move || {
829 for j in 0..10 {
830 let event = RuntimeEvent::BridgeCall {
831 session_id: format!("sess-{}", i),
832 call_id: (i * 100 + j) as u64,
833 method: "_fn".into(),
834 payload: vec![],
835 };
836 sender.send_event(event).expect("send_event");
837 }
838 })
839 })
840 .collect();
841 drop(tx); for h in handles {
844 h.join().expect("thread join");
845 }
846
847 let mut count = 0;
849 while rx.try_recv().is_ok() {
850 count += 1;
851 }
852 assert_eq!(count, 40);
853 }
854
855 #[test]
856 fn channel_runtime_event_sender_with_bridge_context() {
857 let (tx, rx) = crossbeam_channel::unbounded();
859
860 let response_bytes = make_response_bytes(1, Some(vec![0xAB, 0xCD]), None);
862 let router: super::CallIdRouter = Arc::new(Mutex::new(HashMap::new()));
863
864 let ctx = BridgeCallContext::with_receiver(
865 Box::new(super::ChannelRuntimeEventSender::new(tx, None)),
866 Box::new(super::ReaderBridgeResponseReceiver::new(Box::new(
867 Cursor::new(response_bytes),
868 ))),
869 "test-session".into(),
870 router,
871 Arc::new(std::sync::atomic::AtomicU64::new(1)),
872 );
873
874 let result = ctx.sync_call("_fsReadFile", vec![0x01]).unwrap();
875 assert_eq!(result, Some(vec![0xAB, 0xCD]));
876
877 let event = rx.recv().expect("recv bridge call");
879 match event.event {
880 RuntimeEvent::BridgeCall { method, .. } => assert_eq!(method, "_fsReadFile"),
881 _ => panic!("expected BridgeCall"),
882 }
883 }
884
885 #[test]
886 fn sync_call_success_clears_call_id_route() {
887 let (tx, _rx) = crossbeam_channel::unbounded();
888 let response_bytes = make_response_bytes(1, Some(vec![0xAB, 0xCD]), None);
889 let router: super::CallIdRouter = Arc::new(Mutex::new(HashMap::new()));
890
891 let ctx = BridgeCallContext::with_receiver(
892 Box::new(super::ChannelRuntimeEventSender::new(tx, None)),
893 Box::new(super::ReaderBridgeResponseReceiver::new(Box::new(
894 Cursor::new(response_bytes),
895 ))),
896 "test-session".into(),
897 Arc::clone(&router),
898 Arc::new(std::sync::atomic::AtomicU64::new(1)),
899 );
900
901 let result = ctx.sync_call("_fsReadFile", vec![0x01]).unwrap();
902 assert_eq!(result, Some(vec![0xAB, 0xCD]));
903 assert!(
904 router.lock().unwrap().is_empty(),
905 "sync bridge response completion should clear the call_id route"
906 );
907 }
908
909 #[test]
910 fn writer_runtime_event_sender_serializes_events() {
911 let (tx, rx) = crossbeam_channel::unbounded();
912 let sender = super::ChannelRuntimeEventSender::new(tx, None);
913
914 for i in 0..5 {
916 let event = RuntimeEvent::BridgeCall {
917 session_id: "sess-1".into(),
918 call_id: i,
919 method: "_fn".into(),
920 payload: vec![0xAA; 100 * (i as usize + 1)],
921 };
922 sender.send_event(event).expect("send_event");
923 }
924
925 for i in 0..5u64 {
927 let decoded = rx.recv().expect("recv");
928 match decoded.event {
929 RuntimeEvent::BridgeCall {
930 call_id, payload, ..
931 } => {
932 assert_eq!(call_id, i);
933 assert_eq!(payload.len(), 100 * (i as usize + 1));
934 }
935 _ => panic!("expected BridgeCall"),
936 }
937 }
938
939 let small = RuntimeEvent::Log {
941 session_id: "s".into(),
942 channel: 0,
943 message: "x".into(),
944 };
945 sender.send_event(small.clone()).expect("send_event");
946 let decoded = rx.recv().expect("recv");
947 assert_eq!(decoded.event, small);
948 }
949
950 #[test]
951 fn stub_context_panics_on_sync_call() {
952 let ctx = BridgeCallContext::stub();
953 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
954 let _ = ctx.sync_call("_fsReadFile", vec![]);
955 }));
956 assert!(result.is_err(), "stub sync_call should panic");
957 }
958
959 #[test]
960 fn stub_context_panics_on_async_send() {
961 let ctx = BridgeCallContext::stub();
962 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
963 let _ = ctx.async_send("_asyncFn", vec![]);
964 }));
965 assert!(result.is_err(), "stub async_send should panic");
966 }
967}