Files
tsunagi/tests/end_to_end.rs
T
tsunagiandClaude Opus 5 43e8ac8159 Make identity something you can look at and change
`id` now shows what this device is — the key it signs with, the name it
answers to, the secret of every network it has joined — and changes all
of it. One shape throughout: name a thing to see it, name it with a value
to change it. `secret` folded in as `id secret generate`, and the path
flags became global so they work either side of a subcommand.

There is no separate signing certificate to show: the endpoint key is
what signs records, and the report says so rather than leaving it to be
guessed.

Secrets appear in `id`, which is where you go to ask for one, and stay
out of `status`, logs, `Debug` and anything sent to a peer.

The hostname is now a signed claim, which is what makes changing it a
revocation. Records are one per author, so a new version replaces the
whole claim and no replica can keep the old name standing. RecordBody
generalised to Claim { address, range, hostname } + Release for that,
with the signing domain bumped; a name is bounded and canonicalised, and
a non-canonical one is rejected rather than repaired, because a repaired
version is not what its author signed. Two members claiming one name
resolve it like an address: lowest id wins, computed identically
everywhere. A member with only a name now has a record too, so an
IPv6-only network finally has a durable roster and an absent member can
be named rather than shown as a bare id.

Replacing the signing key is allowed and does not break the store. The
outgoing key signs a release for every network first, so the address and
name it held are freed rather than reserved forever to a key nobody has
— nothing can sign for a retired author, and by design no authority
could overrule one. Identity and releases commit together: a crash
between them would leave the old key gone and unable to sign what it
owed. It refuses while an agent holds the directory, rather than failing
on the lock with a message that says nothing about what to do.

The version counter is keyed by author as well as network, so a
replacement key starts its own sequence. The migration drops records
written under the previous signing domain instead of carrying rows that
every read must reject and that look exactly like corruption.

The hostname defaults to the machine's own name. Also fixed a
pre-existing flaky test: 40 random authors in a /24 collide by the
birthday problem often enough that its threshold failed about one run in
six, so the authors are fixed now and it tests a property rather than a
coin flip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 15:59:39 +01:00

66 lines
2.2 KiB
Rust

//! The full vertical slice: persistent identity, network space, discovery,
//! real iroh connections, authentication and a control message exchange.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod common;
use common::{TestAgent, network, wait_event, wait_for_peers};
use tsunagi::agent::Event;
use tsunagi::discovery::SharedMemoryDiscovery;
use tsunagi::proto::ControlMessage;
#[tokio::test]
async fn two_agents_authenticate_and_exchange_messages() {
let discovery = SharedMemoryDiscovery::new();
let (name, secret) = network("vertical-slice");
let a = TestAgent::spawn(&discovery).await.unwrap();
let b = TestAgent::spawn(&discovery).await.unwrap();
let mut events_a = a.agent.subscribe();
let network_id = a.agent.join_network(&name, &secret).await.unwrap();
let same_id = b.agent.join_network(&name, &secret).await.unwrap();
assert_eq!(
network_id, same_id,
"the same name and secret must produce the same network space"
);
let peers = wait_for_peers(&a.agent, network_id, 1).await;
assert_eq!(peers, vec![b.agent.endpoint_id()]);
// The exchange itself: a ping must come back as a matching pong.
a.agent
.send(
network_id,
b.agent.endpoint_id(),
ControlMessage::Ping {
seq: 42,
payload: b"vertical".to_vec(),
},
)
.await
.unwrap();
let payload = wait_event(&mut events_a, |event| match event {
Event::MessageReceived {
network,
peer,
message: ControlMessage::Pong { seq: 42, payload },
} if *network == network_id && *peer == b.agent.endpoint_id() => Some(payload.clone()),
_ => None,
})
.await;
assert_eq!(payload, b"vertical".to_vec());
// Both sides announce a hostname over the authenticated session.
let status = a.agent.network_status(network_id).await.unwrap();
let peer = &status.peers[0];
assert_eq!(peer.hostname.as_deref(), Some(b.agent.hostname().as_str()));
assert!(peer.transport != tsunagi::net::TransportKind::Unknown);
assert!(peer.rtt.is_some(), "a verified path must report an RTT");
a.agent.shutdown().await;
b.agent.shutdown().await;
}