fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    let sink_store = Arc::clone(&store);
    let status_store = Arc::clone(&store);
    RosterExchange {

//! Mesh-D4 — on-connect roster propagation, the integration evidence
//! (REQ-MESH-2 int).
//!
//! Two in-process `NetHost`s dial over hermetic loopback QUIC with the seed-proof
//! membership source AND a roster exchange wired. The roster leg rides the
//! **same** proven control stream (Mesh-D2's), so it runs only after both sides
//! prove. We model the canonical mesh gap: B already knows an **offline** third
//! node C (subnet member, never directly paired to A). When A connects B, A must
//! **learn C** (the transitive directory growth REQ-MESH-2 owns) AND learn an
//! address for both B and C (the reach precondition Mesh-D6 consumes) — with B
//! never relaying a registry row. The store here is an in-memory `RosterStore`
//! per host (the file-backed `production_roster_exchange` is exercised by its own
//! unit tests); this test proves the *propagation over the wire*.

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{MembershipSource, RosterExchange, SubnetCred};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::roster::{RosterEntry, RosterStore};

fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

/// An in-memory roster exchange over a shared store: the provider stamps our
/// live address onto our self-entry (so peers learn how to reach us) and returns
/// the proven subnets' slice; the sink merges the peer's slice in. Mirrors
/// `production_roster_exchange` minus the file/peeraddrs plumbing.
fn test_exchange(store: Arc<Mutex<RosterStore>>, self_hex: String) -> RosterExchange {
    let prov_store = Arc::clone(&store);
    RosterExchange {
        provider: Arc::new(move |proven: &HashSet<String>, self_addr: &serde_json::Value| {
            let mut s = prov_store.lock().unwrap();
            let addr = if self_addr.is_null() {
                None
            } else {
                Some(self_addr.clone())
            };
            for subnet in proven {
                s.upsert_self(subnet, &self_hex, "lbl", "mid", addr.clone(), "1700000000", 5);
            }
            let mut entries = Vec::new();
            let mut tombs = Vec::new();
            for subnet in proven {
                let (m, t) = s.roster_for(subnet);
                entries.extend(m);
                tombs.extend(t);
            }
            (entries, tombs)
        }),
        sink: Arc::new(move |entries, tombs| {
            let mut s = sink_store.lock().unwrap();
            for e in &entries {
                s.merge_entry(e.clone());
            }
            for t in &tombs {
                s.tombstone(&t.subnet, &t.pubkey_hex, &t.stamp);
            }
        }),
        // D7 grace seams — same-epoch tests never trigger a re-seed, so status
        // reads the shared store and adopt is a no-op.
        member_status: Arc::new(move |subnet: &str, hex: &str| {
            let s = status_store.lock().unwrap();
            if s.is_tombstoned(subnet, hex) {
                spt_daemon::seedproofx::MemberStatus::Tombstoned
            } else if s.find(subnet, hex).is_some() {
                spt_daemon::seedproofx::MemberStatus::Present
            } else {
                spt_daemon::seedproofx::MemberStatus::Absent
            }
        }),
        adopt_seed: Arc::new(|_subnet: &str, _seed: &[u8], _epoch: u64| false),
    }
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

// [int->REQ-MESH-2] B knows an offline C; A connects B; A learns C (transitive
// directory growth) with an address (reach precondition), and B learns A — the
// roster converges over a proven member connection, no row ever relayed.
#[test]
fn roster_propagates_transitively_on_connect() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let c_hex = Identity::from_seed(&[0xC5; 32]).public_key().to_hex(); // offline third node

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B already holds C in its roster (a member it paired with while A was
    // offline) — C carries a dialable address but is not connected here.
    let c_addr = serde_json::json!({"id": c_hex, "addrs": ["10.9.9.9:4711"]});
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: c_hex.clone(),
        subnet: "home".to_string(),
        label: "c-host".to_string(),
        machine_id: "c-mid".to_string(),
        address: Some(c_addr.clone()),
        last_seen: "1699999999".to_string(),
        lease_epoch: 2,
    });

    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex.clone()));
    b.set_roster_exchange(test_exchange(Arc::clone(&store_b), b_hex.clone()));

    // A dials B: the proof verifies, then the roster leg runs on the same stream.
    // dial() blocks through the dialer leg, so A's sink has merged B's slice by
    // the time it returns.
    let (_conn_id, remote) = a.dial(b.addr()).expect("dial member");
    assert_eq!(remote, b_hex, "handshake proves B");

    let sa = store_a.lock().unwrap();
    assert!(
        sa.is_member("home", &c_hex),
        "A learned the offline C transitively via B's roster"
    );
    assert_eq!(
        sa.find("home", &c_hex).and_then(|e| e.address.clone()),
        Some(c_addr),
        "A learned C's advertised address (the Mesh-D6 reach precondition)"
    );
    assert!(sa.is_member("home", &b_hex), "A learned B (its self-entry)");
    assert!(
        sa.find("home", &b_hex)
            .map(|e| e.address.is_some())
            .unwrap_or(false),
        "B's entry carries B's live address (the self-address advertise flow)"
    );
    drop(sa);

    // The acceptor's roster leg runs on its spawned task — poll until B's store
    // has A (the reverse direction of the mutual exchange).
    assert!(
        wait_until(|| store_b.lock().unwrap().is_member("home", &a_hex)),
        "B learned A over the same proven connection"
    );
}

// [int->REQ-MESH-2] the leak defense end-to-end: a roster exchange only emits the
// PROVEN subnet's slice. A and B share only `home`; B also holds a `work` member
// D that A must never receive (A is not a `work` member, so `work` is not proven).
#[test]
fn roster_exchange_never_leaks_unproven_subnets() {
    let a_id = Identity::generate();
    let b_id = Identity::generate();
    let a_hex = a_id.public_key().to_hex();
    let b_hex = b_id.public_key().to_hex();
    let d_hex = Identity::from_seed(&[0xD4; 32]).public_key().to_hex();

    let store_a = Arc::new(Mutex::new(RosterStore::default()));
    let store_b = Arc::new(Mutex::new(RosterStore::default()));

    // B holds D in `work` — a subnet A is not a member of.
    store_b.lock().unwrap().merge_entry(RosterEntry {
        pubkey_hex: d_hex.clone(),
        subnet: "work".to_string(),
        label: "d".to_string(),
        machine_id: "d".to_string(),
        address: None,
        last_seen: "1699999999".to_string(),
        lease_epoch: 1,
    });

    // A: home only. B: home + work. Only `home` is mutually proven.
    let a = host(a_id, vec![cred("home", 7, 1)]);
    let b = host(b_id, vec![cred("home", 7, 1), cred("work", 8, 1)]);
    a.set_roster_exchange(test_exchange(Arc::clone(&store_a), a_hex));
    b.set_roster_exchange(test_exchange(Arc::clone(&store_b), b_hex.clone()));

    let _ = a.dial(b.addr()).expect("dial");

    let sa = store_a.lock().unwrap();
    assert!(sa.is_member("home", &b_hex), "A still learned B in home");
    assert!(
        sa.find("work", &d_hex).is_none(),
        "A never received the unproven `work` roster (no cross-subnet leak)"
    );
}

+    let sink_store = Arc::clone(&store);

+    let status_store = Arc::clone(&store);


Now compile the daemon crate to catch errors before adding grading unit tests:


Bash

cargo build -p spt-daemon 2>&1 | tail -30

Build the daemon crate with the reworked seedproofx


   Compiling tokio-rustls v0.26.4
   Compiling spt-store v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
   Compiling hyper-rustls v0.27.9
   Compiling hickory-net v0.26.0-beta.4
   Compiling rustls-platform-verifier v0.7.0
   Compiling noq v0.18.0
   Compiling tokio-websockets v0.13.2
   Compiling reqwest v0.13.4
   Compiling hickory-resolver v0.26.0-beta.4
   Compiling spt-msg v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling spt-runtime v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-term v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-live v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling iroh-relay v0.98.0
   Compiling iroh v0.98.2
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 24.22s


Daemon builds. Now add unit tests for the grading truth table + seed-transfer codec + seed-never-in-gossip:


1	//! Connect-time seed-proof exchange (Mesh-D2, REQ-MESH-1) — the daemon-side


grep -n "mod roster_tests\|^}" crates/spt-daemon/src/seedproofx.rs | tail -5

Find end of file for appending test module


876:}
959:}
966:}
969:mod roster_tests {
1066:}


1060	        assert_eq!(pa.get("bb"), Some(&observed_b), "observed addr not clobbered");
1061	        assert_eq!(pa.get("self"), None, "own entry skipped");
1062
1063	        // Idempotent: a second pass with the same input changes nothing.
1064	        assert!(!gapfill_peeraddrs(&mut pa, "self", &entries), "no further change");
1065	    }
1066	}
1067

        assert_eq!(pa.get("bb"), Some(&observed_b), "observed addr not clobbered");
        assert_eq!(pa.get("self"), None, "own entry skipped");

        // Idempotent: a second pass with the same input changes nothing.
        assert!(!gapfill_peeraddrs(&mut pa, "self", &entries), "no further change");
    }
}


Edit

        // Idempotent: a second pass with the same input changes nothing.
        assert!(!gapfill_peeraddrs(&mut pa, "self", &entries), "no further change");
    }
}

        // Idempotent: a second pass with the same input changes nothing.
        assert!(!gapfill_peeraddrs(&mut pa, "self", &entries), "no further change");
    }
}

#[cfg(test)]
mod grace_tests {
    use super::*;

    // [unit->REQ-MESH-4] the re-seed grant/deny truth table. With a roster seam:
    // exact-epoch from a present (or even not-yet-known) member ⇒ Full; the
    // prior epoch from a PRESENT member ⇒ ReseedOnly; a tombstoned (revoked)
    // peer ⇒ Denied on ANY generation (even exact, before its seed is rotated);
    // an absent peer is never re-seeded; two-stale (no match) ⇒ Denied.
    #[test]
    fn grade_truth_table() {
        use Grade::*;
        use MemberStatus::*;

        // exact-epoch (matched_current = true)
        assert_eq!(grade_subnet(true, false, Some(Present)), Full);
        assert_eq!(grade_subnet(true, false, Some(Absent)), Full, "unknown member, current seed ⇒ admit");
        assert_eq!(grade_subnet(true, false, Some(Tombstoned)), Denied, "revokee denied even exact");

        // prior-epoch only (matched_prev = true, matched_current = false)
        assert_eq!(grade_subnet(false, true, Some(Present)), ReseedOnly, "benign offliner ⇒ grace");
        assert_eq!(grade_subnet(false, true, Some(Tombstoned)), Denied, "revoked N-1 ⇒ denied");
        assert_eq!(grade_subnet(false, true, Some(Absent)), Denied, "off-roster N-1 ⇒ never re-seeded");

        // ≥2 stale / forged (no match at any held generation)
        assert_eq!(grade_subnet(false, false, Some(Present)), Denied);
        assert_eq!(grade_subnet(false, false, Some(Tombstoned)), Denied);

        // No roster seam (D2 mechanics path): epoch-only grading.
        assert_eq!(grade_subnet(true, false, None), Full);
        assert_eq!(grade_subnet(false, true, None), ReseedOnly);
        assert_eq!(grade_subnet(false, false, None), Denied);
    }

