use std::path::PathBuf;
use std::time::{Duration, Instant};
use super::{
CancelToken, Check, CheckResult, ChunkOutcome, ChunkRequest, ChunkResult, CodeHarness,
HarnessCapabilities, HarnessError, Usage, HARNESS_CONTRACT_VERSION,
};
const STUB_POLL: Duration = Duration::from_millis(5);
#[derive(Debug, Clone)]
pub enum StubBehavior {
Commit {
commit: String,
changed_files: Vec<PathBuf>,
fail_first_check: bool,
},
NoChange,
Failed {
reason: String,
},
Timeout,
Cancelled,
SlowUntilCancel {
budget: Duration,
},
Error(HarnessError),
}
#[derive(Debug, Clone)]
pub struct StubHarness {
behavior: StubBehavior,
capabilities: HarnessCapabilities,
}
impl StubHarness {
pub fn new(behavior: StubBehavior) -> Self {
Self {
behavior,
capabilities: HarnessCapabilities {
can_author_tests: true,
reports_usage: true,
honors_file_scope: true,
runs_checks: true,
},
}
}
pub fn with_capabilities(mut self, capabilities: HarnessCapabilities) -> Self {
self.capabilities = capabilities;
self
}
fn synth_checks(&self, checks: &[Check], fail_first: bool) -> Vec<CheckResult> {
if !self.capabilities.runs_checks {
return Vec::new();
}
checks
.iter()
.enumerate()
.map(|(i, c)| {
let passed = !(fail_first && i == 0);
CheckResult {
check_id: c.id.clone(),
desc: c.desc.clone(),
run: c.run.clone(),
passed,
exit_code: Some(i32::from(!passed)),
stdout: String::new(),
stderr: String::new(),
}
})
.collect()
}
}
fn stopped_result(outcome: ChunkOutcome) -> ChunkResult {
ChunkResult {
schema_version: HARNESS_CONTRACT_VERSION,
outcome,
resulting_commit: None,
changed_files: Vec::new(),
check_results: Vec::new(),
transcript_ref: Some(PathBuf::from("stub-transcript.log")),
usage: None,
}
}
impl CodeHarness for StubHarness {
fn capabilities(&self) -> HarnessCapabilities {
self.capabilities
}
fn run_chunk(
&self,
req: &ChunkRequest,
cancel: &CancelToken,
) -> Result<ChunkResult, HarnessError> {
if cancel.is_cancelled() && !matches!(self.behavior, StubBehavior::Error(_)) {
return Ok(stopped_result(ChunkOutcome::Cancelled));
}
if let StubBehavior::SlowUntilCancel { budget } = &self.behavior {
if budget.is_zero() {
return Ok(stopped_result(ChunkOutcome::Timeout));
}
let deadline = Instant::now().checked_add(*budget);
loop {
if cancel.is_cancelled() {
return Ok(stopped_result(ChunkOutcome::Cancelled));
}
if deadline.is_some_and(|d| Instant::now() >= d) {
return Ok(stopped_result(ChunkOutcome::Timeout));
}
std::thread::sleep(STUB_POLL);
}
}
let transcript_ref = Some(PathBuf::from("stub-transcript.log"));
let usage = self.capabilities.reports_usage.then_some(Usage {
input_tokens: Some(100),
output_tokens: Some(50),
total_tokens: Some(150),
cost_usd: Some(0.0001),
});
let result = match &self.behavior {
StubBehavior::Commit {
commit,
changed_files,
fail_first_check,
} => ChunkResult {
schema_version: HARNESS_CONTRACT_VERSION,
outcome: ChunkOutcome::Committed {
commit: commit.clone(),
},
resulting_commit: Some(commit.clone()),
changed_files: changed_files.clone(),
check_results: self.synth_checks(&req.checks, *fail_first_check),
transcript_ref,
usage,
},
StubBehavior::NoChange => ChunkResult {
schema_version: HARNESS_CONTRACT_VERSION,
outcome: ChunkOutcome::NoChange,
resulting_commit: None,
changed_files: Vec::new(),
check_results: self.synth_checks(&req.checks, false),
transcript_ref,
usage,
},
StubBehavior::Failed { reason } => ChunkResult {
schema_version: HARNESS_CONTRACT_VERSION,
outcome: ChunkOutcome::Failed {
reason: reason.clone(),
},
resulting_commit: None,
changed_files: Vec::new(),
check_results: self.synth_checks(&req.checks, true),
transcript_ref,
usage,
},
StubBehavior::Timeout => stopped_result(ChunkOutcome::Timeout),
StubBehavior::Cancelled => stopped_result(ChunkOutcome::Cancelled),
StubBehavior::SlowUntilCancel { .. } => {
unreachable!("SlowUntilCancel is handled before the behavior match")
}
StubBehavior::Error(e) => return Err(e.clone()),
};
Ok(result)
}
}