use crate::models::field_names;
use anyhow::Result;
use clap::Args;
use serde_json::{Value, json};
use crate::cli::CliOutput;
use crate::storage as db;
#[derive(Args, Debug, Clone)]
pub struct ReplayArgs {
#[arg(long = "memory-id", value_name = "ID")]
pub memory_id: String,
#[arg(long)]
pub verbose: bool,
#[arg(long, value_name = "N")]
pub depth: Option<i64>,
#[arg(long = "agent-id", value_name = "AGENT_ID")]
pub agent_id: Option<String>,
#[arg(long)]
pub json: bool,
}
pub fn cmd_replay(
db_path: &std::path::Path,
args: &ReplayArgs,
out: &mut CliOutput<'_>,
) -> Result<()> {
let conn = db::open(db_path)?;
let mut params = json!({
"memory_id": args.memory_id,
"verbose": args.verbose,
});
if let Some(d) = args.depth {
params["depth"] = json!(d);
}
if let Some(a) = &args.agent_id {
params["agent_id"] = json!(a);
}
let envelope = crate::mcp::handle_replay(&conn, ¶ms, None, None)
.map_err(|e| anyhow::anyhow!("replay: {e}"))?;
if args.json {
writeln!(out.stdout, "{}", serde_json::to_string(&envelope)?)?;
return Ok(());
}
let count = envelope
.get(field_names::TRANSCRIPTS)
.and_then(Value::as_array)
.map_or(0, Vec::len);
writeln!(out.stdout, "replay: {count} transcript(s)")?;
if let Some(arr) = envelope
.get(field_names::TRANSCRIPTS)
.and_then(Value::as_array)
{
for t in arr {
let tid = t
.get("transcript_id")
.and_then(Value::as_str)
.unwrap_or("?");
let created = t
.get(field_names::CREATED_AT)
.and_then(Value::as_str)
.unwrap_or("");
let truncated = t.get("truncated").and_then(Value::as_bool).unwrap_or(false);
let osize = t.get("original_size").and_then(Value::as_u64).unwrap_or(0);
writeln!(
out.stdout,
" {tid} created={created} bytes={osize} truncated={truncated}",
)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::test_utils::{TestEnv, seed_memory};
#[test]
fn replay_cli_no_transcripts_returns_zero_count() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let mid = seed_memory(&db, "ns", "replay-src", "content");
let args = ReplayArgs {
memory_id: mid,
verbose: false,
depth: None,
agent_id: None,
json: true,
};
{
let mut out = env.output();
cmd_replay(&db, &args, &mut out).expect("replay ok");
}
let stdout = env.stdout_str();
let envelope: Value = serde_json::from_str(stdout.trim()).expect("parse envelope");
let count = envelope
.get("transcripts")
.and_then(Value::as_array)
.map_or(0, Vec::len);
assert_eq!(count, 0);
}
#[test]
fn replay_cli_invalid_id_returns_err() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let args = ReplayArgs {
memory_id: "bogus id".to_string(),
verbose: false,
depth: None,
agent_id: None,
json: true,
};
let mut out = env.output();
let err = cmd_replay(&db, &args, &mut out).expect_err("must fail");
assert!(err.to_string().contains("replay"), "got: {err}");
}
#[test]
fn replay_cli_text_output_with_transcript_lists_entries() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let caller = crate::identity::resolve_agent_id(None, None).unwrap();
let mid = {
let conn = db::open(&db).unwrap();
let now = chrono::Utc::now().to_rfc3339();
let mem = crate::models::Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: crate::models::Tier::Mid,
namespace: "ns".to_string(),
title: "replay-text-src".to_string(),
content: "content".to_string(),
tags: vec![],
priority: 5,
confidence: 1.0,
source: "import".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: serde_json::json!({"agent_id": caller}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
};
db::insert(&conn, &mem).expect("insert anchor")
};
{
let conn = db::open(&db).unwrap();
let t = crate::transcripts::storage::store(&conn, "ns", "transcript body", None)
.expect("store transcript");
crate::transcripts::storage::link_transcript(&conn, &mid, &t.id, None, None)
.expect("link transcript");
}
let args = ReplayArgs {
memory_id: mid,
verbose: false,
depth: None,
agent_id: None,
json: false,
};
{
let mut out = env.output();
cmd_replay(&db, &args, &mut out).expect("replay ok");
}
let stdout = env.stdout_str();
assert!(stdout.contains("replay: 1 transcript(s)"), "got: {stdout}");
assert!(stdout.contains("created="), "got: {stdout}");
assert!(stdout.contains("truncated=false"), "got: {stdout}");
}
#[test]
fn replay_cli_text_output_no_transcripts() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let mid = seed_memory(&db, "ns", "replay-empty", "content");
let args = ReplayArgs {
memory_id: mid,
verbose: false,
depth: None,
agent_id: None,
json: false,
};
{
let mut out = env.output();
cmd_replay(&db, &args, &mut out).expect("replay ok");
}
assert!(env.stdout_str().contains("replay: 0 transcript(s)"));
}
#[test]
fn replay_cli_depth_and_agent_id_params_threaded() {
let mut env = TestEnv::fresh();
let db = env.db_path.clone();
let mid = seed_memory(&db, "ns", "replay-depth", "content");
let args = ReplayArgs {
memory_id: mid,
verbose: true,
depth: Some(0),
agent_id: Some("ai:replay-tester".to_string()),
json: true,
};
{
let mut out = env.output();
cmd_replay(&db, &args, &mut out).expect("replay ok");
}
let envelope: Value = serde_json::from_str(env.stdout_str().trim()).expect("json");
assert_eq!(
envelope
.get("transcripts")
.and_then(Value::as_array)
.map_or(0, Vec::len),
0
);
}
}