    // [unit->REQ-MESH-4] the seed-transfer frame round-trips (multi-subnet,
    // 20-byte seeds, distinct epochs) and every malformed shape (truncation,
    // non-UTF-8 subnet, trailing garbage) decodes to None — never a partial adopt.
    #[test]
    fn seedxfer_frame_round_trips_and_rejects_malformed() {
        let items: Vec<SeedPush> = vec![
            ("home".to_string(), vec![0xAA; 20], 5),
            ("work".to_string(), vec![0xBB; 20], 9),
        ];
        let buf = enc_seedxfer(&items);
        assert_eq!(dec_seedxfer(&buf), Some(items.clone()), "round-trip");
        assert_eq!(dec_seedxfer(&enc_seedxfer(&[])), Some(vec![]), "empty round-trip");

        assert_eq!(dec_seedxfer(&[]), None, "empty buffer (no count)");
        assert_eq!(dec_seedxfer(&[0, 0, 0, 1]), None, "count claims 1, no item");
        let mut trailing = enc_seedxfer(&items);
        trailing.push(0xff);
        assert_eq!(dec_seedxfer(&trailing), None, "trailing garbage");
    }

    // [unit->REQ-MESH-4] the seed never rides gossip: the same seed bytes that
    // travel the (member-auth'd) SeedTransfer leg are ABSENT from the roster
    // frame — the roster carries identities + addresses, never key material.
    #[test]
    fn seed_never_appears_in_roster_gossip() {
        let seed = vec![0x7Cu8; 20];
        // The roster frame for a member of the same subnet.
        let entry = RosterEntry {
            pubkey_hex: "aa".into(),
            subnet: "home".into(),
            label: "h".into(),
            machine_id: "m".into(),
            address: Some(serde_json::json!({"id": "aa"})),
            last_seen: "1".into(),
            lease_epoch: 1,
        };
        let roster_bytes = enc_roster(&[entry], &[]);
        assert!(
            !contains_subslice(&roster_bytes, &seed),
            "the subnet seed must never appear in a roster gossip record"
        );
        // ...but it IS present in the confidential transfer leg (proving that is
        // the only channel it travels).
        let xfer = enc_seedxfer(&[("home".to_string(), seed.clone(), 2)]);
        assert!(contains_subslice(&xfer, &seed), "the seed rides the transfer leg");
    }

    fn contains_subslice(hay: &[u8], needle: &[u8]) -> bool {
        hay.windows(needle.len()).any(|w| w == needle)
    }
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\seedproofx.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        // Idempotent: a second pass with the same input changes nothing.
        assert!(!gapfill_peeraddrs(&mut pa, "self", &entries), "no further change");
    }
}

#[cfg(test)]
mod grace_tests {
    use super::*;

    // [unit->REQ-MESH-4] the re-seed grant/deny truth table. With a roster seam:
    // exact-epoch from a present (or even not-yet-known) member ⇒ Full; the
    // prior epoch from a PRESENT member ⇒ ReseedOnly; a tombstoned (revoked)
    // peer ⇒ Denied on ANY generation (even exact, before its seed is rotated);
    // an absent peer is never re-seeded; two-stale (no match) ⇒ Denied.
    #[test]
    fn grade_truth_table() {
        use Grade::*;
        use MemberStatus::*;

        // exact-epoch (matched_current = true)
        assert_eq!(grade_subnet(true, false, Some(Present)), Full);
        assert_eq!(grade_subnet(true, false, Some(Absent)), Full, "unknown member, current seed ⇒ admit");
        assert_eq!(grade_subnet(true, false, Some(Tombstoned)), Denied, "revokee denied even exact");

        // prior-epoch only (matched_prev = true, matched_current = false)
        assert_eq!(grade_subnet(false, true, Some(Present)), ReseedOnly, "benign offliner ⇒ grace");
        assert_eq!(grade_subnet(false, true, Some(Tombstoned)), Denied, "revoked N-1 ⇒ denied");
        assert_eq!(grade_subnet(false, true, Some(Absent)), Denied, "off-roster N-1 ⇒ never re-seeded");

        // ≥2 stale / forged (no match at any held generation)
        assert_eq!(grade_subnet(false, false, Some(Present)), Denied);
        assert_eq!(grade_subnet(false, false, Some(Tombstoned)), Denied);

        // No roster seam (D2 mechanics path): epoch-only grading.
        assert_eq!(grade_subnet(true, false, None), Full);
        assert_eq!(grade_subnet(false, true, None), ReseedOnly);
        assert_eq!(grade_subnet(false, false, None), Denied);
    }

    // [unit->REQ-MESH-4] the seed-transfer frame round-trips (multi-subnet,
    // 20-byte seeds, distinct epochs) and every malformed shape (truncation,
    // non-UTF-8 subnet, trailing garbage) decodes to None — never a partial adopt.
    #[test]
    fn seedxfer_frame_round_trips_and_rejects_malformed() {
        let items: Vec<SeedPush> = vec![
            ("home".to_string(), vec![0xAA; 20], 5),
            ("work".to_string(), vec![0xBB; 20], 9),
        ];
        let buf = enc_seedxfer(&items);
        assert_eq!(dec_seedxfer(&buf), Some(items.clone()), "round-trip");
        assert_eq!(dec_seedxfer(&enc_seedxfer(&[])), Some(vec![]), "empty round-trip");

        assert_eq!(dec_seedxfer(&[]), None, "empty buffer (no count)");
        assert_eq!(dec_seedxfer(&[0, 0, 0, 1]), None, "count claims 1, no item");
        let mut trailing = enc_seedxfer(&items);
        trailing.push(0xff);
        assert_eq!(dec_seedxfer(&trailing), None, "trailing garbage");
    }

    // [unit->REQ-MESH-4] the seed never rides gossip: the same seed bytes that
    // travel the (member-auth'd) SeedTransfer leg are ABSENT from the roster
    // frame — the roster carries identities + addresses, never key material.
    #[test]
    fn seed_never_appears_in_roster_gossip() {
        let seed = vec![0x7Cu8; 20];
        // The roster frame for a member of the same subnet.
        let entry = RosterEntry {
            pubkey_hex: "aa".into(),
            subnet: "home".into(),
            label: "h".into(),
            machine_id: "m".into(),
            address: Some(serde_json::json!({"id": "aa"})),
            last_seen: "1".into(),
            lease_epoch: 1,
        };
        let roster_bytes = enc_roster(&[entry], &[]);
        assert!(
            !contains_subslice(&roster_bytes, &seed),
            "the subnet seed must never appear in a roster gossip record"
        );
        // ...but it IS present in the confidential transfer leg (proving that is
        // the only channel it travels).
        let xfer = enc_seedxfer(&[("home".to_string(), seed.clone(), 2)]);
        assert!(contains_subslice(&xfer, &seed), "the seed rides the transfer leg");
    }

    fn contains_subslice(hay: &[u8], needle: &[u8]) -> bool {
        hay.windows(needle.len()).any(|w| w == needle)
    }
}

+    // [unit->REQ-MESH-4] the re-seed grant/deny truth table. With a roster seam:

+    // exact-epoch from a present (or even not-yet-known) member ⇒ Full; the

+    // prior epoch from a PRESENT member ⇒ ReseedOnly; a tombstoned (revoked)

+    // peer ⇒ Denied on ANY generation (even exact, before its seed is rotated);

+    // an absent peer is never re-seeded; two-stale (no match) ⇒ Denied.

+        // exact-epoch (matched_current = true)

+        assert_eq!(grade_subnet(true, false, Some(Present)), Full);

+        assert_eq!(grade_subnet(true, false, Some(Absent)), Full, "unknown member, current seed ⇒ admit");

+        assert_eq!(grade_subnet(true, false, Some(Tombstoned)), Denied, "revokee denied even exact");

+        // prior-epoch only (matched_prev = true, matched_current = false)

+        assert_eq!(grade_subnet(false, true, Some(Present)), ReseedOnly, "benign offliner ⇒ grace");

+        assert_eq!(grade_subnet(false, true, Some(Tombstoned)), Denied, "revoked N-1 ⇒ denied");

+        assert_eq!(grade_subnet(false, true, Some(Absent)), Denied, "off-roster N-1 ⇒ never re-seeded");

+        // ≥2 stale / forged (no match at any held generation)

+        assert_eq!(grade_subnet(false, false, Some(Present)), Denied);

+        assert_eq!(grade_subnet(false, false, Some(Tombstoned)), Denied);

+        // No roster seam (D2 mechanics path): epoch-only grading.

+        assert_eq!(grade_subnet(true, false, None), Full);

+        assert_eq!(grade_subnet(false, true, None), ReseedOnly);

+        assert_eq!(grade_subnet(false, false, None), Denied);

+    // [unit->REQ-MESH-4] the seed-transfer frame round-trips (multi-subnet,

+    // 20-byte seeds, distinct epochs) and every malformed shape (truncation,

+    // non-UTF-8 subnet, trailing garbage) decodes to None — never a partial adopt.

