Skip to main content

ai_agent/bridge/
bridge_debug.rs

1//! Bridge debug utilities for fault injection.
2//!
3//! Translated from openclaudecode/src/bridge/bridgeDebug.ts
4//!
5//! Ant-only fault injection for manually testing bridge recovery paths.
6
7use std::sync::{Arc, Mutex, RwLock};
8
9// =============================================================================
10// TYPES
11// =============================================================================
12
13/// One-shot fault to inject on the next matching API call.
14#[derive(Debug, Clone)]
15pub struct BridgeFault {
16    pub method: BridgeFaultMethod,
17    /// Fatal errors go through handleErrorStatus -> BridgeFatalError.
18    /// Transient errors surface as plain rejections (5xx / network).
19    pub kind: BridgeFaultKind,
20    pub status: u16,
21    pub error_type: Option<String>,
22    /// Remaining injections. Decremented on consume; removed at 0.
23    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
40/// Debug handle for bridge operations
41pub trait BridgeDebugHandle: Send + Sync {
42    /// Invoke the transport's permanent-close handler directly.
43    fn fire_close(&self, code: u16);
44    /// Call reconnectEnvironmentWithSession
45    fn force_reconnect(&self);
46    /// Queue a fault for the next N calls to the named api method.
47    fn inject_fault(&self, fault: BridgeFault);
48    /// Abort the at-capacity sleep so an injected poll fault lands immediately.
49    fn wake_poll_loop(&self);
50    /// env/session IDs for the debug.log grep.
51    fn describe(&self) -> String;
52}
53
54// =============================================================================
55// STATE
56// =============================================================================
57
58static 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
63// =============================================================================
64// FUNCTIONS
65// =============================================================================
66
67/// Register the debug handle.
68pub 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
73/// Clear the debug handle and fault queue.
74pub fn clear_bridge_debug_handle() {
75    // Clear fault queue
76    if let Some(queue) = FAULT_QUEUE.get() {
77        if let Ok(mut faults) = queue.lock() {
78            faults.clear();
79        }
80    }
81    // Clear handle
82    if let Some(handle) = DEBUG_HANDLE_MUT.get() {
83        if let Ok(mut guard) = handle.write() {
84            *guard = None;
85        }
86    }
87}
88
89/// Get the debug handle.
90pub fn get_bridge_debug_handle() -> Option<Arc<dyn BridgeDebugHandle>> {
91    DEBUG_HANDLE.get().cloned()
92}
93
94/// Queue a fault for injection.
95pub 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
115/// Consume a fault for the given method if one is queued.
116pub 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
132/// Throw a fault as an error.
133pub 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        // Transient: mimic a request failure
146        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        // Should be able to consume it
176        let consumed = consume_fault(&BridgeFaultMethod::PollForWork);
177        assert!(consumed.is_some());
178
179        // Should be gone now
180        let consumed2 = consume_fault(&BridgeFaultMethod::PollForWork);
181        assert!(consumed2.is_none());
182    }
183}