mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
Drives the built `amy` binary against a real `amy serve` relay to prove NIP-77
deletion propagation end-to-end — the running answer to "does it actually work",
on top of the in-process geode tests:
T1 (up) we deleted a note the relay still has → `amy sync` sends our kind-5
up and the relay drops it (checked by an isolated third account that
reads the relay only, so no local tombstone masks the result).
T2 (off) `--no-sync-deletions` sends nothing and the relay keeps the note.
T3 (down) the relay deleted a note we still hold → `amy sync --up` pulls the
relay's kind-5 down and applies it locally; a second sync converges.
Each amy account gets its own $HOME (accounts under one $HOME share the file
store). Follows the cli/tests/*-headless.sh pattern; state dir gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgL1WTV4Hkp2uuXcUHCHGt
197 lines
8.4 KiB
Bash
Executable File
197 lines
8.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# sync-deletions-headless.sh — drives the real `amy` binary against a real
|
|
# `amy serve` relay to prove NIP-77 deletion propagation end-to-end.
|
|
#
|
|
# `amy sync` converges deletions in a second pass over the reconcile residual
|
|
# (see quartz `negentropySettleDeletions`). This exercises both directions plus
|
|
# the opt-out:
|
|
#
|
|
# T1 (up) — we deleted a note the relay still has → `amy sync` sends our
|
|
# kind-5 up and the relay drops the note. Verified by an ISOLATED
|
|
# third account whose store reads the relay only (no tombstone).
|
|
# T2 (off) — same setup with `--no-sync-deletions` → the relay keeps the note
|
|
# and nothing is sent.
|
|
# T3 (down) — the relay deleted a note we still hold → `amy sync --up` pulls the
|
|
# relay's kind-5 down and applies it locally (converges on re-sync).
|
|
#
|
|
# Each amy account gets its OWN $HOME so their file stores don't share (accounts
|
|
# under one $HOME share ~/.amy/shared/events-store). The relay (amy serve) keeps
|
|
# a separate store from any client store.
|
|
#
|
|
# Usage: ./sync-deletions-headless.sh [--port N] [--no-build]
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd -- "$SCRIPT_DIR/../../.." && pwd)"
|
|
TESTS_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|
STATE_DIR="$SCRIPT_DIR/state-sync-deletions"
|
|
LOG_DIR="$STATE_DIR/logs"
|
|
RUN_TS="$(date +%Y%m%d-%H%M%S)"
|
|
LOG_FILE="$LOG_DIR/run-$RUN_TS.log"
|
|
RESULTS_FILE="$STATE_DIR/results-$RUN_TS.tsv"
|
|
|
|
AMY_BIN="$REPO_ROOT/cli/build/install/amy/bin/amy"
|
|
RELAY_HOST="127.0.0.1"
|
|
RELAY_PORT="${RELAY_PORT:-7790}"
|
|
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"
|
|
NO_BUILD=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--port) RELAY_PORT="$2"; RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT"; shift ;;
|
|
--no-build) NO_BUILD=1 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Fresh state every run — stale per-account $HOME dirs from a prior run must not
|
|
# leak into this one.
|
|
rm -rf "$STATE_DIR"
|
|
mkdir -p "$LOG_DIR"
|
|
: >"$RESULTS_FILE"
|
|
|
|
# shellcheck source=../lib.sh
|
|
source "$TESTS_DIR/lib.sh"
|
|
|
|
# Leniently-trimmed equality assertion (assert helpers live in the DM-specific
|
|
# helpers.sh, which hardcodes its own amy wrappers — so define our own here).
|
|
assert_eq() {
|
|
local actual="$1" expected="$2" test_id="$3" note="${4:-}"
|
|
if [[ "${actual// /}" == "${expected// /}" ]]; then
|
|
info "assert: $test_id \"$actual\" == \"$expected\""
|
|
return 0
|
|
fi
|
|
fail_msg "$test_id: expected \"$expected\", got \"$actual\" (${note:-})"
|
|
record_result "$test_id" fail "${note:-mismatch}"
|
|
return 1
|
|
}
|
|
|
|
SERVE_PID=""
|
|
RELAY_HOME=""
|
|
cleanup() {
|
|
[[ -n "$SERVE_PID" ]] && kill "$SERVE_PID" 2>/dev/null
|
|
trap - EXIT INT TERM HUP
|
|
print_summary
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
banner "amy sync — NIP-77 deletion propagation headless ($RUN_TS)"
|
|
|
|
# ---- build ------------------------------------------------------------------
|
|
if [[ "$NO_BUILD" -eq 0 ]]; then
|
|
step "Building amy (installDist)…"
|
|
(cd "$REPO_ROOT" && ./gradlew -q :cli:installDist) >>"$LOG_FILE" 2>&1 \
|
|
|| { fail_msg "build failed (see $LOG_FILE)"; exit 1; }
|
|
fi
|
|
[[ -x "$AMY_BIN" ]] || { fail_msg "amy binary not found at $AMY_BIN"; exit 1; }
|
|
|
|
# ---- amy wrappers (one isolated $HOME per account) --------------------------
|
|
strip() { grep -vE "Picked up JAVA_TOOL|DEBUG:|INFO:|MarmotManager|MlsGroup"; }
|
|
mk_home() { mktemp -d "$STATE_DIR/home.XXXXXX"; }
|
|
# amy_run <home> <account> args...
|
|
amy_run() {
|
|
local home="$1" acct="$2"; shift 2
|
|
HOME="$home" "$AMY_BIN" --account "$acct" --secret-backend plaintext --json "$@" 2>>"$LOG_FILE" | strip
|
|
}
|
|
|
|
RELAY_HOME="$(mk_home)"
|
|
amy_run "$RELAY_HOME" a init >/dev/null
|
|
|
|
step "Starting amy serve on $RELAY_URL…"
|
|
HOME="$RELAY_HOME" "$AMY_BIN" --account a --secret-backend plaintext \
|
|
serve --host "$RELAY_HOST" --port "$RELAY_PORT" >>"$LOG_FILE" 2>&1 &
|
|
SERVE_PID=$!
|
|
|
|
# Wait for the relay to accept connections (poll the serve log).
|
|
for _ in $(seq 1 60); do
|
|
grep -q "relay up at" "$LOG_FILE" && break
|
|
sleep 0.5
|
|
done
|
|
grep -q "relay up at" "$LOG_FILE" || { fail_msg "relay did not come up"; exit 1; }
|
|
|
|
# Isolated verifier: its own empty store, reads the relay only (no tombstone).
|
|
VERIFY_HOME="$(mk_home)"
|
|
amy_run "$VERIFY_HOME" v init >/dev/null
|
|
relay_count() { amy_run "$VERIFY_HOME" v fetch --id "$1" --relay "$RELAY_URL" | jq -r '.count // 0'; }
|
|
|
|
# =============================================================================
|
|
# T1 — up direction: we deleted it, the relay still has it → sync sends it up.
|
|
# =============================================================================
|
|
banner "T1 — amy sync sends our deletion up (relay drops the note)"
|
|
NOTE="$(amy_run "$RELAY_HOME" a event --kind 1 --content "delete-me-t1" | jq -c '.event')"
|
|
NID="$(echo "$NOTE" | jq -r '.id')"
|
|
echo "$NOTE" | amy_run "$RELAY_HOME" a publish --relay "$RELAY_URL" >/dev/null
|
|
|
|
before="$(relay_count "$NID")"
|
|
assert_eq "$before" "1" T1.setup "relay should hold the note before sync" \
|
|
&& record_result T1.setup pass "relay has the note"
|
|
|
|
# Delete locally only (no --relay → stored, applied, not sent to the relay).
|
|
amy_run "$RELAY_HOME" a event --kind 5 --tags "[[\"e\",\"$NID\"]]" --content "" --publish >/dev/null
|
|
|
|
SYNC="$(amy_run "$RELAY_HOME" a sync --relay "$RELAY_URL")"
|
|
info "sync: $SYNC"
|
|
sent="$(echo "$SYNC" | jq -r '.deletions_sent_up // 0')"
|
|
assert_eq "$sent" "1" T1.sent_up "sync should report one deletion sent up" \
|
|
&& record_result T1.sent_up pass "deletions_sent_up=1"
|
|
|
|
sleep 1
|
|
after="$(relay_count "$NID")"
|
|
assert_eq "$after" "0" T1.relay_dropped "relay must have removed the note after sync" \
|
|
&& record_result T1.relay_dropped pass "relay note count 1 → 0"
|
|
|
|
# =============================================================================
|
|
# T2 — opt-out: --no-sync-deletions leaves the relay untouched.
|
|
# =============================================================================
|
|
banner "T2 — --no-sync-deletions propagates nothing"
|
|
NOTE2="$(amy_run "$RELAY_HOME" a event --kind 1 --content "keep-me-t2" | jq -c '.event')"
|
|
NID2="$(echo "$NOTE2" | jq -r '.id')"
|
|
echo "$NOTE2" | amy_run "$RELAY_HOME" a publish --relay "$RELAY_URL" >/dev/null
|
|
amy_run "$RELAY_HOME" a event --kind 5 --tags "[[\"e\",\"$NID2\"]]" --content "" --publish >/dev/null
|
|
|
|
SYNC2="$(amy_run "$RELAY_HOME" a sync --relay "$RELAY_URL" --no-sync-deletions)"
|
|
info "sync: $SYNC2"
|
|
sent2="$(echo "$SYNC2" | jq -r '.deletions_sent_up // 0')"
|
|
assert_eq "$sent2" "0" T2.no_send "--no-sync-deletions must send nothing" \
|
|
&& record_result T2.no_send pass "deletions_sent_up=0"
|
|
sleep 1
|
|
kept="$(relay_count "$NID2")"
|
|
assert_eq "$kept" "1" T2.relay_kept "relay must still hold the note" \
|
|
&& record_result T2.relay_kept pass "relay note untouched"
|
|
|
|
# =============================================================================
|
|
# T3 — down direction: the relay deleted it, we still hold it → sync --up pulls
|
|
# the relay's deletion down and applies it locally.
|
|
# =============================================================================
|
|
banner "T3 — amy sync --up applies the relay's deletion locally"
|
|
BOB_HOME="$(mk_home)"
|
|
amy_run "$BOB_HOME" b init >/dev/null
|
|
NOTE3="$(amy_run "$RELAY_HOME" a event --kind 1 --content "delete-me-t3" | jq -c '.event')"
|
|
NID3="$(echo "$NOTE3" | jq -r '.id')"
|
|
echo "$NOTE3" | amy_run "$RELAY_HOME" a publish --relay "$RELAY_URL" >/dev/null
|
|
# bob's isolated store learns the note from the relay…
|
|
amy_run "$BOB_HOME" b fetch --id "$NID3" --relay "$RELAY_URL" >/dev/null
|
|
# …then the relay deletes it (author pushes a kind-5 straight to the relay).
|
|
amy_run "$RELAY_HOME" a event --kind 5 --tags "[[\"e\",\"$NID3\"]]" --content "" | jq -c '.event' \
|
|
| amy_run "$RELAY_HOME" a publish --relay "$RELAY_URL" >/dev/null
|
|
|
|
SYNC3="$(amy_run "$BOB_HOME" b sync --up --relay "$RELAY_URL")"
|
|
info "sync: $SYNC3"
|
|
applied="$(echo "$SYNC3" | jq -r '.deletions_applied_down // 0')"
|
|
assert_eq "$applied" "1" T3.applied_down "sync --up should apply one relay deletion locally" \
|
|
&& record_result T3.applied_down pass "deletions_applied_down=1"
|
|
|
|
# Converged: a second --up sync finds nothing left to apply.
|
|
SYNC3B="$(amy_run "$BOB_HOME" b sync --up --relay "$RELAY_URL")"
|
|
applied2="$(echo "$SYNC3B" | jq -r '.deletions_applied_down // 0')"
|
|
assert_eq "$applied2" "0" T3.converged "re-sync applies nothing (converged)" \
|
|
&& record_result T3.converged pass "second sync stable"
|
|
|
|
# print_summary runs from the cleanup trap; exit non-zero if any test failed.
|
|
grep -q $'\tfail\t' "$RESULTS_FILE" && exit 1
|
|
exit 0
|