+    fn seedxfer_frame_round_trips_and_rejects_malformed() {

+        let items: Vec<SeedPush> = vec![

+        let buf = enc_seedxfer(&items);

+        assert_eq!(dec_seedxfer(&buf), Some(items.clone()), "round-trip");

+        assert_eq!(dec_seedxfer(&enc_seedxfer(&[])), Some(vec![]), "empty round-trip");

+        assert_eq!(dec_seedxfer(&[]), None, "empty buffer (no count)");

+        assert_eq!(dec_seedxfer(&[0, 0, 0, 1]), None, "count claims 1, no item");

+        let mut trailing = enc_seedxfer(&items);

+        assert_eq!(dec_seedxfer(&trailing), None, "trailing garbage");

+    // [unit->REQ-MESH-4] the seed never rides gossip: the same seed bytes that

+    // travel the (member-auth'd) SeedTransfer leg are ABSENT from the roster

+    // frame — the roster carries identities + addresses, never key material.

+    fn seed_never_appears_in_roster_gossip() {

+        // The roster frame for a member of the same subnet.

+            address: Some(serde_json::json!({"id": "aa"})),

+        let roster_bytes = enc_roster(&[entry], &[]);

+            !contains_subslice(&roster_bytes, &seed),

+            "the subnet seed must never appear in a roster gossip record"

+        // ...but it IS present in the confidential transfer leg (proving that is

+        // the only channel it travels).

+        let xfer = enc_seedxfer(&[("home".to_string(), seed.clone(), 2)]);

+        assert!(contains_subslice(&xfer, &seed), "the seed rides the transfer leg");

+    fn contains_subslice(hay: &[u8], needle: &[u8]) -> bool {

+        hay.windows(needle.len()).any(|w| w == needle)


Bash

cargo test -p spt-daemon --lib seedproofx 2>&1 | tail -20

Run the seedproofx unit tests


   Compiling iroh-relay v0.98.0
   Compiling iroh v0.98.2
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 26.58s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-26a17645c4a18da2.exe)

running 7 tests
test seedproofx::grace_tests::grade_truth_table ... ok
test seedproofx::roster_tests::empty_roster_frame_round_trips ... ok
test seedproofx::grace_tests::seed_never_appears_in_roster_gossip ... ok
test seedproofx::grace_tests::seedxfer_frame_round_trips_and_rejects_malformed ... ok
test seedproofx::roster_tests::malformed_roster_frame_rejected ... ok
test seedproofx::roster_tests::roster_frame_round_trips ... ok
test seedproofx::roster_tests::gapfill_fills_absent_only_and_skips_self ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 210 filtered out; finished in 0.00s


Now run the existing D2/D4 integration tests — critical regression check for the reworked handshake:


cargo test -p spt-daemon --test seedproofx --test rosterprop 2>&1 | tail -30

Run D2 + D4 integration regression tests


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
error[E0063]: missing field `prev` in initializer of `SubnetCred`
  --> crates\spt-daemon\tests\seedproofx.rs:55:5
   |
55 |     SubnetCred {
   |     ^^^^^^^^^^ missing `prev`

For more information about this error, try `rustc --explain E0063`.
error: could not compile `spt-daemon` (test "seedproofx") due to 1 previous error
warning: build failed, waiting for other jobs to finish...


Edit

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
    }
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\seedproofx.rs has been updated successfully. (file state is current in your context — no need to Read it back)

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

//! Mesh-D2 — connect-time seed-proof, the integration evidence (REQ-MESH-1 int).
//!
//! Two in-process `NetHost`s dial over hermetic loopback QUIC with the
//! membership source wired. A connection establishes (and caches the proven
//! subnet set on both `ConnEntry`s) **only when both sides prove current-epoch
//! seed-knowledge of a shared subnet**; a non-prover (wrong seed, or no shared
//! subnet) is dropped before the connection ever joins the table. This is the
//! same two-host hermetic shape as `nethost`'s `two_hosts_dial_over_loopback`,
//! plus the seed-proof.

use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{MembershipSource, SubnetCred};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

/// A 20-byte subnet seed from a single filler byte (test material).
fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

/// A membership source returning a fixed credential list (clones per connect,
/// mirroring the live re-read of the production loader).
fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

/// Poll until `f()` holds (the acceptor registers its inbound conn async), or
/// give up after ~2s and return the last value.
fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
    }
}

fn set(names: &[&str]) -> HashSet<String> {
    names.iter().map(|s| s.to_string()).collect()
}

// [int->REQ-MESH-1] two members sharing a subnet (same seed + epoch) connect:
// the conn registers on BOTH sides and each caches the shared subnet as proven —
// the mesh property (a connection establishes only when both prove).
#[test]
fn both_members_prove_and_cache_the_shared_subnet() {
    let a = host(Identity::generate(), vec![cred("home", 7, 1)]);
    let b_id = Identity::generate();
    let b = host(b_id, vec![cred("home", 7, 1)]);

    let (conn_id, _remote) = a.dial(b.addr()).expect("dial member");
    assert_eq!(
        a.conn_proven_subnets(conn_id),
        set(&["home"]),
        "dialer caches the proven subnet"
    );
    assert!(
        wait_until(|| b.conn_count() == 1),
        "acceptor registered the proven inbound conn"
    );
    assert_eq!(
        b.conn_proven_subnets(1),
        set(&["home"]),
        "acceptor caches the proven subnet"
    );
}

// [int->REQ-MESH-1] the proven verdict is a SET, not a bool: two nodes sharing
// {home, work} but not the dialer-only `solo` prove exactly the intersection on
// both sides.
#[test]
fn proven_verdict_is_the_shared_set() {
    let a = host(
        Identity::generate(),
        vec![cred("home", 1, 1), cred("work", 2, 1), cred("solo", 9, 1)],
    );
    let b = host(
        Identity::generate(),
        vec![cred("home", 1, 1), cred("work", 2, 1)],
    );

    let (conn_id, _) = a.dial(b.addr()).expect("dial");
    assert_eq!(
        a.conn_proven_subnets(conn_id),
        set(&["home", "work"]),
        "dialer proves the intersection, not solo"
    );
    assert!(wait_until(|| b.conn_count() == 1), "acceptor registered");
    assert_eq!(b.conn_proven_subnets(1), set(&["home", "work"]));
}

// [int->REQ-MESH-1] a non-prover is dropped: same subnet NAME but a different
// seed (an outsider who can't derive the membership key) fails the mutual proof,
// so the dial errors and the acceptor never registers the connection.
#[test]
fn wrong_seed_is_dropped() {
    let member = host(Identity::generate(), vec![cred("home", 7, 1)]);
    let impostor = host(Identity::generate(), vec![cred("home", 8, 1)]); // wrong seed

    let res = impostor.dial(member.addr());
    assert!(res.is_err(), "dial fails when the peer rejects our proof");
    // The acceptor must not have a usable conn (it dropped the failed proof).
    assert!(
        !wait_until(|| member.conn_count() == 1),
        "acceptor dropped the non-prover; no conn in the table"
    );
}

// [int->REQ-MESH-1] no shared subnet ⇒ empty intersection ⇒ dropped. Two real
// members of DIFFERENT subnets have nothing to mesh, so the connection is not
// established.
#[test]
fn no_shared_subnet_is_dropped() {
    let a = host(Identity::generate(), vec![cred("alpha", 1, 1)]);
    let b = host(Identity::generate(), vec![cred("beta", 2, 1)]);

    let res = a.dial(b.addr());
    assert!(res.is_err(), "dial fails with no shared subnet");
    assert!(
        !wait_until(|| b.conn_count() == 1),
        "acceptor dropped the no-overlap conn"
    );
}

name: name.to_string(),
seed: seed(s),
epoch,
+        prev: None,
}
}


Bash

cargo test -p spt-daemon --test seedproofx --test rosterprop 2>&1 | tail -30

Re-run D2 + D4 integration regression tests


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.97s
     Running tests\rosterprop.rs (target\debug\deps\rosterprop-79961f6ff50ad702.exe)

running 2 tests
test roster_exchange_never_leaks_unproven_subnets ... ok
test roster_propagates_transitively_on_connect ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.09s

     Running tests\seedproofx.rs (target\debug\deps\seedproofx-f675754b303e7255.exe)

running 4 tests
test both_members_prove_and_cache_the_shared_subnet ... ok
test proven_verdict_is_the_shared_set ... ok
test no_shared_subnet_is_dropped ... ok
test wrong_seed_is_dropped ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.18s


Layer 2 regression green — the reworked handshake is backward-compatible. Now the **grace integration test** (the acceptance): a stale node gets re-seeded, a revokee stays denied.


Write

//! Mesh-D7 — the re-seed grace + revoke lockout, integration evidence
//! (REQ-MESH-4 int).
//!
//! Two in-process `NetHost`s over hermetic loopback QUIC, with a roster seam
//! wired so the grace can run. The acceptance the milestone names: after a seed
//! rotation, a **benign offliner** that returns on the prior epoch (`N-1`) is
//! re-seeded over the member control stream and converges to the current
//! generation — while a **revoked** node, even one that kept its prior-epoch
//! seed, is denied and never re-admitted (it must re-pair). Same two-host shape
//! as `seedproofx.rs`/`rosterprop.rs`, plus the rotation skew + the grace seam.

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{
    MemberStatus, MembershipSource, RosterExchange, SubnetCred,
};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

/// A 20-byte subnet seed from a single filler byte (test material).
fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

/// What a node observed adopting over the grace leg: `(subnet, seed, epoch)`.
type Adopted = Arc<Mutex<Vec<(String, Vec<u8>, u64)>>>;

/// A minimal roster seam for the grace: an empty roster exchange (this test does
/// not assert directory growth), a `member_status` that reports every peer as a
/// present member EXCEPT the explicitly-`revoked` set (which it tombstones), and
/// an `adopt_seed` that records the push for assertion (without a real store).
fn grace_exchange(revoked: HashSet<String>, adopted: Adopted) -> RosterExchange {
    RosterExchange {
        provider: Arc::new(|_proven, _self_addr| (Vec::new(), Vec::new())),
        sink: Arc::new(|_entries, _tombs| {}),
        member_status: Arc::new(move |_subnet: &str, peer_hex: &str| {
            if revoked.contains(peer_hex) {
                MemberStatus::Tombstoned
            } else {
                MemberStatus::Present
            }
        }),
        adopt_seed: Arc::new(move |subnet: &str, s: &[u8], epoch: u64| {
            adopted.lock().unwrap().push((subnet.to_string(), s.to_vec(), epoch));
            true
        }),
    }
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

/// A current-generation credential carrying the immediately-prior one.
fn cred_with_prev(name: &str, cur: u8, epoch: u64, prev: u8) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(cur),
        epoch,
        prev: Some((seed(prev), epoch - 1)),
    }
}

/// A bare current-generation credential (no prior — a node that has not rotated).
fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

// [int->REQ-MESH-4] the re-seed grace: a benign offliner (on epoch 1, seed v1)
// dials a node that rotated while it was away (epoch 2, seed v2, holding v1 as
// its prior). The fresh node grades it `ReseedOnly`, pushes the current seed over
// the member control stream, and the offliner adopts it — auto-healing across
// the rotation. The fresh node does NOT register a full conn for it (a pure
// re-seed conn re-forms full once the offliner reconnects on the new seed).
#[test]
fn benign_offliner_is_reseeded_across_a_rotation() {
    // Fresh node A: rotated to epoch 2 (seed v2), prior generation = v1 @ 1.
    let a = host(Identity::generate(), vec![cred_with_prev("home", 2, 2, 1)]);
    // Stale node B: still on epoch 1 (seed v1) — it was offline for the rotation.
    let b_id = Identity::generate();
    let b = host(b_id, vec![cred("home", 1, 1)]);

    let a_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    let b_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    a.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&a_adopt)));
    b.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&b_adopt)));

    // B (stale) dials A (fresh).
    let (_conn_id, _remote) = b.dial(a.addr()).expect("dial reaches the fresh node");

    // B adopted A's CURRENT seed (v2 @ epoch 2) over the grace leg.
    assert!(
        wait_until(|| !b_adopt.lock().unwrap().is_empty()),
        "the offliner received a seed push"
    );
    let pushed = b_adopt.lock().unwrap().clone();
    assert_eq!(pushed, vec![("home".to_string(), seed(2), 2)], "current seed adopted");

    // A re-seeds, it does not pull one (it is the fresh side): nothing adopted.
    assert!(a_adopt.lock().unwrap().is_empty(), "the fresh node adopts nothing");

    // A graded the offliner re-seed-only, so it registered no full conn for it
    // (the conn re-forms full when B reconnects on the new seed).
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "no full conn for a re-seed-only peer"
    );
}

// [int->REQ-MESH-4] the revoke lockout: a revoked node that KEPT its prior-epoch
// seed (epoch 1, seed v1) dials the rotator (epoch 2). The rotator's roster seam
// reports it tombstoned, so the grace is denied — no conn is registered and NO
// seed is pushed. The revokee cannot heal; it must re-pair under a fresh key.
#[test]
fn revoked_node_is_denied_and_never_reseeded() {
    let a = host(Identity::generate(), vec![cred_with_prev("home", 2, 2, 1)]);
    let b_id = Identity::generate();
    let b_hex = b_id.public_key().to_hex();
    let b = host(b_id, vec![cred("home", 1, 1)]); // kept the old seed v1

    let b_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    // A revokes B (tombstoned on A's side); B does not revoke A.
    let mut revoked = HashSet::new();
    revoked.insert(b_hex);
    a.set_roster_exchange(grace_exchange(revoked, Arc::new(Mutex::new(Vec::new()))));
    b.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&b_adopt)));

    // B (revoked) dials A. A denies the grace — it registers no conn for B…
    let _ = b.dial(a.addr());
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "the rotator never admits the revokee"
    );
    // …and crucially never hands it the new seed (no re-seed for a revokee).
    assert!(
        b_adopt.lock().unwrap().is_empty(),
        "a revoked node is never re-seeded — it must re-pair"
    );
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\reseed.rs (file state is current in your context — no need to Read it back)

