test: move re-add-after-remove regression to Quartz MlsGroup test

The bug (re-added user rejoins without a usable own_leaf, so they
can neither decrypt new group messages nor send) reproduces entirely
with two in-process MlsGroup instances — no relay or second Nostr
client needed. A pure Quartz unit test runs in seconds and catches
the regression deterministically, where the bash interop version
depended on whitenoise-rs, wnd daemons and a local relay.

- quartz: add MlsGroupLifecycleTest.testReaddAfterRemove_RejoinerCanEncryptAndDecrypt
  covering create → add → round-trip → remove → re-add with a fresh
  KeyPackage → round-trip again, asserting leafIndex and both
  encrypt/decrypt directions on the rejoined instance.
- interop: drop test_17_readd_after_remove from the headless suite and
  its registration, since the unit test supersedes it.

https://claude.ai/code/session_01JaeqZwVNLKvUUvXWRzPmRP
This commit is contained in:
Claude
2026-04-22 19:52:30 +00:00
parent a10fdb934a
commit 32b0f41be9
3 changed files with 56 additions and 90 deletions
@@ -155,95 +155,6 @@ test_08_admin_promote_demote() {
fi
}
test_17_readd_after_remove() {
banner "Test 17 — Re-add after remove restores B's leaf"
local id="17 re-add after remove"
# Scenario reported on the info screen: A creates a group, adds B,
# removes B, then adds B again. Before the fix B rejoined without a
# usable own_leaf, so B could see metadata but could neither decrypt
# new posts nor sign messages. This test catches that regression by
# driving a full round-trip (A→B and B→A) after the re-add.
local out gid mls_gid
out=$(amy_json marmot group create --name "Interop-17-readd") || {
record_result "$id" fail "amy create failed"; return
}
gid=$(printf '%s' "$out" | jq -r '.group_id')
mls_gid=$(printf '%s' "$out" | jq -r '.mls_group_id')
[[ -n "$gid" && -n "$mls_gid" ]] || {
record_result "$id" fail "missing ids from create"; return
}
# A adds B.
amy_json marmot group add "$gid" "$B_NPUB" >/dev/null || {
record_result "$id" fail "initial add B failed"; return
}
wait_for_invite B 60 >/dev/null || {
record_result "$id" fail "B never received first invite"; return
}
wn_b groups accept "$mls_gid" >/dev/null 2>&1 || true
# Confirm the baseline works: A → B and B → A both deliver.
amy_json marmot message send "$gid" "pre-remove hello" >/dev/null || {
record_result "$id" fail "A send before remove failed"; return
}
wait_for_message B "$mls_gid" "pre-remove hello" 90 || {
record_result "$id" fail "B missed pre-remove hello"; return
}
wn_b messages send "$mls_gid" "pre-remove pong" >/dev/null 2>&1 || true
amy_json marmot await message "$gid" --match "pre-remove pong" --timeout 60 >/dev/null || {
record_result "$id" fail "A missed B's pre-remove pong"; return
}
# A removes B.
amy_json marmot group remove "$gid" "$B_NPUB" >/dev/null || {
record_result "$id" fail "remove B failed"; return
}
# Let the Remove commit propagate to B's wnd.
local deadline=$(( $(date +%s) + 120 )) removed=0
while [[ $(date +%s) -lt $deadline ]]; do
if ! wn_b --json groups members "$mls_gid" 2>/dev/null \
| jq -e --arg p "$B_HEX" '(.result // .) | .[]? | select((.pubkey // .public_key) == $p)' \
>/dev/null 2>&1; then
removed=1; break
fi
sleep 3
done
[[ "$removed" -eq 1 ]] || warn "B still appears in members after remove — continuing"
# B republishes its KP so the next add can find a fresh one.
wn_b keys publish >/dev/null 2>&1 || true
sleep 3
# A re-adds B.
amy_json marmot group add "$gid" "$B_NPUB" >/dev/null || {
record_result "$id" fail "re-add B failed"; return
}
wait_for_invite B 60 >/dev/null || {
record_result "$id" fail "B never received second invite"; return
}
wn_b groups accept "$mls_gid" >/dev/null 2>&1 || true
# Regression probes: after re-add B must have a usable leaf again.
# (1) A→B: proves B can decrypt new group messages.
amy_json marmot message send "$gid" "post-readd hello" >/dev/null || {
record_result "$id" fail "A send after re-add failed"; return
}
wait_for_message B "$mls_gid" "post-readd hello" 120 || {
record_result "$id" fail "B didn't decrypt post-readd message (missing own_leaf?)"; return
}
# (2) B→A: proves B can still sign/commit — the exact breakage reported.
wn_b messages send "$mls_gid" "post-readd pong" >/dev/null 2>&1 || {
record_result "$id" fail "B send after re-add failed (likely no leaf)"; return
}
if amy_json marmot await message "$gid" --match "post-readd pong" --timeout 120 >/dev/null; then
record_result "$id" pass
else
record_result "$id" fail "A never saw B's post-readd pong (B couldn't type)"
fi
}
test_11_leave_group() {
banner "Test 11 — Leave group"
local id="11 leave group"
@@ -123,4 +123,3 @@ test_13_keypackage_rotation
test_14_wn_removes_a
test_15_wn_member_leaves
test_16_wn_keypackage_rotation
test_17_readd_after_remove