tests/osm_cut_is_the_pipeline.rs
//! **The OSM collector cuts a REGION, it does not build a tile pyramid.**
//!
//! The owner, 2026-09-02: *"the collector osm tiles is legacy? should it not be the new
//! 3 files from osm download?"* — he is right. korp's map v2 deleted the pyramid
//! (`e66f846..1d53b1b`: the tiling gate, the tiled/monolith routes, `src/map_tiles.rs`,
//! the S3 tile store and 111.8 GB on disk), and the one map source is the packed cut at
//! `~/.korp/work/<region>/` produced by the lettered `a..g` pipeline. The collector was
//! still cutting a `z6/z9/z12` pyramid into a bucket nobody reads.
//!
//! These are the RED gates for the replacement. Each one FAILED before the rename and
//! the rewrite:
//!
//! | gate | red on the old code |
//! |---|---|
//! | [`the_chain_letters_are_the_pipelines_a_to_i`] | `Step::ALL` was 5 long and `'e'` meant *tiles* |
//! | [`the_osm_collector_is_named_for_the_cut_it_makes`] | the registry had `osm-tiles`, no `osm-cut` |
//! | [`a_legacy_osm_tiles_config_entry_resolves_to_the_cut_collector`] | `daemon_with_config("osm-tiles", …)` built the pyramid daemon |
//! | [`no_collector_string_promises_a_tile_a_pyramid_or_a_zoom`] | title/description/schema said *tile*, *pyramid*, *zoom levels* |
//! | [`the_cut_collector_declares_the_seven_steps_not_a_zoom_ladder`] | the schema had `zooms` + `bucket`, no step vocabulary |
use korp_collectors::{daemon, state};
/// **The chain the collector reports IS korp's `a..g` pipeline.**
///
/// `korp::pipeline::Step` letters them: a planet · b coords · c tables · d relations ·
/// e networks · f packed · g address. This crate is korp-free by charter, so the two
/// enums are peers — and the LETTERS are the whole contract between them. The leaf's
/// enum stopped at `e = tiles`, which meant `korp-server --invalidate sweden:e` and a
/// collector row at `'e'` named two different artefacts.
#[test]
fn the_chain_letters_are_the_pipelines_a_to_i() {
let letters: Vec<char> = state::Step::ALL.iter().map(|s| s.letter()).collect();
assert_eq!(
letters,
vec!['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'],
"the leaf's chain must letter exactly like korp::pipeline::Step.\n\
\n\
IF YOU ARE HERE BECAUSE YOU JUST ADDED A STEP: this list is hand-copied ON \
PURPOSE (this crate is korp-free by charter and cannot import \
korp::pipeline::Step), so adding a step means visiting FOUR sites, and this \
assertion has now been the last one found TWICE — h from f7aa5c8 until \
2026-09-03, i from 439b845 until 2026-09-06, and a plain `cargo test` in this \
repo was red for every lane both times. The four:\n\
1. korp-collectors src/state.rs — `Step` enum + `ALL` + `letter()` + `parse()`\n\
2. korp-collectors THIS TEST — the letter list, the artefact assertion, \
and the parse round-trip below\n\
3. korp src/pipeline.rs — `Step` enum + `ALL` + `outputs()` (the \
PURGE SCOPE: a step whose target is not filesystem paths must declare it \
EMPTY, as Planet and Packed do)\n\
4. korp tests/collector_restart_engine_red.rs — the match over `Step`, which \
is an ARM and not a fixture; it was left short by row 34 too\n\
korp's own cut_driver::the_leaf_and_korp_letter_the_same_chain derives from \
BOTH enums, so it follows automatically once 1 and 3 agree — it is the one \
site you do NOT have to touch."
);
// …and each letter must name the same artefact korp names.
let by_letter = |c: char| {
state::Step::ALL
.iter()
.copied()
.find(|s| s.letter() == c)
.unwrap_or_else(|| panic!("no step {c}"))
};
assert_eq!(by_letter('e'), state::Step::Networks, "e is NETWORKS, not tiles");
assert_eq!(by_letter('f'), state::Step::Packed);
assert_eq!(by_letter('g'), state::Step::Address);
// h joined the chain in f7aa5c8 (the land polygon cut from the region's own
// coastline). korp's `pipeline::Step` letters it 'h' too — VERIFIED against
// korp/src/pipeline.rs:165, which is the only thing that makes this a contract
// and not a copy. This assertion, and the letter list above, sat one step short
// from f7aa5c8 until 2026-09-03: the leaf was right and the test was red.
assert_eq!(by_letter('h'), state::Step::Landmass, "h is LANDMASS");
// **i joined the chain in 439b845** (the buildings table, last in the chain because
// it reads the packed base table). VERIFIED the same way h was, against the only
// thing that makes this a contract and not a copy: `korp/src/pipeline.rs:158`,
// `pub const ALL: [Step; 9]`, whose ninth element is `Step::Buildings`.
//
// **This is the SECOND time this list has been one step short, and the note above
// records the first.** The leaf was right both times and the test was red both
// times — h from f7aa5c8 until 2026-09-03, i from 439b845 until now, during which a
// plain `cargo test` in this repo was red for every lane. The list is hardcoded on
// purpose (this crate is korp-free by charter, so it CANNOT import
// `korp::pipeline::Step`, and a list copied by hand is exactly what a contract
// between two crates that must not depend on each other looks like) — so the cost of
// that charter is that whoever adds a step must come here. That is the price, and it
// is worth naming rather than designing around: a test that derived this list from
// the leaf's own enum would agree with itself forever and check nothing.
assert_eq!(by_letter('i'), state::Step::Buildings, "i is BUILDINGS");
// The operator's spellings korp accepts must round-trip here too.
for (spelling, want) in [
("e", state::Step::Networks),
("networks", state::Step::Networks),
("f", state::Step::Packed),
("packed", state::Step::Packed),
("g", state::Step::Address),
("address", state::Step::Address),
("h", state::Step::Landmass),
("landmass", state::Step::Landmass),
("i", state::Step::Buildings),
("buildings", state::Step::Buildings),
] {
assert_eq!(state::Step::parse(spelling), Some(want), "parse({spelling:?})");
}
// And the pyramid spellings must be GONE — parsing them would keep a legacy
// `--invalidate sweden:tiles` silently meaning something.
for gone in ["tiles", "pyramid"] {
assert_eq!(
state::Step::parse(gone),
None,
"'{gone}' is not a step of this pipeline any more"
);
}
}
/// **The registry offers a collector named for the cut it makes.**
#[cfg(feature = "osm-daemon")]
#[test]
fn the_osm_collector_is_named_for_the_cut_it_makes() {
let names: Vec<String> = daemon::daemon_registry()
.iter()
.map(|c| c.name().to_string())
.collect();
assert!(
names.iter().any(|n| n == korp_collectors::osm_cut::NAME),
"the registry must offer '{}': {names:?}",
korp_collectors::osm_cut::NAME
);
assert!(
!names.iter().any(|n| n == "osm-tiles"),
"the pyramid collector must be GONE from the registry: {names:?}"
);
}
/// **A live server that already has `collectors."osm-tiles"` persisted keeps its
/// setting.** The rename must not silently disable a configured collector — the
/// operator's row resolves to the new daemon, carrying its config across.
#[cfg(feature = "osm-daemon")]
#[test]
fn a_legacy_osm_tiles_config_entry_resolves_to_the_cut_collector() {
let cfg = serde_json::json!({ "regions": ["sweden"], "zooms": [6, 9, 12] });
let d = daemon::daemon_with_config("osm-tiles", true, cfg.clone())
.expect("a persisted 'osm-tiles' entry must still resolve");
assert_eq!(
d.name(),
korp_collectors::osm_cut::NAME,
"the legacy key resolves to the CUT collector, not a second daemon"
);
assert!(d.enabled(), "and it keeps the operator's enabled flag");
// The migration is a pure, testable function — korp calls the same one when it
// loads its settings, so both sides rename by one writer.
assert_eq!(
korp_collectors::osm_cut::migrate_collector_name("osm-tiles"),
korp_collectors::osm_cut::NAME
);
assert_eq!(
korp_collectors::osm_cut::migrate_collector_name("dcat"),
"dcat",
"every other name is left alone"
);
}
/// **Nothing the operator reads promises a tile.** `korp/tests/no_tiles_law.rs` holds
/// that line on korp's user-visible strings; this is the same law on the leaf's, which
/// is where the collector's title, description and every ⚙ Settings field hint are
/// actually written.
#[cfg(feature = "osm-daemon")]
#[test]
fn no_collector_string_promises_a_tile_a_pyramid_or_a_zoom() {
let d = daemon::daemon_by_name(korp_collectors::osm_cut::NAME).expect("the cut collector");
let mut strings = vec![d.title(), d.description(), d.name().to_string()];
for f in d.schema().fields {
strings.push(f.key);
strings.push(f.label);
strings.push(f.hint);
}
strings.push(d.defaults().to_string());
for s in &strings {
let low = s.to_ascii_lowercase();
for word in ["tile", "pyramid", "slippy", "zoom"] {
assert!(
!low.contains(word),
"a user-visible collector string still says {word:?}: {s:?}"
);
}
}
}
/// **The declaration is the seven steps and the regions, not a zoom ladder and a
/// bucket.** The cut writes FILES under `~/.korp/work/<region>/`; there is no tiles
/// bucket to name any more.
#[cfg(feature = "osm-daemon")]
#[test]
fn the_cut_collector_declares_the_seven_steps_not_a_zoom_ladder() {
let d = daemon::daemon_by_name(korp_collectors::osm_cut::NAME).expect("the cut collector");
let keys: Vec<String> = d.schema().fields.iter().map(|f| f.key.clone()).collect();
for gone in ["zooms", "bucket"] {
assert!(
!keys.iter().any(|k| k == gone),
"the cut has no {gone:?} axis: {keys:?}"
);
}
for want in ["regions", "planet", "work_dir", "through_step"] {
assert!(
keys.iter().any(|k| k == want),
"the cut declares {want:?}: {keys:?}"
);
}
let defaults = d.defaults();
let last = *korp_collectors::state::Step::ALL
.last()
.expect("the chain has at least one step");
assert_eq!(
defaults["through_step"],
serde_json::Value::String(last.letter().to_string()),
"the default cut runs the WHOLE chain, ending in {} ({})",
last.letter(),
last.label(),
);
}
/// **A stock cut must be able to produce every step the chain declares.**
///
/// MEASURED on oden 2026-09-10. The chain had grown to `i` (buildings) while
/// `through_step` still defaulted to the frozen letter `g`, so korp-server's resume walk
/// printed
///
/// ```text
/// RESUME AT step h
/// ```
///
/// for a step the stock cut daemon would never run. Nothing on that box could resume it,
/// and it took a hand-edited `/home/korp/.config/korp/config.json` to make the chain
/// completable — a fix for exactly one host.
///
/// This is the invariant, and it holds for `j` too: whatever the LAST step of the chain
/// is, the default reaches it. RED direction: pin `default_through_step` to any step
/// other than `Step::ALL.last()`.
#[test]
fn the_stock_cut_reaches_the_last_step_of_the_chain() {
use korp_collectors::state::Step;
let last = *Step::ALL.last().expect("the chain has at least one step");
let def = korp_collectors::osm_cut::default_through_step();
assert_eq!(
def, last,
"the stock cut stops at {} ({}) while the chain ends at {} ({}) — every step \
after the default has NO producer on a stock install",
def.letter(),
def.label(),
last.letter(),
last.label(),
);
// And the letter the config field actually carries agrees with the enum, so the
// Settings default and the resolver cannot drift the way `g` and `i` did.
assert_eq!(korp_collectors::osm_cut::default_through_step_letter(), last.letter().to_string());
}