use crate::mcp::types::{ToolError, error_class};
use crate::storage::{RunEntry, RunIndex};
use std::path::{Path, PathBuf};
pub fn commit_matches(a: &str, b: &str) -> bool {
!a.is_empty() && !b.is_empty() && (a == b || a.starts_with(b) || b.starts_with(a))
}
fn in_scope(e: &RunEntry, repo: &str, branch_key: &str) -> bool {
e.repo == repo && e.branch == branch_key
}
pub fn latest_for_head<'a>(
index: &'a RunIndex,
repo: &str,
branch_key: &str,
head_short: &str,
) -> Option<&'a RunEntry> {
index
.entries()
.iter()
.filter(|e| in_scope(e, repo, branch_key) && commit_matches(&e.commit, head_short))
.max_by(|a, b| a.created_at.cmp(&b.created_at))
}
pub fn latest_any<'a>(index: &'a RunIndex, repo: &str, branch_key: &str) -> Option<&'a RunEntry> {
index
.entries()
.iter()
.filter(|e| in_scope(e, repo, branch_key))
.max_by(|a, b| a.created_at.cmp(&b.created_at))
}
pub fn resolve_repo_root(repo: &str) -> Result<PathBuf, ToolError> {
let path = PathBuf::from(repo);
if !path.is_absolute() {
return Err(ToolError::new(
error_class::REPO_NOT_FOUND,
format!("repo path must be absolute: {repo}"),
));
}
if !path.exists() {
return Err(ToolError::new(
error_class::REPO_NOT_FOUND,
format!("repo path does not exist: {repo}"),
));
}
let output = crate::git::git_cmd()
.args(["rev-parse", "--show-toplevel"])
.current_dir(&path)
.output()
.map_err(|e| {
ToolError::new(
error_class::NOT_A_GIT_REPO,
format!("failed to run git in {repo}: {e}"),
)
})?;
if !output.status.success() {
return Err(ToolError::new(
error_class::NOT_A_GIT_REPO,
format!("not a git repository: {repo}"),
));
}
let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
if root.is_empty() {
return Err(ToolError::new(
error_class::NOT_A_GIT_REPO,
format!("not a git repository (empty git output): {repo}"),
));
}
Ok(PathBuf::from(root))
}
pub fn read_bases(run_dir: &Path) -> Vec<String> {
let gate_path = run_dir.join("00_summary").join("MERGE_GATE.json");
let Ok(text) = std::fs::read_to_string(&gate_path) else {
return Vec::new();
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
return Vec::new();
};
value
.get("bases")
.and_then(|b| b.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RunningMarker {
pub pid: u32,
pub started_at: String,
pub profile: String,
pub commit: String,
#[serde(default)]
pub base_used: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunStatus {
Completed,
Running { pid: u32 },
Stale { pid: u32, started_at: String },
Failed,
}
const RUNNING_MARKER: &str = "RUNNING.json";
pub fn running_marker_path(run_dir: &Path) -> PathBuf {
run_dir.join(RUNNING_MARKER)
}
pub fn read_running_marker(run_dir: &Path) -> Option<RunningMarker> {
let text = std::fs::read_to_string(running_marker_path(run_dir)).ok()?;
serde_json::from_str(&text).ok()
}
pub fn run_status(run_dir: &Path) -> RunStatus {
if run_dir.join("00_summary").join("SANITY.json").exists() {
return RunStatus::Completed;
}
match read_running_marker(run_dir) {
Some(marker) => {
if crate::storage::is_process_alive(marker.pid) {
RunStatus::Running { pid: marker.pid }
} else {
RunStatus::Stale {
pid: marker.pid,
started_at: marker.started_at,
}
}
}
None => RunStatus::Failed,
}
}
#[derive(Debug, Clone)]
pub struct ResolvedRun {
pub run_dir: PathBuf,
pub run_id: String,
pub commit: String,
}
fn validate_run_id(run_id: &str) -> Result<(), ToolError> {
let safe = !run_id.is_empty()
&& run_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'));
if safe && run_id != "." && run_id != ".." {
Ok(())
} else {
Err(ToolError::new(
error_class::RUN_NOT_FOUND,
format!("invalid run_id: {run_id}"),
))
}
}
fn find_run_dir_by_id(repo_name: &str, run_id: &str) -> Option<PathBuf> {
let base = crate::config::prview_home().join("runs").join(repo_name);
let read = std::fs::read_dir(&base).ok()?;
for branch in read.flatten() {
if !branch.path().is_dir() {
continue;
}
let candidate = branch.path().join(run_id);
if candidate.is_dir() {
return Some(candidate);
}
}
None
}
pub fn resolve_run(root: &Path, run_id: Option<&str>) -> Result<ResolvedRun, ToolError> {
let repo_name = crate::config::repo_name_from_root(root);
let index = RunIndex::load();
match run_id {
Some(id) => {
validate_run_id(id)?;
if let Some(e) = index
.entries()
.iter()
.find(|e| e.repo == repo_name && e.id == id)
{
return Ok(ResolvedRun {
run_dir: e.path.clone(),
run_id: id.to_string(),
commit: e.commit.clone(),
});
}
match find_run_dir_by_id(&repo_name, id) {
Some(run_dir) => {
let commit = read_running_marker(&run_dir)
.map(|m| m.commit)
.unwrap_or_default();
Ok(ResolvedRun {
run_dir,
run_id: id.to_string(),
commit,
})
}
None => Err(ToolError::new(
error_class::RUN_NOT_FOUND,
format!("no run with id {id} for {repo_name}"),
)),
}
}
None => {
let state = crate::state::collect_state(
root,
&crate::state::StateOpts {
fast: true,
json: true,
hot: false,
},
)
.map_err(|e| {
ToolError::new(
error_class::NOT_A_GIT_REPO,
format!("failed to read repo state: {e}"),
)
})?;
let branch_key = crate::config::storage_branch_key(root);
match latest_for_head(&index, &repo_name, &branch_key, &state.head) {
Some(e) => Ok(ResolvedRun {
run_dir: e.path.clone(),
run_id: e.id.clone(),
commit: e.commit.clone(),
}),
None => Err(ToolError::new(
error_class::RUN_NOT_FOUND,
"no run for current HEAD; call run_review",
)),
}
}
}
}
const SARIF_REL: &str = "30_context/INLINE_FINDINGS.sarif";
#[derive(Debug, Clone)]
pub struct FindingItem {
pub file: String,
pub line: u64,
pub severity: String,
pub rule: String,
pub message: String,
}
pub fn read_findings(run_dir: &Path) -> Vec<FindingItem> {
let sarif_path = run_dir.join("30_context").join("INLINE_FINDINGS.sarif");
let Ok(text) = std::fs::read_to_string(&sarif_path) else {
return Vec::new();
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
return Vec::new();
};
let mut items = Vec::new();
let runs = value.get("runs").and_then(|r| r.as_array());
for run in runs.into_iter().flatten() {
let results = run.get("results").and_then(|r| r.as_array());
for result in results.into_iter().flatten() {
let rule = result
.get("ruleId")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let severity = result
.get("level")
.and_then(|v| v.as_str())
.unwrap_or("warning")
.to_string();
let message = result
.get("message")
.and_then(|m| m.get("text"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let physical = result
.get("locations")
.and_then(|l| l.as_array())
.and_then(|arr| arr.first())
.and_then(|loc| loc.get("physicalLocation"));
let file = physical
.and_then(|p| p.get("artifactLocation"))
.and_then(|a| a.get("uri"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let line = physical
.and_then(|p| p.get("region"))
.and_then(|r| r.get("startLine"))
.and_then(|v| v.as_u64())
.unwrap_or(0);
items.push(FindingItem {
file,
line,
severity,
rule,
message,
});
}
}
items.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then(a.line.cmp(&b.line))
.then(a.rule.cmp(&b.rule))
});
items
}
pub fn sarif_ref() -> &'static str {
SARIF_REL
}
pub fn resolve_artifact_path(run_dir: &Path, artifact: &str) -> Result<PathBuf, ToolError> {
crate::paths::resolve_existing_path_within(run_dir, Path::new(artifact)).map_err(|_| {
ToolError::new(
error_class::ARTIFACT_MISSING,
format!("artifact not found within run: {artifact}"),
)
})
}
#[derive(Debug, Clone)]
pub struct NormalizedDecision {
pub merge_recommendation: String,
pub allow_merge: bool,
pub verdict: String,
pub blocking_issues: Vec<String>,
pub caveats: Vec<String>,
pub base_used: Vec<String>,
pub normalized: bool,
}
fn rank_from_merge_rec(s: &str) -> Option<u8> {
match s.to_ascii_lowercase().as_str() {
"block" => Some(3),
"review_required" | "hold" => Some(2),
"approve" => Some(1),
_ => None,
}
}
fn rank_from_verdict(s: &str) -> Option<u8> {
match s.to_ascii_uppercase().as_str() {
"BLOCK" => Some(3),
"CONDITIONAL" | "HOLD" => Some(2),
"PASS" | "APPROVE" | "ALLOW" => Some(1),
_ => None,
}
}
fn merge_rec_from_rank(rank: u8) -> &'static str {
match rank {
3 => "block",
2 => "review_required",
_ => "approve",
}
}
fn verdict_from_rank(rank: u8) -> &'static str {
match rank {
3 => "BLOCK",
2 => "CONDITIONAL",
_ => "PASS",
}
}
fn string_array(value: Option<&serde_json::Value>) -> Vec<String> {
value
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
pub fn read_decision(run_dir: &Path) -> Result<NormalizedDecision, ToolError> {
let gate_path = run_dir.join("00_summary").join("MERGE_GATE.json");
let text = std::fs::read_to_string(&gate_path).map_err(|_| {
ToolError::new(
error_class::STORAGE_CORRUPT,
format!("MERGE_GATE.json not found: {}", gate_path.display()),
)
})?;
let value: serde_json::Value = serde_json::from_str(&text).map_err(|e| {
ToolError::new(
error_class::STORAGE_CORRUPT,
format!("MERGE_GATE.json is not valid JSON: {e}"),
)
})?;
let decision = value.get("decision").ok_or_else(|| {
ToolError::new(
error_class::STORAGE_CORRUPT,
"MERGE_GATE.json missing `decision` object",
)
})?;
let raw_merge = decision
.get("merge_recommendation")
.and_then(|v| v.as_str())
.map(str::to_string);
let raw_verdict = decision
.get("verdict")
.and_then(|v| v.as_str())
.map(str::to_string);
let raw_allow = decision.get("allow_merge").and_then(|v| v.as_bool());
let merge_rank = raw_merge.as_deref().and_then(rank_from_merge_rec);
let verdict_rank = raw_verdict.as_deref().and_then(rank_from_verdict);
if merge_rank.is_none() && verdict_rank.is_none() {
return Err(ToolError::new(
error_class::STORAGE_CORRUPT,
"MERGE_GATE.json decision has no recognizable merge_recommendation or verdict",
));
}
let allow_rank = raw_allow.map(|allow| if allow { 1 } else { 2 });
let final_rank = [merge_rank, verdict_rank, allow_rank]
.into_iter()
.flatten()
.max()
.unwrap_or(2);
let allow_merge = final_rank == 1;
let signal_ranks: Vec<u8> = [merge_rank, verdict_rank].into_iter().flatten().collect();
let signals_disagree = signal_ranks.iter().any(|&r| r != final_rank);
let allow_contradicts = raw_allow.map(|a| a != allow_merge).unwrap_or(false);
let normalized = signals_disagree || allow_contradicts;
let mut caveats = Vec::new();
if normalized {
caveats.push(format!(
"core_inconsistency: original allow_merge={}, merge_recommendation={}, verdict={}",
raw_allow
.map(|b| b.to_string())
.unwrap_or_else(|| "null".to_string()),
raw_merge.as_deref().unwrap_or("null"),
raw_verdict.as_deref().unwrap_or("null"),
));
}
caveats.extend(string_array(decision.get("review_caveats")));
Ok(NormalizedDecision {
merge_recommendation: merge_rec_from_rank(final_rank).to_string(),
allow_merge,
verdict: verdict_from_rank(final_rank).to_string(),
blocking_issues: string_array(decision.get("blocking_issues")),
caveats,
base_used: string_array(value.get("bases")),
normalized,
})
}
pub fn read_generated_at(run_dir: &Path) -> Option<String> {
let gate_path = run_dir.join("00_summary").join("MERGE_GATE.json");
let text = std::fs::read_to_string(&gate_path).ok()?;
let value: serde_json::Value = serde_json::from_str(&text).ok()?;
value
.get("generated_at")
.and_then(|v| v.as_str())
.map(str::to_string)
}
pub fn read_gates(run_dir: &Path) -> Vec<serde_json::Value> {
let gate_path = run_dir.join("00_summary").join("MERGE_GATE.json");
let Ok(text) = std::fs::read_to_string(&gate_path) else {
return Vec::new();
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&text) else {
return Vec::new();
};
value
.get("checks")
.and_then(|c| c.as_array())
.map(|arr| {
arr.iter()
.map(|g| {
serde_json::json!({
"id": g.get("id").cloned().unwrap_or(serde_json::Value::Null),
"status": g.get("status").cloned().unwrap_or(serde_json::Value::Null),
"reason": g.get("reason").cloned().unwrap_or(serde_json::Value::Null),
"evidence": g.get("evidence").cloned().unwrap_or(serde_json::Value::Null),
})
})
.collect()
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::RunEntry;
use std::io::Write;
fn write_gate(run_dir: &Path, gate: &serde_json::Value) {
let summary = run_dir.join("00_summary");
std::fs::create_dir_all(&summary).unwrap();
std::fs::write(
summary.join("MERGE_GATE.json"),
serde_json::to_string_pretty(gate).unwrap(),
)
.unwrap();
}
#[test]
fn consistent_block_is_passthrough() {
let dir = tempfile::tempdir().unwrap();
write_gate(
dir.path(),
&serde_json::json!({
"bases": ["develop", "main"],
"decision": {
"merge_recommendation": "block",
"verdict": "BLOCK",
"allow_merge": false,
"blocking_issues": ["clippy failed"],
"review_caveats": ["High-risk surface"]
}
}),
);
let d = read_decision(dir.path()).unwrap();
assert_eq!(d.merge_recommendation, "block");
assert_eq!(d.verdict, "BLOCK");
assert!(!d.allow_merge);
assert!(!d.normalized);
assert!(!d.caveats.iter().any(|c| c.contains("core_inconsistency")));
assert_eq!(d.base_used, vec!["develop", "main"]);
assert_eq!(d.blocking_issues, vec!["clippy failed"]);
}
#[test]
fn allow_true_with_block_is_normalized_conservative() {
let dir = tempfile::tempdir().unwrap();
write_gate(
dir.path(),
&serde_json::json!({
"bases": ["main"],
"decision": {
"merge_recommendation": "block",
"verdict": "BLOCK",
"allow_merge": true,
"review_caveats": []
}
}),
);
let d = read_decision(dir.path()).unwrap();
assert_eq!(d.merge_recommendation, "block");
assert!(!d.allow_merge, "allow_merge must be derived, not passed");
assert!(d.normalized);
let caveat = d
.caveats
.iter()
.find(|c| c.contains("core_inconsistency"))
.expect("core_inconsistency caveat present");
assert!(caveat.contains("original allow_merge=true"));
assert!(caveat.contains("merge_recommendation=block"));
}
#[test]
fn legacy_hold_verdict_with_allow_true_normalizes_to_conditional() {
let dir = tempfile::tempdir().unwrap();
write_gate(
dir.path(),
&serde_json::json!({
"bases": [],
"decision": {
"merge_recommendation": "approve",
"verdict": "HOLD",
"allow_merge": true
}
}),
);
let d = read_decision(dir.path()).unwrap();
assert_eq!(d.verdict, "CONDITIONAL");
assert_eq!(d.merge_recommendation, "review_required");
assert!(!d.allow_merge);
assert!(d.normalized);
}
#[test]
fn legacy_allow_verdict_normalizes_to_pass() {
let dir = tempfile::tempdir().unwrap();
write_gate(
dir.path(),
&serde_json::json!({
"bases": ["main"],
"decision": {
"verdict": "ALLOW",
"allow_merge": true
}
}),
);
let d = read_decision(dir.path()).unwrap();
assert_eq!(d.verdict, "PASS");
assert_eq!(d.merge_recommendation, "approve");
assert!(d.allow_merge, "legacy ALLOW is a clean pass");
assert!(
!d.normalized,
"ALLOW+allow_merge:true is self-consistent, no core_inconsistency"
);
}
#[test]
fn healthy_conditional_core_is_passthrough_no_inconsistency() {
let dir = tempfile::tempdir().unwrap();
write_gate(
dir.path(),
&serde_json::json!({
"bases": ["main"],
"decision": {
"merge_recommendation": "review_required",
"verdict": "CONDITIONAL",
"allow_merge": false,
"review_caveats": ["3 inline findings"]
}
}),
);
let d = read_decision(dir.path()).unwrap();
assert_eq!(d.verdict, "CONDITIONAL");
assert_eq!(d.merge_recommendation, "review_required");
assert!(!d.allow_merge);
assert!(!d.normalized, "healthy core must not be normalized");
assert!(
!d.caveats.iter().any(|c| c.contains("core_inconsistency")),
"no false core_inconsistency on a healthy core"
);
}
#[test]
fn healthy_pass_core_is_passthrough_no_inconsistency() {
let dir = tempfile::tempdir().unwrap();
write_gate(
dir.path(),
&serde_json::json!({
"bases": ["main"],
"decision": {
"merge_recommendation": "approve",
"verdict": "PASS",
"allow_merge": true
}
}),
);
let d = read_decision(dir.path()).unwrap();
assert_eq!(d.verdict, "PASS");
assert_eq!(d.merge_recommendation, "approve");
assert!(d.allow_merge);
assert!(!d.normalized);
assert!(!d.caveats.iter().any(|c| c.contains("core_inconsistency")));
}
#[test]
fn missing_merge_gate_is_storage_corrupt() {
let dir = tempfile::tempdir().unwrap();
let err = read_decision(dir.path()).unwrap_err();
assert_eq!(err.class, error_class::STORAGE_CORRUPT);
}
fn write_marker(run_dir: &Path, pid: u32) {
let marker = RunningMarker {
pid,
started_at: "2026-07-01T12:00:00Z".to_string(),
profile: "deep".to_string(),
commit: "abc1234".to_string(),
base_used: vec!["main".to_string()],
};
std::fs::write(
running_marker_path(run_dir),
serde_json::to_string(&marker).unwrap(),
)
.unwrap();
}
#[test]
fn run_status_completed_wins_over_marker() {
let dir = tempfile::tempdir().unwrap();
write_marker(dir.path(), std::process::id());
let summary = dir.path().join("00_summary");
std::fs::create_dir_all(&summary).unwrap();
std::fs::write(summary.join("RUN.json"), "{}").unwrap();
std::fs::write(summary.join("SANITY.json"), "{}").unwrap();
assert_eq!(run_status(dir.path()), RunStatus::Completed);
}
#[test]
fn run_status_not_completed_while_pack_still_finalizing() {
let dir = tempfile::tempdir().unwrap();
let summary = dir.path().join("00_summary");
std::fs::create_dir_all(&summary).unwrap();
std::fs::write(summary.join("RUN.json"), "{}").unwrap();
write_marker(dir.path(), std::process::id());
assert_eq!(
run_status(dir.path()),
RunStatus::Running {
pid: std::process::id()
},
);
std::fs::remove_file(running_marker_path(dir.path())).unwrap();
write_marker(dir.path(), 2_147_483_646);
match run_status(dir.path()) {
RunStatus::Stale { pid, .. } => assert_eq!(pid, 2_147_483_646),
other => panic!("expected Stale for a partial pack, got {other:?}"),
}
}
#[test]
fn run_status_running_for_live_pid() {
let dir = tempfile::tempdir().unwrap();
write_marker(dir.path(), std::process::id());
assert_eq!(
run_status(dir.path()),
RunStatus::Running {
pid: std::process::id()
}
);
}
#[test]
fn run_status_stale_for_dead_pid() {
let dir = tempfile::tempdir().unwrap();
write_marker(dir.path(), 2_147_483_646);
match run_status(dir.path()) {
RunStatus::Stale { pid, started_at } => {
assert_eq!(pid, 2_147_483_646);
assert!(!started_at.is_empty());
}
other => panic!("expected Stale, got {other:?}"),
}
}
#[test]
fn run_status_failed_for_empty_dir() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(run_status(dir.path()), RunStatus::Failed);
}
#[test]
fn validate_run_id_rejects_traversal() {
assert!(validate_run_id("20260101-120000").is_ok());
assert!(validate_run_id("../escape").is_err());
assert!(validate_run_id("a/b").is_err());
assert!(validate_run_id("..").is_err());
assert!(validate_run_id("").is_err());
}
fn write_sarif(run_dir: &Path, sarif: &serde_json::Value) {
let ctx = run_dir.join("30_context");
std::fs::create_dir_all(&ctx).unwrap();
std::fs::write(
ctx.join("INLINE_FINDINGS.sarif"),
serde_json::to_string_pretty(sarif).unwrap(),
)
.unwrap();
}
fn sarif_result(uri: &str, line: u64, level: &str, rule: &str) -> serde_json::Value {
serde_json::json!({
"ruleId": rule,
"level": level,
"message": { "text": format!("{rule} at {uri}:{line}") },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": uri },
"region": { "startLine": line }
}
}]
})
}
#[test]
fn read_findings_parses_and_sorts() {
let dir = tempfile::tempdir().unwrap();
write_sarif(
dir.path(),
&serde_json::json!({
"version": "2.1.0",
"runs": [{
"results": [
sarif_result("src/b.rs", 10, "warning", "w1"),
sarif_result("src/a.rs", 5, "error", "e1"),
sarif_result("src/a.rs", 2, "note", "n1"),
]
}]
}),
);
let items = read_findings(dir.path());
assert_eq!(items.len(), 3);
assert_eq!(items[0].file, "src/a.rs");
assert_eq!(items[0].line, 2);
assert_eq!(items[1].line, 5);
assert_eq!(items[2].file, "src/b.rs");
assert_eq!(items[1].severity, "error");
}
#[test]
fn read_findings_missing_sarif_is_empty() {
let dir = tempfile::tempdir().unwrap();
assert!(read_findings(dir.path()).is_empty());
}
#[test]
fn resolve_artifact_path_guards_traversal() {
let dir = tempfile::tempdir().unwrap();
let summary = dir.path().join("00_summary");
std::fs::create_dir_all(&summary).unwrap();
std::fs::write(summary.join("MERGE_GATE.json"), "{}").unwrap();
assert!(resolve_artifact_path(dir.path(), "00_summary/MERGE_GATE.json").is_ok());
assert_eq!(
resolve_artifact_path(dir.path(), "../../../etc/passwd")
.unwrap_err()
.class,
error_class::ARTIFACT_MISSING
);
assert_eq!(
resolve_artifact_path(dir.path(), "/etc/passwd")
.unwrap_err()
.class,
error_class::ARTIFACT_MISSING
);
}
#[cfg(unix)]
#[test]
fn resolve_artifact_path_rejects_symlink_escape() {
let dir = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
let secret = outside.path().join("secret.txt");
std::fs::write(&secret, "top secret").unwrap();
std::os::unix::fs::symlink(&secret, dir.path().join("escape")).unwrap();
assert_eq!(
resolve_artifact_path(dir.path(), "escape")
.unwrap_err()
.class,
error_class::ARTIFACT_MISSING
);
}
fn entry(id: &str, commit: &str, created_at: &str) -> RunEntry {
RunEntry {
id: id.to_string(),
repo: "demo".to_string(),
branch: "main".to_string(),
commit: commit.to_string(),
path: PathBuf::from(format!("/tmp/demo/main/{id}")),
created_at: created_at.to_string(),
quality_pass: true,
merge_status: "ALLOW".to_string(),
policy_mode: "warn".to_string(),
checks_passed: 1,
checks_failed: 0,
files_changed: 1,
size_bytes: 0,
has_dashboard: false,
}
}
fn index_from(entries: &[RunEntry]) -> (tempfile::TempDir, RunIndex) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("index.jsonl");
let mut f = std::fs::File::create(&path).unwrap();
for e in entries {
writeln!(f, "{}", serde_json::to_string(e).unwrap()).unwrap();
}
f.flush().unwrap();
let index = RunIndex::load_from(&path);
(dir, index)
}
#[test]
fn latest_for_head_filters_by_commit() {
let entries = vec![
entry("20260101-000001", "aaaa111", "2026-01-01T00:00:01Z"),
entry("20260101-000002", "aaaa111", "2026-01-01T00:00:02Z"),
entry("20260101-000003", "bbbb222", "2026-01-01T00:00:03Z"),
];
let (_tmp, index) = index_from(&entries);
let head = latest_for_head(&index, "demo", "main", "aaaa111").unwrap();
assert_eq!(head.id, "20260101-000002");
assert!(latest_for_head(&index, "demo", "main", "cccc333").is_none());
let any = latest_any(&index, "demo", "main").unwrap();
assert_eq!(any.id, "20260101-000003");
}
#[test]
fn scope_filters_repo_and_branch() {
let mut other = entry("20260101-000009", "aaaa111", "2026-01-01T00:00:09Z");
other.repo = "elsewhere".to_string();
let entries = vec![
entry("20260101-000001", "aaaa111", "2026-01-01T00:00:01Z"),
other,
];
let (_tmp, index) = index_from(&entries);
assert!(latest_any(&index, "elsewhere", "main").is_some());
assert_eq!(
latest_for_head(&index, "demo", "main", "aaaa111")
.unwrap()
.id,
"20260101-000001"
);
assert!(latest_for_head(&index, "demo", "other-branch", "aaaa111").is_none());
}
#[test]
fn commit_matches_is_prefix_tolerant() {
assert!(commit_matches("aaaa111", "aaaa111"));
assert!(commit_matches("aaaa111", "aaaa111abcdef"));
assert!(commit_matches("aaaa111abcdef", "aaaa111"));
assert!(!commit_matches("aaaa111", "bbbb222"));
assert!(!commit_matches("", "aaaa111"));
}
}