Files
2026-07-13 13:12:00 +08:00

245 lines
7.8 KiB
Rust

//! Exercises the non-TUI setup path: repo detection, pod dir tree, ports.
use std::fs;
use std::sync::{Mutex, MutexGuard};
// The crate is a binary, so pull in the modules under test directly. Each test
// target uses only part of the included source, so allow dead code.
#[allow(dead_code)]
#[path = "../src/lock.rs"]
mod lock;
#[allow(dead_code)]
#[path = "../src/paths.rs"]
mod paths;
#[allow(dead_code)]
#[path = "../src/pod.rs"]
mod pod;
#[allow(dead_code)]
#[path = "../src/ports.rs"]
mod ports;
use pod::Pod;
use ports::Ports;
/// A fake checkout (.git + omnigent/ + web/) is recognized as a root, and a
/// nested subdir resolves up to it.
#[test]
fn finds_repo_root_from_subdir() {
let tmp = tempdir();
fs::create_dir_all(tmp.join(".git")).unwrap();
fs::create_dir_all(tmp.join("omnigent/server")).unwrap();
fs::create_dir_all(tmp.join("web/src")).unwrap();
let root = paths::find_repo_root(&tmp.join("omnigent/server")).unwrap();
assert_eq!(root, tmp.canonicalize().unwrap());
}
/// A VCS root without omnigent/+web/ is rejected.
#[test]
fn rejects_non_omnigent_project() {
let tmp = tempdir();
fs::create_dir_all(tmp.join(".git")).unwrap();
assert!(paths::find_repo_root(&tmp).is_err());
}
/// Two different repo paths get distinct pod dirs; the same path is stable.
#[test]
fn pod_dir_is_per_repo_and_stable() {
let a1 = paths::default_pod_dir(std::path::Path::new("/repos/one")).unwrap();
let a2 = paths::default_pod_dir(std::path::Path::new("/repos/one")).unwrap();
let b = paths::default_pod_dir(std::path::Path::new("/repos/two")).unwrap();
assert_eq!(a1, a2);
assert_ne!(a1, b);
}
/// npm install is needed when node_modules is missing, and when a manifest is
/// newer than it; not needed when node_modules is up to date.
#[test]
fn needs_npm_install_tracks_manifests() {
let repo = tempdir();
let web = repo.join("web");
fs::create_dir_all(&web).unwrap();
fs::write(web.join("package.json"), "{}").unwrap();
let pod = Pod {
repo_root: repo.clone(),
dir: repo.join("pod"),
ports: Ports {
server: 6767,
vite: 5173,
},
vite_host: "127.0.0.1".into(),
trusted_origins: Vec::new(),
};
// No node_modules yet → install needed.
assert!(pod.needs_npm_install());
// Fresh node_modules created after the manifest → up to date.
fs::create_dir_all(web.join("node_modules")).unwrap();
assert!(!pod.needs_npm_install());
// A manifest touched after node_modules → stale, install needed.
// (Sleep briefly so the mtime is observably newer on coarse filesystems.)
std::thread::sleep(std::time::Duration::from_millis(10));
fs::write(web.join("package-lock.json"), "{}").unwrap();
assert!(pod.needs_npm_install());
}
/// Ports probe to bindable values and persist/reuse across calls.
#[test]
fn ports_resolve_and_persist() {
let tmp = tempdir();
let p1 = Ports::resolve(&tmp, None, None).unwrap();
assert_ne!(p1.server, p1.vite);
assert!(tmp.join("pod.toml").is_file());
// A second resolve reuses the persisted pair (both still free).
let p2 = Ports::resolve(&tmp, None, None).unwrap();
assert_eq!(p1.server, p2.server);
assert_eq!(p1.vite, p2.vite);
// Explicit overrides win.
let p3 = Ports::resolve(&tmp, Some(19191), Some(19292)).unwrap();
assert_eq!(p3.server, 19191);
assert_eq!(p3.vite, 19292);
}
/// Two sibling pods under the same cache root never collide, even before their
/// processes have bound anything — the second reads the first's pod.toml.
#[test]
fn sibling_pods_get_distinct_ports() {
let root = tempdir();
let pod_a = root.join("repo-aaaa");
let pod_b = root.join("repo-bbbb");
fs::create_dir_all(&pod_a).unwrap();
fs::create_dir_all(&pod_b).unwrap();
// Pod A resolves and persists first (no process is ever spawned).
let a = Ports::resolve(&pod_a, None, None).unwrap();
// Pod B must avoid A's ports purely from A's persisted claim.
let b = Ports::resolve(&pod_b, None, None).unwrap();
assert_ne!(a.server, b.server);
assert_ne!(a.vite, b.vite);
assert_ne!(a.server, b.vite);
assert_ne!(a.vite, b.server);
}
/// A pod admits one holder; a second acquire fails until the first is dropped.
#[test]
fn pod_lock_is_exclusive() {
let pod = tempdir();
let held = lock::acquire(&pod).expect("first acquire succeeds");
assert!(
lock::acquire(&pod).is_err(),
"second acquire must fail while the first is held"
);
drop(held);
lock::acquire(&pod).expect("acquire succeeds again after release");
}
const ALLOWED_ORIGINS_ENV: &str = "OMNIGENT_WS_ALLOWED_ORIGINS";
/// Tests that read/write the process-global allowlist env var; serialize them.
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn lock_env() -> MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// Run `body` with `OMNIGENT_WS_ALLOWED_ORIGINS` set to `value` (or unset when
/// `None`), restoring the prior value afterward so tests don't leak env state.
fn with_allowlist_env(value: Option<&str>, body: impl FnOnce()) {
let _guard = lock_env();
let prev = std::env::var(ALLOWED_ORIGINS_ENV).ok();
match value {
Some(v) => std::env::set_var(ALLOWED_ORIGINS_ENV, v),
None => std::env::remove_var(ALLOWED_ORIGINS_ENV),
}
body();
match prev {
Some(v) => std::env::set_var(ALLOWED_ORIGINS_ENV, v),
None => std::env::remove_var(ALLOWED_ORIGINS_ENV),
}
}
fn pod_with_trusted(trusted: Vec<String>) -> Pod {
Pod {
repo_root: std::path::PathBuf::from("/repo"),
dir: std::path::PathBuf::from("/pod"),
ports: Ports {
server: 6767,
vite: 5173,
},
vite_host: "0.0.0.0".into(),
trusted_origins: trusted,
}
}
fn allowlist_from_env(pod: &Pod) -> Option<String> {
pod.env()
.into_iter()
.find(|(k, _)| k == ALLOWED_ORIGINS_ENV)
.map(|(_, v)| v)
}
/// With no trusted origins, the pod leaves the allowlist var untouched — even
/// when the developer's shell already exports one (it passes through inherited).
#[test]
fn no_trusted_origins_does_not_set_allowlist() {
with_allowlist_env(Some("https://dev.example.com"), || {
let pod = pod_with_trusted(Vec::new());
assert_eq!(allowlist_from_env(&pod), None);
});
}
/// Trusted origins with no inherited value produce exactly those origins.
#[test]
fn trusted_origins_populate_allowlist() {
with_allowlist_env(None, || {
let pod = pod_with_trusted(vec!["http://192.168.1.42:5173".into()]);
assert_eq!(
allowlist_from_env(&pod).as_deref(),
Some("http://192.168.1.42:5173")
);
});
}
/// A developer's inherited allowlist is preserved and the LAN origins are
/// appended (order-preserving, deduped) rather than clobbered.
#[test]
fn trusted_origins_merge_with_inherited_allowlist() {
with_allowlist_env(
Some("https://dev.example.com, http://192.168.1.42:5173"),
|| {
let pod = pod_with_trusted(vec![
"http://192.168.1.42:5173".into(), // already inherited → not duplicated
"http://10.0.0.9:5173".into(),
]);
assert_eq!(
allowlist_from_env(&pod).as_deref(),
Some("https://dev.example.com,http://192.168.1.42:5173,http://10.0.0.9:5173")
);
},
);
}
/// Minimal unique temp dir without pulling a dev-dependency.
fn tempdir() -> std::path::PathBuf {
let base = std::env::temp_dir();
let unique = format!(
"omnidev-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
let dir = base.join(unique);
std::fs::create_dir_all(&dir).unwrap();
dir
}