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

nordisk/holger

server/lib/tests/mirror_declaration.rs

//! The mirror's DECLARATION and its CYCLE, end to end in one process.
//!
//! `server/lib/src/mirror.rs` shipped as an engine with no caller.
//! `mirror_cycle.rs` is the caller, and this file is the proof that the seven
//! things a wired mirror has to do are the seven things it does:
//!
//! 1. a first cycle takes everything and the bytes are byte-identical,
//! 2. a second cycle over an unchanged upstream moves nothing,
//! 3. an artifact ADDED upstream arrives on the next cycle — the property that
//!    separates a mirror from a one-shot copy,
//! 4. an artifact whose bytes CHANGED is re-fetched,
//! 5. an `unproven` artifact is re-fetched by DEFAULT and skipped only under the
//!    opt-in,
//! 6. the change map survives a restart, so a restarted mirror does not re-pull
//!    the world,
//! 7. an artifact the mirror could not take is asked for again next cycle.
//!
//! The upstream here is a fake in memory. The LIVE proof against `holger.rs` is
//! the transcript in the lane report; this file is what makes a regression show
//! up in `cargo test` instead of on a box.

use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};

use anyhow::Result;
use holger_server_lib::mirror_cycle::{
    coord_to_id, id_to_coord, run_pair, MirrorPair, MirrorSettings, MirrorUpstream, PairReport,
    RonUnprovenPolicy,
};
use traits::{
    ArtifactEntry, ArtifactFormat, ArtifactId, Bytes, RemoteAsset, RepositoryBackendTrait,
};

// ───────────────────────────── the fakes ─────────────────────────────

/// An upstream holding `coord -> bytes`, which publishes a digest only when
/// `digests` is true.
///
/// This one builds its [`RemoteAsset`]s directly, so it stands for "a source
/// that does / does not publish digests" in general. The holger-specific shape
/// — an `ArtifactEntry` page put through `holger_listing` — is
/// [`HolgerListingUpstream`] below, and it is a separate fake on purpose:
/// `digests: false` used to be described here as "the HOLGER shape", and that
/// is no longer what a holger listing is.
#[derive(Clone)]
struct FakeUpstream {
    items: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    digests: bool,
    /// Coordinates whose fetch fails, to exercise the carry-over rule.
    broken: Arc<Mutex<Vec<String>>>,
    fetches: Arc<Mutex<usize>>,
}

impl FakeUpstream {
    fn new(digests: bool) -> Self {
        Self {
            items: Arc::new(Mutex::new(BTreeMap::new())),
            digests,
            broken: Arc::new(Mutex::new(Vec::new())),
            fetches: Arc::new(Mutex::new(0)),
        }
    }
    fn put(&self, coord: &str, bytes: &[u8]) {
        self.items
            .lock()
            .unwrap()
            .insert(coord.to_string(), bytes.to_vec());
    }
    fn remove(&self, coord: &str) {
        self.items.lock().unwrap().remove(coord);
    }
    fn fetch_count(&self) -> usize {
        *self.fetches.lock().unwrap()
    }
}

#[async_trait::async_trait]
impl MirrorUpstream for FakeUpstream {
    async fn list(&self, _repository: &str, _page_size: u32) -> Result<Vec<RemoteAsset>> {
        let items = self.items.lock().unwrap();
        Ok(items
            .iter()
            .map(|(coord, bytes)| RemoteAsset {
                path: coord.clone(),
                download_url: format!("fake://{coord}"),
                content_type: None,
                size: Some(bytes.len() as u64),
                sha256: self.digests.then(|| nornir_hash::sha256_hex(bytes)),
                change_token: None,
            })
            .collect())
    }
    async fn fetch(&self, _repository: &str, id: &ArtifactId) -> Result<Option<traits::Bytes>> {
        *self.fetches.lock().unwrap() += 1;
        let coord = id_to_coord(id);
        if self.broken.lock().unwrap().contains(&coord) {
            anyhow::bail!("fake upstream: {coord} is broken");
        }
        Ok(self
            .items
            .lock()
            .unwrap()
            .get(&coord)
            .cloned()
            .map(traits::Bytes::from))
    }
}

