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/lifecycle_parity.rs

//! ★ **Gunnar's lifecycle from the guest IS the shop's Gunnar rules** (owner 2026-10-17: keep
//! behaviour byte-identical) — the facts against monetize-server's Gunnar-shaped TEST lifecycle
//! (`testing::lifecycles::gunnar()`, a `lifecycle::Lifecycle` over the test facts: the v2 cut
//! replaced the `EstateLifecycle` trait and `TestGunnar` with that one concrete type), and the
//! plan against its plan, for the default shape, a buyer's sizes, a twin plan, the config's
//! labels and a template whose tier a system disk is laid in. The request is the one the server
//! sends (`guest_lifecycle::request`).

use gunnar_monetize_guest::lifecycle;
use monetize_server::estate_order::guest_lifecycle;
use monetize_server::estate_order::{EstateOrderConfig, EstateSizes, ReleaseTemplate};
use monetize_server::testing::lifecycles;

/// The Gunnar rules' plan for `c`.
fn plan_for(
    c: &EstateOrderConfig,
    company: &str,
    sizes: &EstateSizes,
    twin: &str,
) -> monetize_cloud::lay::EstatePlan {
    lifecycles::gunnar()
        .lay_plan(c, company, sizes, twin)
        .expect("the Gunnar rules lay it")
}

fn cfg() -> EstateOrderConfig {
    EstateOrderConfig {
        product: "gunnar".into(),
        zone: "fi-hel2".into(),
        apex: "gunnar.rs".into(),
        vendor: "holdpay".into(),
        shop_door: "https://monetize.example".into(),
        ..EstateOrderConfig::default()
    }
}

#[test]
fn the_guests_facts_are_the_gunnar_lifecycle() {
    let f = lifecycle::facts();
    let native = lifecycles::gunnar();
    let u = native.update_plan();
    assert_eq!(f.kind, native.kind());
    assert_eq!(
        (
            f.update_order.clone(),
            f.app_medium.clone(),
            f.front_medium.clone(),
            f.front_role.clone()
        ),
        (u.order, u.app_medium, u.front_medium, u.front_role)
    );
    assert_eq!(
        (
            f.keep_medium.clone(),
            f.front_keep_medium.clone(),
            f.upgraded_by_shop.clone()
        ),
        (u.keep_medium, u.front_keep_medium, u.upgraded_by_shop)
    );
    assert_eq!(f.backup_roles, native.backup_roles());
    let w = native.words();
    assert_eq!(
        (
            f.words.name.clone(),
            f.words.title.clone(),
            f.words.account_home.clone()
        ),
        (w.name, w.title, w.account_home)
    );
    assert_eq!(f.domain_proof_name, native.proof_name());
    let roles: Vec<(String, bool, String)> = native
        .data_roles()
        .into_iter()
        .map(|r| (r.role, r.mirrors_primary, r.set_suffix))
        .collect();
    assert_eq!(
        f.data_roles
            .iter()
            .map(|r| (r.role.clone(), r.mirrors_primary, r.set_suffix.clone()))
            .collect::<Vec<_>>(),
        roles
    );
    let mut shape = f.default_shape.clone();
    shape
        .as_object_mut()
        .expect("a shape")
        .retain(|k, _| k != "mail_words" && k != "names" && k != "redirect_names");
    assert_eq!(
        shape,
        serde_json::to_value(native.default_shape().expect("Gunnar names its shape")).unwrap(),
        "the default shape, byte for byte"
    );
    // ★ v2.0 P2 12.1 (A28 H5): what the ONE box reads off the guest with NO native Gunnar under
    // it — the mails' words and the served names — is what the native said.
    assert_eq!(
        guest_lifecycle::mail_words_over(None, &f.default_shape),
        native.mail_words(),
        "the mail words, with no native under them"
    );
    assert_eq!(
        guest_lifecycle::names_over(None, &f.default_shape),
        native.served_prefixes(),
        "the served names, with no native under them"
    );
    // HOL-REPO-NAME: the redirect-only names the shop's pairing reads are the TEST facts' (its
    // tests pair what this guest says): `git.`, and no repository route.
    let test = lifecycles::gunnar_facts();
    for rider in [
        guest_lifecycle::RIDER_REDIRECT_NAMES,
        guest_lifecycle::RIDER_REPO_NAMES,
    ] {
        assert_eq!(
            guest_lifecycle::prefixes_named(rider, &f.default_shape),
            guest_lifecycle::prefixes_named(rider, &test.default_shape),
            "{rider}"
        );
    }
    assert_eq!(
        guest_lifecycle::prefixes_named(guest_lifecycle::RIDER_REDIRECT_NAMES, &f.default_shape),
        Some(vec!["git.".to_string()])
    );
    for role in ["front", "main", "twin", "other"] {
        let ours = f
            .medium_of
            .get(role)
            .cloned()
            .unwrap_or_else(|| f.other_medium.clone());
        assert_eq!(ours, native.medium_kind(role), "{role}");
    }
}

