use crate::agent::loop_commands::{
WORKFLOW_AFTER_SNAPSHOT_ARTIFACT, WORKFLOW_AGENT_BRIEF_ARTIFACT,
WORKFLOW_AGENT_PACKET_ARTIFACT, WORKFLOW_AGENT_RECEIPT_ARTIFACT,
WORKFLOW_AGENT_REVIEW_SUMMARY_ARTIFACT, WORKFLOW_AGENT_REVIEW_SUMMARY_MARKDOWN_ARTIFACT,
WORKFLOW_AGENT_STATUS_ARTIFACT, WORKFLOW_AGENT_STATUS_MARKDOWN_ARTIFACT,
WORKFLOW_AGENT_VERIFY_ARTIFACT, WORKFLOW_BEFORE_SNAPSHOT_ARTIFACT, agent_brief_command,
agent_packet_command, agent_receipt_command, agent_review_summary_command,
agent_review_summary_markdown_command, agent_status_command, agent_status_markdown_command,
agent_verify_command, check_repo_exposure_command, display_path,
};
use serde_json::Value;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) const AGENT_STATUS_SCHEMA_VERSION: &str = "0.1";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct AgentStatusArtifactDef {
name: &'static str,
label: &'static str,
path: &'static str,
}
const ARTIFACTS: &[AgentStatusArtifactDef] = &[
AgentStatusArtifactDef {
name: "before_snapshot",
label: "before snapshot",
path: WORKFLOW_BEFORE_SNAPSHOT_ARTIFACT,
},
AgentStatusArtifactDef {
name: "after_snapshot",
label: "after snapshot",
path: WORKFLOW_AFTER_SNAPSHOT_ARTIFACT,
},
AgentStatusArtifactDef {
name: "agent_brief",
label: "agent brief",
path: WORKFLOW_AGENT_BRIEF_ARTIFACT,
},
AgentStatusArtifactDef {
name: "agent_packet",
label: "agent packet",
path: WORKFLOW_AGENT_PACKET_ARTIFACT,
},
AgentStatusArtifactDef {
name: "agent_verify",
label: "agent verify",
path: WORKFLOW_AGENT_VERIFY_ARTIFACT,
},
AgentStatusArtifactDef {
name: "agent_receipt",
label: "agent receipt",
path: WORKFLOW_AGENT_RECEIPT_ARTIFACT,
},
];
const MISSING_COMMAND_ORDER: &[&str] = &[
"before_snapshot",
"agent_packet",
"agent_brief",
"after_snapshot",
"agent_verify",
"agent_receipt",
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentStatusReport {
pub(crate) root: String,
pub(crate) seam: Option<AgentStatusSeam>,
pub(crate) artifacts: Vec<AgentStatusArtifact>,
pub(crate) missing_commands: Vec<AgentStatusCommand>,
pub(crate) warnings: Vec<AgentStatusWarning>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentStatusSeam {
pub(crate) seam_id: String,
pub(crate) source: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentStatusArtifact {
pub(crate) name: String,
pub(crate) label: String,
pub(crate) path: String,
pub(crate) present: bool,
pub(crate) bytes: Option<u64>,
pub(crate) modified: Option<SystemTime>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentStatusCommand {
pub(crate) step: String,
pub(crate) artifact: String,
pub(crate) reason: String,
pub(crate) command: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentStatusWarning {
pub(crate) kind: String,
pub(crate) artifact: String,
pub(crate) message: String,
}
impl AgentStatusReport {
pub(crate) fn status(&self) -> &'static str {
if self.artifacts.iter().any(|artifact| !artifact.present) {
"incomplete"
} else if self.warnings.is_empty() {
"complete"
} else {
"warning"
}
}
}
pub(crate) fn build_agent_status_report(root: &Path, root_argument: &Path) -> AgentStatusReport {
let root_display = display_path(root_argument);
keep_follow_up_templates_reachable(&root_display);
let artifacts = ARTIFACTS
.iter()
.map(|artifact| inspect_artifact(root, artifact))
.collect::<Vec<_>>();
let mut warnings = Vec::new();
let seam = recover_seam_id(root, &artifacts, &mut warnings);
warnings.extend(stale_warnings(&artifacts));
let missing_commands = missing_commands(root_argument, seam.as_ref(), &artifacts);
AgentStatusReport {
root: root_display,
seam,
artifacts,
missing_commands,
warnings,
}
}
fn keep_follow_up_templates_reachable(root: &str) {
drop((
agent_status_command(root, Some(WORKFLOW_AGENT_STATUS_ARTIFACT)),
agent_status_markdown_command(root, Some(WORKFLOW_AGENT_STATUS_MARKDOWN_ARTIFACT)),
agent_review_summary_command(root, Some(WORKFLOW_AGENT_REVIEW_SUMMARY_ARTIFACT)),
agent_review_summary_markdown_command(
root,
Some(WORKFLOW_AGENT_REVIEW_SUMMARY_MARKDOWN_ARTIFACT),
),
));
}
pub(crate) fn render_agent_status_json(report: &AgentStatusReport) -> Result<String, String> {
let next_command = report
.missing_commands
.first()
.map(agent_status_command_json);
let value = serde_json::json!({
"schema_version": AGENT_STATUS_SCHEMA_VERSION,
"tool": "ripr",
"status": report.status(),
"root": report.root,
"seam": report.seam.as_ref().map(agent_status_seam_json),
"artifacts": report.artifacts.iter().map(agent_status_artifact_json).collect::<Vec<_>>(),
"missing_commands": report.missing_commands.iter().map(agent_status_command_json).collect::<Vec<_>>(),
"next_command": next_command,
"warnings": report.warnings.iter().map(agent_status_warning_json).collect::<Vec<_>>()
});
serde_json::to_string_pretty(&value)
.map(|mut rendered| {
rendered.push('\n');
rendered
})
.map_err(|err| format!("failed to render agent status JSON: {err}"))
}
pub(crate) fn render_agent_status_markdown(report: &AgentStatusReport) -> String {
let mut rendered = String::new();
rendered.push_str("# RIPR Agent Status\n\n");
rendered.push_str(&format!("Status: {}\n", report.status()));
rendered.push_str(&format!("Root: {}\n", report.root));
match &report.seam {
Some(seam) => rendered.push_str(&format!("Seam: {} ({})\n", seam.seam_id, seam.source)),
None => rendered.push_str("Seam: unknown\n"),
}
rendered.push_str("\n## Artifacts\n\n");
rendered.push_str("| Artifact | State | Path |\n");
rendered.push_str("| --- | --- | --- |\n");
for artifact in &report.artifacts {
let state = if artifact.present {
"present"
} else {
"missing"
};
rendered.push_str(&format!(
"| {} | {} | `{}` |\n",
artifact.label, state, artifact.path
));
}
if let Some(next) = report.missing_commands.first() {
rendered.push_str("\n## Next Command\n\n");
rendered.push_str(&format!("{}\n\n", next.reason));
rendered.push_str("```bash\n");
rendered.push_str(&next.command);
rendered.push_str("\n```\n");
} else {
rendered.push_str("\nNo missing agent-loop artifacts were detected.\n");
}
if !report.warnings.is_empty() {
rendered.push_str("\n## Warnings\n\n");
for warning in &report.warnings {
rendered.push_str(&format!(
"- {}: {} (`{}`)\n",
warning.kind, warning.message, warning.artifact
));
}
}
rendered.push_str("\n## Limits\n\n");
rendered.push_str("- Reads existing artifacts only.\n");
rendered.push_str("- No repo analysis is run by this command.\n");
rendered.push_str("- No runtime mutation execution.\n");
rendered.push_str("- No automatic source edits.\n");
rendered.push_str("- No generated tests.\n");
rendered
}
fn agent_status_seam_json(seam: &AgentStatusSeam) -> Value {
serde_json::json!({
"seam_id": seam.seam_id,
"source": seam.source
})
}
fn agent_status_artifact_json(artifact: &AgentStatusArtifact) -> Value {
serde_json::json!({
"name": artifact.name,
"label": artifact.label,
"path": artifact.path,
"required": true,
"state": if artifact.present { "present" } else { "missing" },
"bytes": artifact.bytes,
"modified_unix_ms": modified_unix_ms(artifact.modified)
})
}
fn agent_status_command_json(command: &AgentStatusCommand) -> Value {
serde_json::json!({
"step": command.step,
"artifact": command.artifact,
"reason": command.reason,
"command": command.command
})
}
fn agent_status_warning_json(warning: &AgentStatusWarning) -> Value {
serde_json::json!({
"kind": warning.kind,
"artifact": warning.artifact,
"message": warning.message
})
}
fn inspect_artifact(root: &Path, artifact: &AgentStatusArtifactDef) -> AgentStatusArtifact {
let path = root.join(artifact.path);
match std::fs::metadata(&path) {
Ok(metadata) if metadata.is_file() => AgentStatusArtifact {
name: artifact.name.to_string(),
label: artifact.label.to_string(),
path: artifact.path.to_string(),
present: true,
bytes: Some(metadata.len()),
modified: metadata.modified().ok(),
},
_ => AgentStatusArtifact {
name: artifact.name.to_string(),
label: artifact.label.to_string(),
path: artifact.path.to_string(),
present: false,
bytes: None,
modified: None,
},
}
}
fn recover_seam_id(
root: &Path,
artifacts: &[AgentStatusArtifact],
warnings: &mut Vec<AgentStatusWarning>,
) -> Option<AgentStatusSeam> {
for (artifact_name, source) in [
("agent_receipt", "agent_receipt"),
("agent_verify", "agent_verify"),
("agent_packet", "agent_packet"),
("agent_brief", "agent_brief"),
] {
let Some(artifact) = artifact_by_name(artifacts, artifact_name).filter(|a| a.present)
else {
continue;
};
let path = root.join(&artifact.path);
let Some(value) = read_json_artifact(&path, artifact, warnings) else {
continue;
};
if let Some(seam_id) = seam_id_from_source(&value, source) {
return Some(AgentStatusSeam {
seam_id,
source: source.to_string(),
});
}
}
None
}
fn read_json_artifact(
path: &Path,
artifact: &AgentStatusArtifact,
warnings: &mut Vec<AgentStatusWarning>,
) -> Option<Value> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(err) => {
warnings.push(AgentStatusWarning {
kind: "artifact_json_unreadable".to_string(),
artifact: artifact.name.clone(),
message: format!("could not read {}: {err}", artifact.path),
});
return None;
}
};
match serde_json::from_str::<Value>(&text) {
Ok(value) => Some(value),
Err(err) => {
warnings.push(AgentStatusWarning {
kind: "artifact_json_unreadable".to_string(),
artifact: artifact.name.clone(),
message: format!("could not parse {} as JSON: {err}", artifact.path),
});
None
}
}
}
fn seam_id_from_source(value: &Value, source: &str) -> Option<String> {
match source {
"agent_receipt" => value
.get("seam")
.and_then(|seam| string_field(seam, "seam_id")),
"agent_verify" => seam_id_from_verify(value),
"agent_packet" => value
.get("packets")
.and_then(Value::as_array)
.and_then(|packets| packets.first())
.and_then(|packet| string_field(packet, "seam_id")),
"agent_brief" => value
.get("working_set")
.and_then(|working_set| string_field(working_set, "seam_id"))
.or_else(|| {
value
.get("top_seams")
.and_then(Value::as_array)
.and_then(|seams| seams.first())
.and_then(|seam| string_field(seam, "seam_id"))
}),
_ => None,
}
}
fn seam_id_from_verify(value: &Value) -> Option<String> {
for bucket in [
"changed_seams",
"unchanged_seams",
"new_gaps",
"resolved_gaps",
] {
let Some(seam_id) = value
.get(bucket)
.and_then(Value::as_array)
.and_then(|seams| seams.first())
.and_then(|seam| string_field(seam, "seam_id"))
else {
continue;
};
return Some(seam_id);
}
None
}
fn string_field(value: &Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(Value::as_str)
.filter(|value| !value.trim().is_empty())
.map(str::to_string)
}
fn stale_warnings(artifacts: &[AgentStatusArtifact]) -> Vec<AgentStatusWarning> {
let mut warnings = Vec::new();
push_stale_warning(
artifacts,
"agent_verify",
"before_snapshot",
"agent verify is older than the before snapshot; rerun verify after refreshing snapshots",
&mut warnings,
);
push_stale_warning(
artifacts,
"agent_verify",
"after_snapshot",
"agent verify is older than the after snapshot; rerun verify after refreshing snapshots",
&mut warnings,
);
push_stale_warning(
artifacts,
"agent_receipt",
"agent_verify",
"agent receipt is older than agent verify; rerun receipt after refreshing verify",
&mut warnings,
);
warnings
}
fn push_stale_warning(
artifacts: &[AgentStatusArtifact],
stale_candidate: &str,
newer_input: &str,
message: &str,
warnings: &mut Vec<AgentStatusWarning>,
) {
let Some(candidate) = artifact_by_name(artifacts, stale_candidate).filter(|a| a.present) else {
return;
};
let Some(input) = artifact_by_name(artifacts, newer_input).filter(|a| a.present) else {
return;
};
let (Some(candidate_modified), Some(input_modified)) = (candidate.modified, input.modified)
else {
return;
};
if candidate_modified.duration_since(input_modified).is_err() {
warnings.push(AgentStatusWarning {
kind: "stale_artifact".to_string(),
artifact: candidate.name.clone(),
message: message.to_string(),
});
}
}
fn missing_commands(
root_argument: &Path,
seam: Option<&AgentStatusSeam>,
artifacts: &[AgentStatusArtifact],
) -> Vec<AgentStatusCommand> {
let mut commands = Vec::new();
for artifact_name in MISSING_COMMAND_ORDER {
let Some(artifact) = artifact_by_name(artifacts, artifact_name) else {
continue;
};
if artifact.present {
continue;
}
commands.push(AgentStatusCommand {
step: artifact.name.clone(),
artifact: artifact.path.clone(),
reason: format!("{} artifact is missing", artifact.label),
command: command_for_missing_artifact(root_argument, seam, artifact),
});
}
commands
}
fn command_for_missing_artifact(
root_argument: &Path,
seam: Option<&AgentStatusSeam>,
artifact: &AgentStatusArtifact,
) -> String {
let root = display_path(root_argument);
let seam_id = seam
.map(|seam| seam.seam_id.as_str())
.unwrap_or("<seam-id>");
match artifact.name.as_str() {
"before_snapshot" => {
check_repo_exposure_command(&root, "draft", WORKFLOW_BEFORE_SNAPSHOT_ARTIFACT)
}
"after_snapshot" => {
check_repo_exposure_command(&root, "draft", WORKFLOW_AFTER_SNAPSHOT_ARTIFACT)
}
"agent_packet" => agent_packet_command(&root, seam_id, WORKFLOW_AGENT_PACKET_ARTIFACT),
"agent_brief" => agent_brief_command(&root, seam_id, WORKFLOW_AGENT_BRIEF_ARTIFACT),
"agent_verify" => agent_verify_command(
&root,
WORKFLOW_BEFORE_SNAPSHOT_ARTIFACT,
WORKFLOW_AFTER_SNAPSHOT_ARTIFACT,
Some(WORKFLOW_AGENT_VERIFY_ARTIFACT),
),
"agent_receipt" => agent_receipt_command(
&root,
WORKFLOW_AGENT_VERIFY_ARTIFACT,
seam_id,
Some(WORKFLOW_AGENT_RECEIPT_ARTIFACT),
),
_ => String::new(),
}
}
fn artifact_by_name<'a>(
artifacts: &'a [AgentStatusArtifact],
name: &str,
) -> Option<&'a AgentStatusArtifact> {
artifacts.iter().find(|artifact| artifact.name == name)
}
fn modified_unix_ms(time: Option<SystemTime>) -> Option<u64> {
let millis = time?.duration_since(UNIX_EPOCH).ok()?.as_millis();
u64::try_from(millis).ok()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use std::time::Duration;
fn unique_agent_status_test_dir(label: &str) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!(
"ripr-agent-status-{label}-{}-{stamp}",
std::process::id()
))
}
fn write_file(path: &Path, text: &str) -> Result<(), String> {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
std::fs::create_dir_all(parent).map_err(|err| format!("create parent: {err}"))?;
}
std::fs::write(path, text).map_err(|err| format!("write {}: {err}", path.display()))
}
fn artifact(name: &str, present: bool, modified: Option<SystemTime>) -> AgentStatusArtifact {
AgentStatusArtifact {
name: name.to_string(),
label: name.replace('_', " "),
path: format!("target/ripr/{name}.json"),
present,
bytes: Some(1),
modified,
}
}
#[test]
fn agent_status_reports_missing_artifacts_and_next_commands() -> Result<(), String> {
let root = unique_agent_status_test_dir("missing");
std::fs::create_dir_all(&root).map_err(|err| format!("create root: {err}"))?;
let report = build_agent_status_report(&root, Path::new("."));
let rendered = render_agent_status_json(&report)?;
let value: Value =
serde_json::from_str(&rendered).map_err(|err| format!("parse status JSON: {err}"))?;
assert_eq!(value["schema_version"], AGENT_STATUS_SCHEMA_VERSION);
assert_eq!(value["status"], "incomplete");
assert_eq!(value["seam"], Value::Null);
assert_eq!(value["artifacts"].as_array().map(Vec::len), Some(6));
assert_eq!(value["missing_commands"].as_array().map(Vec::len), Some(6));
assert_eq!(value["next_command"]["step"], "before_snapshot");
assert_eq!(
value["next_command"]["command"],
"ripr check --root . --mode draft --format repo-exposure-json > target/ripr/workflow/before.repo-exposure.json"
);
std::fs::remove_dir_all(&root).map_err(|err| format!("remove root: {err}"))?;
Ok(())
}
#[test]
fn agent_status_reports_complete_when_all_artifacts_are_present() -> Result<(), String> {
let root = unique_agent_status_test_dir("complete");
write_file(&root.join(WORKFLOW_BEFORE_SNAPSHOT_ARTIFACT), "{}")?;
write_file(&root.join(WORKFLOW_AFTER_SNAPSHOT_ARTIFACT), "{}")?;
write_file(&root.join(WORKFLOW_AGENT_BRIEF_ARTIFACT), "{}")?;
write_file(&root.join(WORKFLOW_AGENT_PACKET_ARTIFACT), "{}")?;
write_file(
&root.join(WORKFLOW_AGENT_VERIFY_ARTIFACT),
r#"{
"changed_seams": [],
"unchanged_seams": [{"seam_id": "from-verify"}],
"new_gaps": [],
"resolved_gaps": []
}"#,
)?;
write_file(
&root.join(WORKFLOW_AGENT_RECEIPT_ARTIFACT),
r#"{"seam":{"seam_id":"from-receipt"}}"#,
)?;
let report = build_agent_status_report(&root, Path::new("."));
let rendered = render_agent_status_json(&report)?;
let value: Value =
serde_json::from_str(&rendered).map_err(|err| format!("parse status JSON: {err}"))?;
assert_eq!(value["status"], "complete");
assert_eq!(value["seam"]["seam_id"], "from-receipt");
assert_eq!(value["seam"]["source"], "agent_receipt");
assert_eq!(value["missing_commands"].as_array().map(Vec::len), Some(0));
assert_eq!(value["next_command"], Value::Null);
assert_eq!(
value["artifacts"][0]["state"],
serde_json::Value::String("present".to_string())
);
assert!(value["artifacts"][0]["bytes"].as_u64().is_some());
assert!(value["artifacts"][0]["modified_unix_ms"].as_u64().is_some());
std::fs::remove_dir_all(&root).map_err(|err| format!("remove root: {err}"))?;
Ok(())
}
#[test]
fn agent_status_markdown_names_next_command_and_limits() -> Result<(), String> {
let root = unique_agent_status_test_dir("markdown");
std::fs::create_dir_all(&root).map_err(|err| format!("create root: {err}"))?;
let report = build_agent_status_report(&root, Path::new("."));
let rendered = render_agent_status_markdown(&report);
assert!(rendered.contains("# RIPR Agent Status"));
assert!(rendered.contains("Status: incomplete"));
assert!(rendered.contains("| before snapshot | missing |"));
assert!(rendered.contains("## Next Command"));
assert!(rendered.contains("ripr check --root . --mode draft"));
assert!(rendered.contains("No runtime mutation execution."));
assert!(rendered.contains("No generated tests."));
std::fs::remove_dir_all(&root).map_err(|err| format!("remove root: {err}"))?;
Ok(())
}
#[test]
fn agent_status_recovers_seam_id_from_receipt() -> Result<(), String> {
let root = unique_agent_status_test_dir("receipt");
write_file(
&root.join(WORKFLOW_AGENT_RECEIPT_ARTIFACT),
r#"{"seam":{"seam_id":"67fc764ba37d77bd"}}"#,
)?;
let report = build_agent_status_report(&root, Path::new("repo root"));
let seam = report
.seam
.as_ref()
.ok_or_else(|| "expected recovered seam".to_string())?;
assert_eq!(seam.seam_id, "67fc764ba37d77bd");
assert_eq!(seam.source, "agent_receipt");
assert!(report.missing_commands.iter().any(|command| {
command.step == "agent_packet"
&& command.command
== "ripr agent packet --root \"repo root\" --seam-id 67fc764ba37d77bd --json > target/ripr/workflow/agent-packet.json"
}));
std::fs::remove_dir_all(&root).map_err(|err| format!("remove root: {err}"))?;
Ok(())
}
#[test]
fn agent_status_warns_for_malformed_present_json() -> Result<(), String> {
let root = unique_agent_status_test_dir("malformed");
write_file(&root.join(WORKFLOW_AGENT_RECEIPT_ARTIFACT), "{not json")?;
let report = build_agent_status_report(&root, Path::new("."));
assert!(report.seam.is_none());
assert!(report.warnings.iter().any(|warning| {
warning.kind == "artifact_json_unreadable" && warning.artifact == "agent_receipt"
}));
std::fs::remove_dir_all(&root).map_err(|err| format!("remove root: {err}"))?;
Ok(())
}
#[test]
fn agent_status_recovers_seam_id_from_verify_packet_or_brief() {
let verify: Value = serde_json::json!({
"changed_seams": [],
"unchanged_seams": [{"seam_id": "from-verify"}],
"new_gaps": [],
"resolved_gaps": []
});
let packet: Value = serde_json::json!({
"packets": [{"seam_id": "from-packet"}]
});
let brief: Value = serde_json::json!({
"working_set": {"seam_id": "from-brief"},
"top_seams": [{"seam_id": "from-top-seam"}]
});
assert_eq!(
seam_id_from_source(&verify, "agent_verify"),
Some("from-verify".to_string())
);
assert_eq!(
seam_id_from_source(&packet, "agent_packet"),
Some("from-packet".to_string())
);
assert_eq!(
seam_id_from_source(&brief, "agent_brief"),
Some("from-brief".to_string())
);
}
#[test]
fn agent_status_recovers_brief_top_seam_without_working_set_seam() {
let brief: Value = serde_json::json!({
"working_set": {"seam_id": null},
"top_seams": [{"seam_id": "from-top-seam"}]
});
assert_eq!(
seam_id_from_source(&brief, "agent_brief"),
Some("from-top-seam".to_string())
);
}
#[test]
fn agent_status_warns_when_verify_or_receipt_look_stale() {
let early = UNIX_EPOCH + Duration::from_secs(1);
let middle = UNIX_EPOCH + Duration::from_secs(2);
let late = UNIX_EPOCH + Duration::from_secs(3);
let warnings = stale_warnings(&[
artifact("before_snapshot", true, Some(late)),
artifact("after_snapshot", true, Some(late)),
artifact("agent_verify", true, Some(middle)),
artifact("agent_receipt", true, Some(early)),
]);
assert_eq!(warnings.len(), 3);
assert!(warnings.iter().any(|warning| {
warning.artifact == "agent_verify"
&& warning.message.contains("older than the before snapshot")
}));
assert!(warnings.iter().any(|warning| {
warning.artifact == "agent_receipt"
&& warning.message.contains("older than agent verify")
}));
}
#[test]
fn agent_status_emits_all_missing_command_templates() {
let artifacts = ARTIFACTS
.iter()
.map(|def| AgentStatusArtifact {
name: def.name.to_string(),
label: def.label.to_string(),
path: def.path.to_string(),
present: false,
bytes: None,
modified: None,
})
.collect::<Vec<_>>();
let seam = AgentStatusSeam {
seam_id: "seam-a".to_string(),
source: "agent_verify".to_string(),
};
let commands = missing_commands(Path::new("."), Some(&seam), &artifacts);
assert_eq!(commands.len(), 6);
assert!(commands.iter().any(|command| {
command.step == "after_snapshot"
&& command.command
== "ripr check --root . --mode draft --format repo-exposure-json > target/ripr/workflow/after.repo-exposure.json"
}));
assert!(commands.iter().any(|command| {
command.step == "agent_brief"
&& command.command
== "ripr agent brief --root . --seam-id seam-a --json > target/ripr/workflow/agent-brief.json"
}));
assert!(commands.iter().any(|command| {
command.step == "agent_verify"
&& command.command
== "ripr agent verify --root . --before target/ripr/workflow/before.repo-exposure.json --after target/ripr/workflow/after.repo-exposure.json --json > target/ripr/workflow/agent-verify.json"
}));
assert!(commands.iter().any(|command| {
command.step == "agent_receipt"
&& command.command
== "ripr agent receipt --root . --verify-json target/ripr/workflow/agent-verify.json --seam-id seam-a --json --out target/ripr/reports/agent-receipt.json"
}));
}
#[test]
fn agent_status_quotes_paths_with_spaces() {
assert_eq!(
agent_packet_command("repo root", "seam-a", WORKFLOW_AGENT_PACKET_ARTIFACT),
"ripr agent packet --root \"repo root\" --seam-id seam-a --json > target/ripr/workflow/agent-packet.json"
);
}
}