tests/roster_is_honest.rs
//! **The roster tells the truth on BOTH axes, and UNTESTED ONLINE is one of its
//! answers.**
//!
//! The owner's part (4), 2026-09-06: a collector whose parsing is proven on fixtures but
//! whose live authenticated call has never run is UNTESTED ONLINE — "a first-class state
//! to report, not a red and not a lie. Say it in the roster with the reason."
//!
//! # The two ways a roster lies
//!
//! It can say a collector is fine when its config is empty — that was row 1, and the
//! gate now refuses it. And it can say a collector is fine when nothing has ever come
//! back from its real endpoint, which is what this file is about. The second lie is the
//! quieter one: it survives every unit test, because a parser proven on a fixture its
//! own author wrote will pass forever whatever the provider actually sends.
//!
//! So the proof axis is DERIVED from evidence a host already holds — a freshness mark
//! (written only after rows landed) and the committed provider fixtures — and never
//! stored. A stored "works" flag is stale the moment a key is withdrawn.
use korp_collectors::freshness::{MarkStore, MemMarkStore, ReadMark};
use korp_collectors::provenance::{ProvenanceManifest, FIXTURE_DIR};
use korp_collectors::requires::{HaveSecrets, NoSecrets, Readiness};
use korp_collectors::roster::roster;
fn fixtures() -> ProvenanceManifest {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_DIR);
ProvenanceManifest::load(&dir).expect("provenance.json parses")
}
/// **On a host with no credentials, the credential-gated collectors read
/// `needs_config` + `untested_online`, and NEITHER of those is a red.**
#[test]
fn a_credential_gated_collector_is_not_startable_and_untested_online() {
let rows = roster(&NoSecrets, &MemMarkStore::new(), &fixtures());
let entsoe = rows
.iter()
.find(|r| r.collector == "entsoe")
.expect("entsoe is in the registry");
match &entsoe.readiness {
Readiness::NeedsConfig { missing } => assert!(
missing.iter().any(|m| m.line().contains("ENTSOE_TOKEN")),
"the row must NAME the variable; got {missing:?}",
),
other => panic!("entsoe with no token must not be {}", other.key()),
}
assert_eq!(
entsoe.proof.key(),
"untested_online",
"entsoe's parser is proven against a real ENTSO-E document, so it is not \
`unproven`; and no live call has run, so it is not `proven_online`",
);
assert!(
!entsoe.proof.is_red(),
"UNTESTED ONLINE must not be a red: nobody can fix it by changing code, and a \
red nobody can act on is a red everybody learns to ignore",
);
// The reason must say what the fixture DOES and DOES NOT prove. entsoe's only
// credential-free payload is a refusal document, and a roster that let that read as
// "covered" would be the same lie in a new place.
let why = entsoe.proof.why();
assert!(
why.contains("UNTESTED ONLINE") && why.contains("refusal document"),
"entsoe's reason must say the only obtainable payload is the provider's \
refusal; got: {why}",
);
assert!(
why.contains("2026-"),
"LAW 7: a figure taken from a live source is dated. Got: {why}",
);
}
/// **The two axes are INDEPENDENT, and `nordpool` is the proof.**
///
/// Nord Pool declares `NORDPOOL_API_TOKEN` for its licensed API, so it is not startable
/// here — and its parser is proven against 96 real prices from the exchange's own public
/// data portal. A single traffic light cannot hold both facts, and the one it would keep
/// is the one that says less.
#[test]
fn startability_and_proof_are_orthogonal() {
let rows = roster(&NoSecrets, &MemMarkStore::new(), &fixtures());
let np = rows
.iter()
.find(|r| r.collector == "nordpool")
.expect("nordpool is in the registry");
assert_eq!(np.readiness.key(), "needs_config", "the licensed API needs a token");
assert_eq!(
np.proof.key(),
"untested_online",
"its parser IS proven on real provider data — that is not `unproven`",
);
assert!(
np.proof.why().contains("nordpool-dayahead-se3.json"),
"the reason must name the fixture that carries the proof; got: {}",
np.proof.why(),
);
// …and the same collector flips its startability axis alone when a key appears,
// leaving the proof axis exactly where it was. If they were one field this could not
// be asserted at all.
let with_key = roster(
&HaveSecrets::of(&["NORDPOOL_API_TOKEN"]),
&MemMarkStore::new(),
&fixtures(),
);
let np2 = with_key.iter().find(|r| r.collector == "nordpool").unwrap();
assert_eq!(np2.readiness.key(), "runnable", "the key satisfies the declaration");
assert_eq!(
np2.proof, np.proof,
"supplying a key must NOT change what has been proven — holding a credential is \
not evidence that a call was ever made with it",
);
}
/// **A landed run — and only a landed run — makes a row `proven_online`.**
///
/// The green arm of the proof axis. Without it, `untested_online` could be returned
/// unconditionally and every assertion above would still pass.
#[test]
fn a_freshness_mark_is_what_makes_a_collector_proven_online() {
let marks = MemMarkStore::new();
let before = roster(&NoSecrets, &marks, &fixtures());
let polisen_before = before
.iter()
.find(|r| r.collector == "polisen-events")
.expect("polisen-events is in the registry")
.clone();
assert_ne!(
polisen_before.proof.key(),
"proven_online",
"nothing has landed yet, so nothing may claim it has",
);
// A mark is written ONLY after rows land, so writing one is the exact evidence a
// completed live run leaves behind.
let source = korp_collectors::by_name("polisen-events")
.expect("registry")
.requirements()
.source;
marks
.advance(
"polisen-events",
&source,
&ReadMark {
kind: "query_window".into(),
mark: "2026-09-06T09:00:00Z".into(),
etag: None,
last_modified: None,
at: "2026-09-06T09:00:01Z".into(),
..Default::default()
},
)
.expect("advance");
let after = roster(&NoSecrets, &marks, &fixtures());
let polisen_after = after
.iter()
.find(|r| r.collector == "polisen-events")
.unwrap();
assert_eq!(
polisen_after.proof.key(),
"proven_online",
"a freshness mark is evidence that rows landed from the live source",
);
assert!(
!polisen_after.proof.is_red(),
"a proven collector is never red",
);
assert!(
polisen_after.proof.why().contains("2026-09-06T09:00:01Z"),
"the evidence must be dated and quotable; got: {}",
polisen_after.proof.why(),
);
// Only the collector that landed rows moved. A mark for one collector turning the
// whole roster green would be the same class of defect as the readiness that
// reported and never refused.
let moved: Vec<&str> = after
.iter()
.filter(|r| r.proof.key() == "proven_online")
.map(|r| r.collector.as_str())
.collect();
assert_eq!(
moved,
vec!["polisen-events"],
"exactly one collector landed rows; the roster must move exactly one row",
);
}
/// **The roster covers the whole registry and every row carries a reason.**
///
/// A roster row with an empty reason is a state with no cause, which is what a UI cannot
/// explain and an operator cannot act on.
#[test]
fn every_roster_row_carries_a_reason_and_the_sweep_is_not_vacuous() {
let rows = roster(&NoSecrets, &MemMarkStore::new(), &fixtures());
assert_eq!(
rows.len(),
korp_collectors::registry().len(),
"the roster must cover the whole registry",
);
assert!(
rows.len() >= 40,
"only {} rows — the registry is not being seen, so this sweep is vacuous",
rows.len(),
);
let silent: Vec<&str> = rows
.iter()
.filter(|r| r.proof.why().trim().is_empty())
.map(|r| r.collector.as_str())
.collect();
assert!(silent.is_empty(), "rows with no reason: {silent:?}");
// And the roster must actually DISTINGUISH. If every row landed in one bucket, the
// second axis would be decoration.
let untested = rows.iter().filter(|r| r.proof.key() == "untested_online").count();
let unproven = rows.iter().filter(|r| r.proof.key() == "unproven").count();
assert!(
untested >= 3 && unproven >= 1,
"the proof axis put {untested} rows in untested_online and {unproven} in \
unproven — an axis with one value is not an axis",
);
for r in &rows {
eprintln!("{}", r.line());
}
}
/// **"Nobody has done it yet" and "the provider publishes nothing" are DIFFERENT
/// answers, and the roster gives different ones.**
///
/// The owner named Bolagsverket and SCB specifically. Neither can be closed by writing
/// code: bolagsverket.se answers an automated fetch with a CAPTCHA, and SCB does not
/// publish företagsregistret's endpoint at all — it is issued WITH the credential, which
/// is why the collector's declared source is an RFC 2606 placeholder.
///
/// Reporting that as plain `unproven` would send the next reader to find the same
/// CAPTCHA, and painting it red would be a red nobody can act on. So it is its own
/// answer, carrying what was tried and the date it was tried.
#[test]
fn a_provider_that_publishes_nothing_reads_differently_from_work_not_yet_done() {
let rows = roster(&NoSecrets, &MemMarkStore::new(), &fixtures());
let by = |n: &str| {
rows.iter()
.find(|r| r.collector == n)
.unwrap_or_else(|| panic!("{n} is in the registry"))
};
for name in ["bolagsverket-organisationer", "scb-foretagsregistret"] {
let row = by(name);
assert_eq!(
row.proof.key(),
"unprovable_without_account",
"{name} was checked and found to publish nothing an anonymous reader can \
take; that is not the same as nobody having tried",
);
assert!(
!row.proof.is_red(),
"{name} cannot be fixed by writing code, so it must not be a red — a red \
nobody can act on is a red everybody learns to ignore",
);
let why = row.proof.why();
assert!(
why.contains("checked 2026-"),
"{name}'s reason must be DATED, or a stale belief outlives its evidence; \
got: {why}",
);
// The reason has to be concrete enough that the next reader can check it rather
// than take it on trust. A bare "not available" would satisfy every assertion
// above and teach nobody anything.
assert!(
why.len() > 120,
"{name}'s reason is too thin to be checkable: {why}",
);
}
// …and the distinction is real: collectors nobody has got to yet still read
// `unproven`, and they ARE red, because that is work somebody can do.
let unproven: Vec<&str> = rows
.iter()
.filter(|r| r.proof.key() == "unproven")
.map(|r| r.collector.as_str())
.collect();
assert!(
!unproven.is_empty(),
"every row landed in a bucket that excuses it — the honest reading today is that \
most collectors still have no provider fixture, and the roster must keep saying so",
);
assert!(
rows.iter()
.filter(|r| r.proof.key() == "unproven")
.all(|r| r.proof.is_red()),
"work nobody has done yet IS a red; only the two states nobody can act on are not",
);
}