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);
}