/// **A holger upstream, listed the way a holger upstream is actually listed.**
///
/// The point of this fake is that it does not hand `RemoteAsset`s to the engine:
/// it builds `traits::ArtifactEntry` rows the way a repository backend does and
/// puts them through the real
/// [`holger_server_lib::mirror_cycle::holger_listing`], which is the function
/// that used to fill `sha256: None` unconditionally. So a regression in that
/// conversion — dropping the digest, or inventing one — shows up here as a
/// verdict changing, not as a comment going stale.
///
/// `backend_can_prove` is the one knob, and it stands for the real split
/// between holger's own backends: `file-oci` (and a warm `file-rust`) can put a
/// digest in a listing without reading the store; `raw` and every `znippy-*`
/// cannot, and answer `None`.
#[derive(Clone)]
struct HolgerListingUpstream {
    items: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    backend_can_prove: bool,
    fetches: Arc<Mutex<usize>>,
}

impl HolgerListingUpstream {
    fn new(backend_can_prove: bool) -> Self {
        Self {
            items: Arc::new(Mutex::new(BTreeMap::new())),
            backend_can_prove,
            fetches: Arc::new(Mutex::new(0)),
        }
    }
    fn put(&self, coord: &str, bytes: &[u8]) {
        self.items
            .lock()
            .unwrap()
            .insert(coord.to_string(), bytes.to_vec());
    }
    fn fetch_count(&self) -> usize {
        *self.fetches.lock().unwrap()
    }
}

#[async_trait::async_trait]
impl MirrorUpstream for HolgerListingUpstream {
    async fn list(&self, repository: &str, _page_size: u32) -> Result<Vec<RemoteAsset>> {
        let entries: Vec<ArtifactEntry> = self
            .items
            .lock()
            .unwrap()
            .iter()
            .filter_map(|(coord, bytes)| {
                let id = coord_to_id(coord)?;
                let e = ArtifactEntry::unknown_provenance(
                    id,
                    bytes.len() as i64,
                    "application/octet-stream",
                );
                Some(if self.backend_can_prove {
                    e.with_sha256(Some(nornir_hash::sha256_hex(bytes)))
                } else {
                    e
                })
            })
            .collect();
        // THE REAL CONVERSION, not a hand-built RemoteAsset.
        Ok(holger_server_lib::mirror_cycle::holger_listing(
            "http://up:50051",
            repository,
            entries,
        ))
    }
    async fn fetch(&self, _repository: &str, id: &ArtifactId) -> Result<Option<traits::Bytes>> {
        *self.fetches.lock().unwrap() += 1;
        Ok(self
            .items
            .lock()
            .unwrap()
            .get(&id_to_coord(id))
            .cloned()
            .map(traits::Bytes::from))
    }
}

/// A writable in-memory repository, so the cycle's `put` is real without a disk.
#[derive(Default)]
struct MemBackend {
    held: Mutex<BTreeMap<String, Vec<u8>>>,
}

impl RepositoryBackendTrait for MemBackend {
    fn caps(&self) -> traits::Caps {
        traits::Caps {
            listing: true,
            ..traits::Caps::NONE
        }
    }
    fn name(&self) -> &str {
        "mem"
    }
    fn format(&self) -> ArtifactFormat {
        ArtifactFormat::Raw
    }
    fn is_writable(&self) -> bool {
        true
    }
    fn fetch(&self, id: &ArtifactId) -> Result<Option<Bytes>> {
        Ok(self
            .held
            .lock()
            .unwrap()
            .get(&id_to_coord(id))
            .map(|v| Bytes::from(v.clone())))
    }
    fn put(&self, id: &ArtifactId, data: &[u8]) -> Result<()> {
        self.held
            .lock()
            .unwrap()
            .insert(id_to_coord(id), data.to_vec());
        Ok(())
    }
    fn handle_http2_request(
        &self,
        _method: &str,
        _path: &str,
        _body: &[u8],
    ) -> Result<(u16, Vec<(String, String)>, Vec<u8>)> {
        anyhow::bail!("the mirror never goes through the HTTP door")
    }
    fn list(&self, _f: Option<&str>, _l: usize) -> Result<Vec<ArtifactEntry>> {
        Ok(self
            .held
            .lock()
            .unwrap()
            .iter()
            .filter_map(|(c, v)| {
                coord_to_id(c).map(|id| ArtifactEntry {
                    id,
                    size_bytes: v.len() as i64,
                    content_type: String::new(),
                    // The mirror DESTINATION: an in-memory store that knows
                    // neither a publication time nor a digest, and says so.
                    ..ArtifactEntry::UNKNOWN_PROVENANCE
                })
            })
            .collect())
    }
}

