d1ea9c39e8
bash 3.2 (stock on macOS) raises "unbound variable" on "${gids[*]}"
when the array is empty under `set -u`. The happy-path sanity check
still worked (the array was populated), but the shell error aborted
execution before the actual test cases ran on a fresh state/ directory.
Only expand when the array has at least one entry.
586 lines
25 KiB
Bash
586 lines
25 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# lib.sh — helpers for marmot-interop.sh
|
|
#
|
|
# All functions assume the parent script has set:
|
|
# STATE_DIR absolute path to state/ directory
|
|
# LOG_FILE absolute path to current run's log file
|
|
# RESULTS_FILE absolute path to results table (tab-separated)
|
|
# WN_BIN, WND_BIN absolute paths to wn / wnd binaries
|
|
# B_SOCKET, C_SOCKET socket paths for the two daemons
|
|
|
|
set -u
|
|
|
|
# ------- colors --------------------------------------------------------------
|
|
if [[ -t 1 ]]; then
|
|
C_RESET=$'\033[0m'
|
|
C_BOLD=$'\033[1m'
|
|
C_RED=$'\033[31m'
|
|
C_GREEN=$'\033[32m'
|
|
C_YELLOW=$'\033[33m'
|
|
C_BLUE=$'\033[34m'
|
|
C_CYAN=$'\033[36m'
|
|
C_GREY=$'\033[90m'
|
|
else
|
|
C_RESET=""; C_BOLD=""; C_RED=""; C_GREEN=""
|
|
C_YELLOW=""; C_BLUE=""; C_CYAN=""; C_GREY=""
|
|
fi
|
|
|
|
# ------- logging primitives --------------------------------------------------
|
|
log() { printf '%s\n' "$*" | tee -a "$LOG_FILE" >&2; }
|
|
|
|
banner() {
|
|
printf '\n%s%s================================================================%s\n' \
|
|
"$C_BOLD" "$C_BLUE" "$C_RESET" >&2
|
|
printf '%s%s %s%s\n' "$C_BOLD" "$C_BLUE" "$1" "$C_RESET" >&2
|
|
printf '%s%s================================================================%s\n\n' \
|
|
"$C_BOLD" "$C_BLUE" "$C_RESET" >&2
|
|
printf '\n=== %s ===\n' "$1" >>"$LOG_FILE"
|
|
}
|
|
|
|
step() { printf '%s>%s %s\n' "$C_CYAN" "$C_RESET" "$1" >&2; printf '> %s\n' "$1" >>"$LOG_FILE"; }
|
|
info() { printf '%s %s%s\n' "$C_GREY" "$1" "$C_RESET" >&2; printf ' %s\n' "$1" >>"$LOG_FILE"; }
|
|
warn() { printf '%s!%s %s\n' "$C_YELLOW" "$C_RESET" "$1" >&2; printf '! %s\n' "$1" >>"$LOG_FILE"; }
|
|
pass_msg() { printf '%s\u2713 PASS%s %s\n' "$C_GREEN" "$C_RESET" "$1" >&2; printf 'PASS %s\n' "$1" >>"$LOG_FILE"; }
|
|
fail_msg() { printf '%s\u2717 FAIL%s %s\n' "$C_RED" "$C_RESET" "$1" >&2; printf 'FAIL %s\n' "$1" >>"$LOG_FILE"; }
|
|
skip_msg() { printf '%s- SKIP%s %s\n' "$C_YELLOW" "$C_RESET" "$1" >&2; printf 'SKIP %s\n' "$1" >>"$LOG_FILE"; }
|
|
|
|
# ------- result tracking ------------------------------------------------------
|
|
# Each test calls record_result <test-id> <pass|fail|skip> [note]
|
|
record_result() {
|
|
local test_id="$1" status="$2" note="${3:-}"
|
|
printf '%s\t%s\t%s\n' "$test_id" "$status" "$note" >>"$RESULTS_FILE"
|
|
}
|
|
|
|
print_summary() {
|
|
local total=0 passed=0 failed=0 skipped=0
|
|
printf '\n%s%s================================================================%s\n' \
|
|
"$C_BOLD" "$C_BLUE" "$C_RESET"
|
|
printf '%s%s RESULTS%s\n' "$C_BOLD" "$C_BLUE" "$C_RESET"
|
|
printf '%s%s================================================================%s\n\n' \
|
|
"$C_BOLD" "$C_BLUE" "$C_RESET"
|
|
|
|
if [[ ! -s "$RESULTS_FILE" ]]; then
|
|
printf ' (no tests ran)\n\n'
|
|
return
|
|
fi
|
|
|
|
while IFS=$'\t' read -r tid status note; do
|
|
total=$((total+1))
|
|
case "$status" in
|
|
pass) passed=$((passed+1)); col="$C_GREEN"; sym="\u2713" ;;
|
|
fail) failed=$((failed+1)); col="$C_RED"; sym="\u2717" ;;
|
|
skip) skipped=$((skipped+1)); col="$C_YELLOW"; sym="-" ;;
|
|
*) col="$C_GREY"; sym="?" ;;
|
|
esac
|
|
printf ' %b %s%-44s%s %s\n' "$sym" "$col" "$tid" "$C_RESET" "$note"
|
|
done <"$RESULTS_FILE"
|
|
|
|
printf '\n %s%d passed%s, %s%d failed%s, %s%d skipped%s (of %d)\n\n' \
|
|
"$C_GREEN" "$passed" "$C_RESET" \
|
|
"$C_RED" "$failed" "$C_RESET" \
|
|
"$C_YELLOW" "$skipped" "$C_RESET" \
|
|
"$total"
|
|
printf ' full log: %s\n\n' "$LOG_FILE"
|
|
}
|
|
|
|
# ------- human interaction ---------------------------------------------------
|
|
|
|
# Pretty-print multi-line instructions and wait for <Enter>.
|
|
prompt_human() {
|
|
printf '\n%s%s---- DO THIS IN AMETHYST ----%s\n' "$C_BOLD" "$C_YELLOW" "$C_RESET" >&2
|
|
printf '%s%s%s\n' "$C_YELLOW" "$1" "$C_RESET" >&2
|
|
printf '%s%s-----------------------------%s\n' "$C_BOLD" "$C_YELLOW" "$C_RESET" >&2
|
|
printf '%s[Press Enter to continue]%s ' "$C_CYAN" "$C_RESET" >&2
|
|
read -r _
|
|
printf '\n' >>"$LOG_FILE"
|
|
printf 'HUMAN-PROMPT: %s\n' "$1" >>"$LOG_FILE"
|
|
}
|
|
|
|
# Ask the operator to confirm a UI rendering result.
|
|
# Returns 0 on pass, 1 on fail, 2 on skip.
|
|
confirm() {
|
|
local q="$1" ans
|
|
while true; do
|
|
printf '%s? %s%s [p]ass / [f]ail / [s]kip: ' "$C_CYAN" "$q" "$C_RESET" >&2
|
|
read -r ans
|
|
case "$(printf '%s' "$ans" | tr '[:upper:]' '[:lower:]')" in
|
|
p|pass) printf 'CONFIRM PASS: %s\n' "$q" >>"$LOG_FILE"; return 0 ;;
|
|
f|fail) printf 'CONFIRM FAIL: %s\n' "$q" >>"$LOG_FILE"; return 1 ;;
|
|
s|skip) printf 'CONFIRM SKIP: %s\n' "$q" >>"$LOG_FILE"; return 2 ;;
|
|
*) printf 'enter p, f, or s\n' >&2 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ------- wn wrappers ---------------------------------------------------------
|
|
wn_b() { "$WN_BIN" --socket "$B_SOCKET" ${B_NPUB:+--account "$B_NPUB"} "$@"; }
|
|
wn_c() { "$WN_BIN" --socket "$C_SOCKET" ${C_NPUB:+--account "$C_NPUB"} "$@"; }
|
|
|
|
# Run wn with --json and pipe through jq; aborts the test on jq parse failure.
|
|
wn_b_json() { wn_b --json "$@"; }
|
|
wn_c_json() { wn_c --json "$@"; }
|
|
|
|
# ------- assertions ----------------------------------------------------------
|
|
expect_eq() {
|
|
local actual="$1" expected="$2" what="${3:-value}"
|
|
if [[ "$actual" == "$expected" ]]; then
|
|
info "$what == \"$expected\""
|
|
return 0
|
|
fi
|
|
fail_msg "$what mismatch: expected \"$expected\", got \"$actual\""
|
|
return 1
|
|
}
|
|
|
|
expect_contains() {
|
|
local haystack="$1" needle="$2" what="${3:-output}"
|
|
if [[ "$haystack" == *"$needle"* ]]; then
|
|
info "$what contains \"$needle\""
|
|
return 0
|
|
fi
|
|
fail_msg "$what missing \"$needle\""
|
|
info "actual: $haystack"
|
|
return 1
|
|
}
|
|
|
|
# ------- JSON helpers --------------------------------------------------------
|
|
|
|
# Extract MLS group ID as lowercase hex from wn JSON output.
|
|
# Handles both formats:
|
|
# - plain hex string (from `groups list`)
|
|
# - {"value":{"vec":[...]}} serde struct (from `groups create`)
|
|
# - flat byte array [n, ...] (from some responses)
|
|
# Plus the three wrapper shapes wn actually uses:
|
|
# - {"result": {"mls_group_id": ...}} (groups create)
|
|
# - {"group": {"mls_group_id": ...}, "membership": ...} (groups invites[0])
|
|
# - {"mls_group_id": ...} (bare)
|
|
# Input: JSON string via stdin; optional 2nd arg = field name (default: mls_group_id)
|
|
jq_group_id() {
|
|
local field="${1:-mls_group_id}"
|
|
jq -r --arg f "$field" '
|
|
def byte2hex:
|
|
. as $n |
|
|
[($n / 16 | floor), ($n % 16)] |
|
|
map(if . < 10 then (48 + .) else (87 + .) end) |
|
|
implode;
|
|
(.group // .result // .) |
|
|
(.group // .) |
|
|
.[$f] |
|
|
if type == "string" then .
|
|
elif (type == "object" and (.value.vec != null)) then
|
|
[.value.vec[] | byte2hex] | join("")
|
|
elif type == "array" then
|
|
[.[] | byte2hex] | join("")
|
|
else empty end
|
|
' 2>/dev/null || true
|
|
}
|
|
|
|
# ------- polling helpers -----------------------------------------------------
|
|
|
|
# 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)
|
|
# bash 3.2 (stock macOS) treats "${gids[*]}" on an empty array as an
|
|
# unbound reference under `set -u`, so guard the expansion.
|
|
if (( ${#gids[@]} > 0 )); then
|
|
local IFS=','
|
|
printf '%s' "${gids[*]}"
|
|
fi
|
|
}
|
|
|
|
# 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 (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}" 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
|
|
start=$(date +%s)
|
|
deadline=$(( start + timeout ))
|
|
last_hb=$start
|
|
while [[ $(date +%s) -lt $deadline ]]; do
|
|
# 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.
|
|
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=$(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 ignoring=${ignore_csv:-<none>} tail: ${recent:-<no relevant wnd log yet>}"
|
|
last_hb=$now
|
|
fi
|
|
|
|
sleep 2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# wait_for_message <B|C> <group_id> <substring> <timeout>
|
|
# Returns 0 if a message containing the substring shows up via wn messages list.
|
|
wait_for_message() {
|
|
local who="$1" gid="$2" needle="$3" timeout="${4:-30}"
|
|
local deadline payload
|
|
deadline=$(( $(date +%s) + timeout ))
|
|
while [[ $(date +%s) -lt $deadline ]]; do
|
|
if [[ "$who" == "B" ]]; then
|
|
payload=$(wn_b_json messages list "$gid" --limit 20 2>/dev/null || true)
|
|
else
|
|
payload=$(wn_c_json messages list "$gid" --limit 20 2>/dev/null || true)
|
|
fi
|
|
if [[ -n "${payload:-}" ]] && \
|
|
printf '%s' "$payload" | jq -e --arg n "$needle" \
|
|
'(.result // .) | .[]? | select((.content // .text // "") | contains($n))' \
|
|
>/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# wait_for_member <B|C> <group_id> <pubkey-hex> <timeout>
|
|
wait_for_member() {
|
|
local who="$1" gid="$2" pubkey="$3" timeout="${4:-30}"
|
|
local deadline payload
|
|
deadline=$(( $(date +%s) + timeout ))
|
|
while [[ $(date +%s) -lt $deadline ]]; do
|
|
if [[ "$who" == "B" ]]; then
|
|
payload=$(wn_b_json groups members "$gid" 2>/dev/null || true)
|
|
else
|
|
payload=$(wn_c_json groups members "$gid" 2>/dev/null || true)
|
|
fi
|
|
if printf '%s' "${payload:-}" | jq -e --arg p "$pubkey" \
|
|
'(.result // .) | .[]? | select((.pubkey // .public_key // "") == $p)' \
|
|
>/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# ------- output parsing ------------------------------------------------------
|
|
# `wn` prints either JSON (with --json) or a yaml-ish "key: value" pretty form.
|
|
# This helper extracts the pubkey (npub) from whichever form we got.
|
|
extract_pubkey() {
|
|
local raw="$1" v=""
|
|
# JSON: single object
|
|
v=$(printf '%s' "$raw" | jq -r '.pubkey // .npub // .public_key // empty' 2>/dev/null || true)
|
|
if [[ -n "$v" && "$v" != "null" ]]; then printf '%s' "$v"; return; fi
|
|
# JSON: {"result": [ {"pubkey": …}, … ]} — post-v0.2 `wn --json whoami` shape
|
|
v=$(printf '%s' "$raw" | jq -r '.result[0].pubkey // .result[0].npub // .result[0].public_key // empty' 2>/dev/null || true)
|
|
if [[ -n "$v" && "$v" != "null" ]]; then printf '%s' "$v"; return; fi
|
|
# JSON: array of accounts (whoami may return a list)
|
|
v=$(printf '%s' "$raw" | jq -r '.[0].pubkey // .[0].npub // .[0].public_key // empty' 2>/dev/null || true)
|
|
if [[ -n "$v" && "$v" != "null" ]]; then printf '%s' "$v"; return; fi
|
|
# yaml-ish "pubkey: npub1..."
|
|
v=$(printf '%s\n' "$raw" | sed -n 's/^[[:space:]]*pubkey[[:space:]]*:[[:space:]]*//p' | head -n1)
|
|
if [[ -n "$v" ]]; then printf '%s' "$v"; return; fi
|
|
# yaml-ish "npub: npub1..." (alternative key)
|
|
v=$(printf '%s\n' "$raw" | sed -n 's/^[[:space:]]*npub[[:space:]]*:[[:space:]]*//p' | head -n1)
|
|
if [[ -n "$v" ]]; then printf '%s' "$v"; return; fi
|
|
}
|
|
|
|
# Pure-awk bech32 decoder for npub1... → 32-byte lowercase hex. Needed because
|
|
# `wn users show` only resolves identities the local daemon already knows
|
|
# about, so A's npub (the Amethyst account) can't be converted via wn — and a
|
|
# failed conversion leaks the raw npub into hex-equality checks like the
|
|
# member-list assertion in Test 02, which then fails with
|
|
# "member list missing npub1…".
|
|
bech32_decode_npub() {
|
|
local npub="$1"
|
|
[[ "$npub" == npub1* ]] || return 1
|
|
local body="${npub#npub1}"
|
|
# Need at least 6 chars of checksum + some data.
|
|
(( ${#body} > 6 )) || return 1
|
|
local hex
|
|
hex=$(awk -v data="$body" 'BEGIN {
|
|
charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
for (i = 0; i < 32; i++) map[substr(charset, i + 1, 1)] = i
|
|
data = substr(data, 1, length(data) - 6)
|
|
acc = 0; bits = 0; out = ""
|
|
for (i = 1; i <= length(data); i++) {
|
|
c = substr(data, i, 1)
|
|
if (!(c in map)) exit 1
|
|
acc = acc * 32 + map[c]
|
|
bits += 5
|
|
while (bits >= 8) {
|
|
bits -= 8
|
|
pow = 1
|
|
for (j = 0; j < bits; j++) pow *= 2
|
|
byte = int(acc / pow)
|
|
acc -= byte * pow
|
|
out = out sprintf("%02x", byte)
|
|
}
|
|
}
|
|
print out
|
|
}' 2>/dev/null) || return 1
|
|
# 32-byte x-only pubkey = 64 hex chars.
|
|
[[ ${#hex} -eq 64 ]] || return 1
|
|
printf '%s' "$hex"
|
|
}
|
|
|
|
# npub -> hex conversion. Tries `wn users show` first (handy when it works,
|
|
# since it also validates the npub against the daemon's view), then falls
|
|
# back to a local bech32 decode so unknown npubs still convert. Returns the
|
|
# input unchanged only if both paths fail, which should never happen for a
|
|
# well-formed npub.
|
|
npub_to_hex() {
|
|
local npub="$1" raw hex
|
|
raw=$(wn_b --json users show "$npub" 2>/dev/null || true)
|
|
hex=$(printf '%s' "$raw" \
|
|
| jq -r '(.result // .) | (.pubkey // .public_key // .hex // .[0]?.pubkey // .[0]?.public_key // empty)' \
|
|
2>/dev/null || true)
|
|
if [[ -z "$hex" || "$hex" == "null" ]]; then
|
|
# fallback to yaml-ish output
|
|
raw=$(wn_b users show "$npub" 2>/dev/null || true)
|
|
hex=$(printf '%s\n' "$raw" | sed -n 's/^[[:space:]]*\(public_key\|hex\)[[:space:]]*:[[:space:]]*//p' | head -n1)
|
|
fi
|
|
if [[ -n "$hex" && "$hex" != "null" ]]; then
|
|
printf '%s' "$hex"
|
|
return
|
|
fi
|
|
if hex=$(bech32_decode_npub "$npub"); then
|
|
printf '%s' "$hex"
|
|
return
|
|
fi
|
|
printf '%s' "$npub"
|
|
}
|
|
|
|
# ------- state file ----------------------------------------------------------
|
|
# Simple key=value store at $STATE_DIR/run.env
|
|
STATE_FILE_RUNTIME="${STATE_DIR}/run.env"
|
|
|
|
save_state() {
|
|
local key="$1" value="$2"
|
|
: >"${STATE_FILE_RUNTIME}.tmp"
|
|
if [[ -f "$STATE_FILE_RUNTIME" ]]; then
|
|
grep -v "^${key}=" "$STATE_FILE_RUNTIME" >>"${STATE_FILE_RUNTIME}.tmp" || true
|
|
fi
|
|
printf '%s=%s\n' "$key" "$value" >>"${STATE_FILE_RUNTIME}.tmp"
|
|
mv "${STATE_FILE_RUNTIME}.tmp" "$STATE_FILE_RUNTIME"
|
|
}
|
|
|
|
load_state() {
|
|
local key="$1"
|
|
[[ -f "$STATE_FILE_RUNTIME" ]] || return 1
|
|
grep "^${key}=" "$STATE_FILE_RUNTIME" 2>/dev/null | tail -n1 | cut -d= -f2-
|
|
}
|
|
|
|
# ------- daemon diagnostics --------------------------------------------------
|
|
# Dumps everything we know about a daemon's state. Called before polling for
|
|
# invites (baseline) and on every invite-poll failure so the operator can
|
|
# forward a single log file that explains what the daemon saw (or didn't).
|
|
#
|
|
# Output is tee'd to both stderr (for live viewing) and $LOG_FILE (for forward).
|
|
dump_daemon_diagnostics() {
|
|
local who="$1" tag="${2:-diagnostics}" data_dir socket wnfn npub hex
|
|
if [[ "$who" == "B" ]]; then
|
|
data_dir="$B_DIR"; socket="$B_SOCKET"; wnfn=wn_b
|
|
npub="${B_NPUB:-?}"; hex="${B_HEX:-?}"
|
|
else
|
|
data_dir="$C_DIR"; socket="$C_SOCKET"; wnfn=wn_c
|
|
npub="${C_NPUB:-?}"; hex="${C_HEX:-?}"
|
|
fi
|
|
|
|
printf '\n%s%s---- [%s] %s daemon diagnostics ----%s\n' \
|
|
"$C_BOLD" "$C_CYAN" "$tag" "$who" "$C_RESET" >&2
|
|
printf '\n==== [%s] %s daemon diagnostics ====\n' "$tag" "$who" >>"$LOG_FILE"
|
|
|
|
info "$who identity npub=$npub"
|
|
info "$who identity hex =$hex"
|
|
info "$who socket $socket"
|
|
|
|
# Relay list grouped by type — this is the critical bit for invite
|
|
# debugging. whitenoise-rs subscribes to kind:1059 on Inbox relays only,
|
|
# so if Amethyst doesn't see $B_NPUB's kind:10050 list pointing at these
|
|
# URLs, the gift wrap will never reach B.
|
|
step "$who relays (per type):"
|
|
for t in nip65 inbox key_package; do
|
|
local raw urls
|
|
raw=$("$wnfn" --json relays list --type "$t" 2>/dev/null || true)
|
|
urls=$(printf '%s' "$raw" \
|
|
| jq -r '(.result // .) | .[]? | (.url // .relay.url // empty)' 2>/dev/null \
|
|
| paste -sd',' -)
|
|
info " $t: ${urls:-<none>}"
|
|
printf ' %s %s raw: %s\n' "$who" "$t" "$raw" >>"$LOG_FILE"
|
|
done
|
|
|
|
# Connection status per relay — shows which sockets are actually live.
|
|
step "$who relay connection status:"
|
|
"$wnfn" --json relays list 2>/dev/null \
|
|
| jq -r '(.result // .)[]? | " \(.url // .relay.url // "?") [\(.type // "?")] [\(.status // .connection_status // "?")]"' 2>/dev/null \
|
|
| tee -a "$LOG_FILE" >&2 || true
|
|
|
|
# Daemon-level subscription health.
|
|
step "$who wn debug health:"
|
|
local health
|
|
health=$("$wnfn" --json debug health 2>/dev/null || "$wnfn" debug health 2>/dev/null || true)
|
|
printf ' %s\n' "${health:-<no output>}" | tee -a "$LOG_FILE" >&2
|
|
|
|
# Relay-control state (subscriptions, filters, planes). Truncated to keep
|
|
# stderr readable; full copy still goes to $LOG_FILE.
|
|
step "$who wn debug relay-control-state (truncated to stderr; full in $LOG_FILE):"
|
|
local rcs
|
|
rcs=$("$wnfn" --json debug relay-control-state 2>/dev/null || "$wnfn" debug relay-control-state 2>/dev/null || true)
|
|
printf '%s\n' "${rcs:-<no output>}" | head -n 20 | sed 's/^/ /' >&2
|
|
printf '%s wn debug relay-control-state FULL:\n%s\n' "$who" "${rcs:-<no output>}" >>"$LOG_FILE"
|
|
|
|
# Pending welcomes already decrypted by the daemon. Non-empty here while
|
|
# wait_for_invite is polling would be a race worth noticing.
|
|
step "$who pending invites:"
|
|
local inv
|
|
inv=$("$wnfn" --json groups invites 2>/dev/null || true)
|
|
printf ' %s\n' "${inv:-<no output>}" | tee -a "$LOG_FILE" >&2
|
|
|
|
# Tail of daemon stderr — contains the live 1059/welcome processing logs.
|
|
step "$who wnd stderr (last 80 lines from $data_dir/logs/stderr.log):"
|
|
if [[ -f "$data_dir/logs/stderr.log" ]]; then
|
|
tail -n 80 "$data_dir/logs/stderr.log" 2>/dev/null | sed 's/^/ /' | tee -a "$LOG_FILE" >&2 || true
|
|
else
|
|
info " (no stderr log at $data_dir/logs/stderr.log)"
|
|
fi
|
|
|
|
printf '%s%s---- end %s diagnostics ----%s\n\n' \
|
|
"$C_BOLD" "$C_CYAN" "$who" "$C_RESET" >&2
|
|
printf '==== end [%s] %s diagnostics ====\n\n' "$tag" "$who" >>"$LOG_FILE"
|
|
}
|
|
|
|
# Asks the operator to capture Amethyst's MarmotDbg logcat so the script log
|
|
# ends up with both sides of the pipe in one place.
|
|
prompt_amethyst_logcat() {
|
|
local note="${1:-paste below}"
|
|
prompt_human "On the host running adb, capture Amethyst's Marmot logs:
|
|
|
|
adb logcat -d -v time | grep -E 'MarmotDbg|Marmot |MlsWelcome|GiftWrap' \\
|
|
| tail -n 200 > /tmp/amethyst-marmot.log
|
|
|
|
Then paste /tmp/amethyst-marmot.log contents here so the failure report has
|
|
both the Amethyst-side publish log and the wn-side subscription log ($note)."
|
|
}
|
|
|
|
# Dump everything we know about an outbound kind:445 from a wn daemon, so
|
|
# when the operator reports "Amethyst didn't see it" we can tell them which
|
|
# side of the wire to investigate:
|
|
#
|
|
# - `messages list` output (did the daemon persist the message locally,
|
|
# i.e. is this a relay-reach issue, a membership issue, or a pure
|
|
# Amethyst-receive issue?)
|
|
# - `groups show` metadata (which MLS-extension relays is wn using for
|
|
# this group — these are the SAME relays Amethyst subscribes to, so if
|
|
# the lists don't match that's the bug)
|
|
# - live relay connection status
|
|
#
|
|
# Output is tee'd to stderr (live) and $LOG_FILE (forward).
|
|
dump_outbound_diagnostics() {
|
|
local who="$1" gid="$2" needle="${3:-}" tag="${4:-post-send}"
|
|
local wnfn
|
|
if [[ "$who" == "B" ]]; then wnfn=wn_b
|
|
else wnfn=wn_c; fi
|
|
|
|
printf '\n%s%s---- [%s] %s outbound diagnostics (group %.8s…) ----%s\n' \
|
|
"$C_BOLD" "$C_CYAN" "$tag" "$who" "$gid" "$C_RESET" >&2
|
|
printf '\n==== [%s] %s outbound diagnostics (group %s) ====\n' "$tag" "$who" "$gid" >>"$LOG_FILE"
|
|
|
|
step "$who local view of latest messages in group"
|
|
local list_raw
|
|
list_raw=$("$wnfn" --json messages list "$gid" --limit 5 2>/dev/null || true)
|
|
printf ' %s\n' "${list_raw:-<no output>}" >>"$LOG_FILE"
|
|
printf '%s' "$list_raw" \
|
|
| jq -r '(.result // .) | .[]? | " \(.id // .event_id // "?") \((.content // .text // "") | tostring | .[0:80])"' 2>/dev/null \
|
|
| tee -a "$LOG_FILE" >&2 || true
|
|
if [[ -n "$needle" ]]; then
|
|
if printf '%s' "$list_raw" | jq -e --arg n "$needle" \
|
|
'(.result // .) | .[]? | select((.content // .text // "") | contains($n))' \
|
|
>/dev/null 2>&1; then
|
|
info "$who local store contains \"$needle\" — send reached wn's own DB (kind:445 went out)"
|
|
else
|
|
warn "$who local store does NOT contain \"$needle\" — `messages send` never persisted; check stderr.log"
|
|
fi
|
|
fi
|
|
|
|
step "$who MLS group metadata (relays here are what Amethyst subscribes on)"
|
|
local meta_raw
|
|
meta_raw=$("$wnfn" --json groups show "$gid" 2>/dev/null || true)
|
|
printf ' %s\n' "${meta_raw:-<no output>}" >>"$LOG_FILE"
|
|
local meta_relays
|
|
meta_relays=$(printf '%s' "$meta_raw" \
|
|
| jq -r '(.result // .) | (.relays // .group.relays // [])[]? | (. | tostring)' 2>/dev/null \
|
|
| paste -sd',' -)
|
|
info " group relays (MLS extension): ${meta_relays:-<none — Amethyst will fall back to homeRelays>}"
|
|
local meta_name meta_epoch
|
|
meta_name=$(printf '%s' "$meta_raw" | jq -r '(.result // .) | (.name // .group.name // "?")' 2>/dev/null)
|
|
meta_epoch=$(printf '%s' "$meta_raw" | jq -r '(.result // .) | (.epoch // .group.epoch // "?")' 2>/dev/null)
|
|
info " group name: $meta_name epoch: $meta_epoch"
|
|
|
|
step "$who relay connection status right now"
|
|
"$wnfn" --json relays list 2>/dev/null \
|
|
| jq -r '(.result // .)[]? | " \(.url // .relay.url // "?") [\(.type // "?")] [\(.status // .connection_status // "?")]"' 2>/dev/null \
|
|
| tee -a "$LOG_FILE" >&2 || true
|
|
|
|
printf '%s%s---- end outbound diagnostics ----%s\n\n' \
|
|
"$C_BOLD" "$C_CYAN" "$C_RESET" >&2
|
|
printf '==== end [%s] %s outbound diagnostics ====\n\n' "$tag" "$who" >>"$LOG_FILE"
|
|
}
|
|
|
|
# ------- group cleanup -------------------------------------------------------
|
|
cleanup_group() {
|
|
local gid="$1" who="${2:-B}"
|
|
[[ -z "$gid" ]] && return 0
|
|
if [[ "$who" == "B" ]]; then
|
|
wn_b groups leave "$gid" >/dev/null 2>&1 || true
|
|
else
|
|
wn_c groups leave "$gid" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|