facett-demo/tests/region_picker.rs
//! **Region picker — full-flow proof (cache-hit stand-in)**, validated as DATA.
//!
//! The 🌍 Region picker clips a region out of the cached planet (osm-katana) and
//! loads the result into the OSM 2D/3D tabs. We do **not** run a real planet→Sweden
//! clip in a test (hours); that's an integration step once the `--clip` CLI lands on
//! znippy `fix/osm-katana-gatling-saturation`. Instead this test drives the parts
//! that ARE testable, the way the agent-ui-test-fixing doctrine prescribes (assert
//! on emitted data / state_json, no pixels):
//!
//! 1. **Dialog + presets** — the modal lists exactly the three v1 presets
//! (Sverige / Skandinavien / Europa), in order, observable in `state_json`.
//! 2. **Preset → clip-arg mapping** — Sverige→`sweden`, Skandinavien→`nordics`,
//! Europa→`europe`, and the full `convert --clip <arg> <planet> -o <out>` argv.
//! 3. **Cache-hit short-circuit** — when the region's GeoParquet already exists we
//! skip the clip and load instantly. We stage a **tiny stand-in**
//! (liechtenstein's parquets, copied into the `sweden-geo` cache slot under a
//! temp `$XDG_CACHE_HOME`) so the whole pipeline flows with no osm-katana run.
//! 4. **Flows through to a LOADED, non-blank viewer** — after `pick`→`poll` reaches
//! `ready`, the host swaps the geometry into the deck and the `OSM 2D` tab
//! renders real tessellated vertices (proved via the headless harness + the
//! tab's own `state_json`, not a screenshot).
//! 5. **state_json surface** — `region_picker = {region, source, out, cache_hit,
//! status, presets, …}`.
//!
//! Run: cargo test -p facett-demo --test region_picker
//! cargo test -p facett-demo --test region_picker --features testmatrix
use facett::Facet;
use facett::core::harness;
use facett_demo::osm_tabs::{self, OsmSource};
use facett_demo::region_picker::{Preset, RegionPicker, Status};
use facett_demo::DemoState;
/// `$XDG_CACHE_HOME` is process-global; these tests each set it to their own temp
/// cache slot. Serialize them under one lock so a parallel test can't read another's
/// env between `set_var` and the worker thread's `region_cache_dir()` lookup.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// The liechtenstein fixture (the tiny real GeoParquet used as the clip stand-in).
fn lie_nodes() -> String {
format!("{}/work/osm/liechtenstein/nodes.parquet", std::env::var("HOME").unwrap_or_default())
}
fn lie_ways() -> String {
format!("{}/work/osm/liechtenstein/ways.parquet", std::env::var("HOME").unwrap_or_default())
}
/// Stage liechtenstein's parquets into `preset`'s cache slot under a fresh temp
/// `$XDG_CACHE_HOME`, so `pick(preset)` is a **cache hit** that loads the tiny real
/// data instead of running a planet clip. Returns `(temp_cache_root, picker)` with
/// the picker's `XDG_CACHE_HOME` already pointed at the temp root (caller restores
/// env + cleans up). `None` if the fixture isn't on this box.
fn stage_cache_hit(preset: Preset) -> Option<std::path::PathBuf> {
if !std::path::Path::new(&lie_nodes()).exists() || !std::path::Path::new(&lie_ways()).exists() {
return None;
}
let root = std::env::temp_dir().join(format!("facett-region-flow-{}-{}", std::process::id(), preset.clip_arg()));
let dir = root.join("facett-osm").join(format!("{}-geo", preset.clip_arg()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::copy(lie_nodes(), dir.join("nodes.parquet")).unwrap();
std::fs::copy(lie_ways(), dir.join("ways.parquet")).unwrap();
Some(root)
}
#[test]
fn dialog_lists_three_presets_and_maps_to_clip_args() {
// Dialog content (the modal's presets) + the load-bearing mapping, as data.
let rp = RegionPicker::new();
let s = rp.state_json();
let presets: Vec<String> =
s["presets"].as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect();
assert_eq!(presets, vec!["Sverige", "Skandinavien", "Europa"]);
assert_eq!(Preset::Sverige.clip_arg(), "sweden");
assert_eq!(Preset::Skandinavien.clip_arg(), "nordics");
assert_eq!(Preset::Europa.clip_arg(), "europe");
// (The old shell `convert --clip <arg>` argv builder is gone — the picker now
// calls osm_katana::convert() IN-PROCESS, so the preset→clip-arg mapping above
// is the load-bearing surface; argv no longer exists.)
#[cfg(feature = "testmatrix")]
facett::core::testmatrix::emit(
"facett_demo.region_picker",
"dialog_presets_and_mapping",
presets.len() == 3,
"presets=[Sverige,Skandinavien,Europa] → [sweden,nordics,europe]",
);
}
#[test]
fn cache_hit_short_circuits_and_loads_a_non_blank_viewer() {
// Stage liechtenstein into the sweden-geo cache slot (tiny stand-in for the
// clip). If the fixture isn't here, skip the data part but still prove wiring.
let _guard = ENV_LOCK.lock().unwrap();
let Some(root) = stage_cache_hit(Preset::Sverige) else {
eprintln!("region_picker flow test: no liechtenstein fixture; skipping the data leg");
return;
};
let prev = std::env::var("XDG_CACHE_HOME").ok();
// SAFETY: single-threaded test setup before spawning the picker worker.
unsafe {
std::env::set_var("XDG_CACHE_HOME", &root);
}
let mut rp = RegionPicker::new();
rp.pick(Preset::Sverige);
// Cache hit → no osm-katana run; the worker only reads the staged parquets.
assert!(rp.state_json()["cache_hit"].as_bool().unwrap(), "staged region is a cache hit");
// Poll the worker to completion (reading the tiny parquet is fast).
let mut tries = 0;
loop {
rp.poll();
match rp.status() {
Status::Ready => break,
Status::Error(e) => panic!("cache-hit load errored: {e}"),
_ => {
tries += 1;
assert!(tries < 5000, "cache-hit load did not reach ready");
std::thread::sleep(std::time::Duration::from_millis(1));
}
}
}
// state_json surface after the load.
let s = rp.state_json();
assert_eq!(s["region"], "Sverige");
assert_eq!(s["clip_arg"], "sweden");
assert_eq!(s["status"], "ready");
assert_eq!(s["cache_hit"], true);
assert!(s["out"].as_str().unwrap().ends_with("sweden-geo"));
// The loaded geometry flows into the viewer: take it, build the 2D tab, render
// through the CPU headless harness, and PROVE non-blank (real vertices + the
// tab's own state_json reports geometry). This is the "tiny stand-in flows
// through to a loaded viewer" requirement.
let geo = rp.take_loaded().expect("ready → loaded geometry available");
assert_eq!(geo.source, OsmSource::Region("Sverige".to_string()));
assert!(!geo.points.is_empty() || !geo.ways_2d.is_empty(), "loaded real features");
let mut v2 = osm_tabs::osm2d_view(&geo);
#[cfg(feature = "map-wgpu")]
{
v2 = v2.with_gpu(false); // CPU lane for the headless proof
}
let r2 = harness::headless_render(&mut v2);
assert!(r2.drew(), "the region-loaded 2D tab tessellated to vertices (not blank)");
let geom = r2.state["points"].as_u64().unwrap_or(0) + r2.state["ways"].as_u64().unwrap_or(0);
assert!(geom > 0, "the region-loaded 2D tab has geometry to draw");
#[cfg(feature = "testmatrix")]
facett::core::testmatrix::emit(
"facett_demo.region_picker",
"cache_hit_loads_non_blank",
r2.drew() && geom > 0,
&format!(
"region=Sverige cache_hit=true points={} ways={} verts={}",
r2.state["points"], r2.state["ways"], r2.vertices
),
);
unsafe {
match prev {
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
None => std::env::remove_var("XDG_CACHE_HOME"),
}
}
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn demo_state_swaps_loaded_region_into_the_osm_tabs() {
// The host integration: DemoState::apply_loaded_region pulls the picker's loaded
// geometry and swaps it into the OSM 2D/3D deck slots in place. Stage a cache hit
// and drive the picker through DemoState's own field.
let _guard = ENV_LOCK.lock().unwrap();
let Some(root) = stage_cache_hit(Preset::Skandinavien) else {
eprintln!("region_picker host test: no liechtenstein fixture; skipping");
return;
};
let prev = std::env::var("XDG_CACHE_HOME").ok();
unsafe {
std::env::set_var("XDG_CACHE_HOME", &root);
}
let mut demo = DemoState::new();
// The picker + the OSM tabs are both present on the deck.
assert!(demo.state_json()["region_picker"]["presets"].as_array().unwrap().len() == 3);
demo.region_picker.pick(Preset::Skandinavien);
let applied: Option<String>;
let mut tries = 0;
loop {
demo.region_picker.poll();
if let Some(label) = demo.apply_loaded_region() {
applied = Some(label);
break;
}
if let Status::Error(e) = demo.region_picker.status() {
panic!("host region load errored: {e}");
}
tries += 1;
assert!(tries < 5000, "host region load did not complete");
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert_eq!(applied.as_deref(), Some("Skandinavien"));
// The swapped-in OSM 2D tab is the canonical Facet, still discoverable by title,
// and renders the region's data non-blank.
let st = demo.state_json();
assert_eq!(st["region_picker"]["status"], "ready");
// The deck still has the stable OSM tabs (we replaced in place, not appended).
assert!(st["deck"]["facets"].get(osm_tabs::TITLE_2D).is_some(), "OSM 2D slot kept");
assert!(st["deck"]["facets"].get(osm_tabs::TITLE_3D).is_some(), "OSM 3D slot kept");
let v2 = demo.deck.facet_mut::<facett::geomap::OsmView>(osm_tabs::TITLE_2D);
assert!(v2.is_some(), "OSM 2D tab is the swapped-in OsmView");
let r = harness::headless_render(v2.unwrap() as &mut dyn Facet);
assert!(r.drew(), "the host-swapped OSM 2D tab renders non-blank");
#[cfg(feature = "testmatrix")]
facett::core::testmatrix::emit(
"facett_demo.region_picker",
"host_swaps_region_into_tabs",
applied.as_deref() == Some("Skandinavien") && r.drew(),
"DemoState.apply_loaded_region → OSM 2D/3D slots reloaded in place",
);
unsafe {
match prev {
Some(v) => std::env::set_var("XDG_CACHE_HOME", v),
None => std::env::remove_var("XDG_CACHE_HOME"),
}
}
let _ = std::fs::remove_dir_all(&root);
}