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

nordisk/gunnar

crates/gunnar-front/tests/readmit_reads_its_own_log.rs

//! **A re-admission that reads the front's own log never says it again** (gunnar.rs
//! 2026-10-25: 1.9 M nested `readmit:` lines in the front's log ring).
//!
//! The front is a box of its own plan, and its re-admission reads its log over its own door.
//! That log captures the front's fd 2 (`gunnar_common::stderr_capture`), where the sink says
//! every line it reads: each line said came back on the next read as a new line one level
//! deeper (`readmit front: … INFO stderr: readmit front: …`), and no once-only memory stops a
//! line that is new every time. Here the real sink says into the real fd 2, the real capture
//! puts it in BOOT_LOG, the real log door serves it, and what is read is handed back to the
//! sink, round after round.
//!
//! Its own test binary: it takes over this process's fd 2 and installs the global subscriber,
//! as `gunnar-front`'s `main` does. The re-admission's sink is `metered`'s (`required-features`).

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, Sink as _};
use nordisk_pairing::installer_link::DirectPath;
use nordisk_pairing::kept::ReadmitLog;
use nordisk_pairing::mtls;
use nordisk_pairing::seed::{Kind, Seed, TunnelKind};

static LOG: ReadmitLog = ReadmitLog::new();

/// The sink's say, into fd 2 itself (`io::stderr()`, never `eprintln!`, which libtest
/// captures inside a test), where the front's sink says it.
fn to_fd2(line: &str) {
    use std::io::Write as _;
    let _ = writeln!(std::io::stderr(), "{line}");
}

#[test]
fn the_fronts_own_log_read_back_by_its_readmission_is_not_said_again() {
    gunnar_server::installer_listener::capture();
    gunnar_common::stderr_capture::start().expect("fd 2 is taken over");
    gunnar_common::init_tracing(false);

    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 read =
        |from: u64| read_log(Box::new(DirectPath { at }), &m, from).expect("the log door answers");

    // What the front has logged before its re-admission runs.
    use std::io::Write as _;
    writeln!(std::io::stderr(), "korp-installer: INSTALL-OK (a5a7 probe)").unwrap();
    let settled = |want: &dyn Fn(&[String]) -> bool| {
        let deadline = Instant::now() + Duration::from_secs(20);
        loop {
            let lines = read(0).lines;
            if want(&lines) || Instant::now() > deadline {
                return lines;
            }
            std::thread::sleep(Duration::from_millis(50));
        }
    };
    let lines = settled(&|l| l.iter().any(|l| l.contains("(a5a7 probe)")));
    assert!(
        lines.iter().any(|l| l.contains("(a5a7 probe)")),
        "the probe line reached the log: {lines:#?}"
    );
    let before = lines.len();

    // Ten rounds, each reading the front's own log from where the last stopped, as a
    // re-admission's drain does, and handing it to a fresh sink, as each run makes one.
    let mut from = 0u64;
    for round in 0..10 {
        let chunk = read(from);
        from += chunk.lines.len() as u64;
        let sink = gunnar_front::recharge::MemSink::with(&LOG, to_fd2);
        sink.lines("front", &chunk.lines);
        sink.say("front: reaching 10.13.8.1:4443");
        let _ = round;
        std::thread::sleep(Duration::from_millis(200));
    }
    std::thread::sleep(Duration::from_secs(1));
    let lines = read(0).lines;
    let said: Vec<&String> = lines.iter().filter(|l| l.contains("readmit")).collect();
    assert!(
        said.iter().all(|l| l.matches("readmit").count() == 1),
        "no line nested: {said:#?}"
    );
    assert!(
        said.iter().filter(|l| l.contains("(a5a7 probe)")).count() == 1,
        "the probe line said once: {said:#?}"
    );
    assert!(said.len() <= before + 1, "each line the front had logged said once, and the narration once; the log stops growing: {} readmit lines for {before}: {said:#?}", said.len());
}