Files
waste-go/test-network.sh
Fredrik Johansson 5a05e6e1ef Add DM support to test script; fix DM routing and JSON encoding
ipc: CmdSendMessage now routes to the named recipient only (m.SendTo) when
the "to" field is set, rather than broadcasting to all peers. Group messages
continue to use Broadcast. This is the correct privacy behaviour for DMs.

test-network.sh:
- Resolve each peer's hex id from get_state before joining the network,
  using a retry loop with timeout to handle slow daemon startup.
- Added a DM section: alice→bob, bob→alice, charlie→alice, alice→charlie.
  DMs use the "to" + "room":"dm:<peer_id>" convention.
- Fixed jq -cn (compact) instead of jq -n (pretty) so the IPC newline-
  delimited protocol receives a single JSON line per command.
- pretty() now renders DMs as 📨 DM <from→to> rather than 💬 #room.
- Persistence check now breaks down totals by group vs DM message count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 18:13:27 +02:00

340 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
# test-network.sh — spin up anchor + 3 peers, connect them, exchange messages + DMs.
# Usage: ./test-network.sh
# Requires: go, nc, jq, sqlite3
set -euo pipefail
# ── colours ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; BLUE='\033[0;34m'
BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
ANCHOR_COLOR=$YELLOW
ALICE_COLOR=$CYAN
BOB_COLOR=$MAGENTA
CHARLIE_COLOR=$GREEN
# ── config ────────────────────────────────────────────────────────────────────
ANCHOR_PORT=19339
ALICE_IPC=19337
BOB_IPC=19340
CHARLIE_IPC=19343
NETWORK_NAME="test-$(date +%s)"
DATA_ROOT=$(mktemp -d /tmp/waste-test-XXXXXX)
# ── cleanup ───────────────────────────────────────────────────────────────────
PIDS=()
cleanup() {
echo ""
echo -e "${DIM}── cleaning up ──────────────────────────────────────────${RESET}"
for pid in "${PIDS[@]:-}"; do
kill "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
echo -e "${DIM}data left at: ${DATA_ROOT}${RESET}"
echo -e "${DIM} inspect: sqlite3 ${DATA_ROOT}/alice/messages.db${RESET}"
echo -e "${DIM}done.${RESET}"
}
trap cleanup EXIT INT TERM
# ── helpers ───────────────────────────────────────────────────────────────────
log() {
local color="$1"; shift
local label="$1"; shift
echo -e "${color}${BOLD}[${label}]${RESET} $*"
}
# Pretty-print a raw IPC JSON line
pretty() {
local color="$1"
local label="$2"
local line="$3"
local type
type=$(echo "$line" | jq -r '.type // "?"' 2>/dev/null)
case "$type" in
state_snapshot)
local id nick
id=$(echo "$line" | jq -r '.local_peer.id[:16]' 2>/dev/null | sed 's/.\{4\}/& /g')
nick=$(echo "$line" | jq -r '.local_peer.alias' 2>/dev/null)
echo -e "${color}${BOLD}[${label}]${RESET} ${DIM}state_snapshot${RESET} id=${CYAN}${id}${RESET} alias=${BOLD}${nick}${RESET}"
;;
session_ready)
local pid nick
pid=$(echo "$line" | jq -r 'if .peer_id then .peer_id[:16] else "?" end' 2>/dev/null | sed 's/.\{4\}/& /g')
nick=$(echo "$line" | jq -r '.nick // "?"' 2>/dev/null)
echo -e "${color}${BOLD}[${label}]${RESET} ${GREEN}✓ session_ready${RESET} peer=${BOLD}${nick}${RESET} (${DIM}${pid}${RESET})"
;;
peer_connected)
local nick
nick=$(echo "$line" | jq -r '.peer.alias // "?"' 2>/dev/null)
echo -e "${color}${BOLD}[${label}]${RESET} ${GREEN}→ peer_connected${RESET} ${BOLD}${nick}${RESET}"
;;
peer_disconnected)
local pid
pid=$(echo "$line" | jq -r 'if .peer_id then .peer_id[:8] else "?" end' 2>/dev/null)
echo -e "${color}${BOLD}[${label}]${RESET} ${RED}← peer_disconnected${RESET} ${DIM}${pid}${RESET}"
;;
message_received)
local from body room to_field
from=$(echo "$line" | jq -r '.message.from[:8] // "?"' 2>/dev/null)
body=$(echo "$line" | jq -r '.message.body // ""' 2>/dev/null)
room=$(echo "$line" | jq -r '.message.room // ""' 2>/dev/null)
to_field=$(echo "$line" | jq -r '.message.to // ""' 2>/dev/null)
if [ -n "$to_field" ]; then
local to_short
to_short=$(echo "$to_field" | cut -c1-8)
echo -e "${color}${BOLD}[${label}]${RESET} ${BLUE}📨 DM${RESET} ${DIM}<${from}…→${to_short}…>${RESET} ${body}"
else
echo -e "${color}${BOLD}[${label}]${RESET} ${BOLD}💬 #${room}${RESET} ${DIM}<${from}…>${RESET} ${body}"
fi
;;
error)
local msg
msg=$(echo "$line" | jq -r '.error_message // .' 2>/dev/null)
echo -e "${color}${BOLD}[${label}]${RESET} ${RED}✗ error:${RESET} ${msg}"
;;
*)
echo -e "${color}${BOLD}[${label}]${RESET} ${DIM}${line}${RESET}"
;;
esac
}
# Subscribe to a peer's IPC events in the background, pretty-printing each line.
subscribe() {
local color="$1"
local label="$2"
local port="$3"
(
set +e
nc 127.0.0.1 "$port" 2>/dev/null | while IFS= read -r line; do
pretty "$color" "$label" "$line"
done
) &
PIDS+=($!)
}
# Send a single JSON command to a peer's IPC port (fire-and-forget).
ipc() {
local port="$1"
local json="$2"
echo "$json" | nc -q 0 127.0.0.1 "$port" >/dev/null 2>&1 || true
}
# Query get_state and return a single field via jq from the state_snapshot line.
# Retries for up to 3 seconds in case the daemon is still initialising.
# Usage: peer_field <ipc_port> <jq_expr>
peer_field() {
local port="$1"
local expr="$2"
local result=""
local n=0
while [ -z "$result" ] || [ "$result" = "null" ]; do
result=$(echo '{"type":"get_state"}' \
| timeout 2 nc 127.0.0.1 "$port" 2>/dev/null \
| grep '"type":"state_snapshot"' \
| head -1 \
| jq -r "$expr" 2>/dev/null || true)
n=$(( n + 1 ))
[ "$n" -ge 10 ] && break
[ -z "$result" ] || [ "$result" = "null" ] && sleep 0.3
done
echo "$result"
}
# Wait until a TCP port accepts connections (up to N seconds).
wait_port() {
local port="$1"
local label="$2"
local n=0
while ! nc -z 127.0.0.1 "$port" 2>/dev/null; do
sleep 0.1
n=$(( n + 1 ))
if [ "$n" -gt 80 ]; then
echo -e "${RED}timeout waiting for ${label} on :${port}${RESET}" >&2
exit 1
fi
done
}
# ── build ─────────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}waste-go network test${RESET}"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "${DIM}network : ${BOLD}${NETWORK_NAME}${RESET}"
echo -e "${DIM}data : ${DATA_ROOT}${RESET}"
echo ""
echo -e "${DIM}building binaries…${RESET}"
go build -o "$DATA_ROOT/bin/waste-anchor" ./cmd/anchor
go build -o "$DATA_ROOT/bin/waste-daemon" ./cmd/daemon
echo -e "${GREEN}✓ built${RESET}"
echo ""
# ── anchor ────────────────────────────────────────────────────────────────────
log "$ANCHOR_COLOR" "anchor" "starting on :${ANCHOR_PORT}"
"$DATA_ROOT/bin/waste-anchor" -bind "127.0.0.1:${ANCHOR_PORT}" \
2> >(while IFS= read -r l; do echo -e "${ANCHOR_COLOR}${DIM}[anchor] ${l}${RESET}"; done) &
PIDS+=($!)
wait_port "$ANCHOR_PORT" "anchor"
log "$ANCHOR_COLOR" "anchor" "ready"
echo ""
# ── daemons ───────────────────────────────────────────────────────────────────
ANCHOR_URL="ws://127.0.0.1:${ANCHOR_PORT}/ws"
log "$ALICE_COLOR" "alice" "starting daemon (ipc :${ALICE_IPC})"
"$DATA_ROOT/bin/waste-daemon" \
-alias alice \
-data-dir "$DATA_ROOT/alice" \
-ipc-port "$ALICE_IPC" \
-anchor "$ANCHOR_URL" \
2> >(while IFS= read -r l; do echo -e "${ALICE_COLOR}${DIM}[alice] ${l}${RESET}"; done) &
PIDS+=($!)
log "$BOB_COLOR" "bob" "starting daemon (ipc :${BOB_IPC})"
"$DATA_ROOT/bin/waste-daemon" \
-alias bob \
-data-dir "$DATA_ROOT/bob" \
-ipc-port "$BOB_IPC" \
-anchor "$ANCHOR_URL" \
2> >(while IFS= read -r l; do echo -e "${BOB_COLOR}${DIM}[bob] ${l}${RESET}"; done) &
PIDS+=($!)
log "$CHARLIE_COLOR" "charlie" "starting daemon (ipc :${CHARLIE_IPC})"
"$DATA_ROOT/bin/waste-daemon" \
-alias charlie \
-data-dir "$DATA_ROOT/charlie" \
-ipc-port "$CHARLIE_IPC" \
-anchor "$ANCHOR_URL" \
2> >(while IFS= read -r l; do echo -e "${CHARLIE_COLOR}${DIM}[charlie]${l}${RESET}"; done) &
PIDS+=($!)
wait_port "$ALICE_IPC" "alice"
wait_port "$BOB_IPC" "bob"
wait_port "$CHARLIE_IPC" "charlie"
echo ""
# ── subscribe ─────────────────────────────────────────────────────────────────
echo -e "${DIM}subscribing to IPC event streams…${RESET}"
subscribe "$ALICE_COLOR" "alice " "$ALICE_IPC"
subscribe "$BOB_COLOR" "bob " "$BOB_IPC"
subscribe "$CHARLIE_COLOR" "charlie" "$CHARLIE_IPC"
sleep 0.3 # let state_snapshot lines arrive
# ── resolve peer IDs ──────────────────────────────────────────────────────────
# Each daemon knows its own id from the state_snapshot.
ALICE_ID=$(peer_field "$ALICE_IPC" '.local_peer.id')
BOB_ID=$(peer_field "$BOB_IPC" '.local_peer.id')
CHARLIE_ID=$(peer_field "$CHARLIE_IPC" '.local_peer.id')
log "$ANCHOR_COLOR" "ids" "alice = ${ALICE_ID:0:16}"
log "$ANCHOR_COLOR" "ids" "bob = ${BOB_ID:0:16}"
log "$ANCHOR_COLOR" "ids" "charlie = ${CHARLIE_ID:0:16}"
echo ""
# ── join network ──────────────────────────────────────────────────────────────
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "joining all peers to network: ${BOLD}${NETWORK_NAME}${RESET}"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
JOIN=$(printf '{"type":"join_network","network_name":"%s"}' "$NETWORK_NAME")
ipc "$ALICE_IPC" "$JOIN"
sleep 0.2
ipc "$BOB_IPC" "$JOIN"
sleep 0.2
ipc "$CHARLIE_IPC" "$JOIN"
echo -e "${DIM}waiting for ICE / DataChannel setup (up to 10s)…${RESET}"
sleep 6
# ── group chat ────────────────────────────────────────────────────────────────
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "group chat (#general)"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
ipc "$ALICE_IPC" '{"type":"send_message","room":"general","body":"hey everyone, alice here"}'
sleep 0.4
ipc "$BOB_IPC" '{"type":"send_message","room":"general","body":"hi alice! bob checking in"}'
sleep 0.4
ipc "$CHARLIE_IPC" '{"type":"send_message","room":"general","body":"charlie online. the mesh works!"}'
sleep 0.4
ipc "$ALICE_IPC" '{"type":"send_message","room":"general","body":"nice — three-way chat over WebRTC 🎉"}'
sleep 0.4
ipc "$BOB_IPC" '{"type":"send_message","room":"general","body":"no central server, fully peer-to-peer"}'
sleep 0.4
# ── direct messages ───────────────────────────────────────────────────────────
# DMs set the "to" field to the recipient's full hex peer ID.
# The room field is used as "dm:<recipient_id>" by convention so both sides
# can query their DM history for the same conversation.
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "direct messages"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
# Alice → Bob (privately)
ipc "$ALICE_IPC" "$(jq -cn \
--arg to "$BOB_ID" \
--arg room "dm:$BOB_ID" \
--arg body "hey bob, this is just between us" \
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
sleep 0.4
# Bob replies to Alice
ipc "$BOB_IPC" "$(jq -cn \
--arg to "$ALICE_ID" \
--arg room "dm:$ALICE_ID" \
--arg body "got it alice, loud and clear 🤫" \
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
sleep 0.4
# Charlie → Alice (privately)
ipc "$CHARLIE_IPC" "$(jq -cn \
--arg to "$ALICE_ID" \
--arg room "dm:$ALICE_ID" \
--arg body "alice, can you hear me? charlie dm test" \
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
sleep 0.4
# Alice replies to Charlie
ipc "$ALICE_IPC" "$(jq -cn \
--arg to "$CHARLIE_ID" \
--arg room "dm:$CHARLIE_ID" \
--arg body "yes charlie, DMs working perfectly!" \
'{"type":"send_message","room":$room,"body":$body,"to":$to}')"
sleep 0.4
# ── leave ─────────────────────────────────────────────────────────────────────
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "charlie leaves the network"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
ipc "$CHARLIE_IPC" '{"type":"leave_network"}'
sleep 1
# ── persistence check ─────────────────────────────────────────────────────────
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "verifying persistence (SQLite)"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
for peer in alice bob charlie; do
db="$DATA_ROOT/$peer/messages.db"
if [ -f "$db" ]; then
total=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages;" 2>/dev/null || echo "?")
group=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages WHERE room='general';" 2>/dev/null || echo "?")
dms=$(sqlite3 "$db" "SELECT COUNT(*) FROM messages WHERE room LIKE 'dm:%';" 2>/dev/null || echo "?")
peers_count=$(sqlite3 "$db" "SELECT COUNT(*) FROM peers;" 2>/dev/null || echo "?")
echo -e " ${BOLD}${peer}${RESET}: ${total} total (${group} group, ${dms} DM), ${peers_count} known peers"
else
echo -e " ${RED}${peer}: no messages.db found${RESET}"
fi
done
echo ""
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
echo -e "${GREEN}${BOLD}test complete${RESET}"
echo -e "${DIM}────────────────────────────────────────────────────────${RESET}"
sleep 0.5