test(marmot-interop): un-skip tests 14-15 now that filtered UpdatePath works

Tests 14 (wn removes A) and 15 (wn_c leaves) were skipped with a
rationale referring to the pre-filter applyUpdatePath that rejected
wn/mdk-core commits whose UpdatePath omitted nodes with empty-copath
resolution per RFC 9420 §7.7. That code path was fixed in 3279c246
("use filtered direct path in UpdatePath per RFC 9420 §7.9") which
made both outbound generation and inbound validation operate on the
filtered path. The inverted-role interop tests can now actually run.

Test 14 creates a dedicated Interop-14 group, has wn_b admin-remove A,
and asserts amy flips to "not_member" on her next sync.

Test 15 creates Interop-15, has wn_c self-remove, nudges wn_b to
commit the SelfRemove via a rename, and asserts amy sees (C gone,
A present) AND can still publish a kind:9 that B receives — proves
the filtered commit applied cleanly and encryption survived.

https://claude.ai/code/session_01Unm6uLHGLj9UcBY7hWfJVW
This commit is contained in:
Claude
2026-04-23 21:46:14 +00:00
parent 1affe4aa92
commit f08ad845f1
+98 -18
View File
@@ -208,30 +208,110 @@ test_14_wn_removes_a() {
banner "Test 14 — wn (admin) removes A; amy processes Remove"
local id="14 wn removes amy"
# Known gap: wn (mdk-core/openmls) emits the filtered direct-path
# form of UpdatePath on Remove commits per RFC 9420 §7.7 — when the
# copath of a direct-path node has an empty resolution (every leaf
# under it is blank) the corresponding UpdatePathNode is omitted.
# Quartz's RatchetTree.applyUpdatePath currently requires the
# unfiltered node count (`pathNodes.size == directPath.size`) so
# every wn->amy Remove triggers
# "UpdatePath node count (N) doesn't match direct path length (N+k)".
# That's a pre-existing quartz conformance bug, out of scope for
# this branch; the harness carries the test so it starts passing
# the moment the filtered-path path is wired up.
record_result "$id" skip "pending filtered_direct_path support in applyUpdatePath"
# Exercises inbound filtered-direct-path per RFC 9420 §7.7: wn
# (mdk-core/openmls) strips UpdatePathNodes whose copath has empty
# resolution, and amy must accept the short path on receive. Quartz
# wires this on both sides as of commit 3279c246.
local out mls_gid
out=$(wn_b --json groups create "Interop-14" "$C_NPUB" 2>>"$LOG_FILE") || {
record_result "$id" fail "wn create Interop-14 failed"; return
}
mls_gid=$(printf '%s' "$out" | jq_group_id)
[[ -n "$mls_gid" ]] || { record_result "$id" fail "no group_id from create"; return; }
wait_for_invite C 30 >/dev/null && wn_c groups accept "$mls_gid" >/dev/null 2>&1 || true
wn_b groups add-members "$mls_gid" "$A_NPUB" >/dev/null 2>&1 || {
record_result "$id" fail "wn add-members A failed"; return
}
local a_gid
a_gid=$(amy_json marmot await group --name "Interop-14" --timeout 30 | jq -r '.group_id // empty')
[[ -n "$a_gid" ]] || { record_result "$id" fail "A never received Interop-14 invite"; return; }
# B (admin) removes A. The resulting commit carries a filtered
# UpdatePath; amy must apply it without throwing on the node-count
# mismatch that used to happen pre-filter-support.
wn_b groups remove-members "$mls_gid" "$A_NPUB" >/dev/null 2>&1 || {
record_result "$id" fail "wn remove-members A failed"; return
}
# Amy should observe that she's no longer a member. `group show` returns
# a not_member error as soon as the Remove commit is applied locally.
local deadline=$(( $(date +%s) + 120 )) removed=0
while [[ $(date +%s) -lt $deadline ]]; do
local show rc
show=$(amy_json marmot group show "$a_gid" 2>&1)
rc=$?
if [[ $rc -ne 0 ]] && printf '%s' "$show" | jq -e '.error == "not_member"' >/dev/null 2>&1; then
removed=1; break
fi
sleep 3
done
if [[ "$removed" -eq 1 ]]; then
record_result "$id" pass
else
record_result "$id" fail "amy still considered herself a member after wn Remove"
fi
}
test_15_wn_member_leaves() {
banner "Test 15 — wn_c leaves; amy + wn_b process SelfRemove"
local id="15 wn_c leaves"
# Same filtered_direct_path gap as test 14: when wn_c leaves a
# 3-member group, wn_b folds the SelfRemove into a commit whose
# UpdatePath uses RFC 9420 §7.7 filtering, and amy's strict
# applyUpdatePath rejects it. Skip until quartz handles the
# filtered form on inbound.
record_result "$id" skip "pending filtered_direct_path support in applyUpdatePath"
# Same filtered-direct-path path as test 14, but the trigger is a
# SelfRemove proposal from C that wn_b folds into a commit. Amy stays
# a member here — verification is that the commit applies cleanly and
# the group continues to function afterwards.
local out mls_gid
out=$(wn_b --json groups create "Interop-15" "$C_NPUB" 2>>"$LOG_FILE") || {
record_result "$id" fail "wn create Interop-15 failed"; return
}
mls_gid=$(printf '%s' "$out" | jq_group_id)
[[ -n "$mls_gid" ]] || { record_result "$id" fail "no group_id from create"; return; }
wait_for_invite C 30 >/dev/null && wn_c groups accept "$mls_gid" >/dev/null 2>&1 || true
wn_b groups add-members "$mls_gid" "$A_NPUB" >/dev/null 2>&1 || {
record_result "$id" fail "wn add-members A failed"; return
}
local a_gid
a_gid=$(amy_json marmot await group --name "Interop-15" --timeout 30 | jq -r '.group_id // empty')
[[ -n "$a_gid" ]] || { record_result "$id" fail "A never received Interop-15 invite"; return; }
# C self-removes. wn_b picks up the SelfRemove proposal and commits it.
wn_c groups leave "$mls_gid" >/dev/null 2>&1 || true
sleep 5
# Nudge B to commit any outstanding proposals via a no-op rename round-trip.
wn_b groups rename "$mls_gid" "Interop-15 (C left)" >/dev/null 2>&1 || true
# Wait for amy to see C gone AND stay a member herself.
local deadline=$(( $(date +%s) + 120 )) ok=0
while [[ $(date +%s) -lt $deadline ]]; do
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')
local a_still
a_still=$(printf '%s' "$show" | jq --arg p "$A_HEX" '[.members[]? | select(.pubkey == $p)] | length')
if [[ "$c_still" == "0" && "$a_still" == "1" ]]; then
ok=1; break
fi
sleep 3
done
if [[ "$ok" -ne 1 ]]; then
record_result "$id" fail "amy never saw a (C gone, A present) state"; return
fi
# Post-commit round-trip: amy sends, B receives. Confirms encryption
# survived the filtered UpdatePath application on amy's side.
amy_json marmot message send "$a_gid" "after wn_c leave" >/dev/null || {
record_result "$id" fail "amy send failed after wn_c leave"; return
}
if wait_for_message B "$mls_gid" "after wn_c leave" 90; then
record_result "$id" pass
else
record_result "$id" fail "B didn't receive amy's post-leave message"
fi
}
test_16_wn_keypackage_rotation() {