use std::path::Path;
use std::process::Command;
use loopflow::id::WaveId;
use loopflow::store::sqlite::SqliteStore;
use loopflow::store::RunEventRow;
use loopflow::wave::Wave;
fn seed(home: &Path, wave_name: &str) -> Wave {
std::fs::create_dir_all(home).expect("home");
let db = home.join("loopflow.db");
let store = SqliteStore::new(&db).expect("open store");
let wave = Wave::new(
WaveId::new(),
wave_name.to_string(),
home.join("repo").display().to_string(),
);
store.create_wave(&wave).expect("register wave");
let now = chrono::Utc::now().timestamp();
let event = |seq: i64, ts: i64, event: &str| RunEventRow {
run_id: "run-1".to_string(),
process_id: "proc-1".to_string(),
parent_process_id: None,
seq,
ts,
repo: Some(home.join("repo").display().to_string()),
worktree: None,
wave: Some(wave_name.to_string()),
node: "run".to_string(),
event: event.to_string(),
command: Some(r#"["lf","pm","sync"]"#.to_string()),
flow: None,
skill: None,
step_index: None,
error: None,
input_tokens: None,
output_tokens: None,
cache_read_tokens: None,
cost_usd: None,
duration_secs: None,
provider: None,
model: None,
};
store
.insert_run_event(&event(0, now - 120, "started"))
.expect("seed run start");
store
.insert_run_event(&event(1, now - 60, "completed"))
.expect("seed run end");
wave
}
fn status_json(home: &Path, args: &[&str], ambient_wave_id: Option<&str>) -> serde_json::Value {
let mut command = Command::new(env!("CARGO_BIN_EXE_lf"));
command
.arg("status")
.args(args)
.arg("--json")
.env("LF_HOME", home)
.env_remove("LF_DB_PATH")
.env_remove("LF_RUN_ID")
.env_remove("LF_CHANNEL")
.env_remove("LF_WAVE_ID");
if let Some(id) = ambient_wave_id {
command.env("LF_WAVE_ID", id);
}
let output = command.output().expect("lf status runs");
assert!(
output.status.success(),
"lf status failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("utf8");
serde_json::from_str(stdout.trim()).unwrap_or_else(|err| panic!("not JSON: {err}\n{stdout}"))
}
#[test]
fn status_carries_the_runs_and_attention_it_promises() {
let home = tempfile::tempdir().expect("tempdir");
seed(home.path(), "audit-a");
let status = status_json(home.path(), &["audit-a"], None);
assert_eq!(status["wave"]["name"], "audit-a");
assert_eq!(status["runs"]["state"], "ok");
let runs = status["runs"]["items"].as_array().expect("runs array");
let seeded = runs
.iter()
.find(|run| run["run_id"] == "run-1")
.expect("the wave's run is in its status");
assert_eq!(seeded["status"], "ok");
assert_eq!(seeded["label"], "pm sync");
assert_eq!(status["attention"]["state"], "ok");
assert_eq!(status["attention"]["items"], serde_json::json!([]));
}
#[test]
fn ambient_wave_id_resolves_the_wave_it_names() {
let home = tempfile::tempdir().expect("tempdir");
let wave = seed(home.path(), "audit-b");
let status = status_json(home.path(), &[], Some(wave.id().as_str()));
assert_eq!(status["wave"]["id"], wave.id().as_str());
assert_eq!(status["wave"]["name"], "audit-b");
assert_eq!(status["runs"]["state"], "ok");
}
#[test]
fn a_wave_with_no_runs_reports_an_empty_reading_not_a_missing_one() {
let home = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(home.path()).expect("home");
let store = SqliteStore::new(&home.path().join("loopflow.db")).expect("open store");
let wave = Wave::new(
WaveId::new(),
"audit-c".to_string(),
home.path().join("repo").display().to_string(),
);
store.create_wave(&wave).expect("register wave");
let status = status_json(home.path(), &["audit-c"], None);
assert_eq!(status["runs"]["state"], "ok");
assert_eq!(status["runs"]["items"], serde_json::json!([]));
assert_eq!(status["runs"]["truncated"], false);
}