use anyhow::{Context, Result};
use serde_json::json;
use crate::services::output_format::OutputFormat;
use crate::services::style;
use crate::services::trace::status::{DbStatus, StatusReport};
use crate::services::trace::NAME;
const HEADING: &str = "SCE trace status";
pub fn render(report: &StatusReport, format: OutputFormat) -> Result<String> {
match format {
OutputFormat::Text => Ok(render_text(report)),
OutputFormat::Json => render_json(report),
}
}
fn render_text(report: &StatusReport) -> String {
let mut lines = vec![style::heading(HEADING)];
lines.push(format!("Checkout: {}", report.checkout_id));
lines.push(format!("Database: {}", report.database_path.display()));
match &report.db_status {
DbStatus::Ready {
stats,
last_activity,
} => {
lines.push(String::from("Status: ready"));
lines.push(format!("Diff traces: {}", stats.diff_traces));
lines.push(format!("Messages: {}", stats.messages));
lines.push(format!("Parts: {}", stats.parts));
lines.push(format!("Agent traces: {}", stats.agent_traces));
lines.push(format!(
"Post-commit intersections: {}",
stats.post_commit_patch_intersections
));
lines.push(format!(
"Last activity: {}",
last_activity.map_or_else(|| String::from("never"), |dt| dt.to_rfc3339())
));
}
DbStatus::Skipped { missing_table } => {
lines.push(format!("Status: skipped: missing table '{missing_table}'"));
}
}
lines.join("\n")
}
fn render_json(report: &StatusReport) -> Result<String> {
let mut payload = json!({
"status": "ok",
"command": NAME,
"subcommand": "status",
"checkout_id": report.checkout_id,
"database_path": report.database_path.display().to_string(),
});
let object = payload.as_object_mut().expect("payload is object");
match &report.db_status {
DbStatus::Ready {
stats,
last_activity,
} => {
object.insert("db_status".to_string(), json!("ready"));
object.insert(
"stats".to_string(),
json!({
"diff_traces": stats.diff_traces,
"messages": stats.messages,
"parts": stats.parts,
"agent_traces": stats.agent_traces,
"post_commit_patch_intersections": stats.post_commit_patch_intersections,
}),
);
object.insert(
"last_activity".to_string(),
last_activity.map_or(serde_json::Value::Null, |dt| json!(dt.to_rfc3339())),
);
}
DbStatus::Skipped { missing_table } => {
object.insert("db_status".to_string(), json!("skipped"));
object.insert(
"skip_reason".to_string(),
json!(format!("missing table: {missing_table}")),
);
}
}
serde_json::to_string_pretty(&payload)
.context("failed to serialize trace status report to JSON")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use chrono::{DateTime, Utc};
use crate::services::trace::stats::AgentTraceDbStats;
fn ready_report() -> StatusReport {
let last =
DateTime::<Utc>::from_timestamp_millis(1_782_650_096_789).expect("timestamp parses");
StatusReport {
checkout_id: String::from("01900000-0000-7000-8000-000000000abc"),
database_path: PathBuf::from("/tmp/agent-trace-abc.db"),
db_status: DbStatus::Ready {
stats: AgentTraceDbStats {
diff_traces: 7,
messages: 4,
parts: 11,
agent_traces: 3,
post_commit_patch_intersections: 1,
last_activity: Some(last),
},
last_activity: Some(last),
},
}
}
fn skipped_report() -> StatusReport {
StatusReport {
checkout_id: String::from("01900000-0000-7000-8000-000000000def"),
database_path: PathBuf::from("/tmp/agent-trace-def.db"),
db_status: DbStatus::Skipped {
missing_table: String::from("agent_traces"),
},
}
}
#[test]
fn ready_text_renders_all_counts_and_last_activity() {
let rendered = render_text(&ready_report());
assert!(rendered.contains("SCE trace status"));
assert!(rendered.contains("Checkout: 01900000-0000-7000-8000-000000000abc"));
assert!(rendered.contains("Database: /tmp/agent-trace-abc.db"));
assert!(rendered.contains("Status: ready"));
assert!(rendered.contains("Diff traces: 7"));
assert!(rendered.contains("Messages: 4"));
assert!(rendered.contains("Parts: 11"));
assert!(rendered.contains("Agent traces: 3"));
assert!(rendered.contains("Post-commit intersections: 1"));
assert!(rendered.contains("Last activity: 2026-06-28T"));
}
#[test]
fn ready_text_renders_never_when_last_activity_absent() {
let mut report = ready_report();
if let DbStatus::Ready {
ref mut stats,
ref mut last_activity,
} = report.db_status
{
stats.last_activity = None;
*last_activity = None;
}
let rendered = render_text(&report);
assert!(rendered.contains("Last activity: never"));
}
#[test]
fn skipped_text_renders_skip_reason() {
let rendered = render_text(&skipped_report());
assert!(rendered.contains("Status: skipped: missing table 'agent_traces'"));
assert!(!rendered.contains("Diff traces:"));
}
#[test]
fn ready_json_shape_matches_contract() {
let payload = render_json(&ready_report()).expect("json render");
let value: serde_json::Value = serde_json::from_str(&payload).expect("valid json");
assert_eq!(value["status"], "ok");
assert_eq!(value["command"], "trace");
assert_eq!(value["subcommand"], "status");
assert_eq!(value["db_status"], "ready");
assert!(value.get("skip_reason").is_none());
assert_eq!(value["stats"]["diff_traces"], 7);
assert_eq!(value["stats"]["messages"], 4);
assert_eq!(value["stats"]["parts"], 11);
assert_eq!(value["stats"]["agent_traces"], 3);
assert_eq!(value["stats"]["post_commit_patch_intersections"], 1);
assert!(value["last_activity"].is_string());
}
#[test]
fn skipped_json_shape_matches_contract() {
let payload = render_json(&skipped_report()).expect("json render");
let value: serde_json::Value = serde_json::from_str(&payload).expect("valid json");
assert_eq!(value["db_status"], "skipped");
assert_eq!(value["skip_reason"], "missing table: agent_traces");
assert!(value.get("stats").is_none());
assert!(value.get("last_activity").is_none());
}
}