1use std::collections::{HashMap, VecDeque};
4use std::sync::{Arc, Mutex};
5use std::time::Duration;
6
7use serde_json::Value;
8
9use crate::clock::{Clock, TestClock};
10use crate::error::ErrorCode;
11use crate::openapi::{validate_params, validate_session_id, Operation};
12use crate::transport::{Lifecycle, Params, Transport, TransportError, WaitOutcome};
13
14#[derive(Debug, Clone)]
15pub struct RecordedCall {
16 pub op: String,
17 pub params: Params,
18 pub body: Option<Value>,
19 pub at_ms: i64,
20}
21
22struct Script {
23 subset: Vec<(String, String)>,
24 responses: Vec<Result<Value, TransportError>>,
25 next: usize,
26}
27
28struct State {
29 scripts: HashMap<String, Vec<Script>>,
30 waits: VecDeque<(Result<WaitOutcome, TransportError>, u64)>,
31 lifecycle: HashMap<Lifecycle, Result<String, TransportError>>,
32 lifecycle_seq: HashMap<Lifecycle, VecDeque<Result<String, TransportError>>>,
33 cli_version: Result<String, TransportError>,
34 openapi: Option<Value>,
35 calls: Vec<RecordedCall>,
36 wait_calls: Vec<(String, u64, i64)>,
37 lifecycle_calls: Vec<(Lifecycle, i64)>,
38 openapi_fetches: usize,
39}
40
41#[derive(Clone)]
43pub struct FakeTransport {
44 state: Arc<Mutex<State>>,
45 clock: Option<TestClock>,
46}
47
48fn err(code: ErrorCode, message: impl Into<String>) -> TransportError {
49 TransportError {
50 code,
51 message: message.into(),
52 hint: None,
53 details: None,
54 }
55}
56
57impl Default for FakeTransport {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl FakeTransport {
64 pub fn new() -> Self {
65 let openapi: Value =
66 serde_json::from_str(include_str!("../../tests/fixtures/openapi.json"))
67 .expect("fixture openapi.json is valid JSON");
68 Self {
69 state: Arc::new(Mutex::new(State {
70 scripts: HashMap::new(),
71 waits: VecDeque::new(),
72 lifecycle: HashMap::new(),
73 lifecycle_seq: HashMap::new(),
74 cli_version: Ok("opencode v2.0.32".to_string()),
75 openapi: Some(openapi),
76 calls: Vec::new(),
77 wait_calls: Vec::new(),
78 lifecycle_calls: Vec::new(),
79 openapi_fetches: 0,
80 })),
81 clock: None,
82 }
83 }
84
85 pub fn with_clock(mut self, clock: TestClock) -> Self {
86 self.clock = Some(clock);
87 self
88 }
89
90 fn now(&self) -> i64 {
91 self.clock.as_ref().map_or(0, Clock::now_ms)
92 }
93
94 pub fn on(&self, op_id: &str, responses: Vec<Result<Value, TransportError>>) {
95 self.on_matching(op_id, &[], responses);
96 }
97
98 pub fn on_matching(
100 &self,
101 op_id: &str,
102 params_subset: &[(&str, &str)],
103 responses: Vec<Result<Value, TransportError>>,
104 ) {
105 let subset = params_subset
106 .iter()
107 .map(|(k, v)| (k.to_string(), v.to_string()))
108 .collect();
109 self.state
110 .lock()
111 .unwrap()
112 .scripts
113 .entry(op_id.to_string())
114 .or_default()
115 .push(Script {
116 subset,
117 responses,
118 next: 0,
119 });
120 }
121
122 pub fn on_wait(&self, script: Vec<(Result<WaitOutcome, TransportError>, u64)>) {
123 self.state.lock().unwrap().waits = script.into();
124 }
125
126 pub fn on_lifecycle(&self, verb: Lifecycle, result: Result<String, TransportError>) {
127 self.state.lock().unwrap().lifecycle.insert(verb, result);
128 }
129
130 pub fn on_lifecycle_seq(&self, verb: Lifecycle, results: Vec<Result<String, TransportError>>) {
132 self.state
133 .lock()
134 .unwrap()
135 .lifecycle_seq
136 .insert(verb, results.into());
137 }
138
139 pub fn on_cli_version(&self, result: Result<String, TransportError>) {
140 self.state.lock().unwrap().cli_version = result;
141 }
142
143 pub fn set_openapi(&self, doc: Option<Value>) {
144 self.state.lock().unwrap().openapi = doc;
145 }
146
147 pub fn calls(&self) -> Vec<RecordedCall> {
148 self.state.lock().unwrap().calls.clone()
149 }
150
151 pub fn wait_calls(&self) -> Vec<(String, u64, i64)> {
152 self.state.lock().unwrap().wait_calls.clone()
153 }
154
155 pub fn lifecycle_calls(&self) -> Vec<(Lifecycle, i64)> {
156 self.state.lock().unwrap().lifecycle_calls.clone()
157 }
158
159 pub fn openapi_fetches(&self) -> usize {
160 self.state.lock().unwrap().openapi_fetches
161 }
162}
163
164impl Transport for FakeTransport {
165 async fn call(
166 &self,
167 op: &Operation,
168 params: &Params,
169 body: Option<&Value>,
170 _deadline: Duration,
171 ) -> Result<Value, TransportError> {
172 let from_tool = |e: crate::error::ToolError| TransportError {
174 code: e.code,
175 message: e.message,
176 hint: e.hint,
177 details: e.details,
178 };
179 validate_params(op, params).map_err(from_tool)?;
180 for (name, value) in params {
181 if name == "sessionID" {
182 validate_session_id(value).map_err(from_tool)?;
183 }
184 }
185 let at_ms = self.now();
186 let mut st = self.state.lock().unwrap();
187 st.calls.push(RecordedCall {
188 op: op.id.clone(),
189 params: params.clone(),
190 body: body.cloned(),
191 at_ms,
192 });
193 let best = st.scripts.get_mut(&op.id).and_then(|list| {
194 list.iter_mut()
195 .filter(|s| s.subset.iter().all(|kv| params.contains(kv)))
196 .max_by_key(|s| s.subset.len())
197 });
198 match best {
199 Some(s) if !s.responses.is_empty() => {
200 let i = s.next.min(s.responses.len() - 1);
201 s.next += 1;
202 s.responses[i].clone()
203 }
204 _ => Err(err(ErrorCode::CliError, format!("unscripted {}", op.id))),
205 }
206 }
207
208 async fn lifecycle(&self, verb: Lifecycle) -> Result<String, TransportError> {
209 let at_ms = self.now();
210 let mut st = self.state.lock().unwrap();
211 st.lifecycle_calls.push((verb, at_ms));
212 if let Some(r) = st
213 .lifecycle_seq
214 .get_mut(&verb)
215 .and_then(VecDeque::pop_front)
216 {
217 return r;
218 }
219 match st.lifecycle.get(&verb) {
220 Some(r) => r.clone(),
221 None => Ok(match verb {
222 Lifecycle::Status | Lifecycle::Start => "http://127.0.0.1:49374".to_string(),
223 Lifecycle::Stop | Lifecycle::Restart => String::new(),
224 }),
225 }
226 }
227
228 async fn wait_idle(
229 &self,
230 session_id: &str,
231 deadline: Duration,
232 ) -> Result<WaitOutcome, TransportError> {
233 let clock = self
234 .clock
235 .clone()
236 .expect("FakeTransport::wait_idle needs with_clock(TestClock)");
237 let at_ms = clock.now_ms();
238 let (result, virtual_ms) = {
239 let mut st = self.state.lock().unwrap();
240 st.wait_calls
241 .push((session_id.to_string(), deadline.as_millis() as u64, at_ms));
242 let item = if st.waits.len() > 1 {
243 st.waits.pop_front()
244 } else {
245 st.waits.front().cloned()
246 };
247 item.unwrap_or((Err(err(ErrorCode::CliError, "unscripted wait_idle")), 0))
248 };
249 let slept = virtual_ms.min(deadline.as_millis() as u64);
250 clock.sleep(Duration::from_millis(slept)).await;
251 result
252 }
253
254 async fn fetch_openapi(&self) -> Result<Value, TransportError> {
255 let mut st = self.state.lock().unwrap();
256 st.openapi_fetches += 1;
257 st.openapi
258 .clone()
259 .ok_or_else(|| err(ErrorCode::ServiceDown, "service not reachable"))
260 }
261
262 async fn cli_version(&self) -> Result<String, TransportError> {
263 self.state.lock().unwrap().cli_version.clone()
264 }
265}