Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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);
}