use std::collections::HashSet;
use super::{
ChunkOutcome, ChunkRequest, ChunkResult, HarnessCapabilities, HARNESS_CONTRACT_VERSION,
};
fn is_oid(s: &str) -> bool {
matches!(s.len(), 40 | 64) && s.chars().all(|c| c.is_ascii_hexdigit())
}
pub fn assert_result_conforms(
req: &ChunkRequest,
res: &ChunkResult,
caps: HarnessCapabilities,
) -> Result<(), String> {
if res.schema_version != HARNESS_CONTRACT_VERSION {
return Err(format!(
"schema_version {} != linked contract version {HARNESS_CONTRACT_VERSION}",
res.schema_version
));
}
match &res.outcome {
ChunkOutcome::Committed { commit } => {
if !is_oid(commit) {
return Err(format!(
"Committed outcome carries a non-oid commit: {commit:?}"
));
}
match &res.resulting_commit {
Some(rc) if rc == commit => {}
Some(rc) => {
return Err(format!(
"resulting_commit ({rc}) disagrees with outcome commit ({commit})"
));
}
None => return Err("Committed outcome but resulting_commit is None".into()),
}
}
ChunkOutcome::Failed { reason } => {
if reason.trim().is_empty() {
return Err("Failed outcome carries an empty reason".into());
}
}
ChunkOutcome::NoChange | ChunkOutcome::Timeout | ChunkOutcome::Cancelled => {}
}
if !matches!(res.outcome, ChunkOutcome::Committed { .. }) {
if res.resulting_commit.is_some() {
return Err(format!(
"non-committed outcome {:?} must not carry resulting_commit",
res.outcome
));
}
if !res.changed_files.is_empty() {
return Err(format!(
"non-committed outcome {:?} must not report changed_files",
res.outcome
));
}
}
let requested: HashSet<&str> = req.checks.iter().map(|c| c.id.as_str()).collect();
let mut seen: HashSet<&str> = HashSet::new();
for cr in &res.check_results {
if !requested.contains(cr.check_id.as_str()) {
return Err(format!(
"check_results contains an un-requested check_id: {:?}",
cr.check_id
));
}
if !seen.insert(cr.check_id.as_str()) {
return Err(format!(
"duplicate check_id in check_results: {:?}",
cr.check_id
));
}
}
if caps.runs_checks {
if !matches!(res.outcome, ChunkOutcome::Timeout | ChunkOutcome::Cancelled)
&& seen != requested
{
return Err(format!(
"runs_checks adapter reported {} check results for {} requested checks",
seen.len(),
requested.len()
));
}
} else if !res.check_results.is_empty() {
return Err("adapter reports runs_checks=false but returned check_results".into());
}
if let Some(t) = &res.transcript_ref {
if t.as_os_str().is_empty() {
return Err("transcript_ref is present but empty".into());
}
}
Ok(())
}
#[cfg(test)]
pub fn run_and_check(
harness: &dyn super::CodeHarness,
req: &ChunkRequest,
) -> Result<ChunkResult, super::HarnessError> {
run_and_check_with_cancel(harness, req, &super::CancelToken::new())
}
#[cfg(test)]
pub fn run_and_check_with_cancel(
harness: &dyn super::CodeHarness,
req: &ChunkRequest,
cancel: &super::CancelToken,
) -> Result<ChunkResult, super::HarnessError> {
let out = harness.run_chunk(req, cancel);
if let Ok(res) = &out {
assert_result_conforms(req, res, harness.capabilities())
.unwrap_or_else(|e| panic!("adapter produced a non-conforming ChunkResult: {e}"));
}
out
}
#[cfg(test)]
mod tests {
use super::super::stub::{StubBehavior, StubHarness};
use super::super::{Check, ChunkOutcome, ChunkRequest, HarnessCapabilities, HarnessError};
use super::*;
use std::path::PathBuf;
use std::time::Duration;
fn req_with_checks(checks: Vec<Check>) -> ChunkRequest {
ChunkRequest {
run_id: "r".into(),
chunk_id: "c".into(),
attempt_id: "a".into(),
worktree_path: PathBuf::from("/tmp/does-not-matter"),
base_commit: "0".repeat(40),
plan_rev: "v1".into(),
brief: "b".into(),
checks,
files: vec![],
timeout: None,
}
}
fn one_check() -> Vec<Check> {
vec![Check {
id: "chk1".into(),
desc: "d".into(),
run: "true".into(),
timeout: None,
}]
}
fn full_caps() -> HarnessCapabilities {
HarnessCapabilities {
can_author_tests: true,
reports_usage: true,
honors_file_scope: true,
runs_checks: true,
}
}
#[test]
fn scenario_clean_success() {
let stub = StubHarness::new(StubBehavior::Commit {
commit: "a".repeat(40),
changed_files: vec![PathBuf::from("src/lib.rs")],
fail_first_check: false,
});
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert!(matches!(res.outcome, ChunkOutcome::Committed { .. }));
assert_eq!(
res.resulting_commit.as_deref(),
Some("a".repeat(40).as_str())
);
assert_eq!(res.changed_files, vec![PathBuf::from("src/lib.rs")]);
assert!(res.check_results.iter().all(|c| c.passed));
}
#[test]
fn scenario_no_change() {
let stub = StubHarness::new(StubBehavior::NoChange);
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert_eq!(res.outcome, ChunkOutcome::NoChange);
assert!(res.resulting_commit.is_none());
assert!(res.changed_files.is_empty());
}
#[test]
fn scenario_self_check_failure() {
let stub = StubHarness::new(StubBehavior::Commit {
commit: "b".repeat(40),
changed_files: vec![PathBuf::from("x")],
fail_first_check: true,
});
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert!(matches!(res.outcome, ChunkOutcome::Committed { .. }));
assert!(!res.check_results[0].passed);
}
#[test]
fn scenario_failed_run() {
let stub = StubHarness::new(StubBehavior::Failed {
reason: "provider 500".into(),
});
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert!(matches!(res.outcome, ChunkOutcome::Failed { .. }));
}
#[test]
fn scenario_malformed_output_is_error() {
let stub = StubHarness::new(StubBehavior::Error(HarnessError::MalformedOutput {
message: "not json".into(),
}));
let req = req_with_checks(one_check());
let err = run_and_check(&stub, &req).unwrap_err();
assert!(matches!(err, HarnessError::MalformedOutput { .. }));
}
#[test]
fn scenario_dirty_worktree_is_error() {
let stub = StubHarness::new(StubBehavior::Error(HarnessError::DirtyWorktree {
details: " M foo.rs".into(),
}));
let req = req_with_checks(one_check());
let err = run_and_check(&stub, &req).unwrap_err();
assert!(matches!(err, HarnessError::DirtyWorktree { .. }));
}
#[test]
fn scenario_timeout_and_cancelled() {
for behavior in [StubBehavior::Timeout, StubBehavior::Cancelled] {
let stub = StubHarness::new(behavior.clone());
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert!(matches!(
res.outcome,
ChunkOutcome::Timeout | ChunkOutcome::Cancelled
));
}
}
#[test]
fn scenario_transcript_and_usage_capture() {
let stub = StubHarness::new(StubBehavior::Commit {
commit: "c".repeat(40),
changed_files: vec![PathBuf::from("x")],
fail_first_check: false,
});
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert!(res.transcript_ref.is_some());
let usage = res.usage.expect("stub reports usage by default");
assert_eq!(usage.total_tokens, Some(150));
}
#[test]
fn capabilities_can_suppress_usage_and_checks() {
let stub = StubHarness::new(StubBehavior::Commit {
commit: "d".repeat(40),
changed_files: vec![],
fail_first_check: false,
})
.with_capabilities(HarnessCapabilities {
can_author_tests: false,
reports_usage: false,
honors_file_scope: false,
runs_checks: false,
});
let req = req_with_checks(one_check());
let res = run_and_check(&stub, &req).unwrap();
assert!(res.usage.is_none());
assert!(res.check_results.is_empty());
}
#[test]
fn conformance_rejects_commit_without_oid() {
let req = req_with_checks(vec![]);
let mut res = ChunkResult::committed("e".repeat(40), vec![]);
res.resulting_commit = None; assert!(assert_result_conforms(&req, &res, full_caps()).is_err());
}
#[test]
fn conformance_rejects_non_oid_commit() {
let req = req_with_checks(vec![]);
let res = ChunkResult::committed("not-a-real-oid", vec![]);
assert!(assert_result_conforms(&req, &res, full_caps()).is_err());
}
#[test]
fn conformance_rejects_wrong_schema_version() {
let req = req_with_checks(vec![]);
let mut res = ChunkResult::no_change();
res.schema_version = HARNESS_CONTRACT_VERSION + 99;
assert!(assert_result_conforms(&req, &res, full_caps()).is_err());
}
#[test]
fn conformance_rejects_unrequested_check() {
let req = req_with_checks(one_check());
let mut res = ChunkResult::no_change();
res.check_results.push(super::super::CheckResult {
check_id: "not-requested".into(),
desc: "sneaky".into(),
run: "rm -rf /".into(),
passed: true,
exit_code: Some(0),
stdout: String::new(),
stderr: String::new(),
});
assert!(assert_result_conforms(&req, &res, full_caps()).is_err());
}
#[test]
fn conformance_rejects_incomplete_checks_when_runs_checks() {
let req = req_with_checks(vec![
Check {
id: "a".into(),
desc: "d".into(),
run: "true".into(),
timeout: None,
},
Check {
id: "b".into(),
desc: "d".into(),
run: "true".into(),
timeout: None,
},
]);
let res = ChunkResult::no_change(); assert!(assert_result_conforms(&req, &res, full_caps()).is_err());
}
#[test]
fn conformance_accepts_stub_default() {
let stub = StubHarness::new(StubBehavior::NoChange);
let req = req_with_checks(one_check());
run_and_check(&stub, &req).unwrap();
}
#[test]
fn cancel_pretripped_yields_cancelled_for_any_behavior() {
use super::super::CancelToken;
let stub = StubHarness::new(StubBehavior::Commit {
commit: "a".repeat(40),
changed_files: vec![PathBuf::from("x")],
fail_first_check: false,
});
let req = req_with_checks(one_check());
let cancel = CancelToken::new();
cancel.cancel();
let res = run_and_check_with_cancel(&stub, &req, &cancel).unwrap();
assert_eq!(res.outcome, ChunkOutcome::Cancelled);
assert!(res.resulting_commit.is_none());
assert!(res.check_results.is_empty());
}
#[test]
fn slow_run_cancelled_in_flight() {
use super::super::CancelToken;
use std::time::Duration;
let stub = StubHarness::new(StubBehavior::SlowUntilCancel {
budget: Duration::from_secs(30),
});
let req = req_with_checks(one_check());
let cancel = CancelToken::new();
let trip = cancel.clone();
let handle = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
trip.cancel();
});
let res = run_and_check_with_cancel(&stub, &req, &cancel).unwrap();
handle.join().unwrap();
assert_eq!(res.outcome, ChunkOutcome::Cancelled);
}
#[test]
fn slow_run_times_out_when_not_cancelled() {
use super::super::CancelToken;
use std::time::Duration;
let stub = StubHarness::new(StubBehavior::SlowUntilCancel {
budget: Duration::from_millis(0),
});
let req = req_with_checks(one_check());
let res = run_and_check_with_cancel(&stub, &req, &CancelToken::new()).unwrap();
assert_eq!(res.outcome, ChunkOutcome::Timeout);
}
fn live_enabled() -> bool {
std::env::var("OCTL_HARNESS_LIVE").as_deref() == Ok("1")
}
fn on_path(bin: &str) -> bool {
std::env::var("PATH")
.is_ok_and(|path| std::env::split_paths(&path).any(|d| d.join(bin).is_file()))
}
fn live_repo() -> (tempfile::TempDir, String) {
use std::process::Command;
let dir = tempfile::TempDir::new().unwrap();
let git = |args: &[&str]| {
assert!(Command::new("git")
.arg("-C")
.arg(dir.path())
.args(args)
.output()
.unwrap()
.status
.success());
};
git(&["init", "-q", "-b", "main"]);
git(&["config", "user.email", "live@t"]);
git(&["config", "user.name", "live"]);
std::fs::write(dir.path().join("seed.txt"), "seed\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-q", "-m", "seed"]);
let head = String::from_utf8(
Command::new("git")
.arg("-C")
.arg(dir.path())
.args(["rev-parse", "HEAD"])
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_string();
(dir, head)
}
fn live_request(repo: &std::path::Path, head: &str) -> ChunkRequest {
ChunkRequest {
run_id: "live".into(),
chunk_id: "c1".into(),
attempt_id: "a1".into(),
worktree_path: repo.to_path_buf(),
base_commit: head.into(),
plan_rev: "v1".into(),
brief: "Create a file named GREETING.txt whose only content is the word \
`hello`. Then commit it."
.into(),
checks: vec![Check {
id: "exists".into(),
desc: "GREETING.txt exists".into(),
run: "test -f GREETING.txt".into(),
timeout: Some(Duration::from_secs(10)),
}],
files: vec![PathBuf::from("GREETING.txt")],
timeout: Some(Duration::from_secs(600)),
}
}
#[test]
fn live_claude_deepseek_conforms() {
if !live_enabled() || !on_path("claude-deepseek") {
return;
}
use super::super::claude::ClaudeHarness;
let (repo, head) = live_repo();
let h = ClaudeHarness::deepseek("flash");
let res = run_and_check(&h, &live_request(repo.path(), &head)).unwrap();
assert!(matches!(
res.outcome,
ChunkOutcome::Committed { .. } | ChunkOutcome::NoChange
));
}
#[test]
fn live_pi_conforms() {
if !live_enabled() || !on_path("pi") || std::env::var("DEEPSEEK_API_KEY").is_err() {
return;
}
use super::super::pi::{PiConfig, PiHarness};
let (repo, head) = live_repo();
let h = PiHarness::new(PiConfig::deepseek("deepseek-v4-flash"));
let res = run_and_check(&h, &live_request(repo.path(), &head)).unwrap();
assert!(matches!(
res.outcome,
ChunkOutcome::Committed { .. } | ChunkOutcome::NoChange
));
}
}