IrohTransport.send_file now adds the (already-sealed) bytes to a MemStore and
broadcasts a blob announcement over gossip {hash, name, mime, provider}; on
receipt, peers fetch the blob P2P by hash via iroh-blobs and emit MeshEvent::File.
The engine's existing File handler decrypts and delivers — so the clipboard can
carry data/photos/etc, not just text. Replaces the hand-rolled M/C/E frame
protocol entirely.
Verified: `cargo run --features iroh-transport --example iroh_engine` syncs both
a text clipboard (gossip) AND a 200 KB photo (iroh-blobs), both E2E-decrypted
A→B, no SSE/RTC/LAN/relay. Default ship build untouched; feature build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
118 lines
4.2 KiB
Rust
118 lines
4.2 KiB
Rust
//! M4 integration test — the real `Engine` syncing a clipboard over iroh.
|
|
//! cargo run --features iroh-transport --example iroh_engine
|
|
//!
|
|
//! Two engines, same account → same room → same gossip topic. No SSE/RTC/LAN:
|
|
//! with the iroh-transport feature the engine's only transport is IrohTransport.
|
|
//! Proves the public API (Engine::new/start/send + MessageHandler) is unchanged
|
|
//! while the entire Mesh underneath is iroh — the payoff of the MeshEvent seam.
|
|
//!
|
|
//! Bootstrap is wired in-process via the TETHER_IROH_BOOTSTRAP env var (the v1
|
|
//! stopgap until the rendezvous supernode): start A, read its EndpointId, hand
|
|
//! it to B.
|
|
|
|
use std::sync::mpsc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use tethercore::{Engine, Message, MessageHandler};
|
|
|
|
struct Noop;
|
|
impl MessageHandler for Noop {
|
|
fn on_message(&self, _m: Message) {}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, _n: String, _m: String, _d: Vec<u8>) {}
|
|
}
|
|
|
|
struct Collector {
|
|
text: mpsc::Sender<String>,
|
|
file: mpsc::Sender<(String, usize)>,
|
|
}
|
|
impl MessageHandler for Collector {
|
|
fn on_message(&self, m: Message) {
|
|
let _ = self.text.send(m.text);
|
|
}
|
|
fn on_status(&self, _c: bool) {}
|
|
fn on_file(&self, name: String, _mime: String, data: Vec<u8>) {
|
|
let _ = self.file.send((name, data.len()));
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let account = "pecord@gmail.com"; // same account → same room → same topic + key
|
|
let room = "iroh-engine-test";
|
|
let server = "unused"; // iroh ignores cfg.server; it uses n0 infra
|
|
|
|
// Opener (no bootstrap yet).
|
|
let a = Engine::new(
|
|
server.into(), room.into(), "iroh-a".into(), "macos".into(), "A".into(), account.into(),
|
|
);
|
|
a.start(Box::new(Noop));
|
|
|
|
// Wait for A's iroh endpoint to come online, then bootstrap B off it.
|
|
let a_id = {
|
|
let deadline = Instant::now() + Duration::from_secs(20);
|
|
loop {
|
|
if let Some(id) = a.iroh_id() {
|
|
break id;
|
|
}
|
|
if Instant::now() > deadline {
|
|
eprintln!("❌ A never came online");
|
|
std::process::exit(1);
|
|
}
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
}
|
|
};
|
|
eprintln!("A online, id={a_id}");
|
|
std::env::set_var("TETHER_IROH_BOOTSTRAP", &a_id);
|
|
|
|
// Joiner (bootstraps to A via the env var its IrohTransport reads at start).
|
|
let (tx, rx) = mpsc::channel();
|
|
let (ftx, frx) = mpsc::channel();
|
|
let b = Engine::new(
|
|
server.into(), room.into(), "iroh-b".into(), "ios".into(), "B".into(), account.into(),
|
|
);
|
|
b.start(Box::new(Collector { text: tx, file: ftx }));
|
|
|
|
eprintln!("waiting for gossip topic to form…");
|
|
std::thread::sleep(Duration::from_secs(8));
|
|
|
|
// 1) Text clipboard over gossip.
|
|
let payload = "clipboard synced over iroh 🎉";
|
|
eprintln!("A sending text: {payload:?}");
|
|
a.send(payload.to_string());
|
|
match rx.recv_timeout(Duration::from_secs(10)) {
|
|
Ok(t) if t == payload => {
|
|
println!("✅ text: B received {t:?} (E2E-decrypted, via gossip)");
|
|
}
|
|
Ok(t) => {
|
|
println!("⚠️ B got {t:?} (expected {payload:?})");
|
|
std::process::exit(1);
|
|
}
|
|
Err(_) => {
|
|
println!("❌ B received no text");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
// 2) A "photo"/blob over iroh-blobs (announced via gossip, fetched P2P).
|
|
let photo: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
|
|
eprintln!("A sending blob: photo.bin ({} bytes)", photo.len());
|
|
a.send_file("photo.bin".into(), "image/png".into(), photo.clone());
|
|
match frx.recv_timeout(Duration::from_secs(15)) {
|
|
Ok((name, len)) if len == photo.len() => {
|
|
println!("✅ blob: B received {name:?} ({len} bytes, E2E-decrypted, via iroh-blobs)");
|
|
}
|
|
Ok((name, len)) => {
|
|
println!("⚠️ B got {name:?} with {len} bytes (expected {})", photo.len());
|
|
std::process::exit(1);
|
|
}
|
|
Err(_) => {
|
|
println!("❌ B received no blob");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
println!("🎉 engine-over-iroh: text + photo synced, no SSE/RTC/LAN/relay");
|
|
a.stop();
|
|
b.stop();
|
|
}
|