#[test]
fn the_guests_plan_is_plan_for_byte_for_byte() {
    let f = lifecycle::facts();
    let mut templated = cfg();
    templated.media.insert("gunnar".into(), "01-gunnar".into());
    templated.templates.insert(
        "gunnar".into(),
        ReleaseTemplate {
            uuid: "tpl-1".into(),
            for_release: "01-gunnar".into(),
            tier: String::new(),
        },
    );
    let mut labelled = cfg();
    labelled.labels.insert("shop".into(), "one".into());
    let sized = EstateSizes {
        front_plan: "STARTER-4xCPU-8GB".into(),
        main_plan: "STARTER-8xCPU-16GB".into(),
        main_data_gib: 40,
        ..EstateSizes::default()
    };
    for (c, sizes, twin) in [
        (cfg(), EstateSizes::default(), ""),
        (cfg(), sized.clone(), "STARTER-2xCPU-4GB"),
        (templated, sized.clone(), ""),
        (labelled, EstateSizes::default(), "STARTER-1xCPU-2GB"),
    ] {
        let req = guest_lifecycle::request(&f, &c, "acme", &sizes, twin);
        let ours = lifecycle::lay_plan(&req).expect("the guest lays it");
        assert_eq!(ours, plan_for(&c, "acme", &sizes, twin));
        assert_eq!(
            serde_json::to_vec(&ours).unwrap(),
            serde_json::to_vec(&plan_for(&c, "acme", &sizes, twin)).unwrap(),
            "byte for byte"
        );
    }
}

/// ★ **Across the ABI**: the module this crate really builds (wasm32, release), loaded SIGNED by
/// the open guest host, answers `lifecycle.facts` and `lifecycle.lay_plan` as the Gunnar rules
/// do — what the ONE box's monetize registers for `gunnar`.
#[test]
fn the_built_module_answers_its_lifecycle_across_the_abi() {
    use std::path::{Path, PathBuf};
    let target = std::env::var("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target"));
    let wasm = target.join("wasm32-unknown-unknown/release/gunnar_monetize_guest.wasm");
    assert!(wasm.is_file(), "{} has not been built. This test loads the REAL guest; it does not skip.\nBuild it with:\n    cargo build --manifest-path crates/gunnar-monetize-guest/Cargo.toml --target-dir target --release --target wasm32-unknown-unknown", wasm.display());
    let dir = std::env::temp_dir().join(format!("gunnar-lifecycle-abi-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let module = dir.join("gunnar.wasm");
    std::fs::copy(&wasm, &module).unwrap();
    let root = dir.join("korp.der");
    {
        use guest_host::verify::sign::{self, dev};
        let (ca_key, ca_der) = dev::mint_ca("plugin-root").unwrap();
        std::fs::write(&root, &ca_der).unwrap();
        sign::sign_module_file(
            &module,
            &dev::new_ed25519_signer(&ca_key, &ca_der, "plugin-signing").unwrap(),
        )
        .unwrap();
    }
    // No grants: a product guest opens no door (monetize v2.0 P2 T13).
    let spec = monetize_product::GuestSpec {
        trust_roots: vec![root],
        module,
        ..Default::default()
    };
    let build = monetize_product::ProductBuild {
        id: "gunnar",
        endpoint: "127.0.0.1:1",
        identity_key: None,
        appliance_key: None,
        bearer_token: None,
        api_key_env: None,
        restoring_from_twin: None,
        tls_ca: None,
        tls_client: None,
        guest: Some(&spec),
    };
    let p =
        monetize_product_wasm::GuestProduct::build(&build).expect("the signed gunnar guest loads");
    let facts = p
        .lifecycle_facts()
        .expect("answered")
        .expect("gunnar brings its lifecycle");
    assert_eq!(facts, lifecycle::facts());
    let c = cfg();
    let sizes = EstateSizes {
        front_plan: String::new(),
        main_plan: "STARTER-8xCPU-16GB".into(),
        main_data_gib: 20,
        ..EstateSizes::default()
    };
    let req = guest_lifecycle::request(&facts, &c, "acme", &sizes, "STARTER-2xCPU-4GB");
    let plan = p.lifecycle_lay_plan(&req).expect("laid across the ABI");
    assert_eq!(
        plan,
        serde_json::to_value(plan_for(&c, "acme", &sizes, "STARTER-2xCPU-4GB")).unwrap()
    );
    let _ = std::fs::remove_dir_all(&dir);
}