itsgoin/scripts/a3_integration_test.sh
Scott Reimers e756fabb11 feat: v0.8 Iteration C — narrow mesh, N1-N4 uniques index, anchor convection
Topology rework. The mesh's job is width; depth now comes from knowledge.

Slots:
- Local/Wide collapse into ONE mesh pool: 20 desktop / 15 mobile
- Temp referral slots +4..+10 ABOVE the cap (10 desktop / 4 mobile), carry no
  knowledge exchange, graduate into a freed mesh slot or expire (5 min TTL),
  never evict an established mesh peer
- Fixes the Wide-never-fills bug: outbound paths hardcoded Local while the
  growth gate counted Local only, so growth stopped at 71 and 20 slots were
  inbound-only. Also closes register_connection paths that enforced no cap.

Knowledge — two-pool uniques announce (replaces N1/N2/N3 share lists):
- Pool 1 (forwardable): our N0-N2 uniques, deduped. Receiver stores shifted one
  bounce deeper as N1-N3, reporter-tagged.
- Pool 2 (terminal): our N3, deduped against pool 1. Receiver stores as N4,
  USED for search/resolution but NEVER re-announced. No N5 anywhere.
- Uniques merge mesh peers + social directs + CDN file authors + holder peers,
  so receivers cannot tell how we know an ID
- Anchor entries carry addresses; every other entry is a bare ID. The pools
  double as the anchor directory.
- Dedup at both store and announce; per-bounce caps; 2 MB payload ceiling
- Device-tiered depth: mobile holds 3 bounces and drops the terminal pool

Anchor convection (register loop retired — 0xC0 AnchorRegister deleted):
- Rolling in-memory caller window (no registration DB): a caller gets the 2
  most recent viable prior callers, its address goes to the next 2, then
  rotates out; still-connected callers preferred; RelayIntroduce coordinated
  when both ends are live
- Request classes: entry (<2 connections) always served; top-up refused
  cheaply under load, and the refusal feeds the adaptive weights
- Stochastic per-disconnect action {nothing | anchor intro | mesh intro} with
  weights from local anchor density + refusal feedback. No thresholds, no
  jitter timer. Recovery (mesh < 2) stays immediate and non-stochastic.
- Anchors mined from the uniques pools incl. retained pools of disconnected
  peers; known_anchors demoted to a bootstrap cache

Security fixes from review: remote address poisoning (hearsay anchors could be
dialled — known_anchors/is_anchor now written only from completed handshakes),
N4 leaking back into announcements via the anchor mirror (infinite propagation),
missing payload size caps, unbounded candidate scoring, and a bulk SQLite write
held under the ConnectionManager mutex.

deploy.sh: version regex now captures pre-release suffixes (0.8.0-alpha would
have truncated to 0.8.0 and broken every artifact path), announce channel
derives from the suffix, and the anchor runs --publish-registry at genesis.

228 core tests; a3 integration 9/9; new c_topology_test.sh 33/33 (both
independently re-run). EDM corpse, session-relay gating, Iteration A/B intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGiPD2cF75mnvneSCjdDC5
2026-07-30 13:03:12 -04:00