// ───────────────────────────── the harness ─────────────────────────────

struct Bench {
    up: FakeUpstream,
    local: Arc<dyn RepositoryBackendTrait>,
    mem: Arc<MemBackend>,
    dir: tempfile::TempDir,
}

fn bench(digests: bool) -> Bench {
    let mem = Arc::new(MemBackend::default());
    Bench {
        up: FakeUpstream::new(digests),
        local: mem.clone() as Arc<dyn RepositoryBackendTrait>,
        mem,
        dir: tempfile::tempdir().expect("tempdir"),
    }
}

impl Bench {
    async fn cycle(&self, policy: RonUnprovenPolicy) -> PairReport {
        let pair = MirrorPair {
            ron_from: "crates".into(),
            ron_to: "crates".into(),
            ron_unproven: policy,
        };
        run_pair(
            &self.up,
            "up",
            &pair,
            &self.local,
            self.dir.path(),
            1000,
            4,
            holger_server_lib::mirror_cycle::now_nanos(),
        )
        .await
        .expect("cycle")
    }
    fn local_bytes(&self, coord: &str) -> Option<Vec<u8>> {
        self.mem.held.lock().unwrap().get(coord).cloned()
    }
}

// ───────────────────────────── the proofs ─────────────────────────────

#[tokio::test]
async fn a_first_cycle_takes_everything_and_the_bytes_are_identical() {
    let b = bench(true);
    b.up.put("/alpha@1.0", b"alpha-bytes");
    b.up.put("/beta@2.0", b"beta-bytes-longer");

    let r = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(r.listed, 2);
    assert_eq!(
        r.map_before, 0,
        "a mirror that has never synced holds nothing"
    );
    assert_eq!(r.added, 2);
    assert_eq!(r.to_fetch, 2);
    assert_eq!(r.fetched, 2);
    assert_eq!(r.failed, 0);
    assert_eq!(r.map_after, 2);

    assert_eq!(
        b.local_bytes("/alpha@1.0").as_deref(),
        Some(&b"alpha-bytes"[..])
    );
    assert_eq!(
        nornir_hash::sha256_hex(&b.local_bytes("/beta@2.0").unwrap()),
        nornir_hash::sha256_hex(b"beta-bytes-longer"),
        "byte-identical, proved by digest"
    );
}

#[tokio::test]
async fn a_second_cycle_over_a_proved_unchanged_upstream_moves_nothing() {
    let b = bench(/* the upstream publishes digests */ true);
    b.up.put("/alpha@1.0", b"alpha-bytes");
    b.up.put("/beta@2.0", b"beta-bytes");
    let first = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(first.fetched, 2);

    let second = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(second.map_before, 2, "the map carried across the cycle");
    assert_eq!(
        second.unchanged, 2,
        "both digests matched — PROVED identical"
    );
    assert_eq!(second.unproven, 0);
    assert_eq!(second.added, 0);
    assert_eq!(second.changed, 0);
    assert_eq!(second.to_fetch, 0);
    assert!(second.moved_nothing(), "{}", second.line());
    assert_eq!(b.up.fetch_count(), 2, "the upstream was never asked again");
}

/// Run one cycle against an arbitrary upstream, into a fresh in-memory local.
async fn cycle_against(
    up: &dyn MirrorUpstream,
    local: &Arc<dyn RepositoryBackendTrait>,
    state: &std::path::Path,
    policy: RonUnprovenPolicy,
) -> PairReport {
    let pair = MirrorPair {
        ron_from: "crates".into(),
        ron_to: "crates".into(),
        ron_unproven: policy,
    };
    run_pair(
        up,
        "up",
        &pair,
        local,
        state,
        1000,
        4,
        holger_server_lib::mirror_cycle::now_nanos(),
    )
    .await
    .expect("cycle")
}

