ai_agent/bridge/
bridge_debug.rs1use std::sync::{Arc, Mutex, RwLock};
8
9#[derive(Debug, Clone)]
15pub struct BridgeFault {
16 pub method: BridgeFaultMethod,
17 pub kind: BridgeFaultKind,
20 pub status: u16,
21 pub error_type: Option<String>,
22 pub count: u32,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum BridgeFaultMethod {
28 PollForWork,
29 RegisterBridgeEnvironment,
30 ReconnectSession,
31 HeartbeatWork,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum BridgeFaultKind {
36 Fatal,
37 Transient,
38}
39
40pub trait BridgeDebugHandle: Send + Sync {
42 fn fire_close(&self, code: u16);
44 fn force_reconnect(&self);
46 fn inject_fault(&self, fault: BridgeFault);
48 fn wake_poll_loop(&self);
50 fn describe(&self) -> String;
52}
53
54static DEBUG_HANDLE: std::sync::OnceLock<Arc<dyn BridgeDebugHandle>> = std::sync::OnceLock::new();
59static FAULT_QUEUE: std::sync::OnceLock<Mutex<Vec<BridgeFault>>> = std::sync::OnceLock::new();
60static DEBUG_HANDLE_MUT: std::sync::OnceLock<RwLock<Option<Arc<dyn BridgeDebugHandle>>>> =
61 std::sync::OnceLock::new();
62
63pub fn register_bridge_debug_handle(h: Arc<dyn BridgeDebugHandle>) {
69 let _ = DEBUG_HANDLE.set(h.clone());
70 let _ = DEBUG_HANDLE_MUT.set(RwLock::new(Some(h)));
71}
72
73pub fn clear_bridge_debug_handle() {
75 if let Some(queue) = FAULT_QUEUE.get() {
77 if let Ok(mut faults) = queue.lock() {
78 faults.clear();
79 }
80 }
81 if let Some(handle) = DEBUG_HANDLE_MUT.get() {
83 if let Ok(mut guard) = handle.write() {
84 *guard = None;
85 }
86 }
87}
88
89pub fn get_bridge_debug_handle() -> Option<Arc<dyn BridgeDebugHandle>> {
91 DEBUG_HANDLE.get().cloned()
92}
93
94pub fn inject_bridge_fault(fault: BridgeFault) {
96 let queue = FAULT_QUEUE.get_or_init(|| Mutex::new(Vec::new()));
97
98 if let Ok(mut faults) = queue.lock() {
99 eprintln!(
100 "[bridge:debug] Queued fault: {:?} {}/{}{} ×{}",
101 fault.method,
102 fault.kind.as_str(),
103 fault.status,
104 fault
105 .error_type
106 .as_ref()
107 .map(|e| format!("/{}", e))
108 .unwrap_or_default(),
109 fault.count
110 );
111 faults.push(fault);
112 }
113}
114
115pub fn consume_fault(method: &BridgeFaultMethod) -> Option<BridgeFault> {
117 let queue = FAULT_QUEUE.get()?;
118
119 let mut faults = match queue.lock() {
120 Ok(f) => f,
121 Err(_) => return None,
122 };
123
124 let idx = faults.iter().position(|f| &f.method == method)?;
125
126 let mut fault = faults.remove(idx);
127 fault.count -= 1;
128
129 Some(fault)
130}
131
132pub fn throw_fault(fault: &BridgeFault, context: &str) -> Result<(), String> {
134 eprintln!(
135 "[bridge:debug] Injecting {} fault into {}: status={} errorType={}",
136 fault.kind.as_str(),
137 context,
138 fault.status,
139 fault.error_type.as_deref().unwrap_or("none")
140 );
141
142 if fault.kind == BridgeFaultKind::Fatal {
143 Err(format!("[injected] {} {}", context, fault.status))
144 } else {
145 Err(format!("[injected transient] {} {}", context, fault.status))
147 }
148}
149
150impl BridgeFaultKind {
151 pub fn as_str(&self) -> &'static str {
152 match self {
153 BridgeFaultKind::Fatal => "fatal",
154 BridgeFaultKind::Transient => "transient",
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_inject_fault() {
165 let fault = BridgeFault {
166 method: BridgeFaultMethod::PollForWork,
167 kind: BridgeFaultKind::Fatal,
168 status: 404,
169 error_type: Some("not_found".to_string()),
170 count: 1,
171 };
172
173 inject_bridge_fault(fault);
174
175 let consumed = consume_fault(&BridgeFaultMethod::PollForWork);
177 assert!(consumed.is_some());
178
179 let consumed2 = consume_fault(&BridgeFaultMethod::PollForWork);
181 assert!(consumed2.is_none());
182 }
183}