1use super::types::{Harness, HarnessName, HarnessRunConfig, HarnessRunResult};
2use miette::{Result, miette};
3use serde::Deserialize;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct StubStep {
12 pub stdout: String,
14 #[serde(default)]
15 pub stderr: String,
17 #[serde(default)]
18 pub exit_code: i32,
20}
21
22#[derive(Debug, Clone)]
23pub struct StubHarness {
27 steps: Vec<StubStep>,
28 idx: usize,
29}
30
31impl StubHarness {
32 pub fn new(steps: Vec<StubStep>) -> Self {
34 Self { steps, idx: 0 }
35 }
36
37 pub fn from_json_path(path: &Path) -> Result<Self> {
39 let raw = fs::read_to_string(path)
40 .map_err(|e| miette!("Failed to read stub script {p}: {e}", p = path.display()))?;
41 let steps: Vec<StubStep> = serde_json::from_str(&raw)
42 .map_err(|e| miette!("Invalid stub script JSON in {p}: {e}", p = path.display()))?;
43 Ok(Self::new(steps))
44 }
45
46 pub fn from_env_or_default(script_path: Option<PathBuf>) -> Result<Self> {
51 let from_env = std::env::var("ITO_STUB_SCRIPT").ok().map(PathBuf::from);
52 let path = script_path.or(from_env);
53 if let Some(p) = path {
54 return Self::from_json_path(&p);
55 }
56
57 Ok(Self::new(vec![StubStep {
59 stdout: "<promise>COMPLETE</promise>\n".to_string(),
60 stderr: String::new(),
61 exit_code: 0,
62 }]))
63 }
64
65 fn next_step(&mut self) -> Option<StubStep> {
66 if self.steps.is_empty() {
67 return None;
68 }
69 let step = self
70 .steps
71 .get(self.idx)
72 .cloned()
73 .or_else(|| self.steps.last().cloned());
74 self.idx = self.idx.saturating_add(1);
75 step
76 }
77}
78
79impl Harness for StubHarness {
80 fn name(&self) -> HarnessName {
81 HarnessName::Stub
82 }
83
84 fn run(&mut self, _config: &HarnessRunConfig) -> Result<HarnessRunResult> {
85 let started = Instant::now();
86 let step = self
87 .next_step()
88 .ok_or_else(|| miette!("Stub harness has no steps"))?;
89
90 Ok(HarnessRunResult {
91 stdout: step.stdout,
92 stderr: step.stderr,
93 exit_code: step.exit_code,
94 duration: started.elapsed().max(Duration::from_millis(1)),
95 timed_out: false,
96 })
97 }
98
99 fn stop(&mut self) {
100 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::collections::BTreeMap;
108
109 fn dummy_config() -> HarnessRunConfig {
110 HarnessRunConfig {
111 prompt: "test".to_string(),
112 model: None,
113 cwd: std::env::temp_dir(),
114 env: BTreeMap::new(),
115 interactive: false,
116 allow_all: false,
117 inactivity_timeout: None,
118 }
119 }
120
121 #[test]
122 fn name_returns_stub() {
123 let stub = StubHarness::new(vec![StubStep {
124 stdout: "test".to_string(),
125 stderr: String::new(),
126 exit_code: 0,
127 }]);
128 assert_eq!(stub.name(), HarnessName::Stub);
129 }
130
131 #[test]
132 fn streams_output_returns_false() {
133 let stub = StubHarness::new(vec![StubStep {
134 stdout: "test".to_string(),
135 stderr: String::new(),
136 exit_code: 0,
137 }]);
138 assert!(!stub.streams_output());
139 }
140
141 #[test]
142 fn run_sets_timed_out_false() {
143 let mut stub = StubHarness::new(vec![StubStep {
144 stdout: "test".to_string(),
145 stderr: String::new(),
146 exit_code: 0,
147 }]);
148 let config = dummy_config();
149 let result = stub.run(&config).unwrap();
150 assert!(!result.timed_out);
151 }
152
153 #[test]
154 fn run_sets_nonzero_duration() {
155 let mut stub = StubHarness::new(vec![StubStep {
156 stdout: "test".to_string(),
157 stderr: String::new(),
158 exit_code: 0,
159 }]);
160 let config = dummy_config();
161 let result = stub.run(&config).unwrap();
162 assert!(result.duration > Duration::ZERO);
163 }
164
165 #[test]
166 fn from_env_or_default_with_explicit_path() {
167 use std::io::Write;
168 let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
169 let json = r#"[{"stdout": "hello", "stderr": "", "exitCode": 0}]"#;
170 tmpfile.write_all(json.as_bytes()).unwrap();
171 tmpfile.flush().unwrap();
172
173 let mut stub =
174 StubHarness::from_env_or_default(Some(tmpfile.path().to_path_buf())).unwrap();
175 let config = dummy_config();
176 let result = stub.run(&config).unwrap();
177 assert_eq!(result.stdout, "hello");
178 }
179}