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

nordisk/gunnar

crates/gunnar-monetize-guest/tests/the_kits_judge_is_the_guardians.rs

//! ★ **One guard, two callers** (lane G11, 2026-10-21): the guard before a main's data is
//! destroyed (Gunnar ca38566f, R1-R6) is asked by the operator's kit and the console through
//! `gunnar_repair::rebuild::judge` (open gunnar, money-free, with Gunnar's `TwinReview` and a
//! typed confirmation), and by every product's estate guardian through monetize-product
//! `guardian::rebuild::judge` (which Gunnar's and Holger's guardians call, linked and as signed
//! guests). This file holds the two to the same answer for every case R1-R6 reason about, so
//! the rule set is one even though the open crate cannot link the monetize one.

use gunnar_repair::rebuild as kit;
use gunnar_repair::review::{Health, TwinReview};
use monetize_product::guardian::rebuild as shared;

const STORAGE: &str = "01aa-data";

fn review(health: Health, possible: bool, head: u64) -> TwinReview {
    TwinReview {
        twin_addr: "10.0.0.3:50051".into(),
        health: Some(health),
        head_seq: head,
        applied_seq: head,
        recharge_possible: possible,
        verdict: if possible {
            "level".into()
        } else {
            "behind its primary".into()
        },
        ..Default::default()
    }
}

/// The shared guard's reading of Gunnar's twin review: it hears a primary when it is level
/// with one that answers (`Healthy`), and it holds its own head.
fn twin_of(t: &TwinReview) -> shared::Twin {
    shared::Twin {
        addr: t.twin_addr.clone(),
        hears_primary: t.health == Some(Health::Healthy),
        recharge_possible: t.recharge_possible,
        verdict: t.verdict.clone(),
        holds: Some(t.head_seq.max(t.applied_seq)),
    }
}

/// The same facts, in the shared guard's types (the enums are the same JSON shape).
fn shared_of(s: &kit::Seen) -> shared::Seen {
    shared::Seen {
        provider: serde_json::from_value(serde_json::to_value(&s.provider).unwrap()).unwrap(),
        main: serde_json::from_value(serde_json::to_value(&s.main).unwrap()).unwrap(),
        main_head: s.main_head,
        booted_again: s.booted_again,
        installer_refused: s.installer_refused.clone(),
        twin: s.twin.as_ref().map(twin_of),
        data_storage: s.data_storage.clone(),
        confirm: s
            .confirm
            .as_ref()
            .map(|c| serde_json::from_value(serde_json::to_value(c).unwrap()).unwrap()),
        auto: s.auto,
    }
}

/// What a caller acts on: the word, the refusal code, and where it fills from / what it gives back.
fn act(v: serde_json::Value) -> (String, String, String, String) {
    let s = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
    (s("do"), s("code"), s("twin"), s("storage"))
}

fn cases() -> Vec<kit::Seen> {
    let mut out = Vec::new();
    let providers = [
        kit::Provider::Gone,
        kit::Provider::Stopped,
        kit::Provider::Running,
        kit::Provider::Unknown("timeout".into()),
    ];
    let mains = [
        kit::Main::Silent,
        kit::Main::Says { data: "ok".into() },
        kit::Main::Says {
            data: "fresh".into(),
        },
        kit::Main::Says {
            data: "lost".into(),
        },
        kit::Main::Says {
            data: "diverged".into(),
        },
        kit::Main::Says { data: "odd".into() },
    ];
    let twins = [
        None,
        Some(review(Health::PrimarySilent, true, 100)),
        Some(review(Health::Healthy, true, 100)),
        Some(review(Health::Behind, false, 90)),
    ];
    let confirms = [
        None,
        Some(kit::Confirm {
            by: "rickard".into(),
            storage: STORAGE.into(),
            accept_lost: false,
        }),
        Some(kit::Confirm {
            by: "rickard".into(),
            storage: "01bb-other".into(),
            accept_lost: false,
        }),
        Some(kit::Confirm {
            by: "rickard".into(),
            storage: STORAGE.into(),
            accept_lost: true,
        }),
    ];
    for p in &providers {
        for m in &mains {
            for t in &twins {
                for c in &confirms {
                    for (head, booted, refused, storage, auto) in [
                        (Some(100), false, None, STORAGE, false),
                        (Some(120), true, None, STORAGE, false),
                        (
                            None,
                            false,
                            Some("member 1 of 2 is missing".to_string()),
                            STORAGE,
                            true,
                        ),
                        (Some(100), true, None, "", false),
                    ] {
                        out.push(kit::Seen {
                            provider: p.clone(),
                            main: m.clone(),
                            main_head: head,
                            booted_again: booted,
                            installer_refused: refused,
                            twin: t.clone(),
                            data_storage: storage.into(),
                            confirm: c.clone(),
                            auto,
                        });
                    }
                }
            }
        }
    }
    out
}

#[test]
fn the_kits_guard_and_the_guardians_guard_answer_alike() {
    let all = cases();
    assert!(all.len() > 1_000, "{}", all.len());
    for s in &all {
        let k = act(serde_json::to_value(kit::judge(s)).unwrap());
        let g = act(serde_json::to_value(shared::judge(&shared_of(s))).unwrap());
        assert_eq!(k, g, "{s:?}");
    }
}