crates/gunnar-front/tests/stderr_to_bootlog.rs
//! **monetize's lines reach the installer's log door** (2026-09-28).
//!
//! MEASURED on test2's front (gunnar 50c3e69) through `estate logs front`: a
//! tenant's order (`subscription.bought … state="pending"`) and then not one
//! line from monetize-server, which runs in the front's process and says
//! everything with `eprintln!`. The front now takes fd 2 over
//! (`gunnar_common::stderr_capture`) before tracing is installed, so what is
//! printed there is a log line in BOOT_LOG — and the VM's redaction withholds
//! a Stripe secret before it is held.
//!
//! Its own test binary: it takes over this process's fd 2 and installs the
//! global subscriber, as `gunnar-front`'s `main` does.
use std::sync::Arc;
use std::time::{Duration, Instant};
use gunnar_server::installer_listener::{serve_door, Door, BOOT_LOG};
use nordisk_pairing::installer::{read_log, Material};
use nordisk_pairing::installer_link::DirectPath;
use nordisk_pairing::mtls;
use nordisk_pairing::seed::{Kind, Seed, TunnelKind};
#[test]
fn a_monetize_line_reaches_read_log_and_a_stripe_secret_is_withheld() {
// The order main() does it in.
gunnar_server::installer_listener::capture();
gunnar_common::stderr_capture::start().expect("fd 2 is taken over");
gunnar_common::init_tracing(false);
// What monetize-server says, on fd 2 as its `eprintln!` does. Written
// through `io::stderr()` and not with `eprintln!` here: libtest captures
// the macro inside a test, and the bytes would never reach fd 2.
use std::io::Write as _;
let mut fd2 = std::io::stderr();
for line in [
"monetize-server: checkout gunnar/alice/2026-09-28+pack_bytes=10737418240 minted at stripe (session cs_test_buyflow1)",
"monetize-server: stripe: signing the webhook check with whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH",
"monetize-server: stripe: POST /v1/checkout/sessions with sk_live_51HabcDEF",
"monetize-server: grow FAILED: the cloud refused the resize",
] {
writeln!(fd2, "{line}").unwrap();
}
// The installer reads the VM's log over mutual TLS, as `estate logs` does.
let h = Duration::from_secs(3600);
let ca = mtls::InstallerCa::mint("gunnar.rs", h).unwrap();
let boot = ca.issue_bootstrap(h).unwrap();
let seed = Seed {
estate: "gunnar.rs".into(),
tunnel: TunnelKind::Wireguard,
front: nordisk_pairing::seed::Front::default(),
kind: Kind::Front,
installer_ca_pem: ca.cert_pem.clone(),
bootstrap_cert_pem: boot.cert_pem,
bootstrap_key_pem: boot.key_pem,
peers_expected: 3,
mullvad: None,
tailscale: None,
installer_link: None,
};
let me = ca.issue_installer(h).unwrap();
let data = tempfile::tempdir().unwrap();
let vm: Arc<dyn nordisk_pairing::bootstrap::Vm> =
Arc::new(gunnar_front::vm::FrontVm::open(data.path(), seed.clone()).unwrap());
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let std::net::SocketAddr::V4(at) = l.local_addr().unwrap() else {
panic!("v4")
};
let ca_pem = seed.installer_ca_pem.clone();
std::thread::spawn(move || {
serve_door(vm.as_ref(), &BOOT_LOG, &ca_pem, l, Door::Install, &|_| {})
});
let m = Material { ca, me, link: None };
let deadline = Instant::now() + Duration::from_secs(20);
let lines = loop {
let chunk = read_log(Box::new(DirectPath { at }), &m, 0).expect("the log door answers");
if chunk.lines.iter().any(|l| l.contains("grow FAILED")) || Instant::now() > deadline {
break chunk.lines;
}
std::thread::sleep(Duration::from_millis(100));
};
let order = lines
.iter()
.find(|l| l.contains("cs_test_buyflow1"))
.unwrap_or_else(|| panic!("monetize's line reached read_log: {lines:#?}"));
assert!(
order.contains(" INFO stderr: monetize-server: checkout"),
"said through tracing, with a level and the `stderr` target: {order}"
);
assert!(
order.starts_with("20") && order.contains('Z'),
"and a timestamp: {order}"
);
let failed = lines
.iter()
.find(|l| l.contains("grow FAILED"))
.expect("the failure line");
assert!(
failed.contains(" WARN stderr: "),
"a line that says it failed is a WARN: {failed}"
);
for secret in [
"whsec_",
"sk_live_",
"5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH",
"51HabcDEF",
] {
assert!(
!lines.iter().any(|l| l.contains(secret)),
"`{secret}` never reaches the installer: {lines:#?}"
);
}
assert!(
lines
.iter()
.any(|l| l.starts_with("<<WITHHELD:") && l.contains("token-class")),
"the two secret lines are withheld as token-class: {lines:#?}"
);
}