//! Mesh-D7 — the re-seed grace + revoke lockout, integration evidence
//! (REQ-MESH-4 int).
//!
//! Two in-process `NetHost`s over hermetic loopback QUIC, with a roster seam
//! wired so the grace can run. The acceptance the milestone names: after a seed
//! rotation, a **benign offliner** that returns on the prior epoch (`N-1`) is
//! re-seeded over the member control stream and converges to the current
//! generation — while a **revoked** node, even one that kept its prior-epoch
//! seed, is denied and never re-admitted (it must re-pair). Same two-host shape
//! as `seedproofx.rs`/`rosterprop.rs`, plus the rotation skew + the grace seam.

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{
    MemberStatus, MembershipSource, RosterExchange, SubnetCred,
};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

/// A 20-byte subnet seed from a single filler byte (test material).
fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

/// What a node observed adopting over the grace leg: `(subnet, seed, epoch)`.
type Adopted = Arc<Mutex<Vec<(String, Vec<u8>, u64)>>>;

/// A minimal roster seam for the grace: an empty roster exchange (this test does
/// not assert directory growth), a `member_status` that reports every peer as a
/// present member EXCEPT the explicitly-`revoked` set (which it tombstones), and
/// an `adopt_seed` that records the push for assertion (without a real store).
fn grace_exchange(revoked: HashSet<String>, adopted: Adopted) -> RosterExchange {
    RosterExchange {
        provider: Arc::new(|_proven, _self_addr| (Vec::new(), Vec::new())),
        sink: Arc::new(|_entries, _tombs| {}),
        member_status: Arc::new(move |_subnet: &str, peer_hex: &str| {
            if revoked.contains(peer_hex) {
                MemberStatus::Tombstoned
            } else {
                MemberStatus::Present
            }
        }),
        adopt_seed: Arc::new(move |subnet: &str, s: &[u8], epoch: u64| {
            adopted.lock().unwrap().push((subnet.to_string(), s.to_vec(), epoch));
            true
        }),
    }
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

/// A current-generation credential carrying the immediately-prior one.
fn cred_with_prev(name: &str, cur: u8, epoch: u64, prev: u8) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(cur),
        epoch,
        prev: Some((seed(prev), epoch - 1)),
    }
}

/// A bare current-generation credential (no prior — a node that has not rotated).
fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

// [int->REQ-MESH-4] the re-seed grace: a benign offliner (on epoch 1, seed v1)
// dials a node that rotated while it was away (epoch 2, seed v2, holding v1 as
// its prior). The fresh node grades it `ReseedOnly`, pushes the current seed over
// the member control stream, and the offliner adopts it — auto-healing across
// the rotation. The fresh node does NOT register a full conn for it (a pure
// re-seed conn re-forms full once the offliner reconnects on the new seed).
#[test]
fn benign_offliner_is_reseeded_across_a_rotation() {
    // Fresh node A: rotated to epoch 2 (seed v2), prior generation = v1 @ 1.
    let a = host(Identity::generate(), vec![cred_with_prev("home", 2, 2, 1)]);
    // Stale node B: still on epoch 1 (seed v1) — it was offline for the rotation.
    let b_id = Identity::generate();
    let b = host(b_id, vec![cred("home", 1, 1)]);

    let a_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    let b_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    a.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&a_adopt)));
    b.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&b_adopt)));

    // B (stale) dials A (fresh).
    let (_conn_id, _remote) = b.dial(a.addr()).expect("dial reaches the fresh node");

    // B adopted A's CURRENT seed (v2 @ epoch 2) over the grace leg.
    assert!(
        wait_until(|| !b_adopt.lock().unwrap().is_empty()),
        "the offliner received a seed push"
    );
    let pushed = b_adopt.lock().unwrap().clone();
    assert_eq!(pushed, vec![("home".to_string(), seed(2), 2)], "current seed adopted");

    // A re-seeds, it does not pull one (it is the fresh side): nothing adopted.
    assert!(a_adopt.lock().unwrap().is_empty(), "the fresh node adopts nothing");

    // A graded the offliner re-seed-only, so it registered no full conn for it
    // (the conn re-forms full when B reconnects on the new seed).
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "no full conn for a re-seed-only peer"
    );
}

// [int->REQ-MESH-4] the revoke lockout: a revoked node that KEPT its prior-epoch
// seed (epoch 1, seed v1) dials the rotator (epoch 2). The rotator's roster seam
// reports it tombstoned, so the grace is denied — no conn is registered and NO
// seed is pushed. The revokee cannot heal; it must re-pair under a fresh key.
#[test]
fn revoked_node_is_denied_and_never_reseeded() {
    let a = host(Identity::generate(), vec![cred_with_prev("home", 2, 2, 1)]);
    let b_id = Identity::generate();
    let b_hex = b_id.public_key().to_hex();
    let b = host(b_id, vec![cred("home", 1, 1)]); // kept the old seed v1

    let b_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    // A revokes B (tombstoned on A's side); B does not revoke A.
    let mut revoked = HashSet::new();
    revoked.insert(b_hex);
    a.set_roster_exchange(grace_exchange(revoked, Arc::new(Mutex::new(Vec::new()))));
    b.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&b_adopt)));

    // B (revoked) dials A. A denies the grace — it registers no conn for B…
    let _ = b.dial(a.addr());
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "the rotator never admits the revokee"
    );
    // …and crucially never hands it the new seed (no re-seed for a revokee).
    assert!(
        b_adopt.lock().unwrap().is_empty(),
        "a revoked node is never re-seeded — it must re-pair"
    );
}


Bash

cargo test -p spt-daemon --test reseed 2>&1 | tail -25

Run the re-seed grace integration test


   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.86s
     Running tests\reseed.rs (target\debug\deps\reseed-2deb452811e57e44.exe)

running 2 tests
test benign_offliner_is_reseeded_across_a_rotation ... FAILED
test revoked_node_is_denied_and_never_reseeded ... ok

failures:

---- benign_offliner_is_reseeded_across_a_rotation stdout ----
PAIR_MEET_UP:SPT_DEV step=59365499 id=19f53810fe62ecf0bf2bff7b5faf3bb5118cd3f8c97ee8c6d53764559b56c19d addr=EndpointAddr { id: PublicKey(19f53810fe62ecf0bf2bff7b5faf3bb5118cd3f8c97ee8c6d53764559b56c19d), addrs: {Ip(127.0.0.1:60527), Ip([::1]:60528)} }
PAIR_MEET_UP:SPT_DEV step=59365499 id=19f53810fe62ecf0bf2bff7b5faf3bb5118cd3f8c97ee8c6d53764559b56c19d addr=EndpointAddr { id: PublicKey(19f53810fe62ecf0bf2bff7b5faf3bb5118cd3f8c97ee8c6d53764559b56c19d), addrs: {Ip(127.0.0.1:60536), Ip([::1]:60537)} }

thread 'benign_offliner_is_reseeded_across_a_rotation' (45052) panicked at crates\spt-daemon\tests\reseed.rs:121:5:
the offliner received a seed push
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

failures:
    benign_offliner_is_reseeded_across_a_rotation

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.20s

error: test failed, to rerun pass `-p spt-daemon --test reseed`


B didn't receive the push — a delivery race: the acceptor returns `None` and drops the connection before the stale dialer reads the seed frame. Fix: a reseed-only conn must be **registered** (kept alive for delivery), not dropped. Only pure denial drops.


Edit

    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None // nothing fully proven — drop (a pure re-seed conn re-forms full)
    } else {
        Some(graded.full)
    }
}

    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    finalize(graded)
}

/// The connection's verdict from the graded outcome. A purely-denied exchange
/// (nothing full, nothing re-seeded) drops the conn (`None`). A re-seed-only
/// exchange registers with an EMPTY proven set: the conn is kept alive only so
/// the seed push it just carried is actually delivered (dropping it here races
/// the QUIC CLOSE against the peer's read) — it serves no subnet and is replaced
/// when the healed peer reconnects full. Otherwise the proven (full) set joins.
// [impl->REQ-MESH-4]
fn finalize(graded: Graded) -> Option<HashSet<String>> {
    if graded.full.is_empty() && graded.reseed.is_empty() {
        None
    } else {
        Some(graded.full)
    }
}


    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None // nothing fully proven — drop (a pure re-seed conn re-forms full)
    } else {
        Some(graded.full)
    }
}

    exchange_seed_dialer(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    finalize(graded)
}

/// The connection's verdict from the graded outcome. A purely-denied exchange
/// (nothing full, nothing re-seeded) drops the conn (`None`). A re-seed-only
/// exchange registers with an EMPTY proven set: the conn is kept alive only so
/// the seed push it just carried is actually delivered (dropping it here races
/// the QUIC CLOSE against the peer's read) — it serves no subnet and is replaced
/// when the healed peer reconnects full. Otherwise the proven (full) set joins.
// [impl->REQ-MESH-4]
fn finalize(graded: Graded) -> Option<HashSet<String>> {
    if graded.full.is_empty() && graded.reseed.is_empty() {
        None
    } else {
        Some(graded.full)
    }
}

// where the peer proved only the prior generation; adopt any pushed to us.

-        None // nothing fully proven — drop (a pure re-seed conn re-forms full)

+/// The connection's verdict from the graded outcome. A purely-denied exchange

+/// (nothing full, nothing re-seeded) drops the conn (`None`). A re-seed-only

+/// exchange registers with an EMPTY proven set: the conn is kept alive only so

+/// the seed push it just carried is actually delivered (dropping it here races

+/// the QUIC CLOSE against the peer's read) — it serves no subnet and is replaced

+/// when the healed peer reconnects full. Otherwise the proven (full) set joins.

+fn finalize(graded: Graded) -> Option<HashSet<String>> {

+    if graded.full.is_empty() && graded.reseed.is_empty() {


    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None
    } else {
        Some(graded.full)
    }
}

    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    finalize(graded)
}


    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    if graded.full.is_empty() {
        None
    } else {
        Some(graded.full)
    }
}

    exchange_seed_acceptor(send, recv, roster, &graded.reseed, by_name).await;
    let _ = send.finish();
    finalize(graded)
}

exchange_roster_acceptor(send, recv, roster, self_addr, &graded.full).await;

/// Write a length-delimited frame (`u32` BE length prefix + body).


