use std::path::Path;
use std::process::Command;
use loopflow::id::WaveId;
use loopflow::store::sqlite::SqliteStore;
use loopflow::store::{PmSnapshotRow, StorageConfig};
use loopflow::work::wave::context::{resolve_managed_wave, WaveResolveError};
use loopflow::work::wave::Wave;
#[tokio::test]
async fn resolver_matrix_agrees_across_every_environment() {
let tmp = tempfile::tempdir().expect("tempdir");
let store = loopflow::store::open_ephemeral_store(&StorageConfig::sqlite(
tmp.path().join("loopflow.db"),
))
.await
.expect("open store");
let repo = tmp.path().join("repo");
std::fs::create_dir_all(&repo).expect("repo");
let wave = Wave::new(
WaveId::new(),
"product".to_string(),
std::fs::canonicalize(&repo)
.expect("canonical repo")
.display()
.to_string(),
);
store.create_wave(&wave).await.expect("register wave");
let uuid = wave.id().as_str();
assert_eq!(
resolve_managed_wave(Some(&store), Some(&repo), None, Some(uuid))
.await
.expect("uuid resolves")
.id(),
wave.id()
);
assert_eq!(
resolve_managed_wave(Some(&store), Some(&repo), None, Some("product"))
.await
.expect("hand-set name resolves")
.id(),
wave.id()
);
assert_eq!(
resolve_managed_wave(
Some(&store),
Some(&repo),
Some("product"),
Some("something-else"),
)
.await
.expect("explicit wins")
.id(),
wave.id()
);
let stale = WaveId::new();
assert!(matches!(
resolve_managed_wave(Some(&store), Some(&repo), None, Some(stale.as_str())).await,
Err(WaveResolveError::StaleIdentity(value)) if value == stale.as_str()
));
assert!(matches!(
resolve_managed_wave(Some(&store), Some(&repo), None, None).await,
Err(WaveResolveError::NoContext)
));
assert!(matches!(
resolve_managed_wave(Some(&store), Some(&repo), None, Some(" ")).await,
Err(WaveResolveError::NoContext)
));
assert!(matches!(
resolve_managed_wave(Some(&store), Some(&repo), Some(" "), None).await,
Err(WaveResolveError::EmptyExplicit)
));
assert!(matches!(
resolve_managed_wave(
Some(&store),
Some(&repo),
Some("definitely-unknown"),
None,
)
.await,
Err(WaveResolveError::UnknownExplicit(value)) if value == "definitely-unknown"
));
assert!(matches!(
resolve_managed_wave(None, Some(&repo), Some("product"), None).await,
Err(WaveResolveError::Registry(_))
));
assert!(matches!(
resolve_managed_wave(Some(&store), Some(&repo), None, Some("ghost")).await,
Err(WaveResolveError::StaleIdentity(value)) if value == "ghost"
));
let other_repo = tmp.path().join("other-repo");
std::fs::create_dir_all(&other_repo).expect("other repo");
let other = Wave::new(
WaveId::new(),
"product".to_string(),
std::fs::canonicalize(&other_repo)
.expect("canonical other repo")
.display()
.to_string(),
);
store
.create_wave(&other)
.await
.expect("register other Wave");
assert!(matches!(
resolve_managed_wave(Some(&store), None, Some("product"), None).await,
Err(WaveResolveError::AmbiguousWave { .. })
));
assert!(matches!(
resolve_managed_wave(Some(&store), Some(&repo), None, Some(other.id().as_str())).await,
Err(WaveResolveError::RepositoryMismatch { .. })
));
}
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");
store
.put_pm_snapshot(&PmSnapshotRow {
wave_id: wave.id().clone(),
provider: "linear".to_string(),
initiative: "initiative-1".to_string(),
synced_at: chrono::Utc::now().timestamp(),
snapshot: loopflow::pm::PmSnapshot {
projects: vec![],
items: vec![],
},
})
.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_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.env("HOME", home)
.env("LF_HOME", home);
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 cached_status_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,
&["wave", "status", "product", "--json"],
Some(&WaveId::new().to_string()),
);
assert_eq!(wave_field(&overridden), "product");
let named = lf(&home, &repo, &["wave", "status", "--json"], Some("product"));
assert_eq!(wave_field(&named), "product");
let missing = lf(&home, &repo, &["wave", "status", "--json"], None);
assert!(!missing.status.success());
assert!(
String::from_utf8_lossy(&missing.stderr).contains("no wave in context"),
"missing-context stderr: {}",
String::from_utf8_lossy(&missing.stderr)
);
let stale_id = WaveId::new().to_string();
let stale = lf(&home, &repo, &["wave", "status", "--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 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}"
);
};
for ambient in [None, Some(uuid)] {
assert_rejected(
lf(
&home,
&repo,
&["wave", "status", "definitely-unknown"],
ambient,
),
"Wave status",
);
}
}