diff --git a/cli/tests/lib.sh b/cli/tests/lib.sh index 8109f1076b..2bb3f00627 100644 --- a/cli/tests/lib.sh +++ b/cli/tests/lib.sh @@ -145,36 +145,66 @@ expect_contains() { # ------- 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) +# Extract an MLS group id as lowercase hex from `wn --json` output. +# +# MDK 0.9.x settled on one envelope, `{"ok":true,"result":{...}}`, but the +# group id sits at a different place per verb: +# groups create -> .result.group_id +# groups accept / rename -> .result.group.group_id +# groups invites[] -> .group_id (an element, already peeled) +# groups members/admins -> .result.group_id +# Older builds wrapped the id as a serde `{"value":{"vec":[...]}}` struct or a +# bare byte array; both are still decoded so a run against an older `wn` binary +# reports a real mismatch instead of an empty string. +# +# Input: JSON on stdin. Optional 1st arg overrides the field name. jq_group_id() { - local field="${1:-mls_group_id}" + local field="${1:-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 + def as_hex: + 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; + [ (.result? // empty), (.result?.group? // empty), (.group? // empty), . ] + | map(select(type == "object") | .[$f]? // empty | as_hex) + | map(select(. != null and . != "")) + | first // empty ' 2>/dev/null || true } +# Peel MDK 0.9.x's `{"ok":true,"result":{...}}` envelope and hand back the +# named collection as a JSON array. MDK moved every list one level in and gave +# it a name (`invites`, `members`, `admins`, `messages`), so a bare +# `(.result // .) | .[]?` now iterates the RESULT OBJECT'S VALUES — three +# scalars where the harness expected invite objects. That failure is silent: +# every poll simply never matches, and the test reports "never received +# invite" for a welcome that arrived and was accepted. +# +# Input: JSON on stdin, collection name as $1. +jq_list() { + local name="$1" + jq -c --arg n "$name" ' + [ (.result?[$n]? // empty), (.[$n]? // empty), (.result? // empty), . ] + | map(select(type == "array")) + | (first // []) + | .[] + ' 2>/dev/null || true +} + +# npub or hex pubkey of one member/admin entry. MDK names the field per +# collection: members carry `member_id`, admins carry `admin_id`. +jq_member_ids() { + jq -r '.member_id? // .admin_id? // .pubkey? // .public_key? // empty' 2>/dev/null || true +} + # ------- polling helpers ----------------------------------------------------- # Snapshot currently-pending invites on as a comma-separated list of @@ -197,7 +227,7 @@ snapshot_invites() { local g g=$(printf '%s' "$one" | jq_group_id) [[ -n "$g" ]] && gids+=("$g") - done < <(printf '%s' "$raw" | jq -c '(.result // .) | .[]?' 2>/dev/null) + done < <(printf '%s' "$raw" | jq_list invites) # 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 @@ -228,9 +258,10 @@ wait_for_invite() { 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. + # MDK 0.9.x returns `{"ok":true,"result":{"invites":[...], …}}`. + # `jq_list` peels the envelope AND names the collection; iterating + # `.result` directly walks the sibling scalars instead and never + # matches. local raw raw=$("$wnfn" --json groups invites 2>/dev/null || true) # Walk every pending invite (not just .[0]) so we skip past stales. @@ -242,14 +273,14 @@ wait_for_invite() { printf '%s\n' "$gid" return 0 fi - done < <(printf '%s' "$raw" | jq -c '(.result // .) | .[]?' 2>/dev/null) + done < <(printf '%s' "$raw" | jq_list invites) # 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 "?") + pending=$(printf '%s' "$raw" | jq_list invites | wc -l | tr -d ' ') local recent="" if [[ -f "$data_dir/logs/stderr.log" ]]; then recent=$(tail -n 200 "$data_dir/logs/stderr.log" 2>/dev/null \ @@ -277,9 +308,10 @@ wait_for_message() { else payload=$(wn_c_json messages list "$gid" --limit 20 2>/dev/null || true) fi + # MDK 0.9.x: `.result.messages[]`, decrypted body in `plaintext`. if [[ -n "${payload:-}" ]] && \ - printf '%s' "$payload" | jq -e --arg n "$needle" \ - '(.result // .) | .[]? | select((.content // .text // "") | contains($n))' \ + printf '%s' "$payload" | jq_list messages | jq -e --arg n "$needle" \ + 'select((.plaintext // .content // .text // "") | contains($n))' \ >/dev/null 2>&1; then return 0 fi diff --git a/cli/tests/marmot/tests-create.sh b/cli/tests/marmot/tests-create.sh index b868380ef9..5cb2bb2036 100644 --- a/cli/tests/marmot/tests-create.sh +++ b/cli/tests/marmot/tests-create.sh @@ -184,12 +184,19 @@ test_05_b_adds_a_existing() { record_result "$id" fail "wn add-members A failed"; return } - # A joins - if ! amy_json marmot await group --name "Interop-05" --timeout 30 >/dev/null; then + # A joins. `gid` is wn's MLS group id; amy indexes by the MIP-01 + # nostr_group_id, which only exists locally once A has processed the + # welcome — so take amy's id from its own await, never wn's. + local a_out a_gid + a_out=$(amy_json marmot await group --name "Interop-05" --timeout 30) || { record_result "$id" fail "A never received invite to Interop-05"; return + } + a_gid=$(printf '%s' "$a_out" | jq -r '.group_id // empty') + if [[ -z "$a_gid" ]]; then + record_result "$id" fail "A joined Interop-05 but reported no group_id"; return fi - amy_json marmot message send "$gid" "joined from amethyst" >/dev/null || { + amy_json marmot message send "$a_gid" "joined from amethyst" >/dev/null || { record_result "$id" fail "amy send failed"; return } if wait_for_message B "$gid" "joined from amethyst" 90 \ diff --git a/cli/tests/marmot/tests-extras.sh b/cli/tests/marmot/tests-extras.sh index 0c3cbc33f3..e9781fc830 100644 --- a/cli/tests/marmot/tests-extras.sh +++ b/cli/tests/marmot/tests-extras.sh @@ -17,7 +17,8 @@ test_09_reply_react_unreact() { # B anchors. Needs a member to be present — if Test 11 already ran and A left, # skip cleanly so we don't double-fail. if ! wn_b --json groups members "$mls_gid" 2>/dev/null \ - | jq -e --arg p "$A_HEX" '(.result // .) | .[]? | select((.pubkey // .public_key) == $p)' \ + | jq_list members | jq -e --arg p "$A_HEX" \ + 'select((.member_id // .pubkey // .public_key) == $p)' \ >/dev/null 2>&1; then record_result "$id" skip "A already left GROUP_02"; return fi @@ -26,7 +27,9 @@ test_09_reply_react_unreact() { sleep 3 local msg_id msg_id=$(wn_b --json messages list "$mls_gid" --limit 10 2>/dev/null \ - | jq -r '[(.result // .) | .[]? | select((.content // .text // "") == "anchor for reactions")][0].id // empty') + | jq_list messages \ + | jq -r 'select((.plaintext // .content // .text // "") == "anchor for reactions") + | (.message_id // .id)' | head -n 1) if [[ -z "$msg_id" || "$msg_id" == "null" ]]; then record_result "$id" fail "couldn't find anchor message id"; return fi @@ -47,7 +50,9 @@ test_09_reply_react_unreact() { sleep 3 local a_anchor_id a_anchor_id=$(amy_json marmot message list "$gid" --limit 50 2>/dev/null \ - | jq -r '[.messages[]? | select((.content // "") == "anchor for reactions")][0].event_id // empty') + | jq_list messages \ + | jq -r 'select((.plaintext // .content // "") == "anchor for reactions") + | (.message_id // .event_id)' | head -n 1) if [[ -z "$a_anchor_id" || "$a_anchor_id" == "null" ]]; then record_result "$id" fail "amy couldn't find anchor message in local log"; return fi @@ -67,7 +72,7 @@ test_09_reply_react_unreact() { payload=$(wn_b_json messages list "$mls_gid" --limit 50 2>/dev/null || true) if [[ -n "$payload" ]] && \ printf '%s' "$payload" \ - | jq -e '(.result // .) | .[]? | (.reactions.by_emoji // {}) | keys[]?' \ + | jq_list messages | jq -e '(.reactions.by_emoji // {}) | keys[]?' \ 2>/dev/null \ | grep -qF '"🍕"'; then saw=1; break @@ -92,7 +97,8 @@ test_10_concurrent_commits() { record_result "$id" skip "no GROUP_02"; return fi if ! wn_b --json groups members "$mls_gid" 2>/dev/null \ - | jq -e --arg p "$A_HEX" '(.result // .) | .[]? | select((.pubkey // .public_key) == $p)' \ + | jq_list members | jq -e --arg p "$A_HEX" \ + 'select((.member_id // .pubkey // .public_key) == $p)' \ >/dev/null 2>&1; then record_result "$id" skip "A already left GROUP_02"; return fi @@ -110,7 +116,8 @@ test_10_concurrent_commits() { # whitenoise-rs ≥ v0.2.x wraps the group payload one level deeper as # `{"result": {"group": {…name…}}}`; the older shape was a bare group # object under `.result`. Accept both. - b_name=$(wn_b --json groups show "$mls_gid" 2>/dev/null | jq -r '(.result // .) | (.group // .) | .name // empty') + b_name=$(wn_b --json groups show "$mls_gid" 2>/dev/null \ + | jq -r '(.result // .) | (.group // .) | (.profile.name // .name) // empty') local a_name a_name=$(amy_field '.name' marmot group show "$gid" 2>/dev/null || echo "") @@ -194,7 +201,8 @@ test_13_keypackage_rotation() { local id="13 keypackage rotation" local before - before=$(wn_b --json keys check "$A_NPUB" 2>/dev/null | jq -r '.result.event_id // empty') + before=$(wn_b --json keys check "$A_NPUB" 2>/dev/null \ + | jq -r '.result.key_package.key_package_event_id // .result.event_id // empty') if [[ -z "$before" ]]; then record_result "$id" fail "no prior KP for A"; return fi @@ -205,7 +213,8 @@ test_13_keypackage_rotation() { local deadline=$(( $(date +%s) + 60 )) after="" while [[ $(date +%s) -lt $deadline ]]; do - after=$(wn_b --json keys check "$A_NPUB" 2>/dev/null | jq -r '.result.event_id // empty') + after=$(wn_b --json keys check "$A_NPUB" 2>/dev/null \ + | jq -r '.result.key_package.key_package_event_id // .result.event_id // empty') [[ -n "$after" && "$after" != "$before" ]] && break sleep 3 done @@ -323,9 +332,9 @@ test_15_wn_member_leaves() { local show show=$(amy_json marmot group show "$a_gid" 2>/dev/null) || { sleep 3; continue; } local c_still - c_still=$(printf '%s' "$show" | jq --arg p "$C_HEX" '[.members[]? | select(.pubkey == $p)] | length') + c_still=$(printf '%s' "$show" | jq --arg p "$C_HEX" '[.members[]? | select((.pubkey // .member_id) == $p)] | length') local a_still - a_still=$(printf '%s' "$show" | jq --arg p "$A_HEX" '[.members[]? | select(.pubkey == $p)] | length') + a_still=$(printf '%s' "$show" | jq --arg p "$A_HEX" '[.members[]? | select((.pubkey // .member_id) == $p)] | length') if [[ "$c_still" == "0" && "$a_still" == "1" ]]; then ok=1; break fi diff --git a/cli/tests/marmot/tests-manage.sh b/cli/tests/marmot/tests-manage.sh index 46fd29f63c..9a8fcf5fb1 100644 --- a/cli/tests/marmot/tests-manage.sh +++ b/cli/tests/marmot/tests-manage.sh @@ -28,7 +28,8 @@ test_06_member_removal() { local deadline=$(( $(date +%s) + 120 )) removed=0 while [[ $(date +%s) -lt $deadline ]]; do if ! wn_c --json groups members "$mls_gid" 2>/dev/null \ - | jq -e --arg p "$C_HEX" '(.result // .) | .[]? | select((.pubkey // .public_key) == $p)' \ + | jq_list members | jq -e --arg p "$C_HEX" \ + 'select((.member_id // .pubkey // .public_key) == $p)' \ >/dev/null 2>&1; then removed=1; break fi @@ -78,7 +79,10 @@ test_07_metadata_rename() { # `{"result": {"group": {…name…}}}`; older builds returned the bare # group object under `.result`. Probe both shapes so the test survives # either schema. - seen=$(wn_b --json groups show "$mls_gid" 2>/dev/null | jq -r '(.result // .) | (.group // .) | .name // empty') + # MDK 0.9.x keeps the display name in the profile component, not a + # top-level `name`: `.result.group.profile.name`. + seen=$(wn_b --json groups show "$mls_gid" 2>/dev/null \ + | jq -r '(.result // .) | (.group // .) | (.profile.name // .name) // empty') [[ "$seen" == "Interop-02-renamed" ]] && break sleep 3 done @@ -151,7 +155,7 @@ test_08_admin_promote_demote() { local admins admins=$(wn_b --json groups admins "$mls_gid" 2>/dev/null \ - | jq -r '(.result // .) | .[]?.pubkey // .[]?.public_key // .[]?' | tr '\n' ' ') + | jq_list admins | jq_member_ids | tr '\n' ' ') if [[ "$admins" == *"$A_HEX"* ]]; then record_result "$id" fail "A still admin after demote" else @@ -177,7 +181,8 @@ test_11_leave_group() { local deadline=$(( $(date +%s) + 120 )) gone=0 while [[ $(date +%s) -lt $deadline ]]; do if ! wn_b --json groups members "$mls_gid" 2>/dev/null \ - | jq -e --arg p "$A_HEX" '(.result // .) | .[]? | select((.pubkey // .public_key) == $p)' \ + | jq_list admins | jq -e --arg p "$A_HEX" \ + 'select((.admin_id // .pubkey // .public_key) == $p)' \ >/dev/null 2>&1; then gone=1; break fi @@ -218,7 +223,8 @@ test_17_group_image_commit() { # Skip cleanly if A is no longer a member of GROUP_02 (a later test may have removed # A) — this test only makes sense while A can still commit to the group. if ! wn_b --json groups members "$mls_gid" 2>/dev/null \ - | jq -e --arg p "$A_HEX" '(.result // .) | .[]? | select((.pubkey // .public_key) == $p)' \ + | jq_list members | jq -e --arg p "$A_HEX" \ + 'select((.member_id // .pubkey // .public_key) == $p)' \ >/dev/null 2>&1; then record_result "$id" skip "A not in GROUP_02"; return fi