Now update the benign test assertion (A registers a reseed-only conn with empty proven, not zero conns):


    // B (stale) dials A (fresh).
    let (_conn_id, _remote) = b.dial(a.addr()).expect("dial reaches the fresh node");

    // B adopted A's CURRENT seed (v2 @ epoch 2) over the grace leg.
    assert!(
        wait_until(|| !b_adopt.lock().unwrap().is_empty()),
        "the offliner received a seed push"
    );
    let pushed = b_adopt.lock().unwrap().clone();
    assert_eq!(pushed, vec![("home".to_string(), seed(2), 2)], "current seed adopted");

    // A re-seeds, it does not pull one (it is the fresh side): nothing adopted.
    assert!(a_adopt.lock().unwrap().is_empty(), "the fresh node adopts nothing");

    // A graded the offliner re-seed-only, so it registered no full conn for it
    // (the conn re-forms full when B reconnects on the new seed).
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "no full conn for a re-seed-only peer"
    );
}

    // B (stale) dials A (fresh).
    let (_conn_id, _remote) = b.dial(a.addr()).expect("dial reaches the fresh node");

    // B adopted A's CURRENT seed (v2 @ epoch 2) over the grace leg.
    assert!(
        wait_until(|| !b_adopt.lock().unwrap().is_empty()),
        "the offliner received a seed push"
    );
    let pushed = b_adopt.lock().unwrap().clone();
    assert_eq!(pushed, vec![("home".to_string(), seed(2), 2)], "current seed adopted");

    // A re-seeds, it does not pull one (it is the fresh side): nothing adopted.
    assert!(a_adopt.lock().unwrap().is_empty(), "the fresh node adopts nothing");

    // A registered the conn only to deliver the seed — it is re-seed-only, so it
    // proves NO subnet (it serves nothing; it is replaced when B reconnects full).
    assert!(wait_until(|| a.conn_count() == 1), "conn kept alive for delivery");
    assert!(
        a.conn_proven_subnets(1).is_empty(),
        "a re-seed-only conn carries no proven subnet"
    );
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\reseed.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    // B (stale) dials A (fresh).
    let (_conn_id, _remote) = b.dial(a.addr()).expect("dial reaches the fresh node");

    // B adopted A's CURRENT seed (v2 @ epoch 2) over the grace leg.
    assert!(
        wait_until(|| !b_adopt.lock().unwrap().is_empty()),
        "the offliner received a seed push"
    );
    let pushed = b_adopt.lock().unwrap().clone();
    assert_eq!(pushed, vec![("home".to_string(), seed(2), 2)], "current seed adopted");

    // A re-seeds, it does not pull one (it is the fresh side): nothing adopted.
    assert!(a_adopt.lock().unwrap().is_empty(), "the fresh node adopts nothing");

    // A registered the conn only to deliver the seed — it is re-seed-only, so it
    // proves NO subnet (it serves nothing; it is replaced when B reconnects full).
    assert!(wait_until(|| a.conn_count() == 1), "conn kept alive for delivery");
    assert!(
        a.conn_proven_subnets(1).is_empty(),
        "a re-seed-only conn carries no proven subnet"
    );
}

//! Mesh-D7 — the re-seed grace + revoke lockout, integration evidence
//! (REQ-MESH-4 int).
//!
//! Two in-process `NetHost`s over hermetic loopback QUIC, with a roster seam
//! wired so the grace can run. The acceptance the milestone names: after a seed
//! rotation, a **benign offliner** that returns on the prior epoch (`N-1`) is
//! re-seeded over the member control stream and converges to the current
//! generation — while a **revoked** node, even one that kept its prior-epoch
//! seed, is denied and never re-admitted (it must re-pair). Same two-host shape
//! as `seedproofx.rs`/`rosterprop.rs`, plus the rotation skew + the grace seam.

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::seedproofx::{
    MemberStatus, MembershipSource, RosterExchange, SubnetCred,
};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

/// A 20-byte subnet seed from a single filler byte (test material).
fn seed(b: u8) -> Vec<u8> {
    vec![b; 20]
}

fn membership(creds: Vec<SubnetCred>) -> MembershipSource {
    Arc::new(move || creds.clone())
}

fn host(identity: Identity, creds: Vec<SubnetCred>) -> NetHost {
    NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: Some(membership(creds)),
    })
    .expect("net host start")
}

/// What a node observed adopting over the grace leg: `(subnet, seed, epoch)`.
type Adopted = Arc<Mutex<Vec<(String, Vec<u8>, u64)>>>;

/// A minimal roster seam for the grace: an empty roster exchange (this test does
/// not assert directory growth), a `member_status` that reports every peer as a
/// present member EXCEPT the explicitly-`revoked` set (which it tombstones), and
/// an `adopt_seed` that records the push for assertion (without a real store).
fn grace_exchange(revoked: HashSet<String>, adopted: Adopted) -> RosterExchange {
    RosterExchange {
        provider: Arc::new(|_proven, _self_addr| (Vec::new(), Vec::new())),
        sink: Arc::new(|_entries, _tombs| {}),
        member_status: Arc::new(move |_subnet: &str, peer_hex: &str| {
            if revoked.contains(peer_hex) {
                MemberStatus::Tombstoned
            } else {
                MemberStatus::Present
            }
        }),
        adopt_seed: Arc::new(move |subnet: &str, s: &[u8], epoch: u64| {
            adopted.lock().unwrap().push((subnet.to_string(), s.to_vec(), epoch));
            true
        }),
    }
}

fn wait_until(mut f: impl FnMut() -> bool) -> bool {
    for _ in 0..400 {
        if f() {
            return true;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    false
}

/// A current-generation credential carrying the immediately-prior one.
fn cred_with_prev(name: &str, cur: u8, epoch: u64, prev: u8) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(cur),
        epoch,
        prev: Some((seed(prev), epoch - 1)),
    }
}

/// A bare current-generation credential (no prior — a node that has not rotated).
fn cred(name: &str, s: u8, epoch: u64) -> SubnetCred {
    SubnetCred {
        name: name.to_string(),
        seed: seed(s),
        epoch,
        prev: None,
    }
}

// [int->REQ-MESH-4] the re-seed grace: a benign offliner (on epoch 1, seed v1)
// dials a node that rotated while it was away (epoch 2, seed v2, holding v1 as
// its prior). The fresh node grades it `ReseedOnly`, pushes the current seed over
// the member control stream, and the offliner adopts it — auto-healing across
// the rotation. The fresh node does NOT register a full conn for it (a pure
// re-seed conn re-forms full once the offliner reconnects on the new seed).
#[test]
fn benign_offliner_is_reseeded_across_a_rotation() {
    // Fresh node A: rotated to epoch 2 (seed v2), prior generation = v1 @ 1.
    let a = host(Identity::generate(), vec![cred_with_prev("home", 2, 2, 1)]);
    // Stale node B: still on epoch 1 (seed v1) — it was offline for the rotation.
    let b_id = Identity::generate();
    let b = host(b_id, vec![cred("home", 1, 1)]);

    let a_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    let b_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    a.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&a_adopt)));
    b.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&b_adopt)));

    // B (stale) dials A (fresh).
    let (_conn_id, _remote) = b.dial(a.addr()).expect("dial reaches the fresh node");

    // B adopted A's CURRENT seed (v2 @ epoch 2) over the grace leg.
    assert!(
        wait_until(|| !b_adopt.lock().unwrap().is_empty()),
        "the offliner received a seed push"
    );
    let pushed = b_adopt.lock().unwrap().clone();
    assert_eq!(pushed, vec![("home".to_string(), seed(2), 2)], "current seed adopted");

    // A re-seeds, it does not pull one (it is the fresh side): nothing adopted.
    assert!(a_adopt.lock().unwrap().is_empty(), "the fresh node adopts nothing");

    // A graded the offliner re-seed-only, so it registered no full conn for it
    // (the conn re-forms full when B reconnects on the new seed).
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "no full conn for a re-seed-only peer"
    );
}

// [int->REQ-MESH-4] the revoke lockout: a revoked node that KEPT its prior-epoch
// seed (epoch 1, seed v1) dials the rotator (epoch 2). The rotator's roster seam
// reports it tombstoned, so the grace is denied — no conn is registered and NO
// seed is pushed. The revokee cannot heal; it must re-pair under a fresh key.
#[test]
fn revoked_node_is_denied_and_never_reseeded() {
    let a = host(Identity::generate(), vec![cred_with_prev("home", 2, 2, 1)]);
    let b_id = Identity::generate();
    let b_hex = b_id.public_key().to_hex();
    let b = host(b_id, vec![cred("home", 1, 1)]); // kept the old seed v1

    let b_adopt: Adopted = Arc::new(Mutex::new(Vec::new()));
    // A revokes B (tombstoned on A's side); B does not revoke A.
    let mut revoked = HashSet::new();
    revoked.insert(b_hex);
    a.set_roster_exchange(grace_exchange(revoked, Arc::new(Mutex::new(Vec::new()))));
    b.set_roster_exchange(grace_exchange(HashSet::new(), Arc::clone(&b_adopt)));

    // B (revoked) dials A. A denies the grace — it registers no conn for B…
    let _ = b.dial(a.addr());
    assert!(
        !wait_until(|| a.conn_count() == 1),
        "the rotator never admits the revokee"
    );
    // …and crucially never hands it the new seed (no re-seed for a revokee).
    assert!(
        b_adopt.lock().unwrap().is_empty(),
        "a revoked node is never re-seeded — it must re-pair"
    );
}

-    // A graded the offliner re-seed-only, so it registered no full conn for it

-    // (the conn re-forms full when B reconnects on the new seed).

+    // A registered the conn only to deliver the seed — it is re-seed-only, so it

+    // proves NO subnet (it serves nothing; it is replaced when B reconnects full).

+    assert!(wait_until(|| a.conn_count() == 1), "conn kept alive for delivery");

-        !wait_until(|| a.conn_count() == 1),

-        "no full conn for a re-seed-only peer"

+        a.conn_proven_subnets(1).is_empty(),

+        "a re-seed-only conn carries no proven subnet"


Bash

cargo test -p spt-daemon --test reseed --test seedproofx --test rosterprop 2>&1 | tail -25

Run grace + D2/D4 regression integration tests


running 2 tests
test benign_offliner_is_reseeded_across_a_rotation ... ok
test revoked_node_is_denied_and_never_reseeded ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.22s

     Running tests\rosterprop.rs (target\debug\deps\rosterprop-79961f6ff50ad702.exe)

running 2 tests
test roster_exchange_never_leaks_unproven_subnets ... ok
test roster_propagates_transitively_on_connect ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s

     Running tests\seedproofx.rs (target\debug\deps\seedproofx-f675754b303e7255.exe)

running 4 tests
test proven_verdict_is_the_shared_set ... ok
test both_members_prove_and_cache_the_shared_subnet ... ok
test no_shared_subnet_is_dropped ... ok
test wrong_seed_is_dropped ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.18s


Layer 2 complete — grace works, regression holds. **Layer 3 — the CLI `revoke` command.** Add the enum variant:


