use std::collections::BTreeMap;
use std::fs::{self, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::disk_pressure_report::ExecFailureClass;
use crate::incident::IncidentReasonCode;
use crate::proof_intent::ReplayDecision;
use crate::readiness::DecisiveBlocker;
use crate::schema_versions::{SchemaComponent, current_version};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProofState {
Queued,
Blocked,
Replaying,
Passed,
Failed,
Stale,
}
impl ProofState {
pub const ALL: &'static [ProofState] = &[
Self::Queued,
Self::Blocked,
Self::Replaying,
Self::Passed,
Self::Failed,
Self::Stale,
];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
ProofState::Queued => "queued",
ProofState::Blocked => "blocked",
ProofState::Replaying => "replaying",
ProofState::Passed => "passed",
ProofState::Failed => "failed",
ProofState::Stale => "stale",
}
}
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(
self,
ProofState::Passed | ProofState::Failed | ProofState::Stale
)
}
#[must_use]
pub fn can_transition_to(self, next: ProofState) -> bool {
if self == next {
return true;
}
match self {
ProofState::Queued => matches!(
next,
ProofState::Blocked | ProofState::Replaying | ProofState::Stale
),
ProofState::Blocked => matches!(
next,
ProofState::Queued | ProofState::Replaying | ProofState::Stale
),
ProofState::Replaying => matches!(
next,
ProofState::Passed | ProofState::Failed | ProofState::Queued | ProofState::Stale
),
ProofState::Passed | ProofState::Failed | ProofState::Stale => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReplayOutcome {
Succeeded,
ProductFailed,
InfrastructureFailed,
}
impl ReplayOutcome {
#[must_use]
pub fn classify(success: bool, failure_class: ExecFailureClass) -> Self {
if success {
return ReplayOutcome::Succeeded;
}
match failure_class {
ExecFailureClass::ProductCompile => ReplayOutcome::ProductFailed,
ExecFailureClass::WorkerEnvironment | ExecFailureClass::Indeterminate => {
ReplayOutcome::InfrastructureFailed
}
}
}
#[must_use]
pub fn resolved_state(self) -> ProofState {
match self {
ReplayOutcome::Succeeded => ProofState::Passed,
ReplayOutcome::ProductFailed => ProofState::Failed,
ReplayOutcome::InfrastructureFailed => ProofState::Queued,
}
}
}
#[derive(Debug, Clone)]
pub struct ConveyorSignals {
pub replay: ReplayDecision,
pub blocker: DecisiveBlocker,
pub replay_capacity_available: bool,
}
impl ConveyorSignals {
#[must_use]
pub fn ready() -> Self {
Self {
replay: ReplayDecision::Replayable,
blocker: DecisiveBlocker::None,
replay_capacity_available: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConveyorDecision {
pub next_state: ProofState,
pub attempt_replay: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<IncidentReasonCode>,
pub detail: String,
}
impl ConveyorDecision {
fn new(
next_state: ProofState,
attempt_replay: bool,
reason: Option<IncidentReasonCode>,
detail: impl Into<String>,
) -> Self {
Self {
next_state,
attempt_replay,
reason,
detail: detail.into(),
}
}
}
#[must_use]
pub fn decide(current: ProofState, signals: &ConveyorSignals) -> ConveyorDecision {
if current.is_terminal() {
return ConveyorDecision::new(
current,
false,
None,
format!("intent is in terminal state `{}`", current.as_str()),
);
}
if let ReplayDecision::Rejected { reason, detail } = &signals.replay {
return ConveyorDecision::new(ProofState::Stale, false, Some(*reason), detail.clone());
}
match signals.blocker {
DecisiveBlocker::None => {
if signals.replay_capacity_available {
ConveyorDecision::new(
ProofState::Replaying,
true,
None,
"remote ready and replay capacity available; replaying",
)
} else {
ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::InsufficientSlots),
"remote ready but holding replay to avoid starving interactive work",
)
}
}
DecisiveBlocker::DaemonDown => ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::DaemonSocketRefused),
"daemon socket unreachable; queued pending daemon recovery",
),
DecisiveBlocker::NoDesiredFleet => ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::NoAdmissibleWorkers),
"no workers configured/desired; queued pending fleet configuration",
),
DecisiveBlocker::NoHealthyWorkers => ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::NoAdmissibleWorkers),
"workers desired but none live/healthy; queued pending recovery/canary rejoin",
),
DecisiveBlocker::PressureBlocked => ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::CriticalPressure),
"admissibility blocked by critical pressure; queued pending pressure recovery",
),
DecisiveBlocker::ArtifactMissed => ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::ArtifactMiss),
"last attempt failed at artifact retrieval; queued to retry",
),
DecisiveBlocker::NoCommandCapability => ConveyorDecision::new(
ProofState::Blocked,
false,
Some(IncidentReasonCode::MissingRuntimeToolchainTarget),
"no worker can run this command (capability gap); blocked pending capability",
),
DecisiveBlocker::ProofRefused => ConveyorDecision::new(
ProofState::Blocked,
false,
Some(IncidentReasonCode::ProofRefusal),
"proof refused before execution (placement/policy); blocked pending operator",
),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofReplayRecord {
pub schema_version: String,
pub intent_id: String,
pub recorded_at_unix_ms: u64,
pub state: ProofState,
#[serde(default)]
pub attempts: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_reason: Option<IncidentReasonCode>,
pub last_detail: String,
pub updated_at_unix_ms: u64,
}
impl ProofReplayRecord {
#[must_use]
pub fn queued(intent_id: impl Into<String>, recorded_at_unix_ms: u64) -> Self {
Self {
schema_version: proof_replay_schema_version().to_string(),
intent_id: intent_id.into(),
recorded_at_unix_ms,
state: ProofState::Queued,
attempts: 0,
last_reason: None,
last_detail: "recorded; queued for deferred replay".to_string(),
updated_at_unix_ms: recorded_at_unix_ms,
}
}
#[must_use]
pub fn apply(&self, decision: &ConveyorDecision, now_unix_ms: u64) -> Self {
let mut next = self.clone();
next.updated_at_unix_ms = now_unix_ms;
if !self.state.can_transition_to(decision.next_state) {
return next;
}
next.state = decision.next_state;
next.last_reason = decision.reason;
next.last_detail = decision.detail.clone();
if decision.attempt_replay {
next.attempts = next.attempts.saturating_add(1);
}
next
}
#[must_use]
pub fn resolve(&self, outcome: ReplayOutcome, now_unix_ms: u64) -> Self {
let next_state = outcome.resolved_state();
let mut next = self.clone();
next.updated_at_unix_ms = now_unix_ms;
if !self.state.can_transition_to(next_state) {
return next;
}
next.state = next_state;
next.last_detail = match outcome {
ReplayOutcome::Succeeded => "replay succeeded remotely; proof passed".to_string(),
ReplayOutcome::ProductFailed => {
"replay ran remotely; product failed (compile/test)".to_string()
}
ReplayOutcome::InfrastructureFailed => {
"replay hit an infrastructure failure; re-queued for retry".to_string()
}
};
next.last_reason = match outcome {
ReplayOutcome::Succeeded | ReplayOutcome::ProductFailed => None,
ReplayOutcome::InfrastructureFailed => Some(IncidentReasonCode::ArtifactMiss),
};
next
}
}
#[must_use]
pub fn proof_replay_schema_version() -> &'static str {
current_version(SchemaComponent::ProofReplayState)
}
#[derive(Debug, Clone)]
pub struct ConveyorScanItem {
pub intent_id: String,
pub current_state: ProofState,
pub signals: ConveyorSignals,
pub recorded_at_unix_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConveyorScanResult {
pub intent_id: String,
pub decision: ConveyorDecision,
}
#[must_use]
pub fn plan_scan(
items: &[ConveyorScanItem],
max_concurrent_replays: usize,
) -> Vec<ConveyorScanResult> {
let mut decisions: Vec<ConveyorDecision> = items
.iter()
.map(|it| decide(it.current_state, &it.signals))
.collect();
let mut replay_candidates: Vec<usize> = decisions
.iter()
.enumerate()
.filter(|(_, d)| d.next_state == ProofState::Replaying)
.map(|(i, _)| i)
.collect();
replay_candidates.sort_by(|&a, &b| {
items[a]
.recorded_at_unix_ms
.cmp(&items[b].recorded_at_unix_ms)
.then_with(|| items[a].intent_id.cmp(&items[b].intent_id))
});
for &idx in replay_candidates.iter().skip(max_concurrent_replays) {
decisions[idx] = ConveyorDecision::new(
ProofState::Queued,
false,
Some(IncidentReasonCode::InsufficientSlots),
"replay deferred: concurrent replay cap reached (fairness/anti-starvation)",
);
}
items
.iter()
.zip(decisions)
.map(|(it, decision)| ConveyorScanResult {
intent_id: it.intent_id.clone(),
decision,
})
.collect()
}
#[derive(Debug, Clone)]
pub struct ProofReplayStateStore {
path: PathBuf,
}
impl ProofReplayStateStore {
#[must_use]
pub fn with_path(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
pub fn put(&self, record: &ProofReplayRecord) -> std::io::Result<()> {
if let Some(parent) = self.path.parent()
&& !parent.as_os_str().is_empty()
{
fs::create_dir_all(parent)?;
}
let mut line = serde_json::to_string(record)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
line.push('\n');
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
file.write_all(line.as_bytes())?;
file.flush()
}
#[must_use]
pub fn all(&self) -> Vec<ProofReplayRecord> {
let file = match fs::File::open(&self.path) {
Ok(f) => f,
Err(_) => return Vec::new(),
};
let mut order: Vec<String> = Vec::new();
let mut latest: BTreeMap<String, ProofReplayRecord> = BTreeMap::new();
for line in BufReader::new(file).lines() {
let Ok(line) = line else { break };
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Ok(record) = serde_json::from_str::<ProofReplayRecord>(trimmed) {
if !latest.contains_key(&record.intent_id) {
order.push(record.intent_id.clone());
}
latest.insert(record.intent_id.clone(), record);
}
}
order
.into_iter()
.filter_map(|id| latest.remove(&id))
.collect()
}
#[must_use]
pub fn get(&self, intent_id: &str) -> Option<ProofReplayRecord> {
self.all().into_iter().find(|r| r.intent_id == intent_id)
}
#[must_use]
pub fn by_state(&self, state: ProofState) -> Vec<ProofReplayRecord> {
self.all()
.into_iter()
.filter(|r| r.state == state)
.collect()
}
#[must_use]
pub fn census(&self) -> BTreeMap<String, usize> {
let mut census: BTreeMap<String, usize> = BTreeMap::new();
for record in self.all() {
*census.entry(record.state.as_str().to_string()).or_insert(0) += 1;
}
census
}
pub fn advance(
&self,
signals_by_intent: &BTreeMap<String, ConveyorSignals>,
max_concurrent_replays: usize,
now_unix_ms: u64,
) -> std::io::Result<Vec<ConveyorScanResult>> {
let records: Vec<ProofReplayRecord> = self
.all()
.into_iter()
.filter(|r| !r.state.is_terminal())
.collect();
let items: Vec<ConveyorScanItem> = records
.iter()
.filter_map(|r| {
signals_by_intent
.get(&r.intent_id)
.map(|signals| ConveyorScanItem {
intent_id: r.intent_id.clone(),
current_state: r.state,
signals: signals.clone(),
recorded_at_unix_ms: r.recorded_at_unix_ms,
})
})
.collect();
let results = plan_scan(&items, max_concurrent_replays);
for result in &results {
if let Some(record) = records.iter().find(|r| r.intent_id == result.intent_id) {
let next = record.apply(&result.decision, now_unix_ms);
self.put(&next)?;
}
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proof_intent::{
ProofIntent, ReplayConstraints, ReplayContext, SourceFingerprint, StaleSourcePolicy,
validate_replay,
};
fn rejected(detail: &str) -> ReplayDecision {
ReplayDecision::Rejected {
reason: IncidentReasonCode::ProofRefusal,
detail: detail.to_string(),
}
}
#[test]
fn states_have_unique_stable_tokens() {
assert_eq!(ProofState::ALL.len(), 6);
let mut tokens: Vec<&str> = ProofState::ALL.iter().map(|s| s.as_str()).collect();
let before = tokens.len();
tokens.sort_unstable();
tokens.dedup();
assert_eq!(before, tokens.len(), "duplicate proof-state tokens");
}
#[test]
fn terminal_states_are_final() {
for terminal in [ProofState::Passed, ProofState::Failed, ProofState::Stale] {
assert!(terminal.is_terminal());
for &next in ProofState::ALL {
let legal = terminal.can_transition_to(next);
assert_eq!(legal, next == terminal, "{terminal:?} -> {next:?}");
}
}
}
#[test]
fn non_terminal_states_can_reach_terminals() {
for start in [
ProofState::Queued,
ProofState::Blocked,
ProofState::Replaying,
] {
assert!(!start.is_terminal());
assert!(start.can_transition_to(ProofState::Stale));
}
assert!(ProofState::Replaying.can_transition_to(ProofState::Passed));
assert!(ProofState::Replaying.can_transition_to(ProofState::Failed));
assert!(!ProofState::Queued.can_transition_to(ProofState::Passed));
assert!(!ProofState::Queued.can_transition_to(ProofState::Failed));
}
#[test]
fn states_serialize_with_snake_case_tokens() {
assert_eq!(
serde_json::to_value(ProofState::Replaying).unwrap(),
"replaying"
);
let back: ProofState = serde_json::from_str("\"stale\"").unwrap();
assert_eq!(back, ProofState::Stale);
}
#[test]
fn outcome_classification_and_resolution() {
assert_eq!(
ReplayOutcome::classify(true, ExecFailureClass::Indeterminate),
ReplayOutcome::Succeeded
);
assert_eq!(
ReplayOutcome::classify(false, ExecFailureClass::ProductCompile),
ReplayOutcome::ProductFailed
);
assert_eq!(
ReplayOutcome::classify(false, ExecFailureClass::WorkerEnvironment),
ReplayOutcome::InfrastructureFailed
);
assert_eq!(
ReplayOutcome::classify(false, ExecFailureClass::Indeterminate),
ReplayOutcome::InfrastructureFailed
);
assert_eq!(
ReplayOutcome::Succeeded.resolved_state(),
ProofState::Passed
);
assert_eq!(
ReplayOutcome::ProductFailed.resolved_state(),
ProofState::Failed
);
assert_eq!(
ReplayOutcome::InfrastructureFailed.resolved_state(),
ProofState::Queued
);
}
#[test]
fn decide_replays_when_ready_with_capacity() {
let d = decide(ProofState::Queued, &ConveyorSignals::ready());
assert_eq!(d.next_state, ProofState::Replaying);
assert!(d.attempt_replay);
assert!(d.reason.is_none());
}
#[test]
fn decide_holds_queued_when_ready_but_no_capacity_protecting_interactive() {
let signals = ConveyorSignals {
replay_capacity_available: false,
..ConveyorSignals::ready()
};
let d = decide(ProofState::Queued, &signals);
assert_eq!(d.next_state, ProofState::Queued);
assert!(!d.attempt_replay);
assert_eq!(d.reason, Some(IncidentReasonCode::InsufficientSlots));
}
#[test]
fn scenario_recovery_after_disk_pressure() {
let pressured = ConveyorSignals {
replay: ReplayDecision::Replayable,
blocker: DecisiveBlocker::PressureBlocked,
replay_capacity_available: true,
};
let d = decide(ProofState::Queued, &pressured);
assert_eq!(d.next_state, ProofState::Queued);
assert!(!d.attempt_replay);
assert_eq!(d.reason, Some(IncidentReasonCode::CriticalPressure));
let d2 = decide(ProofState::Queued, &ConveyorSignals::ready());
assert_eq!(d2.next_state, ProofState::Replaying);
assert!(d2.attempt_replay);
}
#[test]
fn scenario_source_changed_while_queued_goes_stale() {
let signals = ConveyorSignals {
replay: rejected("source fingerprint mismatch (source changed)"),
blocker: DecisiveBlocker::None,
replay_capacity_available: true,
};
let d = decide(ProofState::Queued, &signals);
assert_eq!(d.next_state, ProofState::Stale);
assert!(!d.attempt_replay);
assert!(d.detail.contains("source changed"));
}
#[test]
fn scenario_capability_still_missing_stays_blocked() {
let signals = ConveyorSignals {
replay: ReplayDecision::Replayable,
blocker: DecisiveBlocker::NoCommandCapability,
replay_capacity_available: true,
};
let d = decide(ProofState::Queued, &signals);
assert_eq!(d.next_state, ProofState::Blocked);
assert!(!d.attempt_replay);
assert_eq!(
d.reason,
Some(IncidentReasonCode::MissingRuntimeToolchainTarget)
);
}
#[test]
fn scenario_worker_eligible_after_canary_rejoin() {
let no_healthy = ConveyorSignals {
replay: ReplayDecision::Replayable,
blocker: DecisiveBlocker::NoHealthyWorkers,
replay_capacity_available: true,
};
let d = decide(ProofState::Queued, &no_healthy);
assert_eq!(d.next_state, ProofState::Queued);
assert!(d.detail.contains("canary rejoin"));
let d2 = decide(ProofState::Queued, &ConveyorSignals::ready());
assert_eq!(d2.next_state, ProofState::Replaying);
assert!(d2.attempt_replay);
}
#[test]
fn decide_never_replays_under_any_safety_block() {
for blocker in [
DecisiveBlocker::DaemonDown,
DecisiveBlocker::NoDesiredFleet,
DecisiveBlocker::NoHealthyWorkers,
DecisiveBlocker::NoCommandCapability,
DecisiveBlocker::PressureBlocked,
DecisiveBlocker::ArtifactMissed,
DecisiveBlocker::ProofRefused,
] {
let signals = ConveyorSignals {
replay: ReplayDecision::Replayable,
blocker,
replay_capacity_available: true,
};
let d = decide(ProofState::Queued, &signals);
assert!(!d.attempt_replay, "must not replay under {blocker:?}");
assert_ne!(d.next_state, ProofState::Replaying);
}
}
#[test]
fn decide_is_noop_on_terminal_states() {
for terminal in [ProofState::Passed, ProofState::Failed, ProofState::Stale] {
let d = decide(terminal, &ConveyorSignals::ready());
assert_eq!(d.next_state, terminal);
assert!(!d.attempt_replay);
}
}
#[test]
fn decide_uses_real_validate_replay_for_staleness() {
let intent = ProofIntent::new(
"blake3:abc",
"/data/projects/foo",
Some("rev-1".to_string()),
"pooled",
IncidentReasonCode::ProofRefusal,
StaleSourcePolicy::RejectIfChanged,
ReplayConstraints {
require_same_revision: true,
require_unchanged_sources: true,
max_age_secs: Some(3600),
},
1_700_000_000_000,
)
.with_source_fingerprints(vec![SourceFingerprint {
path: "src/lib.rs".to_string(),
blake3: "deadbeef".to_string(),
}]);
let ctx = ReplayContext {
current_revision: Some("rev-1".to_string()),
current_fingerprints: vec![SourceFingerprint {
path: "src/lib.rs".to_string(),
blake3: "CHANGED".to_string(),
}],
age_secs: 10,
};
let signals = ConveyorSignals {
replay: validate_replay(&intent, &ctx),
blocker: DecisiveBlocker::None,
replay_capacity_available: true,
};
let _ = &intent;
assert_eq!(
decide(ProofState::Queued, &signals).next_state,
ProofState::Stale
);
}
#[test]
fn record_apply_advances_and_counts_attempts() {
let rec = ProofReplayRecord::queued("pi-1", 1_000);
assert_eq!(rec.state, ProofState::Queued);
assert_eq!(rec.attempts, 0);
let d = decide(ProofState::Queued, &ConveyorSignals::ready());
let replaying = rec.apply(&d, 2_000);
assert_eq!(replaying.state, ProofState::Replaying);
assert_eq!(replaying.attempts, 1, "attempt counted on replay");
assert_eq!(replaying.updated_at_unix_ms, 2_000);
let passed = replaying.resolve(ReplayOutcome::Succeeded, 3_000);
assert_eq!(passed.state, ProofState::Passed);
assert_eq!(passed.attempts, 1, "resolve does not re-count");
}
#[test]
fn record_apply_rejects_illegal_transition_from_terminal() {
let passed = ProofReplayRecord::queued("pi-1", 1)
.apply(&decide(ProofState::Queued, &ConveyorSignals::ready()), 2)
.resolve(ReplayOutcome::Succeeded, 3);
assert_eq!(passed.state, ProofState::Passed);
let illegal = ConveyorDecision {
next_state: ProofState::Queued,
attempt_replay: false,
reason: None,
detail: "spurious".to_string(),
};
let after = passed.apply(&illegal, 4);
assert_eq!(after.state, ProofState::Passed, "terminal state preserved");
assert_eq!(after.updated_at_unix_ms, 4);
}
#[test]
fn record_resolve_infra_failure_requeues_for_retry() {
let replaying = ProofReplayRecord::queued("pi-1", 1)
.apply(&decide(ProofState::Queued, &ConveyorSignals::ready()), 2);
let requeued = replaying.resolve(ReplayOutcome::InfrastructureFailed, 3);
assert_eq!(requeued.state, ProofState::Queued);
assert_eq!(requeued.attempts, 1);
assert_eq!(requeued.last_reason, Some(IncidentReasonCode::ArtifactMiss));
}
#[test]
fn record_roundtrips_and_stamps_schema_version() {
let rec = ProofReplayRecord::queued("pi-xyz", 42);
assert_eq!(rec.schema_version, proof_replay_schema_version());
let back: ProofReplayRecord =
serde_json::from_str(&serde_json::to_string(&rec).unwrap()).unwrap();
assert_eq!(rec, back);
}
fn ready_item(id: &str, recorded: u64) -> ConveyorScanItem {
ConveyorScanItem {
intent_id: id.to_string(),
current_state: ProofState::Queued,
signals: ConveyorSignals::ready(),
recorded_at_unix_ms: recorded,
}
}
#[test]
fn plan_scan_caps_concurrent_replays_oldest_first() {
let items = vec![
ready_item("pi-new", 3_000),
ready_item("pi-old", 1_000),
ready_item("pi-mid", 2_000),
];
let plan = plan_scan(&items, 2);
let by_id = |id: &str| {
plan.iter()
.find(|r| r.intent_id == id)
.unwrap()
.decision
.clone()
};
assert_eq!(by_id("pi-old").next_state, ProofState::Replaying);
assert_eq!(by_id("pi-mid").next_state, ProofState::Replaying);
let newest = by_id("pi-new");
assert_eq!(newest.next_state, ProofState::Queued);
assert!(!newest.attempt_replay);
assert_eq!(newest.reason, Some(IncidentReasonCode::InsufficientSlots));
assert_eq!(plan[0].intent_id, "pi-new");
}
#[test]
fn plan_scan_zero_cap_admits_no_replays() {
let items = vec![ready_item("pi-1", 1), ready_item("pi-2", 2)];
let plan = plan_scan(&items, 0);
assert!(plan.iter().all(|r| !r.decision.attempt_replay));
assert!(
plan.iter()
.all(|r| r.decision.next_state == ProofState::Queued)
);
}
#[test]
fn plan_scan_does_not_demote_non_replay_states() {
let mut blocked = ready_item("pi-blocked", 1);
blocked.signals.blocker = DecisiveBlocker::NoCommandCapability;
let mut stale = ready_item("pi-stale", 2);
stale.signals.replay = rejected("source changed");
let items = vec![blocked, stale, ready_item("pi-ready", 3)];
let plan = plan_scan(&items, 0);
let by_id = |id: &str| plan.iter().find(|r| r.intent_id == id).unwrap();
assert_eq!(by_id("pi-blocked").decision.next_state, ProofState::Blocked);
assert_eq!(by_id("pi-stale").decision.next_state, ProofState::Stale);
assert_eq!(by_id("pi-ready").decision.next_state, ProofState::Queued);
}
#[test]
fn store_dedups_and_reports_latest_state() {
let dir = tempfile::tempdir().unwrap();
let store = ProofReplayStateStore::with_path(dir.path().join("replay.jsonl"));
let rec = ProofReplayRecord::queued("pi-1", 1);
store.put(&rec).unwrap();
let replaying = rec.apply(&decide(ProofState::Queued, &ConveyorSignals::ready()), 2);
store.put(&replaying).unwrap();
let passed = replaying.resolve(ReplayOutcome::Succeeded, 3);
store.put(&passed).unwrap();
let all = store.all();
assert_eq!(all.len(), 1, "dedup by intent_id");
assert_eq!(all[0].state, ProofState::Passed, "latest wins");
assert_eq!(store.get("pi-1").unwrap().state, ProofState::Passed);
assert!(store.get("pi-missing").is_none());
}
#[test]
fn store_by_state_and_census() {
let dir = tempfile::tempdir().unwrap();
let store = ProofReplayStateStore::with_path(dir.path().join("replay.jsonl"));
store.put(&ProofReplayRecord::queued("pi-q1", 1)).unwrap();
store.put(&ProofReplayRecord::queued("pi-q2", 2)).unwrap();
let blocked = {
let mut sig = ConveyorSignals::ready();
sig.blocker = DecisiveBlocker::NoCommandCapability;
ProofReplayRecord::queued("pi-b", 3).apply(&decide(ProofState::Queued, &sig), 4)
};
store.put(&blocked).unwrap();
assert_eq!(store.by_state(ProofState::Queued).len(), 2);
assert_eq!(store.by_state(ProofState::Blocked).len(), 1);
let census = store.census();
assert_eq!(census.get("queued"), Some(&2));
assert_eq!(census.get("blocked"), Some(&1));
}
#[test]
fn store_is_corruption_tolerant() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("replay.jsonl");
let store = ProofReplayStateStore::with_path(&path);
store.put(&ProofReplayRecord::queued("pi-1", 1)).unwrap();
{
let mut f = OpenOptions::new().append(true).open(&path).unwrap();
writeln!(f, "{{ not json").unwrap();
}
store.put(&ProofReplayRecord::queued("pi-2", 2)).unwrap();
assert_eq!(store.all().len(), 2, "corrupt line skipped, not fatal");
}
#[test]
fn missing_store_reads_empty() {
let dir = tempfile::tempdir().unwrap();
let store = ProofReplayStateStore::with_path(dir.path().join("nope.jsonl"));
assert!(store.all().is_empty());
assert!(store.census().is_empty());
}
#[test]
fn record_preserves_recorded_at_across_transitions() {
let rec = ProofReplayRecord::queued("pi-1", 5_000);
let replaying = rec.apply(
&decide(ProofState::Queued, &ConveyorSignals::ready()),
6_000,
);
let passed = replaying.resolve(ReplayOutcome::Succeeded, 7_000);
assert_eq!(rec.recorded_at_unix_ms, 5_000);
assert_eq!(replaying.recorded_at_unix_ms, 5_000, "preserved on apply");
assert_eq!(passed.recorded_at_unix_ms, 5_000, "preserved on resolve");
assert_eq!(passed.updated_at_unix_ms, 7_000);
}
#[test]
fn advance_persists_transitions_and_honors_fairness_and_terminals() {
let dir = tempfile::tempdir().unwrap();
let store = ProofReplayStateStore::with_path(dir.path().join("replay.jsonl"));
store
.put(&ProofReplayRecord::queued("pi-old", 1_000))
.unwrap();
store
.put(&ProofReplayRecord::queued("pi-mid", 2_000))
.unwrap();
store
.put(&ProofReplayRecord::queued("pi-new", 3_000))
.unwrap();
let passed = ProofReplayRecord::queued("pi-done", 500)
.apply(&decide(ProofState::Queued, &ConveyorSignals::ready()), 600)
.resolve(ReplayOutcome::Succeeded, 700);
store.put(&passed).unwrap();
let mut signals: BTreeMap<String, ConveyorSignals> = BTreeMap::new();
for id in ["pi-old", "pi-mid", "pi-new", "pi-done"] {
signals.insert(id.to_string(), ConveyorSignals::ready());
}
let results = store.advance(&signals, 1, 10_000).unwrap();
assert!(results.iter().all(|r| r.intent_id != "pi-done"));
assert_eq!(results.len(), 3);
let replaying: Vec<&str> = results
.iter()
.filter(|r| r.decision.attempt_replay)
.map(|r| r.intent_id.as_str())
.collect();
assert_eq!(replaying, vec!["pi-old"]);
assert_eq!(store.get("pi-old").unwrap().state, ProofState::Replaying);
assert_eq!(store.get("pi-old").unwrap().attempts, 1);
assert_eq!(store.get("pi-mid").unwrap().state, ProofState::Queued);
assert_eq!(store.get("pi-done").unwrap().state, ProofState::Passed);
let results2 = store.advance(&signals, 8, 11_000).unwrap();
assert!(
results2
.iter()
.any(|r| r.intent_id == "pi-mid" && r.decision.attempt_replay)
);
}
#[test]
fn advance_skips_intents_without_signals() {
let dir = tempfile::tempdir().unwrap();
let store = ProofReplayStateStore::with_path(dir.path().join("replay.jsonl"));
store.put(&ProofReplayRecord::queued("pi-seen", 1)).unwrap();
store
.put(&ProofReplayRecord::queued("pi-unseen", 2))
.unwrap();
let mut signals: BTreeMap<String, ConveyorSignals> = BTreeMap::new();
signals.insert("pi-seen".to_string(), ConveyorSignals::ready());
let results = store.advance(&signals, 8, 100).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].intent_id, "pi-seen");
assert_eq!(store.get("pi-unseen").unwrap().state, ProofState::Queued);
assert_eq!(store.get("pi-unseen").unwrap().updated_at_unix_ms, 2);
}
}