diff --git a/tools/marmot-interop/lib.sh b/tools/marmot-interop/lib.sh index 646b2d44b..406dfbcf5 100644 --- a/tools/marmot-interop/lib.sh +++ b/tools/marmot-interop/lib.sh @@ -177,17 +177,45 @@ jq_group_id() { # ------- polling helpers ----------------------------------------------------- -# wait_for_invite -# Echoes the first new group_id that appears in `wn groups invites --json`. +# Snapshot currently-pending invites on as a comma-separated list of +# gids. Use this BEFORE triggering a publish that should produce a new +# welcome, then pass the result to `wait_for_invite` as the ignore list so +# a stale welcome left over from a previous harness run (wnd persists +# pending invites across restarts) doesn't register as the arrival we +# just triggered. +snapshot_invites() { + local who="$1" wnfn + if [[ "$who" == "B" ]]; then wnfn=wn_b + else wnfn=wn_c; fi + local raw + raw=$("$wnfn" --json groups invites 2>/dev/null || true) + # jq_group_id reads one invite at a time, so iterate the array in shell + # rather than trying to handle all shapes in a single jq expression. + local gids=() + while IFS= read -r one; do + [[ -z "$one" || "$one" == "null" ]] && continue + local g + g=$(printf '%s' "$one" | jq_group_id) + [[ -n "$g" ]] && gids+=("$g") + done < <(printf '%s' "$raw" | jq -c '(.result // .) | .[]?' 2>/dev/null) + local IFS=','; printf '%s' "${gids[*]}" +} + +# wait_for_invite [ignore-csv] +# Echoes the first pending group_id that isn't listed in +# (comma-separated hex gids). Use the ignore list to skip welcomes left +# pending by previous harness runs — otherwise `wait_for_invite` can +# fire on a stale invite and the caller ends up driving a group the +# publisher isn't a member of, which then looks like a kind:445 drop. # Returns 1 on timeout. # # Also prints a heartbeat every ~10s with: # - elapsed/remaining time -# - current count of pending welcomes (should be 0 until it isn't) +# - current count of pending welcomes (total, pre-filter) # - last relevant line of wnd stderr (grep for welcome/giftwrap/subscribe/error) # so a stalled poll gives the operator something to forward. wait_for_invite() { - local who="$1" timeout="${2:-60}" deadline gid start last_hb + local who="$1" timeout="${2:-60}" ignore_csv="${3:-}" deadline gid start last_hb local wnfn data_dir if [[ "$who" == "B" ]]; then wnfn=wn_b; data_dir="$B_DIR" else wnfn=wn_c; data_dir="$C_DIR"; fi @@ -198,27 +226,32 @@ wait_for_invite() { # Post-v0.2 `wn --json groups invites` returns `{"result": [...]}` # (older builds returned the bare array). Peel the wrapper when # present so a pending invite is actually detected. - gid=$("$wnfn" --json groups invites 2>/dev/null \ - | jq -c '(.result // .) | .[0] // empty' 2>/dev/null | jq_group_id || true) - if [[ -n "${gid:-}" ]]; then - printf '%s\n' "$gid" - return 0 - fi + local raw + raw=$("$wnfn" --json groups invites 2>/dev/null || true) + # Walk every pending invite (not just .[0]) so we skip past stales. + while IFS= read -r one; do + [[ -z "$one" || "$one" == "null" ]] && continue + gid=$(printf '%s' "$one" | jq_group_id) + [[ -z "$gid" ]] && continue + if [[ ",$ignore_csv," != *",$gid,"* ]]; then + printf '%s\n' "$gid" + return 0 + fi + done < <(printf '%s' "$raw" | jq -c '(.result // .) | .[]?' 2>/dev/null) # Heartbeat every ~10s. local now=$(date +%s) if (( now - last_hb >= 10 )); then local elapsed=$(( now - start )) remaining=$(( deadline - now )) local pending - pending=$("$wnfn" --json groups invites 2>/dev/null \ - | jq '(.result // .) | length' 2>/dev/null || echo "?") + pending=$(printf '%s' "$raw" | jq '(.result // .) | length' 2>/dev/null || echo "?") local recent="" if [[ -f "$data_dir/logs/stderr.log" ]]; then recent=$(tail -n 200 "$data_dir/logs/stderr.log" 2>/dev/null \ | grep -iE 'welcome|giftwrap|gift_wrap|mls|subscribe|1059|444|error|warn' \ | tail -n 1 || true) fi - info "$who wait_for_invite +${elapsed}s (remaining ${remaining}s) pending=$pending tail: ${recent:-}" + info "$who wait_for_invite +${elapsed}s (remaining ${remaining}s) pending=$pending ignoring=${ignore_csv:-} tail: ${recent:-}" last_hb=$now fi diff --git a/tools/marmot-interop/marmot-interop.sh b/tools/marmot-interop/marmot-interop.sh index 3ba20b67d..c18a4bf79 100755 --- a/tools/marmot-interop/marmot-interop.sh +++ b/tools/marmot-interop/marmot-interop.sh @@ -363,7 +363,15 @@ configure_relays() { fi step "sanity-check kinds 10050/1059/445: B creates group + invites C + exchanges a message" - local sanity_gid sanity_c_gid sanity_create + local sanity_gid sanity_c_gid sanity_create c_ignore + # Snapshot C's pending invites before we publish, so wait_for_invite + # doesn't fire on a stale welcome left over from a previous run (wnd + # persists pending invites across restarts — we saw this cause the + # kind:445 sanity probe to send to B's new group while C was still + # sitting on an unaccepted welcome from a previous group that B + # wasn't a member of anymore). + c_ignore=$(snapshot_invites C) + [[ -n "$c_ignore" ]] && info "C already has stale invites, ignoring: $c_ignore" sanity_create=$(wn_b --json groups create "sanity-check" "$C_NPUB" 2>>"$LOG_FILE" || true) sanity_gid=$(printf '%s' "$sanity_create" | jq_group_id) printf 'sanity groups create raw: %s\n' "$sanity_create" >>"$LOG_FILE" @@ -371,8 +379,11 @@ configure_relays() { warn "sanity group create failed — see $LOG_FILE (stale account state? rerun with 'rm -rf state/')" else info "sanity group id: $sanity_gid" - if sanity_c_gid=$(wait_for_invite C 30); then + if sanity_c_gid=$(wait_for_invite C 30 "$c_ignore"); then info "kind:10050+1059 ok — C received welcome (gid: $sanity_c_gid)" + if [[ "$sanity_c_gid" != "$sanity_gid" ]]; then + warn "gid mismatch — C saw $sanity_c_gid, B created $sanity_gid (likely a stale welcome slipped past the filter)" + fi wn_c groups accept "$sanity_c_gid" >/dev/null 2>&1 || true wn_b messages send "$sanity_gid" "sanity-ping" >/dev/null 2>&1 || true if wait_for_message C "$sanity_c_gid" "sanity-ping" 30; then @@ -451,6 +462,12 @@ test_02_amethyst_creates_group() { # Amethyst sends it. dump_daemon_diagnostics B "pre-invite baseline" + # Snapshot B's already-pending invites so we can tell the new welcome + # apart from a leftover from a previous run. + local b_ignore + b_ignore=$(snapshot_invites B) + [[ -n "$b_ignore" ]] && info "B already has stale invites, ignoring: $b_ignore" + prompt_human "In Amethyst: 1. Tap '+' -> Create Group 2. Name: Interop-02 @@ -464,7 +481,7 @@ that's the bug." step "polling B's daemon for invite (60s, heartbeat every ~10s)" local gid - if ! gid=$(wait_for_invite B 60); then + if ! gid=$(wait_for_invite B 60 "$b_ignore"); then fail_msg "no invite arrived at B" dump_daemon_diagnostics B "post-timeout (test_02)" prompt_amethyst_logcat "test_02 timeout" @@ -603,13 +620,17 @@ test_04_three_member_group() { dump_daemon_diagnostics C "pre-invite baseline (test_04)" + local c_ignore + c_ignore=$(snapshot_invites C) + [[ -n "$c_ignore" ]] && info "C already has stale invites, ignoring: $c_ignore" + prompt_human "In Amethyst, open the group from Test 02 (or 'Interop-04-bootstrap'). Group Info -> Add Member -> paste: $C_NPUB Confirm the invite is sent." step "polling C for invite (60s, heartbeat every ~10s)" local c_gid - if ! c_gid=$(wait_for_invite C 60); then + if ! c_gid=$(wait_for_invite C 60 "$c_ignore"); then fail_msg "C never received invite" dump_daemon_diagnostics C "post-timeout (test_04)" prompt_amethyst_logcat "test_04 timeout" @@ -642,7 +663,8 @@ test_05_wn_adds_amethyst_existing() { banner "Test 05 — wn adds Amethyst to existing B+C group" step "B creates group with only C, then adds A" - local out gid + local out gid c_ignore + c_ignore=$(snapshot_invites C) out=$(wn_b --json groups create "Interop-05" "$C_NPUB" 2>>"$LOG_FILE") printf 'wn groups create raw output: %s\n' "$out" >>"$LOG_FILE" gid=$(printf '%s' "$out" | jq_group_id) @@ -652,8 +674,9 @@ test_05_wn_adds_amethyst_existing() { return fi save_state GROUP_05 "$gid" - if wait_for_invite C 30 >/dev/null; then - wn_c groups accept "$gid" >/dev/null 2>&1 || true + local c_new_gid + if c_new_gid=$(wait_for_invite C 30 "$c_ignore"); then + wn_c groups accept "$c_new_gid" >/dev/null 2>&1 || true fi step "B adds A to the group" @@ -770,9 +793,11 @@ test_08_admin_promote_demote() { step "B (creator+admin) adds C so we have 3 members" wn_c keys publish >/dev/null 2>&1 || true sleep 3 + local c_ignore_08 c_new_gid_08 + c_ignore_08=$(snapshot_invites C) wn_b groups add-members "$gid" "$C_NPUB" >/dev/null 2>&1 || warn "add-members C returned nonzero" - if wait_for_invite C 30 >/dev/null; then - wn_c groups accept "$gid" >/dev/null 2>&1 || true + if c_new_gid_08=$(wait_for_invite C 30 "$c_ignore_08"); then + wn_c groups accept "$c_new_gid_08" >/dev/null 2>&1 || true fi step "B promotes A to admin" @@ -946,9 +971,11 @@ test_12_offline_catchup() { done step "B adds C, then sends 3 more messages" + local c_ignore_12 c_new_gid_12 + c_ignore_12=$(snapshot_invites C) wn_b groups add-members "$gid" "$C_NPUB" >/dev/null 2>&1 || true - if wait_for_invite C 30 >/dev/null; then - wn_c groups accept "$gid" >/dev/null 2>&1 || true + if c_new_gid_12=$(wait_for_invite C 30 "$c_ignore_12"); then + wn_c groups accept "$c_new_gid_12" >/dev/null 2>&1 || true fi for i in 6 7 8; do wn_b messages send "$gid" "offline-msg-$i" >/dev/null 2>&1 || true