use async_trait::async_trait;
use harness_core::{Context, World};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verdict {
pub passed: bool,
pub reason: String,
}
impl Verdict {
pub fn passed() -> Self {
Self {
passed: true,
reason: String::new(),
}
}
pub fn failed(reason: impl Into<String>) -> Self {
Self {
passed: false,
reason: reason.into(),
}
}
}
#[async_trait]
pub trait Acceptance: Send + Sync + 'static {
fn name(&self) -> &str;
async fn check(&self, ctx: &Context, world: &World) -> Verdict;
}
pub struct NonEmptyAnswer;
#[async_trait]
impl Acceptance for NonEmptyAnswer {
fn name(&self) -> &str {
"non-empty-answer"
}
async fn check(&self, ctx: &Context, _world: &World) -> Verdict {
let said_something = ctx
.history
.last()
.filter(|t| t.role == harness_core::TurnRole::Assistant)
.is_some_and(|t| {
t.blocks.iter().any(|b| match b {
harness_core::Block::Text(s) => !s.trim().is_empty(),
_ => false,
})
});
if said_something {
Verdict::passed()
} else {
Verdict::failed(
"You ended that turn without answering and without calling a tool — you only \
thought about it. Nothing you described has actually been done yet. Carry on \
now: call the tools you need, and when the work is really finished, say so.",
)
}
}
}
pub struct FilesExist {
paths: Vec<String>,
}
impl FilesExist {
pub fn new<I, S>(paths: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
paths: paths.into_iter().map(Into::into).collect(),
}
}
}
#[async_trait]
impl Acceptance for FilesExist {
fn name(&self) -> &str {
"files-exist"
}
async fn check(&self, _ctx: &Context, world: &World) -> Verdict {
let root = &world.repo.root;
let missing: Vec<&str> = self
.paths
.iter()
.filter(|p| {
let full = root.join(p);
!std::fs::metadata(&full)
.map(|m| m.len() > 0)
.unwrap_or(false)
})
.map(String::as_str)
.collect();
if missing.is_empty() {
Verdict::passed()
} else {
Verdict::failed(format!(
"These files were expected but are missing or empty: {}. \
The work is not done — create them properly, then say so.",
missing.join(", ")
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use harness_core::{Block, Turn, TurnRole};
fn ctx_with(blocks: Vec<Block>) -> Context {
let mut c = Context::new(harness_core::Task {
description: "do it".into(),
source: None,
deadline: None,
});
c.history.push(Turn {
role: TurnRole::Assistant,
blocks,
});
c
}
#[tokio::test]
async fn thinking_out_loud_is_not_an_answer() {
let world = harness_context::default_world(".");
let only_reasoning = ctx_with(vec![Block::Reasoning("I'll write the docx next".into())]);
let v = NonEmptyAnswer.check(&only_reasoning, &world).await;
assert!(!v.passed);
assert!(v.reason.contains("Carry on"), "the reason is actionable");
let blank = ctx_with(vec![Block::Text(" \n".into())]);
assert!(!NonEmptyAnswer.check(&blank, &world).await.passed);
let answered = ctx_with(vec![Block::Text("Done — resume.docx is written.".into())]);
assert!(NonEmptyAnswer.check(&answered, &world).await.passed);
}
#[tokio::test]
async fn a_promised_file_has_to_be_there_and_have_bytes_in_it() {
let dir = std::env::temp_dir().join(format!("acceptance-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let world = harness_context::default_world(&dir);
let ctx = ctx_with(vec![Block::Text("all done!".into())]);
let check = FilesExist::new(["out.docx"]);
let v = check.check(&ctx, &world).await;
assert!(!v.passed, "absent");
assert!(v.reason.contains("out.docx"), "names what's missing: {v:?}");
std::fs::write(dir.join("out.docx"), b"").unwrap();
assert!(!check.check(&ctx, &world).await.passed, "empty");
std::fs::write(dir.join("out.docx"), b"PK\x03\x04").unwrap();
assert!(check.check(&ctx, &world).await.passed);
let _ = std::fs::remove_dir_all(&dir);
}
}