Edit

    /// Remove a dead node identity's trust rows (and registry rows).
    ///
    /// The cleanup verb for a machine that re-paired under a new identity
    /// or is gone for good: its stale trust rows cost a dial every pump
    /// tick. Takes a full pubkey hex, an unambiguous prefix, or a node
    /// label. Gated behind OS elevation (trust mutation).
    Prune {
        /// The dead identity: pubkey hex, unambiguous prefix, or label.
        node: String,
    },

    /// Remove a dead node identity's trust rows (and registry rows).
    ///
    /// The cleanup verb for a machine that re-paired under a new identity
    /// or is gone for good: its stale trust rows cost a dial every pump
    /// tick. Takes a full pubkey hex, an unambiguous prefix, or a node
    /// label. Gated behind OS elevation (trust mutation).
    Prune {
        /// The dead identity: pubkey hex, unambiguous prefix, or label.
        node: String,
    },
    /// Revoke node(s) fleet-wide and rotate the subnet seed.
    ///
    /// The real revocation (vs `prune`'s local cleanup): writes a
    /// PROPAGATING roster tombstone now — so every member drops the node
    /// within a roster round — then schedules one seed rotation at the
    /// close of a coalescing window (default 1h); further revokes in the
    /// window join the same rotation (one epoch bump). Benign offliners
    /// auto-heal across the rotation (re-seed grace); the revoked node is
    /// locked out and must re-pair. Each target is a pubkey hex, an
    /// unambiguous prefix, or a label. Gated behind OS elevation.
    Revoke {
        /// The identities to revoke: pubkey hex, unambiguous prefix, or label.
        #[arg(required = true)]
        nodes: Vec<String>,
        /// Rotate the seed immediately instead of at the window's close — the
        /// compromised-node path (a benign offliner may then fall behind and
        /// must re-pair rather than re-seed).
        #[arg(long)]
        force_rotate_seed: bool,
    },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// Remove a dead node identity's trust rows (and registry rows).
    ///
    /// The cleanup verb for a machine that re-paired under a new identity
    /// or is gone for good: its stale trust rows cost a dial every pump
    /// tick. Takes a full pubkey hex, an unambiguous prefix, or a node
    /// label. Gated behind OS elevation (trust mutation).
    Prune {
        /// The dead identity: pubkey hex, unambiguous prefix, or label.
        node: String,
    },
    /// Revoke node(s) fleet-wide and rotate the subnet seed.
    ///
    /// The real revocation (vs `prune`'s local cleanup): writes a
    /// PROPAGATING roster tombstone now — so every member drops the node
    /// within a roster round — then schedules one seed rotation at the
    /// close of a coalescing window (default 1h); further revokes in the
    /// window join the same rotation (one epoch bump). Benign offliners
    /// auto-heal across the rotation (re-seed grace); the revoked node is
    /// locked out and must re-pair. Each target is a pubkey hex, an
    /// unambiguous prefix, or a label. Gated behind OS elevation.
    Revoke {
        /// The identities to revoke: pubkey hex, unambiguous prefix, or label.
        #[arg(required = true)]
        nodes: Vec<String>,
        /// Rotate the seed immediately instead of at the window's close — the
        /// compromised-node path (a benign offliner may then fall behind and
        /// must re-pair rather than re-seed).
        #[arg(long)]
        force_rotate_seed: bool,
    },

+    /// Revoke node(s) fleet-wide and rotate the subnet seed.

+    /// The real revocation (vs `prune`'s local cleanup): writes a

+    /// PROPAGATING roster tombstone now — so every member drops the node

+    /// within a roster round — then schedules one seed rotation at the

+    /// close of a coalescing window (default 1h); further revokes in the

+    /// window join the same rotation (one epoch bump). Benign offliners

+    /// auto-heal across the rotation (re-seed grace); the revoked node is

+    /// locked out and must re-pair. Each target is a pubkey hex, an

+    /// unambiguous prefix, or a label. Gated behind OS elevation.

+        /// The identities to revoke: pubkey hex, unambiguous prefix, or label.

+        /// Rotate the seed immediately instead of at the window's close — the

+        /// compromised-node path (a benign offliner may then fall behind and

+        /// must re-pair rather than re-seed).

/// Stop serving a held subnet (the daemon keeps running).

/// The membership (seed) stays on disk, but this node neither


Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),

                Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),
                Some(SubnetCmd::Revoke {
                    nodes,
                    force_rotate_seed,
                }) => cmd_subnet_revoke(&nodes, force_rotate_seed),


                Some(SubnetCmd::Prune { node }) => cmd_subnet_prune(&node),
                Some(SubnetCmd::Revoke {
                    nodes,
                    force_rotate_seed,
                }) => cmd_subnet_revoke(&nodes, force_rotate_seed),

Some(SubnetCmd::Attach { name, save }) => cmd_subnet_attachment(&name, false, save),

Some(SubnetCmd::Leave { name }) => cmd_subnet_leave(&name),

+                Some(SubnetCmd::Revoke {

+                }) => cmd_subnet_revoke(&nodes, force_rotate_seed),

// notify's M8 home (decision 4); the hot path above stays

// flat — moved verbs have no top-level shims (decision 9).


Now write the `cmd_subnet_revoke` handler after `cmd_subnet_prune`:


/// `spt subnet detach|attach <NAME> [--save]` — the per-subnet serve-state

/// The coalescing-window length (Mesh-D7, REQ-MESH-4): a revoke schedules its
/// seed rotation this far out, so a burst of revokes inside the window collapses
/// to a single epoch bump (keeping the re-seed grace one-deep). Default 1h.
const REVOKE_ROTATION_WINDOW_MS: u64 = 60 * 60 * 1000;

/// `spt subnet revoke <node>... [--force-rotate-seed]` — fleet-wide revocation
/// with a timeboxed seed rotation (Mesh-D7, REQ-MESH-4). Distinct from `prune`
/// (local-only tombstone, no rotation): revoke writes a PROPAGATING roster
/// tombstone now and schedules ONE seed rotation at the close of a coalescing
/// window; benign offliners auto-heal across it (re-seed grace), the revokee is
/// locked out and must re-pair. `--force-rotate-seed` rotates inline (the
/// compromised path). Elevation-gated, gate-first. Own identity refuses (use
/// `leave`).
// [impl->REQ-MESH-4]
fn cmd_subnet_revoke(nodes: &[String], force_rotate_seed: bool) -> i32 {
    if let Some(msg) =
        trust_mutation_refusal(elevation::current(), "revoking a node and rotating the seed")
    {
        if let Some(code) = try_auto_elevate() {
            return code;
        }
        eprintln!("{}", with_elevation_hint(msg));
        return EXIT_NOT_ELEVATED;
    }
    let mut roster = spt_store::roster::RosterStore::load();
    let own_hex = spt_store::nodeid::load_or_create()
        .ok()
        .map(|id| id.public_key().to_hex());

    // Resolve every target (hex / prefix / label) against the roster, fail-soft:
    // unresolved/ambiguous/own targets are reported and skipped; we proceed with
    // whatever resolved, refusing only when nothing did.
    let mut revokees: Vec<String> = Vec::new();
    for node in nodes {
        match prune_candidates(&roster, node).as_slice() {
            [] => eprintln!("NO_MATCH:{node} — no member identity matches (hex, prefix, or label)"),
            [one] if Some(one) == own_hex.as_ref() => {
                eprintln!("OWN_IDENTITY:{node} — revoke removes OTHER nodes; use `spt subnet leave` to exit a subnet");
            }
            [one] => {
                if !revokees.contains(one) {
                    revokees.push(one.clone());
                }
            }
            several => {
                let pfx: Vec<String> = several
                    .iter()
                    .map(|h| format!("{}…", spt_net::net::registry::key_prefix(h)))
                    .collect();
                eprintln!(
                    "AMBIGUOUS:{node} — several identities match: {} (use a longer prefix)",
                    pfx.join(", ")
                );
            }
        }
    }
    if revokees.is_empty() {
        eprintln!("NOTHING_REVOKED — no target resolved to a member identity");
        return 1;
    }

    // Tombstone (propagating grow-set marker — unlike prune's local-only stance)
    // every subnet each revokee is a member of, and gather the subnets touched so
    // we can schedule (or fire) their rotation.
    let stamp = (now_ms() / 1000).to_string();
    let mut by_subnet: std::collections::BTreeMap<String, Vec<String>> = Default::default();
    for hex in &revokees {
        let subs: Vec<String> = roster
            .members
            .iter()
            .filter(|m| &m.pubkey_hex == hex)
            .map(|m| m.subnet.clone())
            .collect();
        for sub in subs {
            roster.tombstone(&sub, hex, &stamp);
            by_subnet.entry(sub).or_default().push(hex.clone());
        }
    }
    if by_subnet.is_empty() {
        eprintln!("NOTHING_REVOKED — resolved identities are not members of any subnet");
        return 1;
    }
    if let Err(e) = roster.save() {
        eprintln!("REVOKE_FAIL: roster: {e}");
        return 1;
    }

    // Clear the revokees' registry rows now (they are gone for good — same
    // explicit forget `prune` performs), so bare-id resolution never strands.
    let snap_dir = perch::identity_dir().join("registry");
    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {
        let mut n = 0usize;
        for hex in &revokees {
            n += reg.evict_nodes(|node| node == hex) + reg.evict_node_labels(|node| node == hex);
        }
        if n > 0 {
            if let Ok(json) = serde_json::to_string_pretty(&reg) {
                let _ = spt_store::atomic::atomic_write_string(
                    &snap_dir.join(format!("{name}.json")),
                    &json,
                );
            }
        }
    }

    let subnets_touched: Vec<String> = by_subnet.keys().cloned().collect();
    if force_rotate_seed {
        // The compromised path: rotate inline now (bypass the window) and leave
        // no pending entry — the daemon re-reads subnet.json per connect, so the
        // new epoch takes effect without a restart. A benign offliner may now
        // fall to N-2 and must re-pair (the documented force trade-off).
        let mut subnets = SubnetStore::load();
        let mut pending = spt_store::rotation::RotationPending::load();
        let mut rotated = Vec::new();
        for sub in &subnets_touched {
            if subnets.rotate_seed(sub).is_ok() {
                rotated.push(sub.clone());
                pending.clear(sub); // a windowed entry, if any, is now moot
            }
        }
        if let Err(e) = subnets.save() {
            eprintln!("REVOKE_FAIL: seed rotation: {e} (tombstones already written)");
            return 1;
        }
        let _ = pending.save();
        eprintln!(
            "REVOKED:{} node(s); seed rotated NOW for [{}]. Revoked nodes are locked out \
             and must re-pair; benign offliners that missed this rotation must re-pair too \
             (forced rotation skips the re-seed grace).",
            revokees.len(),
            rotated.join(", ")
        );
    } else {
        // Schedule one coalescing rotation per touched subnet; a second revoke in
        // the window joins the same entry (one epoch bump). The daemon pump fires
        // it at the deadline.
        let deadline = now_ms() + REVOKE_ROTATION_WINDOW_MS;
        let mut pending = spt_store::rotation::RotationPending::load();
        for (sub, hexes) in &by_subnet {
            pending.coalesce(sub, hexes, deadline);
        }
        if let Err(e) = pending.save() {
            eprintln!("REVOKE_FAIL: rotation schedule: {e} (tombstones already written)");
            return 1;
        }
        eprintln!(
            "REVOKED:{} node(s); seed rotation scheduled for [{}] within ~1h (revokes in \
             the window coalesce to one bump). The tombstone already locks them out fleet-wide; \
             benign offliners auto-heal across the rotation, revoked nodes must re-pair. \
             Use --force-rotate-seed to rotate now.",
            revokees.len(),
            subnets_touched.join(", ")
        );
    }
    0
}

/// `spt subnet detach|attach <NAME> [--save]` — the per-subnet serve-state


/// The coalescing-window length (Mesh-D7, REQ-MESH-4): a revoke schedules its
/// seed rotation this far out, so a burst of revokes inside the window collapses
/// to a single epoch bump (keeping the re-seed grace one-deep). Default 1h.
const REVOKE_ROTATION_WINDOW_MS: u64 = 60 * 60 * 1000;

