#![allow(dead_code)]
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ProofOutcome {
Passed,
Failed,
TimedOutPartial,
StaleArtifactReused,
InvalidJson,
DidNotRun,
}
impl ProofOutcome {
pub(crate) fn is_pass(self) -> bool {
matches!(self, Self::Passed)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct OutcomeSignals {
pub executed: bool,
pub timed_out: bool,
pub stale_artifact_reused: bool,
pub exit_code: Option<i32>,
pub expects_json: bool,
pub parsed_json_ok: bool,
pub robot_contract_ok: bool,
pub ansi_free_stdout_ok: bool,
}
impl OutcomeSignals {
pub(crate) fn outcome(&self) -> ProofOutcome {
if !self.executed {
return ProofOutcome::DidNotRun;
}
if self.timed_out {
return ProofOutcome::TimedOutPartial;
}
if self.stale_artifact_reused {
return ProofOutcome::StaleArtifactReused;
}
if self.expects_json && !self.parsed_json_ok {
return ProofOutcome::InvalidJson;
}
if self.exit_code == Some(0) && self.robot_contract_ok && self.ansi_free_stdout_ok {
return ProofOutcome::Passed;
}
ProofOutcome::Failed
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ProofRunMeta {
pub cass_binary_path: String,
pub cass_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub git_revision: Option<String>,
pub cargo_profile: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub feature_flags: Vec<String>,
pub target_dir: String,
pub data_dir: String,
pub config_dir: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_dir: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_roots: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ProofExecution {
pub argv: Vec<String>,
#[serde(default)]
pub sanitized_env: BTreeMap<String, String>,
pub timeout_ms: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signal: Option<i32>,
pub timed_out: bool,
pub retry_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ProofArtifacts {
pub stdout_path: String,
pub stderr_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parsed_stdout_json: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parsed_stderr_json: Option<serde_json::Value>,
pub robot_contract_ok: bool,
pub ansi_free_stdout_ok: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ProofLogRecord {
pub run_id: String,
pub scenario_id: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issue_ids_covered: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fixture_id: Option<String>,
pub command_id: String,
pub phase: String,
pub started_at_ms: i64,
pub finished_at_ms: i64,
pub elapsed_ms: i64,
pub meta: ProofRunMeta,
pub execution: ProofExecution,
pub artifacts: ProofArtifacts,
pub outcome: ProofOutcome,
}
impl ProofLogRecord {
pub(crate) fn is_pass(&self) -> bool {
self.outcome.is_pass()
}
}
const SECRET_ENV_MARKERS: &[&str] = &[
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"API_KEY",
"APIKEY",
"CREDENTIAL",
"PRIVATE_KEY",
"SESSION",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct RetentionPolicy {
pub keep_last_n: usize,
pub keep_all_non_pass: bool,
pub max_age_ms: i64,
}
impl Default for RetentionPolicy {
fn default() -> Self {
Self {
keep_last_n: 20,
keep_all_non_pass: true,
max_age_ms: 14 * 24 * 60 * 60 * 1000, }
}
}
impl RetentionPolicy {
pub(crate) fn retains(
&self,
record: &ProofLogRecord,
rank_from_newest: usize,
now_ms: i64,
) -> bool {
if self.keep_all_non_pass && !record.is_pass() {
return true;
}
if rank_from_newest < self.keep_last_n {
return true;
}
let age = (now_ms - record.finished_at_ms).max(0);
age <= self.max_age_ms
}
pub(crate) fn secret_leak_keys(record: &ProofLogRecord) -> Vec<String> {
record
.execution
.sanitized_env
.keys()
.filter(|k| {
let up = k.to_ascii_uppercase();
SECRET_ENV_MARKERS.iter().any(|m| up.contains(m))
})
.cloned()
.collect()
}
pub(crate) fn is_redaction_safe(record: &ProofLogRecord) -> bool {
Self::secret_leak_keys(record).is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn green_signals() -> OutcomeSignals {
OutcomeSignals {
executed: true,
timed_out: false,
stale_artifact_reused: false,
exit_code: Some(0),
expects_json: true,
parsed_json_ok: true,
robot_contract_ok: true,
ansi_free_stdout_ok: true,
}
}
fn record_with(
outcome: ProofOutcome,
env: &[(&str, &str)],
finished_at_ms: i64,
) -> ProofLogRecord {
ProofLogRecord {
run_id: "run-1".to_string(),
scenario_id: "scn-archive-risk".to_string(),
issue_ids_covered: vec!["#248".to_string()],
fixture_id: Some("ts1_high_archive_risk".to_string()),
command_id: "status_json".to_string(),
phase: "verify".to_string(),
started_at_ms: finished_at_ms - 100,
finished_at_ms,
elapsed_ms: 100,
meta: ProofRunMeta {
cass_binary_path: "/tmp/cass-tgt/debug/cass".to_string(),
cass_version: "0.6.13".to_string(),
git_revision: Some("abc1234".to_string()),
cargo_profile: "dev".to_string(),
feature_flags: vec![],
target_dir: "/tmp/cass-tgt".to_string(),
data_dir: "/tmp/data".to_string(),
config_dir: "/tmp/config".to_string(),
model_dir: None,
source_roots: vec!["/dp/proj".to_string()],
},
execution: ProofExecution {
argv: vec![
"cass".to_string(),
"status".to_string(),
"--json".to_string(),
],
sanitized_env: env
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
timeout_ms: 5000,
exit_code: Some(0),
signal: None,
timed_out: false,
retry_count: 0,
},
artifacts: ProofArtifacts {
stdout_path: "/tmp/run-1.stdout".to_string(),
stderr_path: "/tmp/run-1.stderr".to_string(),
parsed_stdout_json: Some(serde_json::json!({"healthy": true})),
parsed_stderr_json: None,
robot_contract_ok: true,
ansi_free_stdout_ok: true,
},
outcome,
}
}
#[test]
fn outcomes_serialize_snake_case() {
assert_eq!(
serde_json::to_string(&ProofOutcome::TimedOutPartial).unwrap(),
"\"timed_out_partial\""
);
assert_eq!(
serde_json::to_string(&ProofOutcome::StaleArtifactReused).unwrap(),
"\"stale_artifact_reused\""
);
assert_eq!(
serde_json::to_string(&ProofOutcome::DidNotRun).unwrap(),
"\"did_not_run\""
);
}
#[test]
fn the_five_confusable_outcomes_are_each_distinguished() {
assert_eq!(green_signals().outcome(), ProofOutcome::Passed);
let mut s = green_signals();
s.executed = false;
assert_eq!(s.outcome(), ProofOutcome::DidNotRun);
let mut s = green_signals();
s.timed_out = true;
assert_eq!(s.outcome(), ProofOutcome::TimedOutPartial);
let mut s = green_signals();
s.stale_artifact_reused = true;
assert_eq!(s.outcome(), ProofOutcome::StaleArtifactReused);
let mut s = green_signals();
s.parsed_json_ok = false;
assert_eq!(s.outcome(), ProofOutcome::InvalidJson);
let mut s = green_signals();
s.exit_code = Some(1);
assert_eq!(s.outcome(), ProofOutcome::Failed);
}
#[test]
fn timeout_is_never_confused_with_a_clean_pass() {
let mut s = green_signals();
s.timed_out = true;
s.exit_code = Some(0);
assert_ne!(s.outcome(), ProofOutcome::Passed);
assert_eq!(s.outcome(), ProofOutcome::TimedOutPartial);
}
#[test]
fn record_round_trips_through_json_with_required_fields() {
let r = record_with(
ProofOutcome::Passed,
&[("CASS_DATA_DIR", "/tmp/data")],
1000,
);
let json = serde_json::to_string(&r).unwrap();
for key in [
"run_id",
"scenario_id",
"command_id",
"phase",
"started_at_ms",
"finished_at_ms",
"elapsed_ms",
"cass_binary_path",
"cass_version",
"argv",
"timeout_ms",
"timed_out",
"retry_count",
"stdout_path",
"stderr_path",
"robot_contract_ok",
"ansi_free_stdout_ok",
"outcome",
] {
assert!(json.contains(key), "record JSON missing {key}");
}
let parsed: ProofLogRecord = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, r);
}
#[test]
fn retention_keeps_failures_and_recent_drops_old_passes() {
let policy = RetentionPolicy {
keep_last_n: 2,
keep_all_non_pass: true,
max_age_ms: 1000,
};
let now = 1_000_000;
let old_fail = record_with(ProofOutcome::Failed, &[], now - 10_000);
assert!(policy.retains(&old_fail, 50, now));
let recent_pass = record_with(ProofOutcome::Passed, &[], now - 10_000);
assert!(policy.retains(&recent_pass, 0, now));
let old_pass = record_with(ProofOutcome::Passed, &[], now - 10_000);
assert!(!policy.retains(&old_pass, 50, now));
let fresh_pass = record_with(ProofOutcome::Passed, &[], now - 500);
assert!(policy.retains(&fresh_pass, 50, now));
}
#[test]
fn redaction_guard_flags_secret_bearing_env_keys() {
let safe = record_with(
ProofOutcome::Passed,
&[("CASS_DATA_DIR", "/tmp/data")],
1000,
);
assert!(RetentionPolicy::is_redaction_safe(&safe));
assert!(RetentionPolicy::secret_leak_keys(&safe).is_empty());
for leak in [
"ANTHROPIC_API_KEY",
"GH_TOKEN",
"DB_PASSWORD",
"AWS_SECRET_ACCESS_KEY",
] {
let bad = record_with(ProofOutcome::Passed, &[(leak, "x")], 1000);
assert!(
!RetentionPolicy::is_redaction_safe(&bad),
"{leak} should be flagged"
);
assert_eq!(
RetentionPolicy::secret_leak_keys(&bad),
vec![leak.to_string()]
);
}
}
#[test]
fn outcome_in_record_matches_serialized_form() {
let r = record_with(ProofOutcome::InvalidJson, &[], 1000);
let json = serde_json::to_string(&r).unwrap();
assert!(json.contains("\"outcome\":\"invalid_json\""));
assert!(!r.is_pass());
}
}