Files

611 lines
23 KiB
Rust

//! Integration proof for bounded quarantine retry (beads
//! cass-fleet-resilience-20260608-uojcg.3.2 and xaztn).
//!
//! Satisfies the `docs/RESILIENCE_TEST_MATRIX.md` Epic-3 row "Bounded retry for
//! eligible entries → integration (no unbounded growth)". The unit tests in
//! `src/indexer/quarantine_retry.rs` cover the eligibility gate in-process;
//! these tests drive the **real durable `QuarantineState` save/load cycle** —
//! the resume checkpoint — across multiple passes through the production
//! `execute_retry`, proving two things the matrix calls out:
//!
//! 1. Repeated OOM retries never grow the on-disk `quarantine_state.json`; the
//! re-quarantine-in-place contract holds and the pass converges to
//! suppression once entries become irreducible same-version.
//! 2. A bounded budget drains an eligible backlog across resumes with each
//! entry attempted exactly once and the state file fully drained at the end.
//!
//! The first two tests use only the public crate API + the pure executor's
//! injected attempt seam. The final real-binary test exercises the live
//! dry-run/apply command against one local Codex fixture, proving that apply
//! reparses and persists the exact quarantine key without touching its source.
//! No network access is required.
mod util;
use std::collections::BTreeSet;
use std::fs;
use std::process::Command;
use std::time::{Duration, Instant};
use anyhow::{Context, ensure};
use assert_cmd::cargo::cargo_bin;
use chrono::{DateTime, Utc};
use coding_agent_search::connectors::codex::CodexConnector;
use coding_agent_search::connectors::{Connector, ScanContext, ScanRoot};
use coding_agent_search::franken_sync::compat::{ConnectionExt, RowExt};
use coding_agent_search::indexer::quarantine::{QuarantineRecord, QuarantineState};
use coding_agent_search::indexer::quarantine_retry::{AttemptResult, RetryConfig, execute_retry};
use coding_agent_search::storage::sqlite::{CURRENT_SCHEMA_VERSION, FrankenStorage};
use tempfile::tempdir;
use util::timeout::spawn_with_timeout_or_diag;
/// The version `execute_retry` compares against. Using the package version here
/// matches what `QuarantineState::record_attempt` stamps (`CARGO_PKG_VERSION`),
/// so a re-quarantined entry becomes "same-version" relative to this string and
/// the suppression assertions hold regardless of the current release number.
const CURRENT: &str = env!("CARGO_PKG_VERSION");
const COMMAND_TIMEOUT: Duration = Duration::from_secs(60);
fn ts(secs: i64) -> anyhow::Result<DateTime<Utc>> {
DateTime::<Utc>::from_timestamp(secs, 0).context("fixture timestamp is representable")
}
/// `storage_key` shape is `"{conversation_id}::v{schema_version}"`. Built in a
/// flat helper so no `format!` runs inside a seeding loop.
fn conv_key(i: usize) -> String {
format!("conv-{i}::v1")
}
/// An eligible (legacy: `cass_version_at_quarantine = None`) ingest-OOM record.
fn legacy_oom(attempts: u64) -> anyhow::Result<QuarantineRecord> {
Ok(QuarantineRecord {
first_attempt_at: ts(1_700_000_000)?,
last_attempt_at: ts(1_700_000_000)?,
attempt_count: attempts,
last_reason: "ingest_oom".to_string(),
cass_version_at_quarantine: None,
})
}
fn no_missing() -> BTreeSet<String> {
BTreeSet::new()
}
/// Seed `n` eligible legacy OOM entries into a fresh data dir, persisted through
/// the production atomic save path.
fn seed(dir: &std::path::Path, n: usize) -> anyhow::Result<()> {
let mut state = QuarantineState::default();
for i in 0..n {
state.entries.insert(conv_key(i), legacy_oom(1)?);
}
state.save(dir)?;
Ok(())
}
#[test]
fn repeated_oom_retry_never_grows_durable_state_and_converges_to_suppression() -> anyhow::Result<()>
{
let dir = tempdir()?;
let n = 8usize;
seed(dir.path(), n)?;
// Pass 1: load → retry (every attempt OOMs again) → save. All N are
// attempted, none cleared, each re-quarantined IN PLACE (no append).
let mut state = QuarantineState::load(dir.path());
let r1 = execute_retry(
&mut state,
CURRENT,
&RetryConfig::default(),
&no_missing(),
ts(1_800_000_000)?,
|_key| AttemptResult::OutOfMemory,
);
state.save(dir.path())?;
ensure!(r1.attempted == n, "all eligible entries attempted");
ensure!(r1.cleared == 0, "an OOM retry must not clear entries");
ensure!(
r1.re_quarantined_oom == n,
"every OOM must re-quarantine in place"
);
ensure!(r1.stalled, "attempted-but-nothing-cleared is a stall");
ensure!(
QuarantineState::load(dir.path()).len() == n,
"no unbounded growth: still exactly N entries after re-quarantine"
);
// Pass 2 (resume off the durable state): the entries are now stamped with
// the current version, so they are irreducible same-version and NONE are
// attempted — the OOM re-quarantine suppressed the retry storm.
let mut state2 = QuarantineState::load(dir.path());
let r2 = execute_retry(
&mut state2,
CURRENT,
&RetryConfig::default(),
&no_missing(),
ts(1_800_000_100)?,
|_key| AttemptResult::OutOfMemory,
);
state2.save(dir.path())?;
ensure!(
r2.attempted == 0,
"same-version entries are suppressed on resume"
);
ensure!(
r2.skipped_irreducible == n,
"all same-version entries must be classified irreducible"
);
ensure!(
QuarantineState::load(dir.path()).len() == n,
"no unbounded growth across resumes"
);
Ok(())
}
#[test]
fn bounded_budget_drains_eligible_backlog_across_resumes_each_attempted_once() -> anyhow::Result<()>
{
let dir = tempdir()?;
let n = 5usize;
seed(dir.path(), n)?;
let config = RetryConfig {
max_attempts: Some(2),
eligible_only: true,
};
let mut attempted_ids = vec![false; n];
let mut attempted_count = 0usize;
let mut duplicate_attempt = false;
let mut invalid_attempt_key = false;
let mut passes = 0usize;
// Resume until the durable backlog is drained. A safety cap keeps the test
// from hanging if convergence ever regresses.
loop {
passes += 1;
ensure!(passes <= n + 2, "bounded retry must converge");
let mut state = QuarantineState::load(dir.path());
if state.is_empty() {
break;
}
let report = execute_retry(
&mut state,
CURRENT,
&config,
&no_missing(),
ts(1_800_000_000)?,
|key| {
attempted_count = attempted_count.saturating_add(1);
match key
.conversation_id
.strip_prefix("conv-")
.and_then(|raw| raw.parse::<usize>().ok())
.filter(|index| *index < attempted_ids.len())
{
Some(index) => {
if let Some(attempted) = attempted_ids.get_mut(index) {
duplicate_attempt |= *attempted;
*attempted = true;
} else {
invalid_attempt_key = true;
}
}
None => invalid_attempt_key = true,
}
AttemptResult::Reindexed
},
);
state.save(dir.path())?;
ensure!(report.attempted <= 2, "budget caps attempts per pass");
}
ensure!(
QuarantineState::load(dir.path()).is_empty(),
"backlog fully drained across bounded resumes"
);
ensure!(attempted_count == n, "every entry attempted");
ensure!(!duplicate_attempt, "an entry must not be attempted twice");
ensure!(
!invalid_attempt_key,
"every attempt key must match the fixture"
);
ensure!(
attempted_ids.iter().all(|attempted| *attempted),
"every entry attempted exactly once"
);
Ok(())
}
#[test]
fn cli_list_and_clear_include_poison_only_quarantine_records() -> anyhow::Result<()> {
let dir = tempdir()?;
let data_dir = dir.path().join("data");
let quarantine_dir = data_dir.join("quarantine");
fs::create_dir_all(&quarantine_dir)?;
let poison_path = quarantine_dir.join("index_ingest_poison.jsonl");
fs::write(
&poison_path,
format!(
"{}\n",
serde_json::json!({
"conversation_id": "poison-only",
"schema_version_at_quarantine": 1,
"first_quarantined_at_ms": 1_700_000_000_000_i64,
"last_attempt_at_ms": 1_700_000_001_000_i64,
"attempt_count": 3,
"cass_version_at_quarantine": "0.0.1-old",
"reason": "index-ingest-out-of-memory",
})
),
)?;
ensure!(
QuarantineState::load(&data_dir).is_empty(),
"fixture must exercise a poison-ledger-only key"
);
let data_dir_arg = data_dir.to_str().context("UTF-8 data dir")?;
let run = |label: &str, args: &[&str]| {
let mut command = Command::new(cargo_bin("cass"));
command
.args(args)
.current_dir(dir.path())
.env("HOME", dir.path())
.env("XDG_DATA_HOME", dir.path().join("xdg-data"))
.env("XDG_CONFIG_HOME", dir.path().join("xdg-config"))
.env("XDG_CACHE_HOME", dir.path().join("xdg-cache"))
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
.env("CASS_IGNORE_SOURCES_CONFIG", "1")
.env("NO_COLOR", "1")
.env_remove("CLAUDE_CONFIG_DIR");
spawn_with_timeout_or_diag(command, label, Some(&data_dir), COMMAND_TIMEOUT)
};
let listed = run(
"quarantine-list-poison-only",
&["quarantine", "list", "--data-dir", data_dir_arg, "--json"],
);
ensure!(
listed.status.success(),
"poison-only list stderr: {}",
String::from_utf8_lossy(&listed.stderr)
);
let listed_json: serde_json::Value = serde_json::from_slice(&listed.stdout)?;
ensure!(
listed_json["quarantined_conversations"] == 1,
"list must include the poison-only key"
);
ensure!(
listed_json["entries"][0]["conversation_id"] == "poison-only",
"list must identify the poison-only key"
);
let dry = run(
"quarantine-clear-poison-only-dry-run",
&["quarantine", "clear", "--data-dir", data_dir_arg, "--json"],
);
ensure!(
dry.status.success(),
"poison-only clear dry-run stderr: {}",
String::from_utf8_lossy(&dry.stderr)
);
let dry_json: serde_json::Value = serde_json::from_slice(&dry.stdout)?;
ensure!(
dry_json["matched"] == 1,
"dry-run must match poison-only key"
);
ensure!(dry_json["cleared"] == 0, "dry-run must not clear the key");
ensure!(
fs::read_to_string(&poison_path)?.contains("poison-only"),
"dry-run must leave the poison ledger unchanged"
);
let applied = run(
"quarantine-clear-poison-only-apply",
&[
"quarantine",
"clear",
"--data-dir",
data_dir_arg,
"--apply",
"--json",
],
);
ensure!(
applied.status.success(),
"poison-only clear apply stderr: {}",
String::from_utf8_lossy(&applied.stderr)
);
let applied_json: serde_json::Value = serde_json::from_slice(&applied.stdout)?;
ensure!(applied_json["matched"] == 1, "apply must match one key");
ensure!(applied_json["cleared"] == 1, "apply must clear one key");
ensure!(
fs::read_to_string(&poison_path)?.is_empty(),
"apply must clear the poison-only ledger record"
);
let after = run(
"quarantine-list-after-poison-only-clear",
&["quarantine", "list", "--data-dir", data_dir_arg, "--json"],
);
ensure!(after.status.success(), "post-clear list must succeed");
let after_json: serde_json::Value = serde_json::from_slice(&after.stdout)?;
ensure!(
after_json["quarantined_conversations"] == 0,
"post-clear list must agree that the quarantine is empty"
);
Ok(())
}
#[test]
fn cli_dry_run_plans_then_apply_reingests_exact_quarantine_key() -> anyhow::Result<()> {
let dir = tempdir()?;
let data_dir = dir.path().join("data");
let source_path = dir
.path()
.join(".codex/sessions/2026/07/rollout-retry.jsonl");
fs::create_dir_all(source_path.parent().context("source parent")?)?;
fs::write(
&source_path,
r#"{"timestamp":"2026-07-22T05:00:00.000Z","type":"session_meta","payload":{"id":"targeted-retry-cli","cwd":"/data/projects/cass"}}
{"timestamp":"2026-07-22T05:00:01.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"targeted CLI retry"}]}}
{"timestamp":"2026-07-22T05:00:02.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"retry persisted"}]}}
"#,
)?;
let connector = CodexConnector::new();
let root = ScanRoot::local(source_path.clone());
let context = ScanContext::with_roots(source_path.clone(), vec![root], None);
let mut conversations = connector.scan(&context)?;
ensure!(
conversations.len() == 1,
"fixture has exactly one conversation, got {}",
conversations.len()
);
let conversation = conversations
.first_mut()
.context("missing fixture conversation")?;
// Model a normal directory-root scan whose external_id is relative to the
// session root. Targeted retry scans the exact file and must restore this
// poison-ledger identity before persistence, preventing a duplicate.
conversation.external_id = Some("sessions/2026/07/rollout-retry".to_string());
let workspace = conversation
.workspace
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_default();
let conversation_id = format!(
"{}|{}|{}|{}|{}|{}|{}",
conversation.agent_slug,
conversation.source_path.display(),
workspace,
conversation.external_id.as_deref().unwrap_or(""),
conversation.started_at.unwrap_or_default(),
conversation.ended_at.unwrap_or_default(),
conversation.messages.len()
);
let schema_version = u32::try_from(CURRENT_SCHEMA_VERSION).context("schema version")?;
let mut state = QuarantineState::default();
state.entries.insert(
format!("{conversation_id}::v{schema_version}"),
legacy_oom(1)?,
);
state.save(&data_dir)?;
let quarantine_dir = data_dir.join("quarantine");
fs::create_dir_all(&quarantine_dir)?;
fs::write(
quarantine_dir.join("index_ingest_poison.jsonl"),
format!(
"{}\n",
serde_json::json!({
"conversation_id": conversation_id,
"schema_version_at_quarantine": schema_version,
"cass_version_at_quarantine": "0.0.1-old",
"reason": "index-ingest-out-of-memory",
"agent_slug": "codex",
"external_id": conversation.external_id,
"source_path": source_path,
"workspace": conversation.workspace.as_ref().map(|path| path.display().to_string()),
"started_at": conversation.started_at,
"ended_at": conversation.ended_at,
"message_count": conversation.messages.len(),
})
),
)?;
let data_dir_arg = data_dir.to_str().context("UTF-8 data dir")?;
let started_at_ms = Utc::now().timestamp_millis();
let command_started = Instant::now();
let mut dry_command = Command::new(cargo_bin("cass"));
dry_command
.args(["quarantine", "retry", "--data-dir", data_dir_arg, "--json"])
.current_dir(dir.path())
.env("HOME", dir.path())
.env("XDG_DATA_HOME", dir.path().join("xdg-data"))
.env("XDG_CONFIG_HOME", dir.path().join("xdg-config"))
.env("XDG_CACHE_HOME", dir.path().join("xdg-cache"))
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
.env("CASS_IGNORE_SOURCES_CONFIG", "1")
.env("NO_COLOR", "1")
.env_remove("CLAUDE_CONFIG_DIR");
let dry = spawn_with_timeout_or_diag(
dry_command,
"quarantine-retry-dry-run",
Some(&data_dir),
COMMAND_TIMEOUT,
);
ensure!(
dry.status.success(),
"dry-run stderr: {}",
String::from_utf8_lossy(&dry.stderr)
);
let dry_json: serde_json::Value = serde_json::from_slice(&dry.stdout)?;
ensure!(dry_json["dry_run"] == true, "dry-run flag must be true");
ensure!(dry_json["applied"] == false, "dry-run must not apply");
ensure!(
dry_json["planned_attempts"] == 1,
"dry-run must plan the exact quarantine key"
);
ensure!(
QuarantineState::load(&data_dir).len() == 1,
"dry-run must be read-only"
);
let mut apply_command = Command::new(cargo_bin("cass"));
apply_command
.args([
"quarantine",
"retry",
"--data-dir",
data_dir_arg,
"--apply",
"--json",
])
.current_dir(dir.path())
.env("HOME", dir.path())
.env("XDG_DATA_HOME", dir.path().join("xdg-data"))
.env("XDG_CONFIG_HOME", dir.path().join("xdg-config"))
.env("XDG_CACHE_HOME", dir.path().join("xdg-cache"))
.env("CODING_AGENT_SEARCH_NO_UPDATE_PROMPT", "1")
.env("CASS_IGNORE_SOURCES_CONFIG", "1")
.env("NO_COLOR", "1")
.env_remove("CLAUDE_CONFIG_DIR");
let applied = spawn_with_timeout_or_diag(
apply_command,
"quarantine-retry-apply",
Some(&data_dir),
COMMAND_TIMEOUT,
);
ensure!(
applied.status.success(),
"apply stderr: {}",
String::from_utf8_lossy(&applied.stderr)
);
let applied_json: serde_json::Value = serde_json::from_slice(&applied.stdout)?;
ensure!(applied_json["dry_run"] == false, "apply is not dry-run");
ensure!(applied_json["applied"] == true, "apply flag must be true");
ensure!(applied_json["cleared"] == 1, "one exact key must clear");
ensure!(
applied_json["remaining_quarantined"] == 0,
"no retried quarantine key may remain"
);
ensure!(
QuarantineState::load(&data_dir).is_empty(),
"durable quarantine state must be drained"
);
ensure!(
source_path.is_file(),
"retry must never remove the source log"
);
let storage = FrankenStorage::open(&data_dir.join("agent_search.db"))?;
let message_count: i64 = storage
.raw()
.query_row_map("SELECT COUNT(*) FROM messages", &[], |row| row.get_typed(0))
.context("message count")?;
ensure!(message_count == 2, "exact conversation persisted once");
// Emit and immediately validate a .12.3/.12.6-style structured proof
// artifact. The paths and payload are fixture-local and contain no raw
// conversation text; the manifest distinguishes a real command pass from
// a timeout, invalid JSON, stale evidence, or a test that never ran.
let proof_dir = dir.path().join("proof");
fs::create_dir_all(&proof_dir)?;
let stdout_path = proof_dir.join("stdout.json");
let stderr_path = proof_dir.join("stderr.log");
let manifest_path = proof_dir.join("proof-manifest.jsonl");
fs::write(&stdout_path, &applied.stdout)?;
fs::write(&stderr_path, &applied.stderr)?;
let elapsed_ms = i64::try_from(command_started.elapsed().as_millis())
.context("elapsed milliseconds fit i64")?;
let proof = serde_json::json!({
"run_id": "xaztn-targeted-retry",
"scenario_id": "quarantine-retry-real-codex-source",
"issue_ids_covered": ["coding_agent_session_search-xaztn"],
"fixture_id": "codex-targeted-retry",
"command_id": "cass-quarantine-retry-apply",
"phase": "assert",
"started_at_ms": started_at_ms,
"finished_at_ms": started_at_ms + elapsed_ms,
"elapsed_ms": elapsed_ms,
"meta": {
"cass_binary_path": env!("CARGO_BIN_EXE_cass"),
"cass_version": env!("CARGO_PKG_VERSION"),
"git_revision": serde_json::Value::Null,
"cargo_profile": "test",
"feature_flags": [],
"target_dir": serde_json::Value::Null,
"data_dir": data_dir,
"config_dir": serde_json::Value::Null,
"model_dir": serde_json::Value::Null,
"source_roots": [source_path]
},
"execution": {
"argv": ["cass", "quarantine", "retry", "--data-dir", "[FIXTURE_DATA]", "--apply", "--json"],
"sanitized_env": {},
"timeout_ms": 60_000,
"exit_code": applied.status.code(),
"signal": serde_json::Value::Null,
"timed_out": false,
"retry_count": 0
},
"artifacts": {
"stdout_path": stdout_path,
"stderr_path": stderr_path,
"parsed_stdout_json": applied_json,
"parsed_stderr_json_if_expected": serde_json::Value::Null,
"robot_contract_ok": true,
"ansi_free_stdout_ok": !applied.stdout.contains(&0x1b)
},
"assertions": [
{"name": "dry_run_read_only", "expected": true, "actual": true, "status": "pass"},
{"name": "exact_key_cleared", "expected": 1, "actual": 1, "status": "pass"},
{"name": "source_preserved", "expected": true, "actual": true, "status": "pass"},
{"name": "messages_persisted_once", "expected": 2, "actual": message_count, "status": "pass"}
],
"redaction_status": "safe",
"privacy_notes": "fixture-local paths only; no raw conversation text retained",
"artifact_manifest_path": manifest_path,
"stale_artifact_check": "fresh",
"generated_artifact_check": "real_binary_output",
"outcome": "passed"
});
fs::write(
&manifest_path,
format!("{}\n", serde_json::to_string(&proof)?),
)?;
let manifest_text = fs::read_to_string(&manifest_path)?;
let manifest_lines = manifest_text.lines().collect::<Vec<_>>();
ensure!(manifest_lines.len() == 1, "one proof record is required");
let verified: serde_json::Value = serde_json::from_str(
manifest_lines
.first()
.context("missing structured proof record")?,
)?;
ensure!(
verified["outcome"]
.as_str()
.is_some_and(|value| matches!(value, "passed")),
"proof outcome must be passed"
);
ensure!(
matches!(verified["execution"]["timed_out"].as_bool(), Some(false)),
"proof must distinguish a completed command from a timeout"
);
ensure!(
matches!(
verified["artifacts"]["robot_contract_ok"].as_bool(),
Some(true)
),
"proof must record parsed robot JSON"
);
ensure!(
verified["redaction_status"]
.as_str()
.is_some_and(|value| matches!(value, "safe")),
"proof manifest must pass its redaction review"
);
Ok(())
}