/// `spt subnet revoke <node>... [--force-rotate-seed]` — fleet-wide revocation
/// with a timeboxed seed rotation (Mesh-D7, REQ-MESH-4). Distinct from `prune`
/// (local-only tombstone, no rotation): revoke writes a PROPAGATING roster
/// tombstone now and schedules ONE seed rotation at the close of a coalescing
/// window; benign offliners auto-heal across it (re-seed grace), the revokee is
/// locked out and must re-pair. `--force-rotate-seed` rotates inline (the
/// compromised path). Elevation-gated, gate-first. Own identity refuses (use
/// `leave`).
// [impl->REQ-MESH-4]
fn cmd_subnet_revoke(nodes: &[String], force_rotate_seed: bool) -> i32 {
    if let Some(msg) =
        trust_mutation_refusal(elevation::current(), "revoking a node and rotating the seed")
    {
        if let Some(code) = try_auto_elevate() {
            return code;
        }
        eprintln!("{}", with_elevation_hint(msg));
        return EXIT_NOT_ELEVATED;
    }
    let mut roster = spt_store::roster::RosterStore::load();
    let own_hex = spt_store::nodeid::load_or_create()
        .ok()
        .map(|id| id.public_key().to_hex());

    // Resolve every target (hex / prefix / label) against the roster, fail-soft:
    // unresolved/ambiguous/own targets are reported and skipped; we proceed with
    // whatever resolved, refusing only when nothing did.
    let mut revokees: Vec<String> = Vec::new();
    for node in nodes {
        match prune_candidates(&roster, node).as_slice() {
            [] => eprintln!("NO_MATCH:{node} — no member identity matches (hex, prefix, or label)"),
            [one] if Some(one) == own_hex.as_ref() => {
                eprintln!("OWN_IDENTITY:{node} — revoke removes OTHER nodes; use `spt subnet leave` to exit a subnet");
            }
            [one] => {
                if !revokees.contains(one) {
                    revokees.push(one.clone());
                }
            }
            several => {
                let pfx: Vec<String> = several
                    .iter()
                    .map(|h| format!("{}…", spt_net::net::registry::key_prefix(h)))
                    .collect();
                eprintln!(
                    "AMBIGUOUS:{node} — several identities match: {} (use a longer prefix)",
                    pfx.join(", ")
                );
            }
        }
    }
    if revokees.is_empty() {
        eprintln!("NOTHING_REVOKED — no target resolved to a member identity");
        return 1;
    }

    // Tombstone (propagating grow-set marker — unlike prune's local-only stance)
    // every subnet each revokee is a member of, and gather the subnets touched so
    // we can schedule (or fire) their rotation.
    let stamp = (now_ms() / 1000).to_string();
    let mut by_subnet: std::collections::BTreeMap<String, Vec<String>> = Default::default();
    for hex in &revokees {
        let subs: Vec<String> = roster
            .members
            .iter()
            .filter(|m| &m.pubkey_hex == hex)
            .map(|m| m.subnet.clone())
            .collect();
        for sub in subs {
            roster.tombstone(&sub, hex, &stamp);
            by_subnet.entry(sub).or_default().push(hex.clone());
        }
    }
    if by_subnet.is_empty() {
        eprintln!("NOTHING_REVOKED — resolved identities are not members of any subnet");
        return 1;
    }
    if let Err(e) = roster.save() {
        eprintln!("REVOKE_FAIL: roster: {e}");
        return 1;
    }

    // Clear the revokees' registry rows now (they are gone for good — same
    // explicit forget `prune` performs), so bare-id resolution never strands.
    let snap_dir = perch::identity_dir().join("registry");
    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {
        let mut n = 0usize;
        for hex in &revokees {
            n += reg.evict_nodes(|node| node == hex) + reg.evict_node_labels(|node| node == hex);
        }
        if n > 0 {
            if let Ok(json) = serde_json::to_string_pretty(&reg) {
                let _ = spt_store::atomic::atomic_write_string(
                    &snap_dir.join(format!("{name}.json")),
                    &json,
                );
            }
        }
    }

    let subnets_touched: Vec<String> = by_subnet.keys().cloned().collect();
    if force_rotate_seed {
        // The compromised path: rotate inline now (bypass the window) and leave
        // no pending entry — the daemon re-reads subnet.json per connect, so the
        // new epoch takes effect without a restart. A benign offliner may now
        // fall to N-2 and must re-pair (the documented force trade-off).
        let mut subnets = SubnetStore::load();
        let mut pending = spt_store::rotation::RotationPending::load();
        let mut rotated = Vec::new();
        for sub in &subnets_touched {
            if subnets.rotate_seed(sub).is_ok() {
                rotated.push(sub.clone());
                pending.clear(sub); // a windowed entry, if any, is now moot
            }
        }
        if let Err(e) = subnets.save() {
            eprintln!("REVOKE_FAIL: seed rotation: {e} (tombstones already written)");
            return 1;
        }
        let _ = pending.save();
        eprintln!(
            "REVOKED:{} node(s); seed rotated NOW for [{}]. Revoked nodes are locked out \
             and must re-pair; benign offliners that missed this rotation must re-pair too \
             (forced rotation skips the re-seed grace).",
            revokees.len(),
            rotated.join(", ")
        );
    } else {
        // Schedule one coalescing rotation per touched subnet; a second revoke in
        // the window joins the same entry (one epoch bump). The daemon pump fires
        // it at the deadline.
        let deadline = now_ms() + REVOKE_ROTATION_WINDOW_MS;
        let mut pending = spt_store::rotation::RotationPending::load();
        for (sub, hexes) in &by_subnet {
            pending.coalesce(sub, hexes, deadline);
        }
        if let Err(e) = pending.save() {
            eprintln!("REVOKE_FAIL: rotation schedule: {e} (tombstones already written)");
            return 1;
        }
        eprintln!(
            "REVOKED:{} node(s); seed rotation scheduled for [{}] within ~1h (revokes in \
             the window coalesce to one bump). The tombstone already locks them out fleet-wide; \
             benign offliners auto-heal across the rotation, revoked nodes must re-pair. \
             Use --force-rotate-seed to rotate now.",
            revokees.len(),
            subnets_touched.join(", ")
        );
    }
    0
}

/// `spt subnet detach|attach <NAME> [--save]` — the per-subnet serve-state

+/// The coalescing-window length (Mesh-D7, REQ-MESH-4): a revoke schedules its

+/// seed rotation this far out, so a burst of revokes inside the window collapses

+/// to a single epoch bump (keeping the re-seed grace one-deep). Default 1h.

+const REVOKE_ROTATION_WINDOW_MS: u64 = 60 * 60 * 1000;

+/// `spt subnet revoke <node>... [--force-rotate-seed]` — fleet-wide revocation

+/// with a timeboxed seed rotation (Mesh-D7, REQ-MESH-4). Distinct from `prune`

+/// (local-only tombstone, no rotation): revoke writes a PROPAGATING roster

+/// tombstone now and schedules ONE seed rotation at the close of a coalescing

+/// window; benign offliners auto-heal across it (re-seed grace), the revokee is

+/// locked out and must re-pair. `--force-rotate-seed` rotates inline (the

+/// compromised path). Elevation-gated, gate-first. Own identity refuses (use

