use std::path::Path;
use std::process::Command;
use loopflow::engine::wave_context::{resolve_managed_wave_name, WaveResolveError};
use loopflow::id::WaveId;
use loopflow::store::sqlite::SqliteStore;
use loopflow::store::{open_store, PmSnapshotRow, StorageConfig};
use loopflow::wave::Wave;
#[tokio::test]
async fn resolver_matrix_agrees_across_every_environment() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = open_store(&StorageConfig::sqlite(tmp.path().join("loopflow.db")))
.await
.expect("open store");
let wave = Wave::new(
WaveId::new(),
"product".to_string(),
tmp.path().join("repo").display().to_string(),
);
store.create_wave(&wave).await.expect("register wave");
let uuid = wave.id().as_str();
assert_eq!(
resolve_managed_wave_name(Some(&store), None, Some(uuid))
.await
.expect("uuid resolves"),
"product"
);
assert_eq!(
resolve_managed_wave_name(Some(&store), None, Some("product"))
.await
.expect("hand-set name resolves"),
"product"
);
assert_eq!(
resolve_managed_wave_name(Some(&store), Some("product"), Some("something-else"))
.await
.expect("explicit wins"),
"product"
);
let stale = WaveId::new();
assert_eq!(
resolve_managed_wave_name(Some(&store), None, Some(stale.as_str())).await,
Err(WaveResolveError::StaleIdentity(stale.as_str().to_string()))
);
assert_eq!(
resolve_managed_wave_name(Some(&store), None, None).await,
Err(WaveResolveError::NoContext)
);
assert_eq!(
resolve_managed_wave_name(Some(&store), None, Some(" ")).await,
Err(WaveResolveError::NoContext)
);
assert_eq!(
resolve_managed_wave_name(Some(&store), Some(" "), None).await,
Err(WaveResolveError::EmptyExplicit)
);
assert_eq!(
resolve_managed_wave_name(Some(&store), Some("definitely-unknown"), None).await,
Err(WaveResolveError::UnknownExplicit(
"definitely-unknown".to_string()
))
);
assert!(matches!(
resolve_managed_wave_name(None, Some("product"), None).await,
Err(WaveResolveError::Registry(_))
));
assert_eq!(
resolve_managed_wave_name(Some(&store), None, Some("ghost"))
.await
.expect("unregistered name resolves to itself"),
"ghost"
);
}
fn seed(home: &Path, repo: &Path, wave_name: &str) -> Wave {
std::fs::create_dir_all(home).expect("home");
std::fs::create_dir_all(repo).expect("repo");
let store = SqliteStore::new(&home.join("loopflow.db")).expect("open store");
let wave = Wave::new(
WaveId::new(),
wave_name.to_string(),
repo.display().to_string(),
);
store.create_wave(&wave).expect("register wave");
let repo_key = std::fs::canonicalize(repo)
.expect("canonicalize repo")
.display()
.to_string();
store
.put_pm_snapshot(&PmSnapshotRow {
repo: repo_key,
wave: wave_name.to_string(),
provider: "linear".to_string(),
initiative: "initiative-1".to_string(),
synced_at: chrono::Utc::now().timestamp(),
payload: r#"{"projects":[],"items":[]}"#.to_string(),
})
.expect("seed pm snapshot");
wave
}
fn lf(home: &Path, repo: &Path, args: &[&str], wave_id: Option<&str>) -> std::process::Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_lf"));
command
.args(args)
.current_dir(repo)
.env("LF_HOME", home)
.env_remove("LF_DB_PATH")
.env_remove("LF_CONTROL_HOME")
.env_remove("LF_CONTROL_DB_PATH")
.env_remove("LF_RUN_ID")
.env_remove("LF_CHANNEL")
.env_remove("LF_WAVE_ID");
if let Some(id) = wave_id {
command.env("LF_WAVE_ID", id);
}
command.output().expect("lf runs")
}
fn wave_field(output: &std::process::Output) -> String {
assert!(
output.status.success(),
"command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).expect("stdout is JSON");
json.get("wave")
.and_then(|wave| wave.get("name").or(Some(wave)))
.and_then(serde_json::Value::as_str)
.expect("a wave name")
.to_string()
}
#[test]
fn pm_show_and_status_agree_from_a_resident_uuid() {
let tmp = tempfile::tempdir().expect("tempdir");
let home = tmp.path().join("home");
let repo = tmp.path().join("repo");
let wave = seed(&home, &repo, "product");
let uuid = wave.id().as_str();
let pm = lf(
&home,
&repo,
&["pm", "show", "--no-sync", "--json"],
Some(uuid),
);
let status = lf(&home, &repo, &["status", "--json"], Some(uuid));
assert_eq!(wave_field(&pm), "product");
assert_eq!(wave_field(&status), "product");
}
#[test]
fn pm_show_honors_the_shared_resolution_rules() {
let tmp = tempfile::tempdir().expect("tempdir");
let home = tmp.path().join("home");
let repo = tmp.path().join("repo");
let wave = seed(&home, &repo, "product");
let uuid = wave.id().as_str();
let overridden = lf(
&home,
&repo,
&["pm", "show", "--wave", "product", "--no-sync", "--json"],
Some(&WaveId::new().to_string()),
);
assert_eq!(wave_field(&overridden), "product");
let named = lf(
&home,
&repo,
&["pm", "show", "--no-sync", "--json"],
Some("product"),
);
assert_eq!(wave_field(&named), "product");
let missing = lf(&home, &repo, &["pm", "show", "--no-sync", "--json"], None);
assert!(!missing.status.success());
assert!(
String::from_utf8_lossy(&missing.stderr).contains("determine wave"),
"missing-context stderr: {}",
String::from_utf8_lossy(&missing.stderr)
);
let stale_id = WaveId::new().to_string();
let stale = lf(
&home,
&repo,
&["pm", "show", "--no-sync", "--json"],
Some(&stale_id),
);
assert!(!stale.status.success());
let stale_err = String::from_utf8_lossy(&stale.stderr);
assert!(
stale_err.contains("stale") && stale_err.contains(&stale_id),
"stale stderr: {stale_err}"
);
let _ = uuid;
}
#[test]
fn memory_show_resolves_like_status_across_environments() {
let tmp = tempfile::tempdir().expect("tempdir");
let home = tmp.path().join("home");
let repo = tmp.path().join("repo");
let wave = seed(&home, &repo, "product");
let uuid = wave.id().as_str();
let wave_dir = repo.join("wave/product");
std::fs::create_dir_all(&wave_dir).expect("wave dir");
std::fs::write(wave_dir.join("MEMORY.md"), "PRODUCT MEMORY\n").expect("seed memory");
let memory =
|args: &[&str], id: Option<&str>| -> std::process::Output { lf(&home, &repo, args, id) };
let stdout = |output: &std::process::Output| -> String {
assert!(
output.status.success(),
"memory show failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).to_string()
};
assert_eq!(
stdout(&memory(&["memory", "show"], Some(uuid))),
"PRODUCT MEMORY\n"
);
assert_eq!(
stdout(&memory(&["memory", "show"], Some("product"))),
"PRODUCT MEMORY\n"
);
assert_eq!(
stdout(&memory(
&["memory", "show", "--wave", "product"],
Some(&WaveId::new().to_string()),
)),
"PRODUCT MEMORY\n"
);
let stale_id = WaveId::new().to_string();
let stale = memory(&["memory", "show"], Some(&stale_id));
assert!(!stale.status.success());
let stale_err = String::from_utf8_lossy(&stale.stderr);
assert!(
stale_err.contains("stale") && stale_err.contains(&stale_id),
"stale stderr: {stale_err}"
);
let missing = memory(&["memory", "show"], None);
assert!(!missing.status.success());
assert!(
String::from_utf8_lossy(&missing.stderr).contains("--wave"),
"missing-context stderr: {}",
String::from_utf8_lossy(&missing.stderr)
);
}
#[test]
fn unknown_explicit_wave_is_rejected_identically_by_every_consumer() {
let tmp = tempfile::tempdir().expect("tempdir");
let home = tmp.path().join("home");
let repo = tmp.path().join("repo");
let wave = seed(&home, &repo, "product");
let uuid = wave.id().as_str();
let assert_rejected = |output: std::process::Output, label: &str| {
assert!(
!output.status.success(),
"{label}: unknown explicit wave should exit non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("definitely-unknown"),
"{label}: stderr should name the unknown wave: {stderr}"
);
assert!(
stderr.contains("is not registered on this machine"),
"{label}: stderr should classify as unregistered: {stderr}"
);
};
assert_rejected(
lf(
&home,
&repo,
&["memory", "show", "--wave", "definitely-unknown"],
None,
),
"memory show (no ambient)",
);
assert_rejected(
lf(
&home,
&repo,
&["status", "--wave", "definitely-unknown"],
None,
),
"status (no ambient)",
);
assert_rejected(
lf(
&home,
&repo,
&["pm", "show", "--wave", "definitely-unknown", "--no-sync"],
None,
),
"pm show (no ambient)",
);
assert_rejected(
lf(
&home,
&repo,
&["memory", "show", "--wave", "definitely-unknown"],
Some(uuid),
),
"memory show (with ambient)",
);
assert_rejected(
lf(
&home,
&repo,
&["status", "--wave", "definitely-unknown"],
Some(uuid),
),
"status (with ambient)",
);
assert_rejected(
lf(
&home,
&repo,
&["pm", "show", "--wave", "definitely-unknown", "--no-sync"],
Some(uuid),
),
"pm show (with ambient)",
);
}