use std::collections::BTreeMap;
use crate::{
config::TruthMirrorConfig,
debt::{age_label, group_by_finding_class},
ledger::{Disposition, LedgerEntry, LedgerStats, StructuredFinding, Verdict},
reviewer::{QueuedReview, ReviewRunStatusCounts},
};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ViewId {
#[default]
Dashboard,
Queue,
Ledger,
Config,
Debt,
}
impl ViewId {
pub const ALL: [ViewId; 5] = [
ViewId::Dashboard,
ViewId::Queue,
ViewId::Ledger,
ViewId::Config,
ViewId::Debt,
];
pub fn title(self) -> &'static str {
match self {
Self::Dashboard => "Dashboard",
Self::Queue => "Queue",
Self::Ledger => "Ledger",
Self::Config => "Config",
Self::Debt => "Debt",
}
}
pub fn short_key(self) -> char {
match self {
Self::Dashboard => '1',
Self::Queue => '2',
Self::Ledger => '3',
Self::Config => '4',
Self::Debt => '5',
}
}
pub fn from_digit(c: char) -> Option<Self> {
match c {
'1' => Some(Self::Dashboard),
'2' => Some(Self::Queue),
'3' => Some(Self::Ledger),
'4' => Some(Self::Config),
'5' => Some(Self::Debt),
_ => None,
}
}
pub fn next(self) -> Self {
match self {
Self::Dashboard => Self::Queue,
Self::Queue => Self::Ledger,
Self::Ledger => Self::Config,
Self::Config => Self::Debt,
Self::Debt => Self::Dashboard,
}
}
pub fn prev(self) -> Self {
match self {
Self::Dashboard => Self::Debt,
Self::Queue => Self::Dashboard,
Self::Ledger => Self::Queue,
Self::Config => Self::Ledger,
Self::Debt => Self::Config,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DashboardVm {
pub version: String,
pub config_path: String,
pub state_dir: String,
pub queue_pending: usize,
pub queue_oldest_age: String,
pub runs_queued: usize,
pub runs_running: usize,
pub runs_completed: usize,
pub runs_failed: usize,
pub ledger_open: usize,
pub ledger_needs_human: usize,
pub ledger_waived: usize,
pub ledger_passed: usize,
pub ledger_flag: usize,
pub watcher_alive: bool,
pub watcher_pid: Option<u32>,
pub cards: Vec<(String, String)>,
}
pub struct DashboardInputs<'a> {
pub version: &'a str,
pub config_path: &'a str,
pub state_dir: &'a str,
pub queue_pending: usize,
pub oldest_age_secs: Option<u64>,
pub runs: &'a ReviewRunStatusCounts,
pub stats: &'a LedgerStats,
pub watcher_alive: bool,
pub watcher_pid: Option<u32>,
}
pub fn dashboard_vm(input: DashboardInputs<'_>) -> DashboardVm {
let DashboardInputs {
version,
config_path,
state_dir,
queue_pending,
oldest_age_secs,
runs,
stats,
watcher_alive,
watcher_pid,
} = input;
let queue_oldest_age = oldest_age_secs.map_or_else(|| "—".to_owned(), age_label);
let cards = vec![
(
"Queue".to_owned(),
format!("{queue_pending} pending · oldest {queue_oldest_age}"),
),
(
"Reviews".to_owned(),
format!(
"running {} · queued {} · done {} · fail {}",
runs.running, runs.queued, runs.completed, runs.failed
),
),
(
"Ledger".to_owned(),
format!(
"open {} · needs-human {} · waived {} · pass {} · flag {}",
stats.unresolved, stats.needs_human, stats.waived, stats.pass, stats.flag
),
),
(
"Watcher".to_owned(),
if watcher_alive {
watcher_pid.map_or_else(|| "alive".to_owned(), |pid| format!("alive · pid {pid}"))
} else {
"down".to_owned()
},
),
("Version".to_owned(), version.to_owned()),
("Config".to_owned(), config_path.to_owned()),
("State".to_owned(), state_dir.to_owned()),
];
DashboardVm {
version: version.to_owned(),
config_path: config_path.to_owned(),
state_dir: state_dir.to_owned(),
queue_pending,
queue_oldest_age,
runs_queued: runs.queued,
runs_running: runs.running,
runs_completed: runs.completed,
runs_failed: runs.failed,
ledger_open: stats.unresolved,
ledger_needs_human: stats.needs_human,
ledger_waived: stats.waived,
ledger_passed: stats.pass,
ledger_flag: stats.flag,
watcher_alive,
watcher_pid,
cards,
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct QueueRow {
pub sha: String,
pub short_sha: String,
pub subject: String,
pub age: String,
pub petition_for: String,
pub run_id: String,
}
pub fn queue_rows(
items: &[QueuedReview],
subjects: &BTreeMap<String, String>,
now: u64,
) -> Vec<QueueRow> {
items
.iter()
.map(|item| {
let age = age_label(now.saturating_sub(item.enqueued_at_unix));
let subject = subjects
.get(&item.commit_sha)
.cloned()
.unwrap_or_else(|| "(unknown subject)".to_owned());
QueueRow {
short_sha: short_sha(&item.commit_sha),
sha: item.commit_sha.clone(),
subject,
age,
petition_for: item
.petition_for
.as_ref()
.map(|sha| short_sha(sha))
.unwrap_or_else(|| "—".to_owned()),
run_id: item.run_id.clone(),
}
})
.collect()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LedgerRow {
pub sha: String,
pub short_sha: String,
pub status: String,
pub age: String,
pub summary: String,
pub is_actionable_reject: bool,
}
pub fn ledger_rows(entries: &[LedgerEntry], now: u64) -> Vec<LedgerRow> {
let mut indexed: Vec<(u64, LedgerRow)> = entries
.iter()
.map(|entry| {
let status = status_label(entry);
(
entry.updated_at_unix,
LedgerRow {
short_sha: short_sha(&entry.commit_sha),
sha: entry.commit_sha.clone(),
status,
age: age_label(now.saturating_sub(entry.updated_at_unix)),
summary: first_line(&entry.summary, &entry.claim),
is_actionable_reject: entry.is_unresolved_rejection() || entry.is_needs_human(),
},
)
})
.collect();
indexed.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.sha.cmp(&b.1.sha)));
indexed.into_iter().map(|(_, row)| row).collect()
}
fn status_label(entry: &LedgerEntry) -> String {
match entry.disposition {
Disposition::NeedsHuman => "needs-human".to_owned(),
Disposition::Waived => "waived".to_owned(),
Disposition::Resolved => match entry.verdict {
Verdict::Pass => "passed".to_owned(),
Verdict::Flag => "flag/resolved".to_owned(),
Verdict::Reject => "resolved".to_owned(),
},
Disposition::Open => match entry.verdict {
Verdict::Reject => "open".to_owned(),
Verdict::Flag => "flag".to_owned(),
Verdict::Pass => "pass/open".to_owned(),
},
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindingCard {
pub severity: String,
pub location: String,
pub title: String,
pub body: String,
pub recommendation: String,
}
pub fn finding_cards(entry: &LedgerEntry) -> Vec<FindingCard> {
if !entry.structured_findings.is_empty() {
return entry
.structured_findings
.iter()
.map(structured_to_card)
.collect();
}
entry
.findings
.iter()
.map(|line| FindingCard {
severity: "info".to_owned(),
location: "—".to_owned(),
title: first_line(line, line),
body: line.clone(),
recommendation: String::new(),
})
.collect()
}
fn structured_to_card(finding: &StructuredFinding) -> FindingCard {
FindingCard {
severity: finding.severity.to_string(),
location: format!("{}:{}", finding.file, finding.line_start),
title: finding.title.clone(),
body: finding.body.clone(),
recommendation: finding.recommendation.clone(),
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DebtClassRow {
pub class: String,
pub entry_count: usize,
pub sample_shas: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DebtClassGroup {
pub class: String,
pub entries: Vec<DebtEntryVm>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DebtEntryVm {
pub sha: String,
pub short_sha: String,
pub age: String,
pub summary: String,
pub findings: Vec<FindingCard>,
}
pub fn debt_groups(entries: &[LedgerEntry], now: u64) -> Vec<DebtClassGroup> {
let mut sorted = entries.to_vec();
sorted.sort_by_key(|entry| std::cmp::Reverse(entry.created_at_unix));
let grouped = group_by_finding_class(&sorted);
grouped
.into_iter()
.map(|(class, group)| DebtClassGroup {
class,
entries: group
.into_iter()
.map(|entry| DebtEntryVm {
short_sha: short_sha(&entry.commit_sha),
sha: entry.commit_sha.clone(),
age: age_label(now.saturating_sub(entry.updated_at_unix)),
summary: first_line(&entry.summary, &entry.claim),
findings: finding_cards(entry),
})
.collect(),
})
.collect()
}
pub fn debt_class_rows(groups: &[DebtClassGroup]) -> Vec<DebtClassRow> {
groups
.iter()
.map(|group| DebtClassRow {
class: group.class.clone(),
entry_count: group.entries.len(),
sample_shas: group
.entries
.iter()
.take(3)
.map(|entry| entry.short_sha.clone())
.collect(),
})
.collect()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigRow {
pub path: String,
pub section: String,
pub key: String,
pub value: String,
pub is_default: bool,
pub editable: bool,
}
pub fn config_rows(
config: &TruthMirrorConfig,
present_paths: &BTreeMap<String, bool>,
) -> Vec<ConfigRow> {
let defaults = TruthMirrorConfig::default();
let mut rows = Vec::new();
let allow_same = config.allow_same_model.to_string();
let allow_same_def = defaults.allow_same_model.to_string();
let stop_lies = config.strict.stop_after_lies.to_string();
let stop_lies_def = defaults.strict.stop_after_lies.to_string();
let stop_fuckups = config.strict.stop_after_fuckups.to_string();
let stop_fuckups_def = defaults.strict.stop_after_fuckups.to_string();
let max_passes = config.strict.max_passes.to_string();
let max_passes_def = defaults.strict.max_passes.to_string();
let fake_markers = join_list(&config.gates.fake_markers);
let fake_markers_def = join_list(&defaults.gates.fake_markers);
let evidence = join_list(&config.gates.evidence_patterns);
let evidence_def = join_list(&defaults.gates.evidence_patterns);
let ignore_paths = join_list(&config.gates.marker_ignore_paths);
let ignore_paths_def = join_list(&defaults.gates.marker_ignore_paths);
let gt_enabled = config.ground_truth.enabled.to_string();
let gt_enabled_def = defaults.ground_truth.enabled.to_string();
let gt_max = config.ground_truth.max_bytes.to_string();
let gt_max_def = defaults.ground_truth.max_bytes.to_string();
let gt_openspec = config.ground_truth.include_openspec_specs.to_string();
let gt_openspec_def = defaults.ground_truth.include_openspec_specs.to_string();
let hist_user = config.history.window_user.to_string();
let hist_user_def = defaults.history.window_user.to_string();
let hist_agent = config.history.window_agent.to_string();
let hist_agent_def = defaults.history.window_agent.to_string();
let hist_max = config.history.max_bytes.to_string();
let hist_max_def = defaults.history.max_bytes.to_string();
let enf_unresolved = config.enforcement.block_tools_after_unresolved.to_string();
let enf_unresolved_def = defaults
.enforcement
.block_tools_after_unresolved
.to_string();
let enf_secs = config.enforcement.block_tools_after_secs.to_string();
let enf_secs_def = defaults.enforcement.block_tools_after_secs.to_string();
let skills_enabled = config.skills.enabled.to_string();
let skills_enabled_def = defaults.skills.enabled.to_string();
let specs = [
ScalarSpec {
path: "ledger_dir",
section: "root",
key: "ledger_dir",
value: &config.ledger_dir,
default_value: &defaults.ledger_dir,
editable: true,
},
ScalarSpec {
path: "allow_same_model",
section: "root",
key: "allow_same_model",
value: &allow_same,
default_value: &allow_same_def,
editable: true,
},
ScalarSpec {
path: "default_writer",
section: "root",
key: "default_writer",
value: &config.default_writer,
default_value: &defaults.default_writer,
editable: true,
},
ScalarSpec {
path: "strict.stop_after_lies",
section: "strict",
key: "stop_after_lies",
value: &stop_lies,
default_value: &stop_lies_def,
editable: true,
},
ScalarSpec {
path: "strict.stop_after_fuckups",
section: "strict",
key: "stop_after_fuckups",
value: &stop_fuckups,
default_value: &stop_fuckups_def,
editable: true,
},
ScalarSpec {
path: "strict.max_passes",
section: "strict",
key: "max_passes",
value: &max_passes,
default_value: &max_passes_def,
editable: true,
},
ScalarSpec {
path: "gates.fake_markers",
section: "gates",
key: "fake_markers",
value: &fake_markers,
default_value: &fake_markers_def,
editable: true,
},
ScalarSpec {
path: "gates.evidence_patterns",
section: "gates",
key: "evidence_patterns",
value: &evidence,
default_value: &evidence_def,
editable: true,
},
ScalarSpec {
path: "gates.marker_ignore_paths",
section: "gates",
key: "marker_ignore_paths",
value: &ignore_paths,
default_value: &ignore_paths_def,
editable: true,
},
ScalarSpec {
path: "ground_truth.enabled",
section: "ground_truth",
key: "enabled",
value: >_enabled,
default_value: >_enabled_def,
editable: true,
},
ScalarSpec {
path: "ground_truth.max_bytes",
section: "ground_truth",
key: "max_bytes",
value: >_max,
default_value: >_max_def,
editable: true,
},
ScalarSpec {
path: "ground_truth.include_openspec_specs",
section: "ground_truth",
key: "include_openspec_specs",
value: >_openspec,
default_value: >_openspec_def,
editable: true,
},
ScalarSpec {
path: "history.window_user",
section: "history",
key: "window_user",
value: &hist_user,
default_value: &hist_user_def,
editable: true,
},
ScalarSpec {
path: "history.window_agent",
section: "history",
key: "window_agent",
value: &hist_agent,
default_value: &hist_agent_def,
editable: true,
},
ScalarSpec {
path: "history.max_bytes",
section: "history",
key: "max_bytes",
value: &hist_max,
default_value: &hist_max_def,
editable: true,
},
ScalarSpec {
path: "enforcement.block_tools_after_unresolved",
section: "enforcement",
key: "block_tools_after_unresolved",
value: &enf_unresolved,
default_value: &enf_unresolved_def,
editable: true,
},
ScalarSpec {
path: "enforcement.block_tools_after_secs",
section: "enforcement",
key: "block_tools_after_secs",
value: &enf_secs,
default_value: &enf_secs_def,
editable: true,
},
ScalarSpec {
path: "skills.enabled",
section: "skills",
key: "enabled",
value: &skills_enabled,
default_value: &skills_enabled_def,
editable: true,
},
];
for spec in specs {
push_scalar(&mut rows, present_paths, spec);
}
for (writer, pair) in &config.pairs {
let section = format!("pairs.{writer}");
let harness_path = format!("pairs.{writer}.reviewer.harness");
let model_path = format!("pairs.{writer}.reviewer.model");
let effort_path = format!("pairs.{writer}.reviewer.effort");
push_scalar(
&mut rows,
present_paths,
ScalarSpec {
path: &harness_path,
section: §ion,
key: "reviewer.harness",
value: &pair.reviewer.harness,
default_value: "",
editable: true,
},
);
push_scalar(
&mut rows,
present_paths,
ScalarSpec {
path: &model_path,
section: §ion,
key: "reviewer.model",
value: &pair.reviewer.model,
default_value: "",
editable: true,
},
);
push_scalar(
&mut rows,
present_paths,
ScalarSpec {
path: &effort_path,
section: §ion,
key: "reviewer.effort",
value: pair.reviewer.effort.as_str(),
default_value: "",
editable: true,
},
);
}
rows
}
struct ScalarSpec<'a> {
path: &'a str,
section: &'a str,
key: &'a str,
value: &'a str,
default_value: &'a str,
editable: bool,
}
fn push_scalar(
rows: &mut Vec<ConfigRow>,
present_paths: &BTreeMap<String, bool>,
spec: ScalarSpec<'_>,
) {
let explicitly_present = present_paths.get(spec.path).copied().unwrap_or(false);
let is_default =
!explicitly_present && (spec.value == spec.default_value || spec.default_value.is_empty());
rows.push(ConfigRow {
path: spec.path.to_owned(),
section: spec.section.to_owned(),
key: spec.key.to_owned(),
value: spec.value.to_owned(),
is_default,
editable: spec.editable,
});
}
fn join_list(values: &[String]) -> String {
values.join(", ")
}
pub fn short_sha(sha: &str) -> String {
let end = sha.chars().take(8).collect::<String>();
if end.is_empty() {
"????????".to_owned()
} else {
end
}
}
fn first_line(primary: &str, fallback: &str) -> String {
let source = if primary.trim().is_empty() {
fallback
} else {
primary
};
source
.lines()
.next()
.unwrap_or("")
.chars()
.take(96)
.collect()
}
pub fn validate_config_edit(path: &str, raw: &str) -> Result<String, String> {
let trimmed = raw.trim();
if trimmed.is_empty() && !path.ends_with("fake_markers") && !path.ends_with("evidence_patterns")
{
return Err("value must not be empty".to_owned());
}
if path.ends_with("allow_same_model")
|| path.ends_with("enabled")
|| path.ends_with("include_openspec_specs")
{
return parse_bool(trimmed).map(|v| v.to_string());
}
if path.contains("stop_after")
|| path.ends_with("max_passes")
|| path.ends_with("max_bytes")
|| path.ends_with("window_user")
|| path.ends_with("window_agent")
|| path.ends_with("block_tools_after_unresolved")
|| path.ends_with("block_tools_after_secs")
{
trimmed
.parse::<u64>()
.map(|v| v.to_string())
.map_err(|_| "expected a non-negative integer".to_owned())
} else if path.ends_with("effort") {
match trimmed.to_ascii_lowercase().as_str() {
"minimal" | "low" | "medium" | "high" | "xhigh" => Ok(trimmed.to_ascii_lowercase()),
_ => Err("effort must be minimal|low|medium|high|xhigh".to_owned()),
}
} else {
Ok(trimmed.to_owned())
}
}
fn parse_bool(raw: &str) -> Result<bool, String> {
match raw.to_ascii_lowercase().as_str() {
"true" | "1" | "yes" | "on" => Ok(true),
"false" | "0" | "no" | "off" => Ok(false),
_ => Err("expected true/false".to_owned()),
}
}
pub fn waiver_template(sha: &str) -> String {
format!(
"Human waiver for {sha}\n\nReason: <why this rejection is accepted without a fix>\nRisk accepted: <what we are choosing to live with>\n"
)
}
pub fn validate_fix_sha(raw: &str) -> Result<String, String> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err("fix ref must not be empty".to_owned());
}
if trimmed.chars().any(char::is_whitespace) {
return Err("fix ref must not contain whitespace".to_owned());
}
Ok(trimmed.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ledger::{FindingSeverity, LedgerEntry, ReviewerConfig, StructuredFinding, Verdict};
fn reviewer() -> ReviewerConfig {
ReviewerConfig::new("claude", "claude-opus-4-1", false)
}
#[test]
fn view_id_digit_round_trip() {
for view in ViewId::ALL {
assert_eq!(ViewId::from_digit(view.short_key()), Some(view));
}
assert_eq!(ViewId::from_digit('9'), None);
}
#[test]
fn queue_rows_map_age_and_petition() {
let items = vec![
QueuedReview {
run_id: "r1".into(),
commit_sha: "abcdef0123456789".into(),
enqueued_at_unix: 1000,
petition_for: None,
batch_id: None,
},
QueuedReview {
run_id: "r2".into(),
commit_sha: "deadbeefcafebabe".into(),
enqueued_at_unix: 900,
petition_for: Some("facefeed00112233".into()),
batch_id: None,
},
];
let mut subjects = BTreeMap::new();
subjects.insert("abcdef0123456789".into(), "feat: thing".into());
let rows = queue_rows(&items, &subjects, 1060);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].short_sha, "abcdef01");
assert_eq!(rows[0].subject, "feat: thing");
assert_eq!(rows[0].age, "1m");
assert_eq!(rows[0].petition_for, "—");
assert_eq!(rows[1].petition_for, "facefeed");
assert_eq!(rows[1].age, "2m");
}
#[test]
fn ledger_rows_status_and_actionable_flag() {
let open = LedgerEntry::new_at(
"rej1aaaa",
Verdict::Reject,
"CLAIM: bad",
vec![],
reviewer(),
vec!["lie".into()],
100,
);
let pass = LedgerEntry::new_at(
"pass1bbb",
Verdict::Pass,
"CLAIM: ok",
vec![],
reviewer(),
vec![],
200,
);
let rows = ledger_rows(&[open, pass], 260);
assert_eq!(rows[0].sha, "pass1bbb"); assert_eq!(rows[0].status, "passed");
assert!(!rows[0].is_actionable_reject);
assert_eq!(rows[1].status, "open");
assert!(rows[1].is_actionable_reject);
assert_eq!(rows[1].age, "2m");
}
#[test]
fn finding_cards_prefer_structured() {
let entry = LedgerEntry::new_at(
"abc",
Verdict::Reject,
"CLAIM: x",
vec![],
reviewer(),
vec!["legacy finding".into()],
1,
)
.with_structured_review(
"summary",
vec![StructuredFinding {
severity: FindingSeverity::High,
title: "missing evidence".into(),
body: "No tests: pointer".into(),
file: "src/lib.rs".into(),
line_start: 10,
line_end: 12,
confidence: 80,
recommendation: "Add evidence".into(),
}],
vec![],
"{}",
);
let cards = finding_cards(&entry);
assert_eq!(cards.len(), 1);
assert_eq!(cards[0].severity, "high");
assert_eq!(cards[0].location, "src/lib.rs:10");
assert_eq!(cards[0].title, "missing evidence");
}
#[test]
fn debt_groups_reuse_class_grouping() {
let mut a = LedgerEntry::new_at(
"flag1",
Verdict::Flag,
"CLAIM: ok",
vec![],
reviewer(),
vec![],
100,
)
.with_structured_review(
"thin",
vec![StructuredFinding {
severity: FindingSeverity::Medium,
title: "thin evidence".into(),
body: "body".into(),
file: "a.rs".into(),
line_start: 1,
line_end: 1,
confidence: 50,
recommendation: "fix".into(),
}],
vec![],
"{}",
);
a.disposition = Disposition::Open;
let groups = debt_groups(&[a], 160);
let rows = debt_class_rows(&groups);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].class, "medium:thin evidence");
assert_eq!(rows[0].entry_count, 1);
assert_eq!(rows[0].sample_shas, vec!["flag1".to_owned()]);
}
#[test]
fn dashboard_vm_builds_cards() {
let runs = ReviewRunStatusCounts {
queued: 1,
running: 2,
completed: 3,
failed: 0,
cancelled: 0,
skipped_records: 0,
};
let stats = LedgerStats {
total: 10,
pass: 7,
reject: 2,
flag: 1,
unresolved: 1,
resolved: 7,
waived: 1,
needs_human: 1,
petition_attempts: 0,
};
let vm = dashboard_vm(DashboardInputs {
version: "0.9.2",
config_path: ".truth/config.toml",
state_dir: ".truth",
queue_pending: 3,
oldest_age_secs: Some(125),
runs: &runs,
stats: &stats,
watcher_alive: true,
watcher_pid: Some(4242),
});
assert_eq!(vm.queue_pending, 3);
assert_eq!(vm.queue_oldest_age, "2m");
assert!(vm.watcher_alive);
assert_eq!(vm.cards.len(), 7);
assert!(vm.cards[3].1.contains("4242"));
}
#[test]
fn config_rows_mark_defaults_dim() {
let config = TruthMirrorConfig::default();
let present = BTreeMap::new();
let rows = config_rows(&config, &present);
assert!(
rows.iter()
.any(|r| r.path == "default_writer" && r.is_default)
);
assert!(rows.iter().any(|r| r.path == "strict.max_passes"));
let mut present = BTreeMap::new();
present.insert("default_writer".into(), true);
let rows = config_rows(&config, &present);
let writer = rows.iter().find(|r| r.path == "default_writer").unwrap();
assert!(!writer.is_default);
}
#[test]
fn validate_config_edit_types() {
assert_eq!(
validate_config_edit("allow_same_model", "true").unwrap(),
"true"
);
assert!(validate_config_edit("allow_same_model", "maybe").is_err());
assert_eq!(validate_config_edit("strict.max_passes", "5").unwrap(), "5");
assert!(validate_config_edit("strict.max_passes", "x").is_err());
assert_eq!(
validate_config_edit("pairs.codex.reviewer.effort", "High").unwrap(),
"high"
);
}
#[test]
fn validate_fix_sha_rules() {
assert_eq!(validate_fix_sha("HEAD").unwrap(), "HEAD");
assert_eq!(validate_fix_sha("main").unwrap(), "main");
assert_eq!(validate_fix_sha("abc").unwrap(), "abc");
assert_eq!(validate_fix_sha("abcdef0").unwrap(), "abcdef0");
assert_eq!(
validate_fix_sha(" abcdef0123456789 ").unwrap(),
"abcdef0123456789"
);
assert!(validate_fix_sha("").is_err());
assert!(validate_fix_sha(" ").is_err());
assert!(validate_fix_sha("two words").is_err());
}
#[test]
fn waiver_template_mentions_sha() {
let t = waiver_template("deadbeef");
assert!(t.contains("deadbeef"));
assert!(t.contains("Reason:"));
}
}