/// **★ A holger → holger mirror REACHES `unchanged`, with no policy written.**
///
/// This is the defect this lane exists for, stated as a cycle. Before the
/// digest field there was no way for the second cycle to prove sameness, so
/// every size-unchanged coordinate landed in `unproven` forever and the default
/// `Refetch` re-pulled the whole repository on every tick, for ever.
///
/// The upstream here lists through the real `holger_listing`, and the policy is
/// the DEFAULT — `Refetch` — precisely to show the policy is not what saved it.
#[tokio::test]
async fn a_holger_upstream_whose_backend_can_prove_reaches_unchanged_on_the_default_policy() {
    let up = HolgerListingUpstream::new(/* backend_can_prove */ true);
    up.put("/alpha@1.0", b"alpha-bytes");
    up.put("/beta@2.0", b"beta-bytes");
    let mem = Arc::new(MemBackend::default());
    let local = mem.clone() as Arc<dyn RepositoryBackendTrait>;
    let dir = tempfile::tempdir().expect("tempdir");

    let first = cycle_against(&up, &local, dir.path(), RonUnprovenPolicy::Refetch).await;
    assert_eq!(first.added, 2);
    assert_eq!(first.fetched, 2);

    let second = cycle_against(&up, &local, dir.path(), RonUnprovenPolicy::Refetch).await;
    assert_eq!(
        second.unchanged,
        2,
        "a holger listing that carries a digest must reach `unchanged`: {}",
        second.line()
    );
    assert_eq!(
        second.unproven,
        0,
        "nothing is left in doubt: {}",
        second.line()
    );
    assert_eq!(second.to_fetch, 0);
    assert!(second.moved_nothing(), "{}", second.line());
    assert_eq!(up.fetch_count(), 2, "the upstream was never asked again");
}

/// The other half of the same truth: an upstream repository served by a backend
/// that CANNOT prove sameness cheaply — `raw`, every `znippy-*` — still lands in
/// `unproven`, and `ron_unproven` still decides. An old upstream, which sends
/// neither field, behaves identically; that is what proto3 explicit presence
/// buys.
#[tokio::test]
async fn a_holger_upstream_whose_backend_cannot_prove_is_still_unproven() {
    let up = HolgerListingUpstream::new(/* backend_can_prove */ false);
    up.put("/alpha@1.0", b"alpha-bytes");
    let mem = Arc::new(MemBackend::default());
    let local = mem.clone() as Arc<dyn RepositoryBackendTrait>;
    let dir = tempfile::tempdir().expect("tempdir");

    cycle_against(&up, &local, dir.path(), RonUnprovenPolicy::Refetch).await;
    let second = cycle_against(&up, &local, dir.path(), RonUnprovenPolicy::Refetch).await;
    assert_eq!(
        second.unchanged,
        0,
        "equal size with NO digest is still not a proof of sameness: {}",
        second.line()
    );
    assert_eq!(second.unproven, 1, "{}", second.line());
    assert_eq!(second.to_fetch, 1, "the default still re-fetches the doubt");
}

#[tokio::test]
async fn a_source_that_publishes_no_digest_at_all_is_refetched_by_default() {
    // A source of any kind whose listing carries no sha256.
    let b = bench(false);
    b.up.put("/alpha@1.0", b"alpha-bytes");
    b.up.put("/beta@2.0", b"beta-bytes");
    let first = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(first.added, 2);

    let second = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(
        second.unchanged, 0,
        "equal size with NO digest is not a proof of sameness and must never be counted as one"
    );
    assert_eq!(
        second.unproven,
        2,
        "it is its own bucket: {}",
        second.line()
    );
    assert_eq!(
        second.to_fetch, 2,
        "the DEFAULT re-fetches the unproven — bandwidth is cheaper than a stale mirror"
    );
    assert_eq!(second.fetched, 2);
}

#[tokio::test]
async fn trust_size_skips_the_unproven_and_still_counts_it() {
    let b = bench(false);
    b.up.put("/alpha@1.0", b"alpha-bytes");
    b.up.put("/beta@2.0", b"beta-bytes");
    b.cycle(RonUnprovenPolicy::TrustSize).await;
    let before = b.up.fetch_count();

    let second = b.cycle(RonUnprovenPolicy::TrustSize).await;
    assert_eq!(
        second.unproven, 2,
        "the policy decides the FETCH, never the admission — the doubt stays visible"
    );
    assert_eq!(second.to_fetch, 0);
    assert!(second.moved_nothing(), "{}", second.line());
    assert_eq!(b.up.fetch_count(), before, "nothing was asked for");
}

