feat(marmot-interop): discover A's relays instead of forcing a shared set

The interop harness was forcing specific relays on the Amethyst account —
"configure Amethyst with these five relays, also add them to your DM
Inbox list, also to your Key Package Relays, then republish" — which
made the test artificial:

  - It masked the real failure modes (wn publishing to an A-advertised
    relay wn can't reach, A's 10050 pointing at auth-required relays,
    etc.) because everyone was always on the same happy-path set.
  - It mutated the operator's real Amethyst account config — those
    five relays persist after the run.
  - It short-circuited wn's discovery chain (kind:10002 → 10050 →
    10051 resolution), so the one path most likely to hold subtle bugs
    was never exercised under divergent configs.

Real-world interop is: both clients have their own independently-chosen
relay sets and use the advertised kind:10002/10050/10051 to find each
other. Make the harness match that:

  - Keep the five-relay set as the wn B/C bootstrap (they still need
    somewhere to publish their own identity). Drop the "configure
    Amethyst to match" steps from `instruct_amethyst_setup`;
    confirmation only now ("is A logged in as <npub>, and has A
    published its four lists to any public relay?").

  - Add `discover_a_relays` step between configure + the operator
    prompt. Calls `wn keys check $A_NPUB` on both daemons to trigger
    wn's targeted fetch for kinds [0, 10002, 10050, 10051] (the
    only side-effect available to populate wn's user_relays cache).
    Then reads the cache directly via sqlite3 against wn's DB and
    prints what wn actually learned, grouped by relay_type. Warns
    loudly if A's 10050 shares zero relays with wn's bootstrap — the
    exact failure mode that caused the prior round of "Test 03
    silently times out" reports.

  - `--local-relays` path keeps the old behavior: offline/sandbox
    mode, the harness owns the relay so forcing Amethyst onto it is
    correct.

Scope is the harness only. No Amethyst / quartz / commons changes.
This commit is contained in:
Claude
2026-04-22 21:13:16 +00:00
parent 34f6be225f
commit a6388a6751
+154 -21
View File
@@ -255,6 +255,117 @@ prompt_for_a_npub() {
[[ "$A_HEX" != "$A_NPUB" ]] && info "A hex: $A_HEX" [[ "$A_HEX" != "$A_NPUB" ]] && info "A hex: $A_HEX"
} }
# Trigger wn's on-demand discovery for A and dump what wn actually
# learned — kinds 10002 / 10050 / 10051 from A's configured relays.
# Called after prompt_for_a_npub so wn's SQLite cache is populated
# BEFORE the test suite starts, and so the operator can spot "wn sees
# nothing for A" failures up front instead of as a cryptic Test 03
# timeout.
#
# Implementation:
# - `wn keys check <npub>` calls `resolve_user_blocking` which issues
# a targeted fetch for kinds [0, 10002, 10050, 10051] on the
# discovery-plane relays. That's the only published side-effect
# way to populate wn's user_relays table (no public CLI reads
# another user's relay lists directly).
# - After the fetch, `sqlite3` is used to SELECT from `user_relays`
# joined against `users` + `relays`. If sqlite3 isn't installed
# the probe degrades to "trust it, go run the tests" with a warning.
discover_a_relays() {
banner "Discovering A's advertised relays via wn"
step "wn_b keys check \$A_NPUB — populates wn's user_relays cache for A"
local kp_raw kp_event_id
kp_raw=$(wn_b --json keys check "$A_NPUB" 2>>"$LOG_FILE" || true)
printf 'wn_b keys check A raw: %s\n' "$kp_raw" >>"$LOG_FILE"
kp_event_id=$(printf '%s' "$kp_raw" | jq -r '.result.event_id // .event_id // empty' 2>/dev/null || true)
if [[ -n "$kp_event_id" && "$kp_event_id" != "null" ]]; then
info "wn_b found A's KeyPackage (kind:30443) — discovery plane is working"
else
warn "wn_b could NOT find A's KeyPackage. wn is bootstrapped on ${DEFAULT_RELAYS[*]}."
warn "Either Amethyst never published a KeyPackage, or it's only on relays wn can't reach."
warn "All later tests will fail. Fix this before continuing (tap KP publish in Amethyst settings)."
fi
# Also prime wn_c so Tests 04+ (which use C as a third member) can
# target A without another round-trip later.
wn_c --json keys check "$A_NPUB" >>"$LOG_FILE" 2>&1 || true
# Give wn ~3s to process the kind:10002/10050/10051 events that
# came back in the same fetch (they're not event_id-returned, they
# just land in user_relays via the subscription callback).
sleep 3
if ! command -v sqlite3 >/dev/null 2>&1; then
warn "sqlite3 not installed — skipping wn user_relays cache probe."
warn "If Test 03 fails with 'no invite arrived', install sqlite3 or rerun with --local-relays."
return
fi
# wn stores its database under $data_dir/release/<something>.sqlite.
# Find the DB file by looking for the only *.sqlite under release/.
local db
db=$(find "$B_DIR/release" -maxdepth 1 -name '*.sqlite' -print -quit 2>/dev/null || true)
if [[ -z "$db" || ! -s "$db" ]]; then
warn "wn SQLite DB not found under $B_DIR/release — cannot probe user_relays."
return
fi
step "wn_b's cached view of A's relay lists (SELECT from user_relays)"
local rows
rows=$(sqlite3 -readonly "$db" \
"SELECT ur.relay_type, r.url FROM user_relays ur
JOIN users u ON u.id = ur.user_id
JOIN relays r ON r.id = ur.relay_id
WHERE LOWER(HEX(u.pubkey)) = LOWER('$A_HEX')
ORDER BY ur.relay_type, r.url;" 2>>"$LOG_FILE" || true)
if [[ -z "$rows" ]]; then
warn "wn has NO cached relay entries for A ($A_HEX)."
warn " → Amethyst hasn't published kind:10002/10050/10051 to any relay wn_b can reach."
warn " → Welcomes and gift wraps will not reach Amethyst. Tests 03+ will fail."
return
fi
# Collate by relay_type for readability.
local nip65 inbox kp
nip65=$(printf '%s\n' "$rows" | awk -F'|' '$1=="nip65"{print " "$2}')
inbox=$(printf '%s\n' "$rows" | awk -F'|' '$1=="inbox"{print " "$2}')
kp=$(printf '%s\n' "$rows" | awk -F'|' '$1=="key_package"{print " "$2}')
info "A's kind:10002 (nip65):"
printf '%s\n' "${nip65:- <none — A hasn't published or wn can't reach that list>}" | tee -a "$LOG_FILE" >&2
info "A's kind:10050 (DM inbox, used for Marmot welcome delivery):"
printf '%s\n' "${inbox:- <none — Marmot welcomes will fall back to NIP-65 read relays>}" | tee -a "$LOG_FILE" >&2
info "A's kind:10051 (KeyPackage relays):"
printf '%s\n' "${kp:- <none — KeyPackage discovery may still work via NIP-65>}" | tee -a "$LOG_FILE" >&2
# Warn loudly if A's DM inbox is non-empty but shares no relay with
# wn_b's own configured set — that's the exact failure mode the
# operator already hit ("my DM inbox has auth.nostr1.com, wn can't
# publish there") and it silently breaks Test 03+.
if [[ -n "$inbox" ]]; then
local wn_relays
wn_relays=$(wn_b --json relays list 2>/dev/null \
| jq -r '(.result // .)[]? | (.url // .relay.url // empty)' 2>/dev/null)
local overlap=""
while IFS= read -r a_url; do
a_url="${a_url# }"
[[ -z "$a_url" ]] && continue
if printf '%s\n' "$wn_relays" | grep -qxF "$a_url"; then
overlap+="$a_url,"
fi
done <<< "$inbox"
overlap="${overlap%,}"
if [[ -z "$overlap" ]]; then
warn "A's DM inbox (kind:10050) shares ZERO relays with wn_b's bootstrap set."
warn " wn may still reach A's 10050 relays if they're public — but if any require"
warn " NIP-42 AUTH (auth.nostr1.com) or a whitelist (relay.0xchat.com), the welcome"
warn " gift wrap silently fails to publish."
warn " Real-world fix: edit Amethyst's DM Inbox list to include at least one of:"
warn " ${DEFAULT_RELAYS[*]}"
else
info "A's DM inbox overlaps wn's bootstrap at: $overlap — welcomes should flow"
fi
fi
}
# --- relays ------------------------------------------------------------------ # --- relays ------------------------------------------------------------------
configure_relays() { configure_relays() {
banner "Configuring relays" banner "Configuring relays"
@@ -409,29 +520,42 @@ configure_relays() {
} }
instruct_amethyst_setup() { instruct_amethyst_setup() {
local list
if [[ "$USE_LOCAL_RELAYS" -eq 1 ]]; then if [[ "$USE_LOCAL_RELAYS" -eq 1 ]]; then
list=" ws://10.0.2.2:8080 (Android emulator) # Offline/sandbox path: we own the only relay, so the harness DOES
ws://<your-LAN-ip>:8080 (physical device on same Wi-Fi)" # need to dictate Amethyst's relay config — nothing is discoverable
else # via the public network.
list=$(printf ' %s\n' "${DEFAULT_RELAYS[@]}") prompt_human "Configure Amethyst to match this --local-relays harness:
1. Settings -> Relays: add as READ+WRITE
ws://10.0.2.2:8080 (Android emulator)
ws://<your-LAN-ip>:8080 (physical device on same Wi-Fi)
2. Settings -> Key Package Relays: add the SAME URL
3. Settings -> DM Inbox Relays (NIP-17/kind:10050): add the SAME URL
4. Trigger key-package publish (toggle KP relay on/off if needed)
5. Confirm your Amethyst account is logged in with npub: $A_NPUB"
return
fi fi
prompt_human "Configure Amethyst to match this harness:
1. Settings -> Relays: add the following as READ+WRITE # Public-relay path: the harness should behave like any real Nostr
$list # client — discover A's advertised relays via kind:10002 / 10050 /
2. Settings -> Key Package Relays: add the SAME URLs # 10051 and publish there, rather than forcing A to adopt the
3. Settings -> DM Inbox Relays (NIP-17/kind:10050): add the SAME URLs. # harness's own relay set. That lets the tests surface real-world
CRITICAL for Test 03+: when wn invites Amethyst it fetches A's # interop failures (e.g. A's DM inbox points at auth-required or
kind:10050 and publishes the welcome gift wrap there. Amethyst's # unreachable relays) instead of hiding them behind shared config.
built-in DM inbox defaults (auth.nostr1.com, relay.0xchat.com) prompt_human "No relay reconfiguration required. Just confirm:
are not reachable from the wn daemon in this harness, so unless 1. Amethyst is logged in as npub:
the five harness relays are in the 10050 list the gift wrap lands $A_NPUB
on relays Amethyst-the-reader isn't subscribed to. 2. Amethyst has published (on its own chosen relays):
4. Trigger key-package publish (toggle KP relay on/off if needed). - kind:30443 KeyPackage
5. Re-publish the NIP-65 + DM-inbox lists if Amethyst hasn't already - kind:10051 Key Package Relay List
(relay toggles usually republish automatically; verify by looking - kind:10050 DM Inbox Relay List
for kind:10002 / 10050 in any relay inspector). - kind:10002 NIP-65 Outbox/Inbox Relay List
6. Confirm your Amethyst account is logged in with npub: $A_NPUB" Any one of Amethyst's public write relays will do — the wn daemons
below are bootstrapped on $(printf '%s, ' "${DEFAULT_RELAYS[@]}" | sed 's/, $//') and will
discover A's advertised relays automatically.
3. If the wn-side diagnostic after this prompt shows that wn cannot
see any of A's four lists, Amethyst hasn't published them to any
relay wn can reach — republish them (toggle a relay off/on in
Settings, then verify via an external relay inspector)."
} }
# ==== tests ================================================================== # ==== tests ==================================================================
@@ -1104,6 +1228,15 @@ main() {
dump_daemon_diagnostics B "post-configure" dump_daemon_diagnostics B "post-configure"
dump_daemon_diagnostics C "post-configure" dump_daemon_diagnostics C "post-configure"
# Let wn discover A's advertised relays via its normal subscription
# plane, then dump what wn actually sees. This is the "Am I going to
# be able to reach this user?" probe — surfaces up front the kind of
# failure (A's 10050 unreachable from wn, missing KP list, etc.) that
# would otherwise bite as a silent Test 03 timeout.
if [[ "$USE_LOCAL_RELAYS" -ne 1 ]]; then
discover_a_relays
fi
instruct_amethyst_setup instruct_amethyst_setup
test_01_keypackage_discovery test_01_keypackage_discovery