use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::future::Future;
use std::time::Duration;
const CONTRACT_GEN_TIMEOUT: Duration = Duration::from_secs(120);
use super::session::{CoderEventKind, EventSink};
use super::shell_tool::WorktreeExecutor;
fn default_true() -> bool {
true
}
fn default_check_timeout() -> u64 {
120
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OutcomeContract {
pub description: String,
pub checks: Vec<ContractCheck>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContractCheck {
pub name: String,
pub command: String,
#[serde(default = "default_true")]
pub expect_exit_zero: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_contains: Option<String>,
#[serde(default = "default_check_timeout")]
pub timeout_secs: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CheckResult {
pub name: String,
pub passed: bool,
pub exit_code: Option<i64>,
pub output_tail: String,
pub duration_ms: u64,
}
impl OutcomeContract {
pub fn validate(&self) -> Vec<String> {
let mut issues = Vec::new();
if self.checks.is_empty() {
issues.push("contract has no checks — at least one is required".to_string());
}
let mut seen = std::collections::HashSet::new();
for (i, c) in self.checks.iter().enumerate() {
let name = c.name.trim();
if name.is_empty() {
issues.push(format!("check #{i} has an empty name"));
}
if name == "unique_snake_case_label" {
issues.push(format!(
"check #{i} kept the literal placeholder name \
'unique_snake_case_label' — give it a real descriptive label"
));
}
if c.command.trim().is_empty() {
issues.push(format!("check '{}' has an empty command", c.name));
} else if is_toolchain_only(c.command.trim()) {
issues.push(format!(
"check '{}' runs a toolchain-only no-op (`{}`) that verifies the \
tool is installed, not the task — replace it with a command that \
exercises the actual change",
c.name,
c.command.trim()
));
}
if !seen.insert(name.to_string()) {
issues.push(format!("duplicate check name '{}'", c.name));
}
if !c.expect_exit_zero && c.output_contains.is_none() {
issues.push(format!(
"check '{}' asserts nothing (expect_exit_zero=false and no output_contains)",
c.name
));
}
}
issues
}
pub fn repair_cosmetic_names(&mut self) {
let mut seen = std::collections::HashSet::new();
for i in 0..self.checks.len() {
let name = self.checks[i].name.trim().to_string();
let base = if name.is_empty() || name == "unique_snake_case_label" {
format!("check_{}", i + 1)
} else {
name
};
let mut candidate = base.clone();
let mut k = 2;
while !seen.insert(candidate.clone()) {
candidate = format!("{base}_{k}");
k += 1;
}
self.checks[i].name = candidate;
}
}
pub fn strip_absolute_cd_prefixes(&mut self) {
for check in &mut self.checks {
check.command = strip_leading_absolute_cd(&check.command);
}
}
pub fn strip_exit_masking_pipes(&mut self) {
for check in &mut self.checks {
if check.expect_exit_zero {
check.command = strip_trailing_output_filter(&check.command);
}
}
}
pub fn render(&self) -> String {
let mut out = format!("{}\nChecks:\n", self.description.trim());
for c in &self.checks {
out.push_str(&format!("- {}: `{}`", c.name, c.command));
let mut expects = Vec::new();
if c.expect_exit_zero {
expects.push("exit 0".to_string());
}
if let Some(s) = &c.output_contains {
expects.push(format!("output contains {s:?}"));
}
if !expects.is_empty() {
out.push_str(&format!(" (expects {})", expects.join(", ")));
}
out.push('\n');
}
out
}
}
const OUTPUT_FILTERS: [&str; 3] = ["tail", "head", "cat"];
fn strip_trailing_output_filter(command: &str) -> String {
let mut rest = command.trim().to_string();
loop {
let Some(idx) = last_top_level_pipe(&rest) else {
return rest;
};
let tail_seg = rest[idx + 1..].trim();
let head_word = tail_seg.split_whitespace().next().unwrap_or("");
if !OUTPUT_FILTERS.contains(&head_word) {
return rest;
}
if tail_seg.contains("&&") || tail_seg.contains(';') || tail_seg.contains("||") {
return rest;
}
rest = rest[..idx].trim_end().to_string();
if rest.is_empty() {
return command.trim().to_string(); }
}
}
fn last_top_level_pipe(s: &str) -> Option<usize> {
let b = s.as_bytes();
let (mut sq, mut dq) = (false, false);
let mut found = None;
let mut i = 0;
while i < b.len() {
match b[i] {
b'\\' => i += 1, b'\'' if !dq => sq = !sq,
b'"' if !sq => dq = !dq,
b'|' if !sq && !dq => {
if b.get(i + 1) == Some(&b'|') {
i += 1; } else if i > 0 && b[i - 1] == b'|' {
} else {
found = Some(i);
}
}
_ => {}
}
i += 1;
}
found
}
fn strip_leading_absolute_cd(command: &str) -> String {
let mut rest = command.trim();
while let Some(after_cd) = rest.strip_prefix("cd ") {
let sep = after_cd
.find("&&")
.map(|i| (i, 2))
.into_iter()
.chain(after_cd.find(';').map(|i| (i, 1)))
.min_by_key(|(i, _)| *i);
let Some((idx, sep_len)) = sep else {
break;
};
let path = after_cd[..idx].trim();
if !path.starts_with('/') || path.split_whitespace().count() != 1 {
break;
}
rest = after_cd[idx + sep_len..].trim_start();
}
rest.to_string()
}
fn is_toolchain_only(command: &str) -> bool {
if command.contains("&&")
|| command.contains("||")
|| command.contains('|')
|| command.contains(';')
|| command.contains('\n')
{
return false;
}
let tokens: Vec<&str> = command.split_whitespace().collect();
let [tool, flag] = tokens.as_slice() else {
return false;
};
const TOOLS: &[&str] = &[
"cargo", "rustc", "rustup", "node", "npm", "npx", "yarn", "pnpm", "python", "python3",
"pip", "pip3", "go", "java", "javac", "ruby", "gem", "dotnet", "deno", "bun", "tsc", "gcc",
"clang", "make", "cmake",
];
const FLAGS: &[&str] = &["--version", "-V", "-v", "--help", "-h", "version"];
TOOLS.contains(tool) && FLAGS.contains(flag)
}
fn build_contract_prompt(intent: &str, repo_summary: &str, issues: &[String]) -> String {
let mut p = format!(
"You are deriving an OUTCOME CONTRACT for a coding task: a small set of shell \
commands that objectively verify the task is done. The commands run at the root of a \
fresh git checkout of the repository, non-interactively, with no TTY.\n\n\
Task intent:\n{intent}\n\n\
Repository summary:\n{repo_summary}\n\n\
Respond with ONLY a JSON object, no prose, no markdown fences, in this shape:\n\
{{\n \"description\": \"one-sentence definition of done\",\n \"checks\": [\n \
{{\"name\": \"unique_snake_case_label\", \"command\": \"shell command\", \
\"expect_exit_zero\": true, \"output_contains\": null, \"timeout_secs\": 120}}\n ]\n}}\n\n\
Rules:\n\
- Commands run at the repository root ALREADY (the runtime sets the working \
directory). Do NOT prefix a command with `cd` into an absolute path, and do NOT \
assume a specific mount like `/repo`, `/workspace`, or `/app` — those paths do not \
exist here and every such command fails before it runs. Write commands relative to \
the repo root (e.g. `python -m pytest tests/test_x.py`, not `cd /repo && python …`).\n\
Do NOT pipe a check into `tail`/`head`/`cat` to shorten output: a pipeline exits with \
the LAST command's status, so `pytest … | tail -20` always exits 0 and the check can \
never fail. The runtime captures full output itself.\n\
- 1 to 5 checks. Each must verify THE TASK ITSELF, not just that the toolchain works \
(e.g. `rustc --version` or `cargo --version` prove nothing about the change).\n\
- At least one check should exercise the actual new behaviour the intent describes \
(run the program/test that the change affects).\n\
- For a \"make the failing tests pass\" task, verify by running the failing test's \
own FILE (e.g. `python -m pytest tests/test_x.py`), NOT a bespoke reproduction \
snippet and NOT a narrow `-k` filter — a hand-written snippet or a guessed filter \
routinely passes while the real failing test is untouched, so the session reports \
done on an incomplete fix. If specific failing tests are listed below, name them \
explicitly. Do NOT run the whole suite (`pytest tests/`): it may contain unrelated \
pre-existing failures that your change is not responsible for.\n\
- `name` must be a real, descriptive snake_case label unique within the contract — \
never the literal placeholder `unique_snake_case_label`.\n\
- Every command must run non-interactively and deterministically (no prompts, no \
watchers, no servers that don't exit). Use the repo's own build/test commands when \
the summary reveals them — a build that must compile the change is a strong check.\n\
- `expect_exit_zero: true` (the default) is usually enough. Only set `output_contains` \
to a substring you are CERTAIN will appear verbatim in stdout/stderr; if unsure, \
leave it null. Do NOT invent example output or placeholder values.\n\
- Never use git push, network access, sudo, or anything destructive outside the \
checkout. Timeouts are in seconds; keep them realistic for a build.\n"
);
if !issues.is_empty() {
p.push_str("\nYour previous attempt FAILED validation with these issues — fix them:\n");
for i in issues {
p.push_str(&format!("- {i}\n"));
}
}
p
}
pub(crate) fn extract_json_object(text: &str) -> Result<Value, String> {
let start = text.find('{').ok_or("no JSON object found in output")?;
let end = text.rfind('}').ok_or("no closing brace found in output")?;
if end < start {
return Err("malformed JSON object in output".to_string());
}
serde_json::from_str(&text[start..=end]).map_err(|e| format!("invalid JSON: {e}"))
}
pub fn intent_targets_tests(intent: &str) -> bool {
let i = intent.to_ascii_lowercase();
let mentions_tests = i.contains("test");
let mentions_failure = [
"fail",
"failing",
"broken",
"passing",
"pass the",
"make the tests",
]
.iter()
.any(|k| i.contains(k));
mentions_tests && mentions_failure
}
pub fn parse_test_failures(output: &str) -> Vec<String> {
let mut seen = std::collections::HashSet::new();
let mut ids = Vec::new();
for line in output.lines() {
let Some(rest) = line.trim().strip_prefix("FAILED ") else {
continue;
};
let id = rest.split_whitespace().next().unwrap_or("").trim();
if id.is_empty() || !id.contains(".py") {
continue;
}
if seen.insert(id.to_string()) {
ids.push(id.to_string());
}
}
ids
}
pub fn summary_with_failures(repo_summary: &str, failing: &[String]) -> String {
if failing.is_empty() {
return repo_summary.to_string();
}
let list = failing
.iter()
.map(|f| format!(" - {f}"))
.collect::<Vec<_>>()
.join("\n");
format!(
"{repo_summary}\n\nObserved failing tests (the suite was run before you; these node \
ids currently FAIL). Your contract MUST verify that the ones your change addresses \
now pass — run them by their exact node id or their file:\n{list}"
)
}
pub async fn derive_contract<F, Fut>(
generate: F,
intent: &str,
repo_summary: &str,
max_attempts: u32,
constraints: &[String],
) -> Result<OutcomeContract, String>
where
F: Fn(String) -> Fut + Send + Sync,
Fut: Future<Output = Result<String, String>> + Send,
{
let max = max_attempts.max(1);
let mut issues: Vec<String> = Vec::new();
let mut last_err = String::new();
let mut best_incomplete: Option<(OutcomeContract, Vec<UngatedConstraint>)> = None;
for _ in 0..max {
let prompt = build_contract_prompt(intent, repo_summary, &issues);
let text = match tokio::time::timeout(CONTRACT_GEN_TIMEOUT, generate(prompt)).await {
Ok(Ok(t)) => t,
Ok(Err(e)) => {
last_err = format!("generation failed: {e}");
continue;
}
Err(_) => {
last_err = format!(
"contract generation timed out after {}s. The selected model may still \
be downloading — a first-use fetch can far exceed this budget. Check \
`car models list` for what is actually on disk, pre-pull with \
`car models pull <id>`, or sign in for a cloud model that needs no \
download.",
CONTRACT_GEN_TIMEOUT.as_secs()
);
continue;
}
};
let value = match extract_json_object(&text) {
Ok(v) => v,
Err(e) => {
issues = vec![format!(
"output did not parse: {e}. Return ONLY the JSON object."
)];
last_err = issues.join("; ");
continue;
}
};
let mut contract: OutcomeContract = match serde_json::from_value(value) {
Ok(c) => c,
Err(e) => {
issues = vec![format!("JSON did not match the contract schema: {e}")];
last_err = issues.join("; ");
continue;
}
};
contract.repair_cosmetic_names();
contract.strip_absolute_cd_prefixes();
contract.strip_exit_masking_pipes();
let problems = contract.validate();
if !problems.is_empty() {
last_err = problems.join("; ");
issues = problems;
continue;
}
let ungated = ungated_constraints(&generate, &contract, constraints).await;
if ungated.is_empty() {
return Ok(contract);
}
if best_incomplete
.as_ref()
.is_none_or(|(_, prior)| ungated.len() < prior.len())
{
best_incomplete = Some((contract, ungated.clone()));
}
issues = ungated
.iter()
.map(|c| match c.coverage {
Coverage::Absent => format!(
"you DROPPED this constraint, which the operator agreed and which is not \
optional: \"{}\". Express it as a CHECK whose command actually verifies \
it. Keep every check you already had.",
c.text
),
Coverage::ProseOnly => format!(
"this constraint appears only in `description`, where NOTHING VERIFIES \
IT: \"{}\". A contract's force is its checks — prose gates nothing. Add \
a check whose command fails when the constraint is violated (a grep, a \
test, a diff), and keep every check you already had.",
c.text
),
})
.collect();
last_err = format!(
"ungated constraint(s): {}",
ungated
.iter()
.map(|c| c.text.as_str())
.collect::<Vec<_>>()
.join("; ")
);
}
if let Some((mut contract, ungated)) = best_incomplete {
contract.description = format!(
"{}\n\nNOT VERIFIED BY THIS CONTRACT — these constraints from the discussion are \
not gated by any check here, so nothing enforces them (a mention above is not a \
check); review them by hand before approving:\n{}",
contract.description.trim_end(),
ungated
.iter()
.map(|c| format!(" - {}", c.text))
.collect::<Vec<_>>()
.join("\n")
);
return Ok(contract);
}
Err(format!(
"could not derive a valid outcome contract after {max} attempts: {last_err}"
))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Coverage {
Absent,
ProseOnly,
}
#[derive(Debug, Clone)]
struct UngatedConstraint {
text: String,
coverage: Coverage,
}
async fn ungated_constraints<F, Fut>(
generate: &F,
contract: &OutcomeContract,
constraints: &[String],
) -> Vec<UngatedConstraint>
where
F: Fn(String) -> Fut + Send + Sync,
Fut: Future<Output = Result<String, String>> + Send,
{
if constraints.is_empty() {
return Vec::new();
}
let rendered = constraints
.iter()
.enumerate()
.map(|(i, c)| format!("{}. {c}", i + 1))
.collect::<Vec<_>>()
.join("\n");
let contract_json = serde_json::to_string_pretty(contract).unwrap_or_default();
let prompt = format!(
"A verifiable outcome contract was drafted for a coding task. The operator agreed \
these constraints beforehand. A constraint counts as SATISFIED only when some \
check's `command` would actually FAIL if the constraint were violated. Being \
mentioned in `description` does NOT count — the description is prose and runs \
nothing.\n\n\
CONSTRAINTS\n{rendered}\n\n\
CONTRACT\n{contract_json}\n\n\
Return ONLY a JSON object with the 1-based numbers of the constraints that are NOT \
satisfied, split by which failure it is:\n\
{{\"missing\": [1], \"prose_only\": [2]}}\n\n\
- `missing`: the constraint appears nowhere in the contract.\n\
- `prose_only`: the constraint is stated in `description` (or a check NAME) but no \
check command verifies it.\n\n\
Return both arrays empty if every constraint is verified by a check. Judge \
substance, not wording — a check that genuinely verifies the constraint counts even \
if it uses completely different words. Judge the COMMAND, never the name."
);
let text = match tokio::time::timeout(CONTRACT_GEN_TIMEOUT, generate(prompt)).await {
Ok(Ok(t)) => t,
_ => return Vec::new(),
};
let Ok(value) = extract_json_object(&text) else {
return Vec::new();
};
let indices = |field: &str| -> Vec<usize> {
value
.get(field)
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_u64)
.filter(|n| *n >= 1 && (*n as usize) <= constraints.len())
.map(|n| n as usize - 1)
.collect()
})
.unwrap_or_default()
};
let absent = indices("missing");
let prose_only = indices("prose_only");
(0..constraints.len())
.filter_map(|i| {
let coverage = if absent.contains(&i) {
Coverage::Absent
} else if prose_only.contains(&i) {
Coverage::ProseOnly
} else {
return None;
};
Some(UngatedConstraint {
text: constraints[i].clone(),
coverage,
})
})
.collect()
}
async fn run_check(check: &ContractCheck, executor: &WorktreeExecutor) -> CheckResult {
let started = std::time::Instant::now();
let outcome = executor
.run_shell(&check.command, Some(check.timeout_secs))
.await;
let duration_ms = started.elapsed().as_millis() as u64;
match outcome {
Ok(v) => {
let exit_code = v.get("exit_code").and_then(Value::as_i64);
let output = v.get("output").and_then(Value::as_str).unwrap_or_default();
let timed_out = v.get("timed_out").and_then(Value::as_bool).unwrap_or(false);
let exit_ok = !check.expect_exit_zero || exit_code == Some(0);
let contains_ok = check
.output_contains
.as_deref()
.map(|needle| output.contains(needle))
.unwrap_or(true);
CheckResult {
name: check.name.clone(),
passed: exit_ok && contains_ok && !timed_out,
exit_code,
output_tail: super::shell_tool::tail(output, 4 * 1024),
duration_ms,
}
}
Err(e) => CheckResult {
name: check.name.clone(),
passed: false,
exit_code: None,
output_tail: format!("check failed to run: {e}"),
duration_ms,
},
}
}
pub async fn evaluate_contract(
contract: &OutcomeContract,
executor: &WorktreeExecutor,
sink: &EventSink,
) -> Vec<CheckResult> {
let mut results = Vec::with_capacity(contract.checks.len());
for check in &contract.checks {
sink.emit(CoderEventKind::CheckStarted {
name: check.name.clone(),
});
let result = run_check(check, executor).await;
sink.emit(CoderEventKind::CheckCompleted {
result: result.clone(),
});
results.push(result);
}
results
}
pub async fn evaluate_contract_baseline(
contract: &OutcomeContract,
executor: &WorktreeExecutor,
) -> Vec<CheckResult> {
let mut results = Vec::with_capacity(contract.checks.len());
for check in &contract.checks {
results.push(run_check(check, executor).await);
}
results
}
pub fn baseline_gates_nothing(results: &[CheckResult]) -> bool {
!results.is_empty() && results.iter().all(|r| r.passed)
}
#[cfg(test)]
mod tests {
#[test]
fn parse_test_failures_pulls_node_ids_from_pytest_summary() {
let out = "=========================== short test summary info ============================
FAILED tests/test_basic.py::test_session_using_session_settings - AssertionError
FAILED tests/test_reqctx.py::test_environ_for_valid_idna - ValueError: x
ERROR tests/test_instance_config.py::test_installed_package_paths[True] - AttributeError
FAILED tests/test_basic.py::test_session_using_session_settings - AssertionError
1 failed in 0.10s";
let ids = parse_test_failures(out);
assert_eq!(
ids,
vec![
"tests/test_basic.py::test_session_using_session_settings".to_string(),
"tests/test_reqctx.py::test_environ_for_valid_idna".to_string(),
],
"FAILED node ids only, deduped, order-preserved — the ERROR \
(collection/environment drift) is excluded"
);
assert!(parse_test_failures("125 passed in 0.12s").is_empty());
assert!(parse_test_failures("FAILED something-weird - boom").is_empty());
}
#[test]
fn intent_targets_tests_fires_only_on_test_fixing_intents() {
assert!(intent_targets_tests(
"In this repository, the tests fail because of a bug. Fix the source so the tests pass."
));
assert!(intent_targets_tests("make the failing tests pass"));
assert!(!intent_targets_tests("Add a --json flag to the CLI"));
assert!(!intent_targets_tests("Refactor the parser for clarity"));
}
#[test]
fn summary_with_failures_injects_observed_ids_and_is_a_noop_when_empty() {
let base = "Top-level entries: src, tests";
assert_eq!(summary_with_failures(base, &[]), base);
let with = summary_with_failures(
base,
&["tests/test_basic.py::test_session_using_session_settings".to_string()],
);
assert!(with.contains("Observed failing tests"));
assert!(with.contains("tests/test_basic.py::test_session_using_session_settings"));
assert!(with.starts_with(base));
}
#[test]
fn strips_trailing_output_filters_that_mask_the_exit_code() {
let mut c = OutcomeContract {
description: "tests pass".into(),
checks: vec![
ContractCheck {
name: "run_full_test_suite".into(),
command: "python -m pytest tests/ -x -q 2>&1 | tail -20".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 120,
},
ContractCheck {
name: "chained".into(),
command: "pytest -q | head -n 50 | tail -5".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 120,
},
],
};
c.strip_exit_masking_pipes();
assert_eq!(c.checks[0].command, "python -m pytest tests/ -x -q 2>&1");
assert_eq!(c.checks[1].command, "pytest -q");
}
#[test]
fn leaves_meaningful_pipes_and_or_lists_alone() {
let keep = [
"pytest -q | grep -q PASSED",
"cmd || echo fallback",
"python -c \"print('a|b')\"",
"pytest -q",
];
for cmd in keep {
let mut c = OutcomeContract {
description: "d".into(),
checks: vec![ContractCheck {
name: "k".into(),
command: cmd.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 120,
}],
};
c.strip_exit_masking_pipes();
assert_eq!(c.checks[0].command, cmd, "must not rewrite: {cmd}");
}
}
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
const VALID: &str = r#"{
"description": "file exists",
"checks": [{"name": "exists", "command": "test -f x.txt"}]
}"#;
#[test]
fn prompt_steers_toward_verifying_the_task_and_real_labels() {
let p = build_contract_prompt(
"add a --version flag",
"Top-level entries: Cargo.toml, src\nBuild systems detected: Rust (cargo)",
&[],
);
assert!(p.contains("add a --version flag"));
assert!(p.contains("Rust (cargo)"));
assert!(p.contains("verify THE TASK ITSELF"));
assert!(
p.contains("rustc --version"),
"names the toolchain-only anti-pattern"
);
assert!(p.contains("never the literal placeholder"));
assert!(p.contains("non-interactively"));
assert!(
p.contains("CERTAIN will appear"),
"output_contains caution present"
);
assert!(p.contains("no markdown fences"));
}
#[test]
fn repair_prompt_appends_prior_issues() {
let p = build_contract_prompt("t", "r", &["check 'a' has an empty command".into()]);
assert!(p.contains("FAILED validation"));
assert!(p.contains("empty command"));
}
#[tokio::test]
async fn derives_on_first_valid_attempt() {
let c = derive_contract(
|_p| async { Ok::<_, String>(VALID.into()) },
"make x",
"repo",
3,
&[],
)
.await
.unwrap();
assert_eq!(c.checks.len(), 1);
assert!(c.checks[0].expect_exit_zero, "default applies");
assert_eq!(c.checks[0].timeout_secs, 120);
}
#[tokio::test(start_paused = true)]
async fn times_out_when_generation_hangs() {
let err = derive_contract(
|_p| async {
tokio::time::sleep(std::time::Duration::from_secs(10_000)).await;
Ok::<_, String>(VALID.into())
},
"make x",
"repo",
1,
&[],
)
.await
.unwrap_err();
assert!(
err.contains("timed out"),
"expected timeout error, got: {err}"
);
}
#[tokio::test]
async fn repairs_fenced_and_chatty_output() {
let fenced = format!("Sure! Here is the contract:\n```json\n{VALID}\n```");
let c = derive_contract(
|_p| {
let text = fenced.clone();
async move { Ok::<_, String>(text) }
},
"x",
"r",
3,
&[],
)
.await
.unwrap();
assert_eq!(c.checks[0].name, "exists");
}
#[tokio::test]
async fn invalid_then_repaired() {
let calls = AtomicUsize::new(0);
let c = derive_contract(
|prompt: String| {
let n = calls.fetch_add(1, Ordering::SeqCst) + 1;
async move {
if n == 1 {
Ok::<_, String>(r#"{"description": "no checks", "checks": []}"#.into())
} else {
assert!(
prompt.contains("FAILED validation"),
"repair prompt carries issues"
);
Ok(VALID.into())
}
}
},
"x",
"r",
3,
&[],
)
.await
.unwrap();
assert_eq!(c.checks.len(), 1);
}
#[tokio::test]
async fn gives_up_with_error_after_max() {
let err = derive_contract(
|_p| async { Ok::<_, String>("not json at all".into()) },
"x",
"r",
2,
&[],
)
.await
.unwrap_err();
assert!(err.contains("after 2 attempts"), "{err}");
}
#[test]
fn is_toolchain_only_flags_bare_version_probes_only() {
for c in [
"cargo --version",
"cargo -V",
"rustc --version",
"node -v",
"npm --version",
"python3 --version",
"go version",
"make --help",
] {
assert!(is_toolchain_only(c), "should flag `{c}`");
}
for c in [
"cargo build",
"cargo test",
"cargo run -- --version",
"cargo run --release -- --version",
"./target/debug/greeter --version",
"cargo --version && cargo build",
"test -f src/main.rs",
"grep -q version Cargo.toml",
"rustc src/main.rs -o /tmp/x",
] {
assert!(!is_toolchain_only(c), "should NOT flag `{c}`");
}
}
#[test]
fn validate_rejects_toolchain_only_and_placeholder_name() {
let c = OutcomeContract {
description: "d".into(),
checks: vec![ContractCheck {
name: "unique_snake_case_label".into(),
command: "cargo --version".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 120,
}],
};
let issues = c.validate();
assert!(
issues.iter().any(|i| i.contains("placeholder name")),
"{issues:?}"
);
assert!(
issues.iter().any(|i| i.contains("toolchain-only no-op")),
"{issues:?}"
);
}
#[test]
fn repair_cosmetic_names_fixes_placeholder_empty_and_duplicates() {
let mk = |name: &str, cmd: &str| ContractCheck {
name: name.into(),
command: cmd.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 60,
};
let mut c = OutcomeContract {
description: "d".into(),
checks: vec![
mk("unique_snake_case_label", "pytest a"),
mk("", "pytest b"),
mk("run_tests", "pytest c"),
mk("run_tests", "pytest d"),
],
};
c.repair_cosmetic_names();
let names: Vec<&str> = c.checks.iter().map(|x| x.name.as_str()).collect();
assert_eq!(
names,
vec!["check_1", "check_2", "run_tests", "run_tests_2"]
);
assert_eq!(c.checks[0].command, "pytest a");
assert!(c.validate().is_empty(), "{:?}", c.validate());
}
#[test]
fn strip_absolute_cd_prefixes_drops_repo_but_keeps_relative_and_body() {
let mk = |cmd: &str| ContractCheck {
name: "c".into(),
command: cmd.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 60,
};
let mut c = OutcomeContract {
description: "d".into(),
checks: vec![
mk("cd /repo && python -m pytest tests/ -v 2>&1"),
mk("cd /workspace ; ./run.sh"),
mk("cd subpkg && cargo test"),
mk("python -m pytest -q tests/test_x.py"),
mk("echo hi && cd /repo && pytest"),
],
};
c.strip_absolute_cd_prefixes();
let cmds: Vec<&str> = c.checks.iter().map(|x| x.command.as_str()).collect();
assert_eq!(
cmds,
vec![
"python -m pytest tests/ -v 2>&1",
"./run.sh",
"cd subpkg && cargo test",
"python -m pytest -q tests/test_x.py",
"echo hi && cd /repo && pytest",
]
);
}
#[tokio::test]
async fn derive_strips_hallucinated_repo_cd_first_try() {
let with_repo_cd = r#"{"description":"tests pass","checks":[
{"name":"run_tests","command":"cd /repo && python -m pytest -q tests/test_x.py"}]}"#;
let c = derive_contract(
|_p: String| async move { Ok::<_, String>(with_repo_cd.into()) },
"fix the bug so pytest passes",
"Python",
3,
&[],
)
.await
.unwrap();
assert_eq!(
c.checks[0].command, "python -m pytest -q tests/test_x.py",
"the hallucinated `cd /repo &&` prefix must be stripped"
);
}
#[tokio::test]
async fn derive_succeeds_first_try_when_model_only_leaves_placeholder_name() {
let calls = AtomicUsize::new(0);
let placeholder_named = r#"{"description":"tests pass","checks":[
{"name":"unique_snake_case_label","command":"python3 -m pytest -q"}]}"#;
let c = derive_contract(
|_p: String| {
calls.fetch_add(1, Ordering::SeqCst);
async move { Ok::<_, String>(placeholder_named.into()) }
},
"fix the bug so pytest passes",
"Python",
3,
&[],
)
.await
.unwrap();
assert_eq!(calls.load(Ordering::SeqCst), 1, "no repair attempt needed");
assert_eq!(c.checks[0].name, "check_1");
assert_eq!(c.checks[0].command, "python3 -m pytest -q");
}
#[tokio::test]
async fn derive_repairs_a_toolchain_only_first_attempt() {
let calls = AtomicUsize::new(0);
let toolchain_only = r#"{"description":"v","checks":[
{"name":"unique_snake_case_label","command":"cargo --version"}]}"#;
let real = r#"{"description":"v","checks":[
{"name":"version_flag_prints","command":"cargo run -- --version"}]}"#;
let c = derive_contract(
|prompt: String| {
let n = calls.fetch_add(1, Ordering::SeqCst) + 1;
async move {
if n == 1 {
Ok::<_, String>(toolchain_only.into())
} else {
assert!(prompt.contains("toolchain-only no-op"), "{prompt}");
assert!(!prompt.contains("placeholder name"), "{prompt}");
Ok(real.into())
}
}
},
"add a --version flag",
"Rust (cargo)",
3,
&[],
)
.await
.unwrap();
assert_eq!(c.checks[0].command, "cargo run -- --version");
assert_eq!(calls.load(Ordering::SeqCst), 2, "took exactly one repair");
}
#[test]
fn validate_catches_empty_and_duplicate_and_assertless() {
let c = OutcomeContract {
description: "d".into(),
checks: vec![
ContractCheck {
name: "a".into(),
command: "true".into(),
expect_exit_zero: false,
output_contains: None,
timeout_secs: 5,
},
ContractCheck {
name: "a".into(),
command: "".into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 5,
},
],
};
let issues = c.validate();
assert!(issues.iter().any(|i| i.contains("asserts nothing")));
assert!(issues.iter().any(|i| i.contains("empty command")));
assert!(issues.iter().any(|i| i.contains("duplicate")));
}
#[tokio::test]
async fn evaluate_passes_and_fails_checks_in_a_real_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("present.txt"), "hello needle").unwrap();
let exec = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let contract = OutcomeContract {
description: "d".into(),
checks: vec![
ContractCheck {
name: "exists".into(),
command: crate::coder::test_cmds::file_exists("present.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
},
ContractCheck {
name: "content".into(),
command: crate::coder::test_cmds::cat("present.txt"),
expect_exit_zero: true,
output_contains: Some("needle".into()),
timeout_secs: 10,
},
ContractCheck {
name: "missing".into(),
command: crate::coder::test_cmds::file_exists("absent.txt"),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
},
],
};
let results = evaluate_contract(&contract, &exec, &sink).await;
assert_eq!(results.len(), 3, "all checks run even after a failure");
assert!(results[0].passed);
assert!(results[1].passed);
assert!(!results[2].passed);
assert_eq!(results[2].exit_code, Some(1));
}
#[tokio::test]
async fn evaluate_fails_on_missing_substring() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let contract = OutcomeContract {
description: "d".into(),
checks: vec![ContractCheck {
name: "needle".into(),
command: "echo haystack".into(),
expect_exit_zero: true,
output_contains: Some("needle".into()),
timeout_secs: 10,
}],
};
let results = evaluate_contract(&contract, &exec, &sink).await;
assert!(!results[0].passed, "exit 0 but substring missing must fail");
assert_eq!(results[0].exit_code, Some(0));
}
fn check(name: &str, command: &str) -> ContractCheck {
ContractCheck {
name: name.into(),
command: command.into(),
expect_exit_zero: true,
output_contains: None,
timeout_secs: 10,
}
}
#[tokio::test]
async fn an_all_green_baseline_is_flagged_as_gating_nothing() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
let contract = OutcomeContract {
description: "d".into(),
checks: vec![check("a", "exit 0"), check("b", "exit 0")],
};
let baseline = evaluate_contract_baseline(&contract, &exec).await;
assert_eq!(baseline.len(), 2);
assert!(baseline.iter().all(|r| r.passed));
assert!(baseline_gates_nothing(&baseline));
}
#[tokio::test]
async fn a_mixed_baseline_is_not_flagged() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
let contract = OutcomeContract {
description: "d".into(),
checks: vec![
check("already_green", "exit 0"),
check("must_fix", "exit 1"),
],
};
let baseline = evaluate_contract_baseline(&contract, &exec).await;
assert!(baseline[0].passed);
assert!(
!baseline[1].passed,
"the red check is what gates the session"
);
assert!(
!baseline_gates_nothing(&baseline),
"one green check among red ones is information, not a fault"
);
}
#[tokio::test]
async fn an_all_red_baseline_is_not_flagged() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
let contract = OutcomeContract {
description: "d".into(),
checks: vec![check("must_fix", "exit 1")],
};
let baseline = evaluate_contract_baseline(&contract, &exec).await;
assert!(!baseline_gates_nothing(&baseline));
}
#[test]
fn an_empty_baseline_is_not_all_green() {
assert!(!baseline_gates_nothing(&[]));
}
#[tokio::test]
async fn baseline_agrees_with_the_narrated_evaluation() {
let dir = tempfile::tempdir().unwrap();
let exec = WorktreeExecutor::new(dir.path());
let sink = EventSink::test_sink();
let contract = OutcomeContract {
description: "d".into(),
checks: vec![check("green", "exit 0"), check("red", "exit 1")],
};
let baseline = evaluate_contract_baseline(&contract, &exec).await;
let narrated = evaluate_contract(&contract, &exec, &sink).await;
let verdicts = |rs: &[CheckResult]| -> Vec<(String, bool)> {
rs.iter().map(|r| (r.name.clone(), r.passed)).collect()
};
assert_eq!(verdicts(&baseline), verdicts(&narrated));
}
#[tokio::test]
async fn a_dropped_constraint_is_repaired_into_the_contract() {
use std::sync::Mutex;
let script = Mutex::new(vec![
r#"{"description":"tests pass","checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[1]}"#.to_string(),
r#"{"description":"tests pass and the public signature is untouched",
"checks":[{"name":"tests","command":"exit 0"},
{"name":"signature_unchanged","command":"grep -q 'fn add(a: i32, b: i32)' src/lib.rs"}]}"#
.to_string(),
r#"{"missing":[]}"#.to_string(),
]);
let prompts: Mutex<Vec<String>> = Mutex::new(Vec::new());
let constraint = "The public signature of add() must stay exactly as it is.";
let contract = derive_contract(
|p: String| {
prompts.lock().unwrap().push(p);
let next = script.lock().unwrap().remove(0);
async move { Ok::<_, String>(next) }
},
"make the failing tests pass",
"Top-level entries: src, Cargo.toml",
3,
&[constraint.to_string()],
)
.await
.expect("the repair pass must produce a contract");
assert!(
contract
.checks
.iter()
.any(|c| c.name == "signature_unchanged"),
"the dropped constraint must be repaired into the contract: {contract:?}"
);
let prompts = prompts.lock().unwrap();
assert!(
prompts[2].contains(constraint) && prompts[2].contains("DROPPED"),
"the retry must name the dropped constraint verbatim: {}",
prompts[2]
);
}
#[tokio::test]
async fn an_unexpressible_constraint_is_disclosed_not_dropped() {
use std::sync::Mutex;
let script = Mutex::new(vec![
r#"{"description":"tests pass","checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[1]}"#.to_string(),
r#"{"description":"tests pass","checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[1]}"#.to_string(),
]);
let constraint = "Get written sign-off from the CFO before merging.";
let contract = derive_contract(
|_p: String| {
let next = script.lock().unwrap().remove(0);
async move { Ok::<_, String>(next) }
},
"make the failing tests pass",
"Top-level entries: src",
2,
&[constraint.to_string()],
)
.await
.expect("a valid draft beats no session, provided the gap is stated");
assert!(
contract
.description
.contains("NOT VERIFIED BY THIS CONTRACT")
&& contract.description.contains(constraint),
"an unexpressible constraint must be disclosed in the description: {}",
contract.description
);
assert!(contract.checks.iter().any(|c| c.name == "tests"));
}
#[tokio::test]
async fn a_constraint_captured_only_in_prose_fires_the_disclosure() {
use std::sync::Mutex;
let script = Mutex::new(vec![
r#"{"description":"tests pass","checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[1],"prose_only":[]}"#.to_string(),
r#"{"description":"tests pass, and the public signature of add() is unchanged",
"checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[],"prose_only":[1]}"#.to_string(),
r#"{"description":"tests pass, and the public signature of add() is unchanged",
"checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[],"prose_only":[1]}"#.to_string(),
]);
let prompts: Mutex<Vec<String>> = Mutex::new(Vec::new());
let constraint = "The public signature of add() must stay exactly as it is.";
let contract = derive_contract(
|p: String| {
prompts.lock().unwrap().push(p);
let next = script.lock().unwrap().remove(0);
async move { Ok::<_, String>(next) }
},
"make the failing tests pass",
"Top-level entries: src",
3,
&[constraint.to_string()],
)
.await
.expect("a valid draft beats no session, provided the gap is stated");
assert!(
contract
.description
.contains("NOT VERIFIED BY THIS CONTRACT")
&& contract.description.contains(constraint),
"a prose-only constraint must be disclosed, not passed off as captured: {}",
contract.description
);
assert!(
contract.checks.iter().all(|c| c.name == "tests"),
"nothing here gates the constraint: {contract:?}"
);
let prompts = prompts.lock().unwrap();
assert!(
prompts[4].contains(constraint) && prompts[4].contains("NOTHING VERIFIES IT"),
"the repair must name the prose-only failure: {}",
prompts[4]
);
}
#[tokio::test]
async fn the_disclosed_draft_is_the_best_one_seen_not_the_newest() {
use std::sync::Mutex;
let script = Mutex::new(vec![
r#"{"description":"d","checks":[{"name":"first_gated","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[2],"prose_only":[]}"#.to_string(),
r#"{"description":"d","checks":[{"name":"gates_neither","command":"exit 0"}]}"#
.to_string(),
r#"{"missing":[1,2],"prose_only":[]}"#.to_string(),
]);
let contract = derive_contract(
|_p: String| {
let next = script.lock().unwrap().remove(0);
async move { Ok::<_, String>(next) }
},
"do the thing",
"Top-level entries: src",
2,
&["constraint one".to_string(), "constraint two".to_string()],
)
.await
.unwrap();
assert!(
contract.checks.iter().any(|c| c.name == "first_gated"),
"the better draft must survive: {contract:?}"
);
assert!(
contract.description.contains("constraint two")
&& !contract.description.contains("constraint one"),
"only the genuinely ungated constraint is disclosed: {}",
contract.description
);
}
#[tokio::test]
async fn a_failing_constraint_judge_does_not_block_derivation() {
use std::sync::Mutex;
let script = Mutex::new(vec![
r#"{"description":"tests pass","checks":[{"name":"tests","command":"exit 0"}]}"#
.to_string(),
"the judge returned prose, not JSON".to_string(),
]);
let contract = derive_contract(
|_p: String| {
let next = script.lock().unwrap().remove(0);
async move { Ok::<_, String>(next) }
},
"make the failing tests pass",
"Top-level entries: src",
3,
&["some constraint".to_string()],
)
.await
.expect("an unusable judge must not fail the derivation");
assert_eq!(contract.checks.len(), 1);
}
}