#[tokio::test]
async fn an_artifact_added_upstream_arrives_on_the_next_cycle() {
    // THE property that separates a mirror from a one-shot copy.
    let b = bench(true);
    b.up.put("/alpha@1.0", b"alpha-bytes");
    let first = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(first.fetched, 1);
    assert_eq!(b.local_bytes("/gamma@3.0"), None);

    b.up.put("/gamma@3.0", b"gamma-bytes");
    let second = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(second.added, 1, "{}", second.line());
    assert_eq!(second.unchanged, 1);
    assert_eq!(second.fetched, 1, "ONLY the new one moved");
    assert_eq!(
        b.local_bytes("/gamma@3.0").as_deref(),
        Some(&b"gamma-bytes"[..])
    );
    assert_eq!(second.map_after, 2);
}

#[tokio::test]
async fn an_artifact_whose_bytes_changed_upstream_is_refetched() {
    let b = bench(true);
    b.up.put("/alpha@1.0", b"alpha-bytes");
    b.cycle(RonUnprovenPolicy::Refetch).await;

    // A DIFFERENT length: proved changed by size alone, and by digest too.
    b.up.put("/alpha@1.0", b"alpha-bytes-v2");
    let second = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(second.changed, 1, "{}", second.line());
    assert_eq!(second.unchanged, 0);
    assert_eq!(second.fetched, 1);
    assert_eq!(
        b.local_bytes("/alpha@1.0").as_deref(),
        Some(&b"alpha-bytes-v2"[..]),
        "the local copy converged on the upstream"
    );
}

#[tokio::test]
async fn a_same_size_edit_is_caught_under_the_default_and_missed_under_trust_size() {
    // This is the whole reason `unproven` is not folded into `unchanged`, and the
    // whole reason TrustSize is a word an operator has to write.
    for (policy, expect_caught) in [
        (RonUnprovenPolicy::Refetch, true),
        (RonUnprovenPolicy::TrustSize, false),
    ] {
        let b = bench(/* no digests anywhere */ false);
        b.up.put("/alpha@1.0", b"AAAAAAAA");
        b.cycle(policy).await;
        assert_eq!(
            b.local_bytes("/alpha@1.0").as_deref(),
            Some(&b"AAAAAAAA"[..])
        );

        // Same length, different bytes. `size` cannot see this.
        b.up.put("/alpha@1.0", b"BBBBBBBB");
        let second = b.cycle(policy).await;
        assert_eq!(second.unproven, 1);
        let caught = b.local_bytes("/alpha@1.0").as_deref() == Some(&b"BBBBBBBB"[..]);
        assert_eq!(
            caught, expect_caught,
            "{policy:?}: a same-size edit is caught by Refetch and missed by TrustSize — \
             that is the trade the operator opted into"
        );
    }
}

#[tokio::test]
async fn an_upstream_that_dropped_a_coordinate_reports_it_removed() {
    let b = bench(true);
    b.up.put("/alpha@1.0", b"a");
    b.up.put("/beta@2.0", b"b");
    b.cycle(RonUnprovenPolicy::Refetch).await;

    b.up.remove("/beta@2.0");
    let second = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(second.removed, 1, "{}", second.line());
    assert_eq!(second.map_after, 1);
    assert!(
        b.local_bytes("/beta@2.0").is_some(),
        "a mirror does not delete local bytes because an upstream stopped listing them — \
         retention is a separate, deliberate decision"
    );
}

#[tokio::test]
async fn a_fetch_that_failed_is_asked_for_again_next_cycle() {
    let b = bench(true);
    b.up.put("/alpha@1.0", b"a");
    b.up.put("/beta@2.0", b"b");
    b.up.broken.lock().unwrap().push("/beta@2.0".to_string());

    let first = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(first.fetched, 1);
    assert_eq!(first.failed, 1);
    assert_eq!(
        first.map_after, 1,
        "the map NEVER claims a coordinate whose bytes the mirror does not hold"
    );

    b.up.broken.lock().unwrap().clear();
    let second = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(
        second.added,
        1,
        "still unknown to the map: {}",
        second.line()
    );
    assert_eq!(second.fetched, 1);
    assert_eq!(b.local_bytes("/beta@2.0").as_deref(), Some(&b"b"[..]));
}

