tests/every_collector_declares_its_config.rs
//! **THE DECLARATION LAW: a collector that needs credentials NAMES them, on every face
//! it wears.**
//!
//! The owner's part (1), 2026-09-06: "each collector declares the config it must have (env
//! var names or config keys) as data, in one place, next to the collector."
//!
//! # The hole this closes
//!
//! `CollectorConfig::requirements`'s doc comment on `439b845` said a daemon whose key
//! comes from `CollectorCtx::secret` "MUST override this and name the variable, because
//! a daemon whose key is absent can only report `Failed` at the wire". A MUST that
//! nothing checks is a wish. Measured on `439b845`, by enumerating both registries:
//!
//! * the **ingest** registry (`registry_requirements()`) declared named secrets for 17
//! collectors — `ENTSOE_TOKEN`, `TRAFIKVERKET_API_KEY`, `BOLAGSVERKET_CLIENT_ID` … —
//! and that is the list korp's object registry renders, the one that showed 21
//! collectors refusing on the live oden server;
//! * the **daemon** registry (`daemon_registry()`) — the face that owns `run()`, the one
//! that actually ticks — declared `secrets: []` for **all 22 daemons, without
//! exception**, including the six that appear in BOTH registries under the same name.
//!
//! So one collector answered "what do you need?" two different ways, and the face that
//! ran was the one that answered wrong: readiness computed `Runnable`, the host offered
//! ▶ Run, and the daemon nagged hourly forever.
//!
//! # Why a test and not just the fix
//!
//! The fix (`Requirements::inheriting_credentials_of`) makes the daemon read the ingest
//! declaration, so the two cannot drift. But a fix is a fact about today and a law is a
//! fact about tomorrow: the next provider added to `registry()` with an `AuthKind` and
//! no `requirements()` override would slip straight back through. This file fails the
//! build for that.
#![cfg(feature = "daemon")]
use std::collections::BTreeMap;
use korp_collectors::{AuthKind, registry, registry_requirements};
/// **Every credential-gated INGEST collector names at least one variable.**
///
/// "Names" is the operative word: `SecretReq::undeclared` produces a requirement with an
/// EMPTY name, which is honest ("I need something, I cannot say what") and is still
/// unusable — an operator cannot set a variable nobody named, which is the whole of
/// The owner's complaint. So an empty name fails here exactly like a missing one.
#[test]
fn every_credential_gated_collector_names_its_variable() {
let declared: BTreeMap<String, _> = registry_requirements().into_iter().collect();
let mut gated = 0usize;
let mut offenders: Vec<String> = Vec::new();
for c in registry() {
let meta = c.meta();
if meta.auth == AuthKind::None {
continue;
}
gated += 1;
let req = declared.get(&meta.name).expect("registry twin");
let named: Vec<&str> = req
.secrets
.iter()
.map(|s| s.name.as_str())
.filter(|n| !n.is_empty())
.collect();
// A collector may be gated by a config FIELD rather than a secret — Bolagsverket
// needs `identitetsbeteckning` as well as its client id. Either is a name an
// operator can act on; having NEITHER is not.
if named.is_empty() && req.fields.is_empty() {
offenders.push(format!(
" {} (auth = {}) declares {} secret(s), none of them named, and no \
required field",
meta.name,
meta.auth.as_str(),
req.secrets.len(),
));
}
}
// ── Anti-vacuity. A sweep that examined nothing passes every assertion below it,
// and this crate has had exactly that mistake found in it before (a bencher that
// scored 100% because the budget was larger than the fixture). If the registry is
// ever feature-gated down to nothing, or `auth` stops being set, this fires. ────
assert!(
gated >= 15,
"only {gated} credential-gated collectors were examined — the sweep is not \
seeing the registry, so every assertion below it is vacuous",
);
assert!(
offenders.is_empty(),
"{} credential-gated collector(s) cannot be configured, because nobody can say \
WHICH variable to set:\n{}\n\nDeclare it: `.with_secrets(vec![SecretReq::new(\
\"PROVIDER_TOKEN\", \"what it is and where to get one\")])` beside the \
collector.",
offenders.len(),
offenders.join("\n"),
);
}
/// **The two faces of one collector declare the SAME credentials.**
///
/// This is the anti-drift guard, and it is the one that was RED on `439b845`: six
/// collectors live in both registries under one name and the daemon face declared
/// nothing for all six.
///
/// It compares NAME SETS, not whole `Requirements`: the `source` is deliberately allowed
/// to differ, because a daemon's endpoint comes from its live config (an operator may
/// have pointed it at a mirror) while the ingest twin carries the shipped default.
#[test]
fn a_daemons_credentials_agree_with_its_ingest_twin() {
let ingest: BTreeMap<String, _> = registry_requirements().into_iter().collect();
let mut compared = 0usize;
let mut drifted: Vec<String> = Vec::new();
for d in korp_collectors::daemon::daemon_registry() {
let name = d.name().to_string();
let Some(twin) = ingest.get(&name) else {
continue; // a daemon with no ingest twin (osm-cut, wfs) — nothing to agree with
};
compared += 1;
let want: Vec<&str> = twin.secrets.iter().map(|s| s.name.as_str()).collect();
let got: Vec<String> = d
.requirements()
.secrets
.iter()
.map(|s| s.name.clone())
.collect();
if want.iter().any(|w| !got.iter().any(|g| g == w)) {
drifted.push(format!(
" {name}: ingest declares {want:?}, the daemon declares {got:?}",
));
}
}
assert!(
compared >= 6,
"only {compared} daemon/ingest twins were compared — on 439b845 there were at \
least six (entsoe, entsoe_day_ahead, entsoe_generation, nordpool, \
openaq-air-quality, trafikverket-trainpos). Fewer means this guard has stopped \
looking at the thing it guards.",
);
assert!(
drifted.is_empty(),
"{} collector(s) answer \"what credentials do you need?\" differently depending \
on which face you ask. The face that RUNS is the daemon one, so this is a \
collector reported as runnable that cannot run:\n{}",
drifted.len(),
drifted.join("\n"),
);
}
/// **A collector with no auth declares no secret** — the other direction, and it is not
/// pedantry: a keyless collector that declared a secret would sit at `NeedsConfig`
/// forever and be refused at load by the gate this lane just built. Getting the
/// declaration wrong in THIS direction takes a working collector off the air.
#[test]
fn a_keyless_collector_declares_no_secret() {
let declared: BTreeMap<String, _> = registry_requirements().into_iter().collect();
let mut checked = 0usize;
let mut wrong: Vec<String> = Vec::new();
for c in registry() {
let meta = c.meta();
if meta.auth != AuthKind::None {
continue;
}
checked += 1;
let req = declared.get(&meta.name).expect("registry twin");
if !req.secrets.is_empty() {
wrong.push(format!(
" {} declares auth None but requires {:?} — it would refuse at load",
meta.name,
req.secrets.iter().map(|s| &s.name).collect::<Vec<_>>(),
));
}
}
assert!(checked >= 20, "only {checked} keyless collectors examined — vacuous");
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}