Files
clawmates/crates/cm-api/tests/phase_conditions.rs
T
Omar SobhandClaude Opus 5 1fc6cb41ba feat(judge): the judge commits to a verification plan before it reads the evidence
One tool-free round on the condition alone: which files, strings and tests
would show MET, and which commands would settle it. The plan is placed
between the condition and the evidence in the verifying prompt and stored
as mission_phase_evaluations.expectation beside the verdict, so an operator
can see whether the checks the judge ran are the ones it said it would run.

Self-Play Reward Hacking of Reference-Free Judges (arXiv 2607.05904): a
judge's pass rate climbed 0.72 -> 0.94 across rounds while accuracy stayed
0.20; cross-family judges and ensembles did not help; the judge committing
its own answer first cut the false-positive rate 0.719 -> 0.012. Ours
commits to a plan, not a value — re-deriving values is the failure
done-when-wording measured, and the commit prompt forbids it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
2026-09-20 22:07:39 -05:00

382 lines
13 KiB
Rust

//! Coverage for goal conditions and phase iteration (migration 0061).
//!
//! These tests exercise the SQL directly rather than the sweep loop, because
//! the part that is easy to get wrong is the *iteration scoping*: "are this
//! phase's runs all finished?" must ask about the CURRENT pass. Without that,
//! pass 1's completed rows satisfy pass 2 the instant it is enqueued and the
//! phase completes without doing any work.
//!
//! What this locks in:
//! * A phase with no `done_when` still goes running -> completed on terminal
//! runs (the regression guard: existing missions are unaffected).
//! * A phase with `done_when` goes running -> evaluating instead.
//! * A failed run fails the phase outright, condition or not.
//! * Pass 2 is not satisfied by pass 1's completed runs.
//! * `mission_phase_evaluations` is unique per (phase, iteration) and
//! upserts.
use cm_db::repo::workspaces;
use cm_domain::{Workspace, WorkspaceId};
use sqlx::Row;
use uuid::Uuid;
async fn seed_workspace(pool: &sqlx::PgPool) -> WorkspaceId {
let ws = Workspace {
id: WorkspaceId::new(),
name: "Phase Conditions Test".into(),
plan: "team".into(),
};
workspaces::insert(pool, &ws).await.unwrap();
ws.id
}
async fn seed_mission(pool: &sqlx::PgPool, ws: WorkspaceId) -> Uuid {
let id = Uuid::now_v7();
sqlx::query(
"INSERT INTO missions (id, workspace_id, title, template_kind, status)
VALUES ($1, $2, 'test mission', 'research_only', 'running')",
)
.bind(id)
.bind(ws.as_uuid())
.execute(pool)
.await
.unwrap();
id
}
/// A phase in `running`, optionally carrying a completion condition.
async fn seed_phase(
pool: &sqlx::PgPool,
mission_id: Uuid,
done_when: Option<&str>,
max_iterations: i32,
iteration: i32,
) -> Uuid {
let id = Uuid::now_v7();
sqlx::query(
"INSERT INTO mission_phases
(id, mission_id, kind, order_idx, status, done_when, max_iterations, iteration)
VALUES ($1, $2, 'research', 0, 'running', $3, $4, $5)",
)
.bind(id)
.bind(mission_id)
.bind(done_when)
.bind(max_iterations)
.bind(iteration)
.execute(pool)
.await
.unwrap();
id
}
async fn seed_run(
pool: &sqlx::PgPool,
ws: WorkspaceId,
mission_id: Uuid,
phase_id: Uuid,
status: &str,
iteration: i32,
) {
sqlx::query(
"INSERT INTO topology_runs
(id, workspace_id, task, kind, status, tier, mission_id, mission_phase_id, iteration)
VALUES ($1, $2, 'task', 'run', $3, 'team', $4, $5, $6)",
)
.bind(Uuid::now_v7())
.bind(ws.as_uuid())
.bind(status)
.bind(mission_id)
.bind(phase_id)
.bind(iteration)
.execute(pool)
.await
.unwrap();
}
/// The exact statement `phase_runner::close_finished_phases` runs.
async fn close_finished_phases(pool: &sqlx::PgPool) {
sqlx::query(
"UPDATE mission_phases mp
SET status =
CASE
WHEN EXISTS (
SELECT 1 FROM topology_runs r
WHERE r.mission_phase_id = mp.id
AND r.iteration = mp.iteration
AND r.status = 'failed'
) THEN 'failed'
WHEN mp.done_when IS NOT NULL AND btrim(mp.done_when) <> '' THEN 'evaluating'
ELSE 'completed'
END,
completed_at =
CASE
WHEN mp.done_when IS NOT NULL AND btrim(mp.done_when) <> ''
AND NOT EXISTS (
SELECT 1 FROM topology_runs r
WHERE r.mission_phase_id = mp.id
AND r.iteration = mp.iteration
AND r.status = 'failed'
)
THEN NULL ELSE now()
END
WHERE mp.status = 'running'
AND EXISTS (
SELECT 1 FROM topology_runs r
WHERE r.mission_phase_id = mp.id AND r.iteration = mp.iteration
)
AND NOT EXISTS (
SELECT 1 FROM topology_runs r
WHERE r.mission_phase_id = mp.id
AND r.iteration = mp.iteration
AND r.status NOT IN ('completed', 'failed', 'cancelled')
)",
)
.execute(pool)
.await
.unwrap();
}
async fn phase_status(pool: &sqlx::PgPool, phase_id: Uuid) -> String {
sqlx::query("SELECT status FROM mission_phases WHERE id = $1")
.bind(phase_id)
.fetch_one(pool)
.await
.unwrap()
.get::<String, _>("status")
}
/// The regression guard. A mission that never opts into a condition must
/// behave exactly as it did before conditions existed.
#[tokio::test]
async fn phase_without_condition_completes_as_before() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, None, 1, 0).await;
seed_run(&pool, ws, mission, phase, "completed", 0).await;
close_finished_phases(&pool).await;
assert_eq!(phase_status(&pool, phase).await, "completed");
}
#[tokio::test]
async fn phase_with_condition_goes_to_evaluating() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, Some("a brief exists"), 3, 0).await;
seed_run(&pool, ws, mission, phase, "completed", 0).await;
close_finished_phases(&pool).await;
assert_eq!(
phase_status(&pool, phase).await,
"evaluating",
"a phase with a condition must be judged before it can complete"
);
// completed_at must stay NULL while the phase is still being judged.
let completed_at: Option<time::OffsetDateTime> =
sqlx::query("SELECT completed_at FROM mission_phases WHERE id = $1")
.bind(phase)
.fetch_one(&pool)
.await
.unwrap()
.get("completed_at");
assert!(completed_at.is_none(), "not finished, so not timestamped");
}
/// A blank condition is not a condition — otherwise a UI that sends "" would
/// silently park every phase in `evaluating` forever.
#[tokio::test]
async fn blank_condition_is_treated_as_none() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, Some(" "), 3, 0).await;
seed_run(&pool, ws, mission, phase, "completed", 0).await;
close_finished_phases(&pool).await;
assert_eq!(phase_status(&pool, phase).await, "completed");
}
#[tokio::test]
async fn failed_run_fails_the_phase_even_with_a_condition() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, Some("a brief exists"), 3, 0).await;
seed_run(&pool, ws, mission, phase, "failed", 0).await;
close_finished_phases(&pool).await;
assert_eq!(
phase_status(&pool, phase).await,
"failed",
"there is nothing to evaluate when the work itself failed"
);
}
/// The subtle one. On pass 2 the phase has `iteration = 1`, but pass 1's
/// completed run is still in the table. Without scoping the check to the
/// current iteration, that stale row satisfies "all runs finished" and the
/// phase completes having done no work on this pass.
#[tokio::test]
async fn second_pass_is_not_satisfied_by_first_pass_runs() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
// Phase is on pass 2 (iteration=1) and running.
let phase = seed_phase(&pool, mission, Some("a brief exists"), 3, 1).await;
// Pass 1 left a completed run behind.
seed_run(&pool, ws, mission, phase, "completed", 0).await;
// Pass 2's run is still queued.
seed_run(&pool, ws, mission, phase, "queued", 1).await;
close_finished_phases(&pool).await;
assert_eq!(
phase_status(&pool, phase).await,
"running",
"pass 1's completed run must not close out pass 2"
);
// Finish pass 2 for real.
sqlx::query(
"UPDATE topology_runs SET status = 'completed'
WHERE mission_phase_id = $1 AND iteration = 1",
)
.bind(phase)
.execute(&pool)
.await
.unwrap();
close_finished_phases(&pool).await;
assert_eq!(phase_status(&pool, phase).await, "evaluating");
}
/// A phase whose current pass has enqueued nothing yet must not be closed by
/// an earlier pass's rows either.
#[tokio::test]
async fn phase_with_no_runs_this_pass_stays_running() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, None, 3, 1).await;
seed_run(&pool, ws, mission, phase, "completed", 0).await;
close_finished_phases(&pool).await;
assert_eq!(phase_status(&pool, phase).await, "running");
}
#[tokio::test]
async fn evaluations_are_unique_per_iteration_and_upsert() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, Some("done"), 3, 0).await;
let first = cm_api::evaluator::Verdict {
met: false,
reason: "no brief yet".into(),
guidance: "no brief yet".into(),
model: "runtime:coordinator".into(),
error: None,
checks: Vec::new(),
independent: false,
usage: Default::default(),
expectation: None,
};
cm_api::evaluator::record(&pool, mission, phase, 0, &first)
.await
.unwrap();
// Same iteration again — upsert, not a duplicate row or a constraint error.
let second = cm_api::evaluator::Verdict {
met: true,
reason: "brief written".into(),
guidance: String::new(),
model: "runtime:coordinator".into(),
error: None,
checks: vec![cm_api::evaluator_tools::CheckOutcome {
argv: vec!["cargo".into(), "test".into()],
ran: true,
refused: false,
exit_code: Some(0),
evidence: "exit status: 0".into(),
}],
independent: false,
usage: Default::default(),
expectation: Some("rg 'brief' docs/".into()),
};
cm_api::evaluator::record(&pool, mission, phase, 0, &second)
.await
.unwrap();
let count: i64 =
sqlx::query("SELECT count(*) AS n FROM mission_phase_evaluations WHERE phase_id = $1")
.bind(phase)
.fetch_one(&pool)
.await
.unwrap()
.get("n");
assert_eq!(count, 1, "one row per (phase, iteration)");
// The upsert replaced the verdict: met flipped false -> true, and the
// guidance went empty, which is what a met verdict carries (there is no
// next pass to brief).
let latest = cm_api::evaluator::latest(&pool, phase).await.unwrap();
assert_eq!(latest, Some((0, true, String::new())));
// The operator-facing reason is still stored in full — it is only the
// agent-facing half that is allowed to be empty here.
let reason: String =
sqlx::query("SELECT reason FROM mission_phase_evaluations WHERE phase_id = $1")
.bind(phase)
.fetch_one(&pool)
.await
.unwrap()
.get("reason");
assert_eq!(reason, "brief written");
// The commit-first plan is stored beside the verdict it governed.
let expectation: Option<String> =
sqlx::query("SELECT expectation FROM mission_phase_evaluations WHERE phase_id = $1")
.bind(phase)
.fetch_one(&pool)
.await
.unwrap()
.get("expectation");
assert_eq!(expectation.as_deref(), Some("rg 'brief' docs/"));
}
/// `latest` must return the newest pass, which is what feeds guidance into the
/// next attempt.
#[tokio::test]
async fn latest_returns_the_most_recent_iteration() {
let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await;
let mission = seed_mission(&pool, ws).await;
let phase = seed_phase(&pool, mission, Some("done"), 3, 0).await;
for (i, reason) in [(0, "first"), (1, "second"), (2, "third")] {
cm_api::evaluator::record(
&pool,
mission,
phase,
i,
&cm_api::evaluator::Verdict {
met: false,
reason: reason.into(),
// `latest` must return the agent-facing guidance, never the
// operator-facing reason — the two are deliberately different
// here so a regression to `reason` fails this test.
guidance: format!("{reason}-guidance"),
model: "m".into(),
error: None,
checks: Vec::new(),
independent: false,
usage: Default::default(),
expectation: None,
},
)
.await
.unwrap();
}
let latest = cm_api::evaluator::latest(&pool, phase).await.unwrap();
assert_eq!(latest, Some((2, false, "third-guidance".into())));
}