use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn fresh_home() -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("wire-cli-test-{pid}-{n}"));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).unwrap();
path
}
fn wire_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_wire"))
}
fn run(home: &PathBuf, args: &[&str]) -> std::process::Output {
Command::new(wire_bin())
.args(args)
.env("WIRE_HOME", home)
.env_remove("RUST_LOG")
.output()
.expect("failed to spawn wire")
}
#[test]
fn version_flag_prints_semver() {
let home = fresh_home();
let out = run(&home, &["--version"]);
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
let expected = env!("CARGO_PKG_VERSION");
assert!(
s.contains(expected),
"got: {s} (expected to contain {expected})"
);
}
#[test]
fn help_flag_lists_subcommands() {
let home = fresh_home();
let out = run(&home, &["--help"]);
assert!(out.status.success(), "help failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
for cmd in [
"init", "join", "whoami", "peers", "send", "tail", "verify", "mcp",
] {
assert!(s.contains(cmd), "missing subcommand {cmd} in help: {s}");
}
}
#[test]
fn whoami_before_init_errors() {
let home = fresh_home();
let out = run(&home, &["whoami"]);
assert!(!out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("not initialized"), "stderr: {stderr}");
}
#[test]
fn init_creates_keypair_and_card() {
let home = fresh_home();
let out = run(&home, &["init", "paul", "--json"]);
assert!(out.status.success(), "init failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
{
let d = parsed["did"].as_str().unwrap();
assert!(d.starts_with("did:wire:paul-"), "got: {d}");
}
assert!(parsed["fingerprint"].as_str().unwrap().len() == 8);
assert!(home.join("config/wire/private.key").exists());
assert!(home.join("config/wire/agent-card.json").exists());
assert!(home.join("config/wire/trust.json").exists());
}
#[test]
fn init_twice_refuses_to_clobber() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["init", "paul"]);
assert!(!out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("already initialized"), "stderr: {stderr}");
}
#[test]
fn whoami_after_init_returns_did_and_fingerprint() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["whoami", "--json"]);
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
{
let d = parsed["did"].as_str().unwrap();
assert!(d.starts_with("did:wire:paul-"), "got: {d}");
}
assert_eq!(parsed["handle"], "paul");
assert!(parsed["capabilities"].is_array());
}
#[test]
fn peers_empty_after_init_is_self_filtered() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["peers", "--json"]);
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed.as_array().unwrap().len(), 0);
}
#[test]
fn send_writes_to_outbox() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(
&home,
&[
"send",
"willard",
"decision",
"ship the v0.1 demo",
"--json",
],
);
assert!(out.status.success(), "send failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["status"], "queued");
assert_eq!(parsed["peer"], "willard");
assert!(parsed["event_id"].as_str().unwrap().len() == 64);
let outbox = home.join("state/wire/outbox/willard.jsonl");
assert!(outbox.exists(), "outbox file not created: {outbox:?}");
let body = std::fs::read_to_string(&outbox).unwrap();
let lines: Vec<&str> = body.lines().collect();
assert_eq!(lines.len(), 1);
let event: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
{
let from = event["from"].as_str().unwrap();
assert!(from.starts_with("did:wire:paul-"), "from: {from}");
}
assert_eq!(event["to"], "did:wire:willard");
assert!(event.get("signature").is_some());
assert!(event.get("event_id").is_some());
}
fn write_pending_inbound_fixture(home: &std::path::Path, peer_handle: &str) {
let dir = home.join("state/wire/pending-inbound-pairs");
std::fs::create_dir_all(&dir).unwrap();
let body = serde_json::json!({
"peer_handle": peer_handle,
"peer_did": format!("did:wire:{peer_handle}-abcdef12"),
"peer_card": {"did": format!("did:wire:{peer_handle}-abcdef12")},
"peer_relay_url": "https://relay.example",
"peer_slot_id": "slot-xyz",
"peer_slot_token": "token-xyz",
"event_id": "evt-1",
"event_timestamp": "2026-05-17T20:00:00Z",
"received_at": "2026-05-17T20:00:01Z",
});
std::fs::write(
dir.join(format!("{peer_handle}.json")),
serde_json::to_vec_pretty(&body).unwrap(),
)
.unwrap();
}
#[test]
fn pair_list_inbound_surfaces_pending_v0_5_14() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
write_pending_inbound_fixture(&home, "stranger");
let out = run(&home, &["pair-list-inbound", "--json"]);
assert!(out.status.success(), "pair-list-inbound failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
let arr = parsed.as_array().expect("flat array of pending-inbound");
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["peer_handle"], "stranger");
assert_eq!(arr[0]["peer_slot_token"], "token-xyz");
let out2 = run(&home, &["pair-list", "--json"]);
assert!(out2.status.success());
let s2 = String::from_utf8(out2.stdout).unwrap();
let parsed2: serde_json::Value = serde_json::from_str(&s2).unwrap();
assert!(parsed2.as_array().expect("flat array").is_empty());
}
#[test]
fn status_reports_pending_inbound_count_v0_5_14() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
write_pending_inbound_fixture(&home, "alice");
write_pending_inbound_fixture(&home, "bob");
let out = run(&home, &["status", "--json"]);
assert!(out.status.success(), "status failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["pending_pairs"]["inbound_count"], 2);
let mut handles: Vec<&str> = parsed["pending_pairs"]["inbound_handles"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
handles.sort();
assert_eq!(handles, vec!["alice", "bob"]);
}
#[test]
fn pair_reject_deletes_pending_inbound_v0_5_14() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
write_pending_inbound_fixture(&home, "spammer");
let path = home.join("state/wire/pending-inbound-pairs/spammer.json");
assert!(path.exists(), "fixture file should exist pre-reject");
let out = run(&home, &["pair-reject", "spammer", "--json"]);
assert!(out.status.success(), "pair-reject failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["rejected"], true);
assert!(!path.exists(), "pending file should be deleted after reject");
let out2 = run(&home, &["pair-list-inbound", "--json"]);
let s2 = String::from_utf8(out2.stdout).unwrap();
let parsed2: serde_json::Value = serde_json::from_str(&s2).unwrap();
assert!(parsed2.as_array().unwrap().is_empty());
}
fn write_session_fixture(
home: &std::path::Path,
session_name: &str,
cwd_key: Option<&str>,
) -> std::path::PathBuf {
let sessions_root = home.join("sessions");
let session_home = sessions_root.join(session_name);
let card_dir = session_home.join("config").join("wire");
std::fs::create_dir_all(&card_dir).unwrap();
let card = serde_json::json!({
"did": format!("did:wire:{session_name}-deadbeef"),
"handle": session_name,
"verify_keys": {
format!("ed25519:{session_name}:deadbeef"): {
"active": true,
"alg": "ed25519",
"key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
}
});
std::fs::write(
card_dir.join("agent-card.json"),
serde_json::to_vec_pretty(&card).unwrap(),
)
.unwrap();
if let Some(cwd) = cwd_key {
let registry = serde_json::json!({
"by_cwd": {cwd: session_name}
});
std::fs::write(
sessions_root.join("registry.json"),
serde_json::to_vec_pretty(®istry).unwrap(),
)
.unwrap();
} else {
std::fs::create_dir_all(&sessions_root).unwrap();
}
session_home
}
#[test]
fn session_list_empty_reports_no_sessions_v0_5_16() {
let home = fresh_home();
let out = run(&home, &["session", "list"]);
assert!(out.status.success(), "session list failed: {:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains("no sessions on this machine"),
"expected empty hint, got: {stdout}"
);
}
#[test]
fn session_list_enumerates_on_disk_sessions_v0_5_16() {
let home = fresh_home();
write_session_fixture(&home, "wire", Some("/Users/paul/Source/wire"));
write_session_fixture(&home, "slancha-mesh", Some("/Users/paul/Source/slancha-mesh"));
let out = run(&home, &["session", "list", "--json"]);
assert!(out.status.success(), "session list --json failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let arr: serde_json::Value = serde_json::from_str(&s).unwrap();
let items = arr.as_array().expect("flat array");
assert_eq!(items.len(), 2);
let names: std::collections::HashSet<&str> = items
.iter()
.filter_map(|v| v["name"].as_str())
.collect();
assert!(names.contains("wire"));
assert!(names.contains("slancha-mesh"));
for item in items {
assert_eq!(item["daemon_running"], false);
}
}
#[test]
fn session_env_emits_export_line_for_named_session_v0_5_16() {
let home = fresh_home();
write_session_fixture(&home, "wire", None);
let out = run(&home, &["session", "env", "wire"]);
assert!(out.status.success(), "session env failed: {:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.starts_with("export WIRE_HOME="), "got: {stdout}");
assert!(stdout.contains("/sessions/wire"), "got: {stdout}");
}
#[test]
fn session_env_errors_cleanly_for_missing_session_v0_5_16() {
let home = fresh_home();
let out = run(&home, &["session", "env", "ghost"]);
assert!(!out.status.success(), "expected failure: {:?}", out);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("no session named"), "stderr: {stderr}");
assert!(stderr.contains("wire session list") || stderr.contains("wire session new"), "should hint: {stderr}");
}
#[test]
fn session_destroy_requires_force_flag_v0_5_16() {
let home = fresh_home();
let session_home = write_session_fixture(&home, "wire", None);
let out = run(&home, &["session", "destroy", "wire"]);
assert!(!out.status.success(), "destroy without --force must fail: {:?}", out);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("--force"), "stderr: {stderr}");
assert!(session_home.exists(), "session dir should not be deleted");
}
#[test]
fn session_destroy_with_force_removes_state_and_registry_entry_v0_5_16() {
let home = fresh_home();
let session_home = write_session_fixture(&home, "wire", Some("/Users/paul/Source/wire"));
let registry_path = home.join("sessions").join("registry.json");
assert!(registry_path.exists());
let out = run(&home, &["session", "destroy", "wire", "--force", "--json"]);
assert!(out.status.success(), "destroy failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["destroyed"], true);
assert!(!session_home.exists(), "session dir should be gone");
let registry_bytes = std::fs::read(®istry_path).unwrap();
let registry: serde_json::Value = serde_json::from_slice(®istry_bytes).unwrap();
let by_cwd = registry["by_cwd"].as_object().unwrap();
assert!(
!by_cwd.values().any(|v| v == "wire"),
"registry must not reference destroyed session"
);
}
fn write_local_endpoint(
session_home: &std::path::Path,
local_relay: &str,
slot_id: &str,
) {
let cfg = session_home.join("config").join("wire");
let body = serde_json::json!({
"self": {
"relay_url": "https://wireup.net",
"slot_id": format!("{slot_id}-fed"),
"slot_token": "fed-tok",
"endpoints": [
{
"relay_url": "https://wireup.net",
"slot_id": format!("{slot_id}-fed"),
"slot_token": "fed-tok",
"scope": "federation"
},
{
"relay_url": local_relay,
"slot_id": format!("{slot_id}-loop"),
"slot_token": "loop-tok",
"scope": "local"
}
]
}
});
std::fs::write(
cfg.join("relay-state.json"),
serde_json::to_vec_pretty(&body).unwrap(),
)
.unwrap();
}
#[test]
fn session_list_local_reports_empty_state_v0_5_19() {
let home = fresh_home();
let out = run(&home, &["session", "list-local"]);
assert!(out.status.success(), "list-local failed: {:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
stdout.contains("no sessions on this machine"),
"expected empty hint, got: {stdout}"
);
}
#[test]
fn session_list_local_groups_by_local_relay_url_v0_5_19() {
let home = fresh_home();
let alpha = write_session_fixture(&home, "alpha", Some("/Users/paul/Source/alpha"));
let beta = write_session_fixture(&home, "beta", Some("/Users/paul/Source/beta"));
let _legacy = write_session_fixture(&home, "legacy", Some("/Users/paul/Source/legacy"));
write_local_endpoint(&alpha, "http://127.0.0.1:8771", "alpha");
write_local_endpoint(&beta, "http://127.0.0.1:8771", "beta");
let out = run(&home, &["session", "list-local", "--json"]);
assert!(out.status.success(), "list-local --json failed: {:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let local_group = parsed["local"]["http://127.0.0.1:8771"]
.as_array()
.expect("expected one local-relay group");
let names: std::collections::HashSet<&str> = local_group
.iter()
.filter_map(|v| v["name"].as_str())
.collect();
assert!(names.contains("alpha"), "alpha missing: {stdout}");
assert!(names.contains("beta"), "beta missing: {stdout}");
assert!(!names.contains("legacy"), "legacy must NOT be in local group: {stdout}");
let fed_only = parsed["federation_only"].as_array().expect("federation_only array");
let fed_names: std::collections::HashSet<&str> = fed_only
.iter()
.filter_map(|v| v["name"].as_str())
.collect();
assert!(fed_names.contains("legacy"), "legacy should be federation-only: {stdout}");
}
#[test]
fn session_list_local_redacts_slot_token_in_json_v0_5_19() {
let home = fresh_home();
let alpha = write_session_fixture(&home, "alpha", None);
write_local_endpoint(&alpha, "http://127.0.0.1:8771", "alpha");
let out = run(&home, &["session", "list-local", "--json"]);
assert!(out.status.success(), "list-local --json failed: {:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
!stdout.contains("loop-tok"),
"slot_token must be redacted from list-local --json: {stdout}"
);
assert!(
!stdout.contains("\"slot_token\""),
"the slot_token field name must not appear: {stdout}"
);
}
#[test]
fn pair_accept_errors_cleanly_when_no_pending_request_v0_5_14() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["pair-accept", "ghost"]);
assert!(!out.status.success(), "expected failure: {:?}", out);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("no pending pair request from ghost"),
"stderr should explain the missing record: {stderr}"
);
assert!(
stderr.contains("wire pair-list-inbound") || stderr.contains("wire add"),
"stderr should hint at the right command: {stderr}"
);
}
#[test]
fn pair_reject_idempotent_on_missing_peer_v0_5_14() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["pair-reject", "ghost", "--json"]);
assert!(out.status.success(), "pair-reject failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["rejected"], false);
}
#[test]
fn send_with_fqdn_peer_normalizes_to_bare_handle_outbox() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(
&home,
&[
"send",
"willard@wireup.net",
"claim",
"fqdn-peer test",
"--json",
],
);
assert!(out.status.success(), "send failed: {:?}", out);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["peer"], "willard", "peer field must be bare handle");
let bare = home.join("state/wire/outbox/willard.jsonl");
let fqdn = home.join("state/wire/outbox/willard@wireup.net.jsonl");
assert!(bare.exists(), "bare-handle outbox missing: {bare:?}");
assert!(
!fqdn.exists(),
"fqdn-suffixed outbox MUST NOT be created: {fqdn:?}"
);
let body = std::fs::read_to_string(&bare).unwrap();
let event: serde_json::Value = serde_json::from_str(body.lines().next().unwrap()).unwrap();
assert_eq!(event["to"], "did:wire:willard");
}
#[test]
fn send_deadline_writes_signed_time_sensitive_until() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let deadline = "2030-01-02T03:04:05Z";
let out = run(
&home,
&[
"send",
"willard",
"decision",
"ship before the window closes",
"--deadline",
deadline,
"--json",
],
);
assert!(out.status.success(), "send failed: {:?}", out);
let outbox = home.join("state/wire/outbox/willard.jsonl");
let body = std::fs::read_to_string(&outbox).unwrap();
let event: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
assert_eq!(event["time_sensitive_until"], deadline);
let event_path = home.join("deadline-event.json");
std::fs::write(&event_path, body.trim_end()).unwrap();
let verify = run(&home, &["verify", event_path.to_str().unwrap(), "--json"]);
assert!(
verify.status.success(),
"verify failed: stderr={}",
String::from_utf8_lossy(&verify.stderr)
);
}
#[test]
fn send_idempotent_under_identical_body() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out1 = run(
&home,
&["send", "willard", "decision", "fixed-body", "--json"],
);
let out2 = run(
&home,
&["send", "willard", "decision", "fixed-body", "--json"],
);
let p1: serde_json::Value = serde_json::from_slice(&out1.stdout).unwrap();
let p2: serde_json::Value = serde_json::from_slice(&out2.stdout).unwrap();
assert_ne!(
p1["event_id"], p2["event_id"],
"iter 6 should make these equal"
);
}
#[test]
fn verify_round_trips_a_send() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let _ = run(&home, &["send", "paul", "decision", "self-test", "--json"]);
let outbox = home.join("state/wire/outbox/paul.jsonl");
let line = std::fs::read_to_string(&outbox).unwrap();
let event_path = home.join("event.json");
std::fs::write(&event_path, line.trim_end()).unwrap();
let out = run(&home, &["verify", event_path.to_str().unwrap(), "--json"]);
assert!(
out.status.success(),
"verify failed: stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["verified"], true);
}
#[test]
fn verify_rejects_tampered_event() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let _ = run(&home, &["send", "paul", "decision", "original", "--json"]);
let outbox = home.join("state/wire/outbox/paul.jsonl");
let line = std::fs::read_to_string(&outbox).unwrap();
let mut event: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
event["body"] = serde_json::json!("tampered");
let event_path = home.join("event.json");
std::fs::write(&event_path, serde_json::to_string(&event).unwrap()).unwrap();
let out = run(&home, &["verify", event_path.to_str().unwrap(), "--json"]);
assert!(!out.status.success(), "verify should have failed");
let s = String::from_utf8(out.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(parsed["verified"], false);
}
#[test]
fn join_alias_resolves_to_pair_join() {
let home = fresh_home();
let out = run(
&home,
&["join", "12-ABCDEF", "--relay", "http://127.0.0.1:1"],
);
assert!(!out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("not initialized") || stderr.contains("healthz"),
"join alias didn't dispatch to pair-join (stderr: {stderr})"
);
}
#[test]
fn mcp_initialize_then_tools_list_round_trip() {
use std::io::Write as _;
use std::process::Stdio;
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let mut child = Command::new(wire_bin())
.arg("mcp")
.env("WIRE_HOME", &home)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn wire mcp");
let initialize = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}"#;
let initialized = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
let tools_list = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, "{initialize}").unwrap();
writeln!(stdin, "{initialized}").unwrap();
writeln!(stdin, "{tools_list}").unwrap();
}
let out = child.wait_with_output().expect("server didn't exit");
assert!(out.status.success(), "mcp server crashed: {:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines.len(),
2,
"expected 2 responses (initialize + tools/list), got {}: {stdout}",
lines.len()
);
let init_resp: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(init_resp["result"]["protocolVersion"], "2025-06-18");
let list_resp: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
let names: Vec<&str> = list_resp["result"]["tools"]
.as_array()
.unwrap()
.iter()
.filter_map(|t| t["name"].as_str())
.collect();
assert!(names.contains(&"wire_whoami"));
assert!(names.contains(&"wire_send"));
assert!(names.contains(&"wire_init"));
assert!(names.contains(&"wire_pair_initiate"));
assert!(names.contains(&"wire_pair_join"));
assert!(names.contains(&"wire_pair_check"));
assert!(names.contains(&"wire_pair_confirm"));
assert!(
!names.contains(&"wire_join"),
"wire_join is the deprecated alias; surface wire_pair_join instead"
);
}
#[test]
fn mcp_tools_call_wire_whoami() {
use std::io::Write as _;
use std::process::Stdio;
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let mut child = Command::new(wire_bin())
.arg("mcp")
.env("WIRE_HOME", &home)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn wire mcp");
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"initialize"}}"#).unwrap();
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"wire_whoami","arguments":{{}}}}}}"#
)
.unwrap();
}
let out = child.wait_with_output().expect("server didn't exit");
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
let last_line = stdout.lines().last().unwrap();
let resp: serde_json::Value = serde_json::from_str(last_line).unwrap();
assert_eq!(resp["result"]["isError"], false);
let text = resp["result"]["content"][0]["text"].as_str().unwrap();
let parsed: serde_json::Value = serde_json::from_str(text).unwrap();
{
let d = parsed["did"].as_str().unwrap();
assert!(d.starts_with("did:wire:paul-"), "got: {d}");
}
assert_eq!(parsed["handle"], "paul");
}
#[test]
fn mcp_tools_call_wire_init_idempotent_on_repeat() {
use std::io::Write as _;
use std::process::Stdio;
let home = fresh_home();
let mut child = Command::new(wire_bin())
.arg("mcp")
.env("WIRE_HOME", &home)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn wire mcp");
{
let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, r#"{{"jsonrpc":"2.0","id":1,"method":"initialize"}}"#).unwrap();
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"wire_init","arguments":{{"handle":"alice"}}}}}}"#
)
.unwrap();
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{{"name":"wire_init","arguments":{{"handle":"alice"}}}}}}"#
)
.unwrap();
}
let out = child.wait_with_output().expect("server didn't exit");
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 3, "expected init + 2 tools/call responses");
let r1: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
assert_eq!(r1["result"]["isError"], false);
let p1: serde_json::Value =
serde_json::from_str(r1["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
{
let d = p1["did"].as_str().unwrap();
assert!(d.starts_with("did:wire:alice-"), "got: {d}");
}
assert_eq!(p1["already_initialized"], false);
let r2: serde_json::Value = serde_json::from_str(lines[2]).unwrap();
assert_eq!(r2["result"]["isError"], false);
let p2: serde_json::Value =
serde_json::from_str(r2["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
assert_eq!(p2["already_initialized"], true);
assert_eq!(p2["fingerprint"], p1["fingerprint"]);
assert!(home.join("config/wire/private.key").exists());
assert!(home.join("config/wire/agent-card.json").exists());
}
#[test]
fn handle_validation_rejects_special_chars() {
let home = fresh_home();
let out = run(&home, &["init", "paul/etc"]);
assert!(!out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("ASCII alphanumeric"), "stderr: {stderr}");
}
#[test]
fn status_before_init_says_not_initialized() {
let home = fresh_home();
let out = run(&home, &["status"]);
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(stdout.contains("not initialized"), "stdout: {stdout}");
}
#[test]
fn status_after_init_shows_did_and_zero_peers() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["status", "--json"]);
assert!(out.status.success());
let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(parsed["initialized"], true);
{
let d = parsed["did"].as_str().unwrap();
assert!(d.starts_with("did:wire:paul-"), "got: {d}");
}
assert_eq!(parsed["peers"].as_array().unwrap().len(), 0);
assert_eq!(parsed["self_relay"], serde_json::Value::Null);
assert_eq!(parsed["outbox"]["events"], 0);
}
#[test]
fn forget_peer_removes_pinned_record() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let trust_path = home.join("config/wire/trust.json");
let mut trust: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&trust_path).unwrap()).unwrap();
trust["agents"]["willard"] = serde_json::json!({"tier": "VERIFIED", "did": "did:wire:willard"});
std::fs::write(&trust_path, serde_json::to_string(&trust).unwrap()).unwrap();
let out = run(&home, &["forget-peer", "willard", "--json"]);
assert!(out.status.success());
let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(parsed["removed_from_trust"], true);
assert_eq!(parsed["handle"], "willard");
let trust_after: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&trust_path).unwrap()).unwrap();
assert!(trust_after["agents"]["willard"].is_null());
}
#[test]
fn forget_peer_unknown_returns_removed_false() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let out = run(&home, &["forget-peer", "ghost", "--json"]);
assert!(out.status.success());
let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(parsed["removed"], false);
}
#[test]
fn forget_peer_purge_deletes_jsonl_files() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let _ = run(&home, &["send", "willard", "decision", "stuff"]);
let outbox_path = home.join("state/wire/outbox/willard.jsonl");
assert!(outbox_path.exists());
let trust_path = home.join("config/wire/trust.json");
let mut trust: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&trust_path).unwrap()).unwrap();
trust["agents"]["willard"] = serde_json::json!({"tier": "VERIFIED"});
std::fs::write(&trust_path, serde_json::to_string(&trust).unwrap()).unwrap();
let out = run(&home, &["forget-peer", "willard", "--purge", "--json"]);
assert!(out.status.success());
let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert!(!parsed["purged_files"].as_array().unwrap().is_empty());
assert!(
!outbox_path.exists(),
"outbox file should be deleted with --purge"
);
}
#[test]
fn status_after_send_shows_outbox_depth() {
let home = fresh_home();
let _ = run(&home, &["init", "paul"]);
let _ = run(&home, &["send", "willard", "decision", "hello"]);
let _ = run(&home, &["send", "willard", "decision", "world"]);
let out = run(&home, &["status", "--json"]);
let parsed: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(parsed["outbox"]["files"], 1);
assert_eq!(parsed["outbox"]["events"], 2);
}