#[tokio::test]
async fn the_change_map_survives_a_restart() {
    // A "restart" here is the only thing a restart IS to this engine: a brand new
    // in-memory map, loaded from the same state dir.
    let b = bench(true);
    for i in 0..50 {
        b.up.put(&format!("/pkg{i:02}@1.0"), format!("body-{i}").as_bytes());
    }
    let first = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(first.fetched, 50);
    let after_first = b.up.fetch_count();

    // Same state dir, same upstream, same local backend: exactly what comes back
    // up after `systemctl restart holger`.
    let restarted = b.cycle(RonUnprovenPolicy::Refetch).await;
    assert_eq!(
        restarted.map_before,
        50,
        "the map was READ from disk, not rebuilt: {}",
        restarted.line()
    );
    assert_eq!(restarted.unchanged, 50);
    assert_eq!(restarted.to_fetch, 0);
    assert_eq!(
        b.up.fetch_count(),
        after_first,
        "a restarted mirror re-pulls NOTHING"
    );

    // And the file is where the declaration says it is.
    let f = holger_server_lib::mirror::ChangeMap::path_in(b.dir.path(), "up", "crates");
    assert!(f.exists(), "{} should hold the persisted map", f.display());
    let reloaded = holger_server_lib::mirror::ChangeMap::read_file(&f).expect("reload");
    assert_eq!(reloaded.len(), 50);
    assert_eq!(reloaded.upstream(), "up");
    assert_eq!(reloaded.repository(), "crates");
}

// ───────────────────── the declaration, as a config ─────────────────────

#[test]
fn a_config_written_before_the_key_existed_mirrors_nothing() {
    // Byte-for-byte the shape that was deployed before this lane. It must still
    // parse, and it must describe a holger that takes nothing from anywhere.
    let older = r#"(
    repositories: [
        ( ron_name: "bundles", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
          ron_out: None, ron_data_dir: Some("/tmp/x") ),
    ],
    exposed_endpoints: [ (ron_name: "doors", ron_url: "127.0.0.1:0", ron_http_url: None) ],
    storage_endpoints: [ (ron_name: "store", ron_storage_type: "znippy", ron_path: "/tmp/x") ],
)"#;
    let h: holger_server_lib::Holger = ron::from_str(older).expect("an older config still parses");
    assert!(
        h.mirror.is_none(),
        "absent means NO mirror — a config cannot acquire an upstream by being old"
    );
}

#[test]
fn the_block_parses_on_the_root_config_and_a_typo_in_it_is_a_boot_error() {
    let with_mirror = r#"(
    repositories: [
        ( ron_name: "crates", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
          ron_out: None, ron_data_dir: Some("/tmp/x") ),
    ],
    exposed_endpoints: [ (ron_name: "doors", ron_url: "127.0.0.1:0", ron_http_url: None) ],
    storage_endpoints: [ (ron_name: "store", ron_storage_type: "znippy", ron_path: "/tmp/x") ],
    mirror: Some((
        ron_state_dir: "holger-mirror",
        ron_sources: [
            (
                ron_name: "holger-rs",
                ron_kind: Holger,
                ron_endpoint: "http://holger.rs:50051",
                ron_credential: Anonymous,
                ron_every_secs: 300,
                ron_repositories: [ ( ron_from: "crates", ron_to: "crates" ) ],
            ),
        ],
    )),
)"#;
    let h: holger_server_lib::Holger = ron::from_str(with_mirror).expect("parse");
    let m = h.mirror.expect("declared");
    assert_eq!(m.ron_sources.len(), 1);
    assert_eq!(
        m.ron_sources[0].ron_repositories[0].ron_unproven,
        RonUnprovenPolicy::Refetch
    );

    // The whole point of deny_unknown_fields: a mirror that reads as configured
    // and runs as absent is the failure this refuses.
    let typo = with_mirror.replace("ron_every_secs", "ron_every_sec");
    assert!(
        ron::from_str::<holger_server_lib::Holger>(&typo).is_err(),
        "ron_every_sec must be a boot error, not a silently defaulted cadence"
    );
}

#[test]
fn validate_is_what_the_boot_calls_and_it_refuses_by_name() {
    let s: MirrorSettings = ron::from_str(
        r#"( ron_sources: [ ( ron_name: "up", ron_kind: Holger,
              ron_endpoint: "http://holger.rs:50051",
              ron_repositories: [ ( ron_from: "crates", ron_to: "absent" ) ] ) ] )"#,
    )
    .expect("parse");
    let e = format!("{}", s.validate(&[("crates".into(), true)]).unwrap_err());
    assert!(e.contains("absent"), "{e}");
}