142 lines
5.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# v0.8 A3 integration test — registry + greetings across 3 local nodes.
#
# Spec: A3-SPEC §6 multi-node scenario. Run from the repo root after
# `cargo build -p itsgoin-cli`. Uses FIFOs to drive the interactive REPL.
#
# 1. All nodes self-materialize the same registry post ID; node1 also
# runs the --publish-registry genesis one-shot (debug gate override).
# 2. node2 registers "Alice rust p2p" → node3 `search rust` finds her.
# node2 `unregister` → node3 re-search finds nothing.
# 3. node2's bio has a greeting slot; node3 greets it; node2 sees the
# unsealed text + real sender persona; node2 replies; node3 sees the
# reply. node1 (holder) stores only ciphertext.
# 5. Newest-wins: node2 re-registers; node3 sees exactly one entry.
# 6. (Optional, ITSGOIN_TEST_TTL_SECS) comment expiry sweep.
#
# Exit code 0 = all checks passed. Logs: /tmp/itsgoin-cli{1,2,3}.log
set -u
cd "$(dirname "$0")/.."
BIN=target/debug/itsgoin
[ -x "$BIN" ] || { echo "build first: cargo build -p itsgoin-cli"; exit 2; }
PASS=0; FAIL=0
check() { # check <desc> <cmd...>
local desc="$1"; shift
if "$@" >/dev/null 2>&1; then PASS=$((PASS+1)); echo "PASS: $desc";
else FAIL=$((FAIL+1)); echo "FAIL: $desc"; fi
}
strip_ansi() { sed 's/\x1b\[[0-9;]*m//g' "$1" | grep -v "WARN.*netlink\|WARN.*buffer_tool"; }
# Same noise filter for the tail-window checks below. The netlink/mDNS watcher
# logs in unpredictable bursts, and a burst landing between a REPL command and
# its assertion pushes the answer out of a raw `tail -N` window — a false
# failure with nothing wrong in the node.
quiet_tail() { grep -av "netlink\|buffer_tool\|iroh_quinn\|swarm_discovery" "$1" | tail -"$2"; }
export -f quiet_tail
sq() { sqlite3 "/tmp/itsgoin-test$1/itsgoin.db" "$2"; }
cleanup() {
{ exec 13>&- 14>&- 15>&-; } 2>/dev/null || true
kill $(pgrep -f 'itsgoin.*itsgoin-test') 2>/dev/null
rm -f /tmp/itsgoin-cmd{1,2,3}
}
trap cleanup EXIT
echo "== setup =="
kill $(pgrep -f 'itsgoin.*itsgoin-test') 2>/dev/null; sleep 1
for i in 1 2 3; do
rm -rf /tmp/itsgoin-test$i /tmp/itsgoin-cmd$i /tmp/itsgoin-cli$i.log
mkdir -p /tmp/itsgoin-test$i
mkfifo /tmp/itsgoin-cmd$i
done
export ITSGOIN_TEST_ALLOW_REGISTRY_GENESIS=1
# Genesis one-shot on node1's data dir (debug-gate override).
"$BIN" /tmp/itsgoin-test1 --bind 127.0.0.1:18411 --publish-registry > /tmp/itsgoin-genesis.log 2>&1
check "genesis prints registry post id matching shipped constant" \
bash -c 'g=$(grep registry_post_id /tmp/itsgoin-genesis.log | awk "{print \$2}");
c=$(grep shipped_constant /tmp/itsgoin-genesis.log | awk "{print \$2}");
[ -n "$g" ] && [ "$g" = "$c" ]'
# Start the three nodes with FIFO-driven stdin.
for i in 1 2 3; do
( exec "$BIN" /tmp/itsgoin-test$i --bind 127.0.0.1:1841$i \
< /tmp/itsgoin-cmd$i > /tmp/itsgoin-cli$i.log 2>&1 ) &
done
# Keep FIFO write ends open for the whole run.
exec 13>/tmp/itsgoin-cmd1 14>/tmp/itsgoin-cmd2 15>/tmp/itsgoin-cmd3
sleep 6
n1_id=$(strip_ansi /tmp/itsgoin-cli1.log | grep -m1 "Node ID:" | awk '{print $3}')
n2_id=$(strip_ansi /tmp/itsgoin-cli2.log | grep -m1 "Node ID:" | awk '{print $3}')
echo "node1=$n1_id node2=$n2_id"
check "all nodes self-materialized the registry post" \
bash -c 'for i in 1 2 3; do sqlite3 /tmp/itsgoin-test$i/itsgoin.db \
"SELECT count(*) FROM posts WHERE hex(id) = upper(\"'"$(grep shipped_constant /tmp/itsgoin-genesis.log | awk '{print $2}')"'\")" \
| grep -q 1 || exit 1; done'
echo "== mesh: 2,3 -> 1; 3 -> 2 =="
echo "connect $n1_id@127.0.0.1:18411" >&14
echo "connect $n1_id@127.0.0.1:18411" >&15
sleep 4
echo "connect $n2_id@127.0.0.1:18412" >&15
sleep 4
echo "== step 2: register / search / unregister =="
echo "name Alice" >&14
sleep 3
echo "register Alice rust p2p" >&14
sleep 3
echo "search rust" >&15
sleep 8
check "node3 search finds Alice" grep -q "Alice" /tmp/itsgoin-cli3.log
echo "unregister" >&14
sleep 3
echo "search rust" >&15
sleep 8
check "node3 re-search finds nothing after signed delete" \
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 20 | grep -q "no registry matches"'
echo "== step 3: greeting roundtrip =="
# node2's bio post id (latest Profile post) from its DB.
bio2=$(sq 2 'SELECT lower(hex(id)) FROM posts WHERE visibility_intent = '"'"'"Profile"'"'"' ORDER BY timestamp_ms DESC LIMIT 1')
check "node2 has a bio post with a greeting slot" \
bash -c '[ -n "'"$bio2"'" ] && sqlite3 /tmp/itsgoin-test2/itsgoin.db \
"SELECT fof_gating_json FROM posts WHERE lower(hex(id))=\"'"$bio2"'\"" | grep -q Greeting'
echo "name Bob" >&15
sleep 3
echo "greet $bio2 hello alice from bob" >&15
sleep 6
echo "greetings" >&14
sleep 4
check "node2 unseals the greeting text" grep -q "hello alice from bob" /tmp/itsgoin-cli2.log
echo "reply 0 hello back bob" >&14
sleep 6
echo "greetings" >&15
sleep 4
check "node3 unseals the reply via its stored fresh reply key" \
grep -q "hello back bob" /tmp/itsgoin-cli3.log
# Holder-side opacity: node1 never sees plaintext greeting bodies.
check "node1 stores only ciphertext (no greeting plaintext in db)" \
bash -c '! sqlite3 /tmp/itsgoin-test1/itsgoin.db \
"SELECT content FROM comments" | grep -q "hello alice"'
echo "== step 5: newest-wins =="
echo "register Alice rust p2p" >&14
sleep 2
echo "register AliceV2 rust mesh" >&14
sleep 4
echo "search rust" >&15
sleep 8
check "node3 sees exactly one (newest) entry for node2's persona" \
bash -c 'quiet_tail /tmp/itsgoin-cli3.log 6 | grep -c "rust" | grep -q "^1$" &&
quiet_tail /tmp/itsgoin-cli3.log 6 | grep -q AliceV2'
echo
echo "== results: $PASS passed, $FAIL failed =="
[ "$FAIL" -eq 0 ]