+fn cmd_subnet_revoke(nodes: &[String], force_rotate_seed: bool) -> i32 {

+        trust_mutation_refusal(elevation::current(), "revoking a node and rotating the seed")

+        if let Some(code) = try_auto_elevate() {

+        eprintln!("{}", with_elevation_hint(msg));

+    let mut roster = spt_store::roster::RosterStore::load();

+    let own_hex = spt_store::nodeid::load_or_create()

+        .map(|id| id.public_key().to_hex());

+    // Resolve every target (hex / prefix / label) against the roster, fail-soft:

+    // unresolved/ambiguous/own targets are reported and skipped; we proceed with

+    // whatever resolved, refusing only when nothing did.

+    let mut revokees: Vec<String> = Vec::new();

+        match prune_candidates(&roster, node).as_slice() {

+            [] => eprintln!("NO_MATCH:{node} — no member identity matches (hex, prefix, or label)"),

+            [one] if Some(one) == own_hex.as_ref() => {

+                eprintln!("OWN_IDENTITY:{node} — revoke removes OTHER nodes; use `spt subnet leave` to exit a subnet");

+                if !revokees.contains(one) {

+                    revokees.push(one.clone());

+                let pfx: Vec<String> = several

+                    .map(|h| format!("{}…", spt_net::net::registry::key_prefix(h)))

+                    "AMBIGUOUS:{node} — several identities match: {} (use a longer prefix)",

+        eprintln!("NOTHING_REVOKED — no target resolved to a member identity");

+    // Tombstone (propagating grow-set marker — unlike prune's local-only stance)

+    // every subnet each revokee is a member of, and gather the subnets touched so

+    // we can schedule (or fire) their rotation.

+    let stamp = (now_ms() / 1000).to_string();

+    let mut by_subnet: std::collections::BTreeMap<String, Vec<String>> = Default::default();

+            .filter(|m| &m.pubkey_hex == hex)

+            roster.tombstone(&sub, hex, &stamp);

+            by_subnet.entry(sub).or_default().push(hex.clone());

+        eprintln!("NOTHING_REVOKED — resolved identities are not members of any subnet");

+        eprintln!("REVOKE_FAIL: roster: {e}");

+    // Clear the revokees' registry rows now (they are gone for good — same

+    // explicit forget `prune` performs), so bare-id resolution never strands.

+    let snap_dir = perch::identity_dir().join("registry");

+    for (name, mut reg) in crate::wansend::load_snapshots(&snap_dir) {

+            n += reg.evict_nodes(|node| node == hex) + reg.evict_node_labels(|node| node == hex);

+            if let Ok(json) = serde_json::to_string_pretty(&reg) {

+                let _ = spt_store::atomic::atomic_write_string(

+                    &snap_dir.join(format!("{name}.json")),

+    let subnets_touched: Vec<String> = by_subnet.keys().cloned().collect();

+        // The compromised path: rotate inline now (bypass the window) and leave

+        // no pending entry — the daemon re-reads subnet.json per connect, so the

+        // new epoch takes effect without a restart. A benign offliner may now

+        // fall to N-2 and must re-pair (the documented force trade-off).

+        let mut subnets = SubnetStore::load();

+        let mut pending = spt_store::rotation::RotationPending::load();

+            if subnets.rotate_seed(sub).is_ok() {

+                rotated.push(sub.clone());

+                pending.clear(sub); // a windowed entry, if any, is now moot

+        if let Err(e) = subnets.save() {

+            eprintln!("REVOKE_FAIL: seed rotation: {e} (tombstones already written)");

+            "REVOKED:{} node(s); seed rotated NOW for [{}]. Revoked nodes are locked out \

+             and must re-pair; benign offliners that missed this rotation must re-pair too \

+             (forced rotation skips the re-seed grace).",

+        // Schedule one coalescing rotation per touched subnet; a second revoke in

+        // the window joins the same entry (one epoch bump). The daemon pump fires

+        let deadline = now_ms() + REVOKE_ROTATION_WINDOW_MS;

+        for (sub, hexes) in &by_subnet {

+            pending.coalesce(sub, hexes, deadline);

+        if let Err(e) = pending.save() {

+            eprintln!("REVOKE_FAIL: rotation schedule: {e} (tombstones already written)");

+            "REVOKED:{} node(s); seed rotation scheduled for [{}] within ~1h (revokes in \

+             the window coalesce to one bump). The tombstone already locks them out fleet-wide; \

+             benign offliners auto-heal across the rotation, revoked nodes must re-pair. \

+             Use --force-rotate-seed to rotate now.",

/// flip (M8 decision 6, REQ-SUBNET-5). Ungated: a serve-state toggle

/// mutates no trust material (unlike `leave`/`prune`). The daemon's pump,


Bash

Build the spt CLI crate


   Compiling tokio-rustls v0.26.4
   Compiling spt-msg v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
   Compiling rustls-platform-verifier v0.7.0
   Compiling noq v0.18.0
   Compiling hyper-rustls v0.27.9
   Compiling hickory-net v0.26.0-beta.4
   Compiling tokio-websockets v0.13.2
   Compiling spt-runtime v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
   Compiling spt-term v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling reqwest v0.13.4
   Compiling spt-live v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
   Compiling hickory-resolver v0.26.0-beta.4
   Compiling iroh-relay v0.98.0
   Compiling iroh v0.98.2
   Compiling spt-net v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.2.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 28.80s


CLI builds. Let me check the CLI parse-routing test near line 6322 and add a `revoke` case:


6470	        // name-only (prompts the code), and fully non-interactive.
6471	        assert!(matches!(
6472	            subnet_cmd(&["spt", "subnet", "join"]),
6473	            Some(SubnetCmd::Join {
6474	                name: None,
6475	                code: None
6476	            })
6477	        ));
6478	        assert!(matches!(
6479	            subnet_cmd(&["spt", "subnet", "join", "home"]),
6480	            Some(SubnetCmd::Join { name: Some(n), code: None }) if n == "home"
6481	        ));
6482	        assert!(matches!(
6483	            subnet_cmd(&["spt", "subnet", "join", "home", "--code", "123456"]),
6484	            Some(SubnetCmd::Join { name: Some(_), code: Some(c) }) if c == "123456"
6485	        ));
6486	        // [unit->REQ-SUBNET-5] attach/detach: positional name + --save
6487	        // (M8 decision 6; --save renamed from --auto per decision 25).
6488	        assert!(matches!(
6489	            subnet_cmd(&["spt", "subnet", "detach", "home"]),
6490	            Some(SubnetCmd::Detach { name, save: false }) if name == "home"
6491	        ));
6492	        assert!(matches!(
6493	            subnet_cmd(&["spt", "subnet", "attach", "home", "--save"]),
6494	            Some(SubnetCmd::Attach { name, save: true }) if name == "home"
6495	        ));
6496	        assert!(
6497	            parse(&["spt", "subnet", "detach"]).is_err(),
6498	            "detach needs a name"
6499	        );
6500	        // [unit->REQ-SUBNET-6] leave/prune: positional target, no flags.
6501	        assert!(matches!(
6502	            subnet_cmd(&["spt", "subnet", "leave", "home"]),
6503	            Some(SubnetCmd::Leave { name }) if name == "home"
6504	        ));
6505	        assert!(matches!(
6506	            subnet_cmd(&["spt", "subnet", "prune", "09ef831e"]),
6507	            Some(SubnetCmd::Prune { node }) if node == "09ef831e"
6508	        ));
6509	        assert!(

        // name-only (prompts the code), and fully non-interactive.
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "join"]),
            Some(SubnetCmd::Join {
                name: None,
                code: None
            })
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "join", "home"]),
            Some(SubnetCmd::Join { name: Some(n), code: None }) if n == "home"
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "join", "home", "--code", "123456"]),
            Some(SubnetCmd::Join { name: Some(_), code: Some(c) }) if c == "123456"
        ));
        // [unit->REQ-SUBNET-5] attach/detach: positional name + --save
        // (M8 decision 6; --save renamed from --auto per decision 25).
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "detach", "home"]),
            Some(SubnetCmd::Detach { name, save: false }) if name == "home"
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "attach", "home", "--save"]),
            Some(SubnetCmd::Attach { name, save: true }) if name == "home"
        ));
        assert!(
            parse(&["spt", "subnet", "detach"]).is_err(),
            "detach needs a name"
        );
        // [unit->REQ-SUBNET-6] leave/prune: positional target, no flags.
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "leave", "home"]),
            Some(SubnetCmd::Leave { name }) if name == "home"
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "prune", "09ef831e"]),
            Some(SubnetCmd::Prune { node }) if node == "09ef831e"
        ));
        assert!(


6509	        assert!(
6510	            parse(&["spt", "subnet", "leave"]).is_err(),
6511	            "leave needs a name"
6512	        );
6513	        assert!(
6514	            parse(&["spt", "subnet", "prune"]).is_err(),
6515	            "prune needs a node"
6516	        );
6517	    }
6518
6519	    // [unit->REQ-SUBNET-6] leave/prune are elevation-gated with the join
6520	    // gate's fail-safe posture: NotElevated and Unknown both refuse.
6521	    #[test]
6522	    fn trust_mutation_gate_refuses_without_confirmed_elevation() {
6523	        assert!(trust_mutation_refusal(Elevation::Elevated, "leaving").is_none());
6524	        assert!(trust_mutation_refusal(Elevation::NotElevated, "leaving").is_some());
6525	        assert!(trust_mutation_refusal(Elevation::Unknown, "leaving").is_some());
6526	    }
6527
6528	    // [unit->REQ-SUBNET-6] prune target resolution: full hex, unambiguous

        assert!(
            parse(&["spt", "subnet", "leave"]).is_err(),
            "leave needs a name"
        );
        assert!(
            parse(&["spt", "subnet", "prune"]).is_err(),
            "prune needs a node"
        );
    }

    // [unit->REQ-SUBNET-6] leave/prune are elevation-gated with the join
    // gate's fail-safe posture: NotElevated and Unknown both refuse.
    #[test]
    fn trust_mutation_gate_refuses_without_confirmed_elevation() {
        assert!(trust_mutation_refusal(Elevation::Elevated, "leaving").is_none());
        assert!(trust_mutation_refusal(Elevation::NotElevated, "leaving").is_some());
        assert!(trust_mutation_refusal(Elevation::Unknown, "leaving").is_some());
    }

    // [unit->REQ-SUBNET-6] prune target resolution: full hex, unambiguous


Edit

        assert!(
            parse(&["spt", "subnet", "prune"]).is_err(),
            "prune needs a node"
        );
    }

        assert!(
            parse(&["spt", "subnet", "prune"]).is_err(),
            "prune needs a node"
        );
        // [unit->REQ-MESH-4] revoke: a list of targets (required) + the
        // --force-rotate-seed flag (off by default = windowed coalescing).
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "revoke", "09ef831e"]),
            Some(SubnetCmd::Revoke { nodes, force_rotate_seed: false }) if nodes == ["09ef831e"]
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "revoke", "aa", "bb", "--force-rotate-seed"]),
            Some(SubnetCmd::Revoke { nodes, force_rotate_seed: true }) if nodes == ["aa", "bb"]
        ));
        assert!(
            parse(&["spt", "subnet", "revoke"]).is_err(),
            "revoke needs at least one node"
        );
    }


        assert!(
            parse(&["spt", "subnet", "prune"]).is_err(),
            "prune needs a node"
        );
    }

        assert!(
            parse(&["spt", "subnet", "prune"]).is_err(),
            "prune needs a node"
        );
        // [unit->REQ-MESH-4] revoke: a list of targets (required) + the
        // --force-rotate-seed flag (off by default = windowed coalescing).
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "revoke", "09ef831e"]),
            Some(SubnetCmd::Revoke { nodes, force_rotate_seed: false }) if nodes == ["09ef831e"]
        ));
        assert!(matches!(
            subnet_cmd(&["spt", "subnet", "revoke", "aa", "bb", "--force-rotate-seed"]),
            Some(SubnetCmd::Revoke { nodes, force_rotate_seed: true }) if nodes == ["aa", "bb"]
        ));
        assert!(
            parse(&["spt", "subnet", "revoke"]).is_err(),
            "revoke needs at least one node"
        );
    }

+        // [unit->REQ-MESH-4] revoke: a list of targets (required) + the

+        // --force-rotate-seed flag (off by default = windowed coalescing).

+            subnet_cmd(&["spt", "subnet", "revoke", "09ef831e"]),

+            Some(SubnetCmd::Revoke { nodes, force_rotate_seed: false }) if nodes == ["09ef831e"]

+            subnet_cmd(&["spt", "subnet", "revoke", "aa", "bb", "--force-rotate-seed"]),

+            Some(SubnetCmd::Revoke { nodes, force_rotate_seed: true }) if nodes == ["aa", "bb"]

+            parse(&["spt", "subnet", "revoke"]).is_err(),

+            "revoke needs at least one node"

// [unit->REQ-SUBNET-6] leave/prune are elevation-gated with the join


Bash

cargo test -p spt --lib subnet 2>&1 | tail -25

Run CLI subnet unit tests


error: no library targets found in package `spt`


cargo test -p spt --bins 2>&1 | tail -30

Run spt binary unit tests


test cli::tests::unknown_named_subnet_is_usage_error ... ok
test elevation::tests::admin_terminal_style_has_no_command_prefix ... ok
test elevation::tests::auto_elevate_only_when_unix_interactive_and_unelevated ... ok
test elevation::tests::current_returns_a_determinate_level_on_supported_platforms ... ok
test elevation::tests::fallback_message_names_authenticator_app_and_subnet ... ok
test elevation::tests::gate_shows_only_when_confirmed_elevated ... ok
test elevation::tests::sudo_rerun_shell_quotes_spaced_args ... ok
test elevation::tests::sudo_rerun_uses_absolute_path_not_bare_name ... ok
test cli::tests::single_subnet_auto_selects_and_code_matches_seed ... ok
test wansend::tests::refusals_render_copy_paste_targets ... ok
test wansend::tests::load_snapshots_degrades_on_junk ... ok
test cli::tests::subnet_arg_shapes_parse ... ok
test api::worker::tests::guard_clears_only_when_all_children_stopped ... ok
test api::worker::tests::worker_poll_drains_spool ... ok
test api::worker::tests::worker_poll_holds_deferred_while_resting ... ok
test api::worker::tests::worker_start_nests_under_parent ... ok
test cli::tests::fork_forks_homes_and_deletes_exactly_the_source ... ok
test cli::tests::owner_shutdown_gated_by_can_shutdown_grant ... ok
test cli::tests::shell_channels_relay_sensory_and_text_file ... ok
test cli::tests::shell_owner_exclusivity_negatives ... ok
test cli::tests::shell_relink_relaunches_offline_instances_only ... ok
test cli::tests::shell_spawn_approval_floor_gates ... ok
test cli::tests::shell_spawn_cap_and_over_cap_gates ... ok
test cli::tests::shell_spawn_gates_on_the_registered_set ... ok
test cli::tests::shutdown_soft_stops_and_suspends ... ok
test wansend::tests::remote_rest_suspends_and_wakes_with_the_deferred_gate ... ok
test wansend::tests::wan_send_ships_to_the_resolved_node ... ok

test result: ok. 102 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 28.50s