fix(marmot-interop): filter stale welcomes in wait_for_invite

wnd persists pending group invites across daemon restarts, so on any
rerun C (and B) already have unaccepted welcomes from previous runs
lying around. `wait_for_invite` was pulling `.[0]` out of the invite
list, which is the oldest pending — i.e. the stalest — and treating
it as the arrival the caller just triggered. In the sanity probe this
caused the kind:445 step to send to B's NEW group id while C was
accepting a DIFFERENT (old) group, producing a bogus "kind:445 failed
— relays may be dropping group messages" with no actual relay issue.

Add `snapshot_invites <B|C>` which returns the current gids as CSV,
and teach `wait_for_invite` to accept that CSV as an ignore list so
it walks past stale welcomes and only returns a freshly-arrived one.
Use the new snapshot→wait pattern at every call site: sanity check,
Test 02 (B), Test 04 (C), Test 05 (C), Test 08 (C), Test 12 (C).
Also log a gid-mismatch warning in the sanity check when B's created
group and C's accepted welcome disagree, as an extra guard against
the filter being bypassed.
This commit is contained in:
Claude
2026-04-22 18:43:28 +00:00
parent 414fd31bf9
commit 76f23c0798
2 changed files with 84 additions and 24 deletions
+46 -13
View File
@@ -177,17 +177,45 @@ jq_group_id() {
# ------- polling helpers -----------------------------------------------------
# wait_for_invite <B|C> <timeout-seconds>
# Echoes the first new group_id that appears in `wn groups invites --json`.
# Snapshot currently-pending invites on <B|C> 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 <B|C> <timeout-seconds> [ignore-csv]
# Echoes the first pending group_id that isn't listed in <ignore-csv>
# (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:-<no relevant wnd log yet>}"
info "$who wait_for_invite +${elapsed}s (remaining ${remaining}s) pending=$pending ignoring=${ignore_csv:-<none>} tail: ${recent:-<no relevant wnd log yet>}"
last_hb=$now
fi