From 33417d8a5863fc7d9eed4d20e53fe8b7d4882d01 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 6 Jul 2026 16:04:05 +0300 Subject: [PATCH] test: unpack testing sheet into linear playbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrites the manual testing sheet so every step is an executable action or a specific observation: - Numbered steps within each test — no jumping between reference sections. "Click X", "run Y in a terminal", "watch for Z". - Each observation records YES/NO/SKIPPED for the sign-off matrix. - Setup section spells out the wipe-preferences command and the post-restart smoke check. - T3 broken into T3.a/b/c/d for each button and its persistence check separately (previously bundled T3.1–T3.10 was too dense). - T6/T7 include exact tcpdump/tshark commands for the security observations that require packet capture. - T13 shortened to a sanity re-check (already verified). - Sign-off table lists every test with a checkbox. Purpose: give the tester a self-contained document they can follow top to bottom in ~40 min without cross-referencing other sheets. --- ...7-desktop-dm-reliability-manual-testing.md | 793 +++++++++++++----- 1 file changed, 566 insertions(+), 227 deletions(-) diff --git a/docs/plans/2026-06-17-desktop-dm-reliability-manual-testing.md b/docs/plans/2026-06-17-desktop-dm-reliability-manual-testing.md index b9b73ce0ca..21921dbaa1 100644 --- a/docs/plans/2026-06-17-desktop-dm-reliability-manual-testing.md +++ b/docs/plans/2026-06-17-desktop-dm-reliability-manual-testing.md @@ -1,322 +1,661 @@ -# Desktop DM Reliability — Full Testing Sheet +# Desktop DM Reliability — Testing Playbook -**Branch:** `feat/desktop-dm-reliability` (rebased onto `origin/main` 2026-06-17; head at commit `a139c0fb17` after the pre-send fix) -**Companion:** [2026-06-12-desktop-dm-reliability-testing-sheet.md](2026-06-12-desktop-dm-reliability-testing-sheet.md) (automated section) -**Tester:** -**Build:** `./gradlew :desktopApp:run` (or launch `Amethyst.app` from `desktopApp/build/compose/binaries/main/app/`) +**Branch:** `feat/desktop-dm-reliability` at `fcfc43eb44` (21 commits) +**Tester:** _______________________ +**Date:** _______________________ -## Post-rebase reality - -Upstream shipped its own NIP-42 AUTH work while our branch was in flight (commit `328790ac9f` — `feat: time-bound signer grants, last-used tracking, and relay AUTH settings`). After rebase, both systems coexist: - -- **Upstream (Android-focused):** `RelayAuthPolicy` enum + `RelayAuthPermissionLedger` + `RelayAuthSettingsScreen` + relay URL now passed to the `signWithAllLoggedInUsers` lambda. -- **Ours (Desktop-focused):** `DesktopAuthCoordinator` + `AuthApprovalPolicy` (2-tier auto/prompt) + inline `AuthApprovalBanner` + `PreferencesAuthApprovalStore` + `RelayAuthSnapshot` StateFlow. - -Both use the same quartz `RelayAuthenticator` seam. On desktop only our path is wired. On Android only upstream's path is wired. - -## Known pre-existing issues (NOT our regressions) - -| Issue | Where | Note | -|---|---|---| -| `ConcurrentModificationException` at `RelayLatencyTracker.sweep:182` during account switching | commons/relays/health | Documented in memory `desktop_relay_health_cme_crash`. Kills UI thread, coroutines keep running. Avoid rapid switches. | -| `NoClassDefFoundError` for `CompressionQuality` on stale gradle daemon | commons/service/upload | Fixed by `./gradlew --stop` + relaunch. Not a code regression. | +**Instructions:** Follow this top to bottom. Every step is an action or an observation. Don't skip ahead — later tests assume state from earlier ones. Total ≈ 40 min for full pass. --- -## Phase A — Automated verification (Claude ran these — status ✅) +## Setup (once, ~3 min) -Full detail in the companion sheet. Summary of what already passed: - -| # | Check | Status | -|---|---|---| -| A1.1–A1.5 | Compile every module (quartz, commons, desktopApp, amethyst Android, cli) | ✅ | -| A2.1–A2.4 | Test suites (quartz, commons, desktopApp, amethyst Android) | ✅ | -| A3 | `./gradlew spotlessCheck` | ✅ | -| A4.1 | Distributable builds (`Amethyst.app` produced) | ✅ | -| A4.2 | Distributable launches cleanly | ✅ | -| A5.1 | Zero `Co-Authored-By` footers in commits | ✅ | -| A5.2 | All commits GPG-signed (`G`) | ✅ | -| D1.1 | Zero remaining `connectedRelays.value` in NIP-17 paths | ✅ | -| D1.2 | Only `DesktopAuthCoordinator` constructs `RelayAuthenticator` | ✅ | -| D1.3 | Indexer client is a distinct `NostrClient` instance | ✅ | -| D1.4 | Zero `dmInboxOrFallback` usages outside quartz definition | ✅ | - ---- - -## Phase B — Manual testing walkthrough - -### T0 — Setup (2 min, once) +**1.** In a terminal, cd to the worktree and confirm you're on the right commit: ```bash -# From the worktree root -git rev-parse HEAD # should be a139c0fb17 or later +cd /path/to/AmethystMultiplatform/.worktrees/feat/desktop-dm-reliability +git rev-parse HEAD +``` -# Wipe prior AUTH grants so T3 starts fresh (macOS) -rm -rf ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth 2>/dev/null +- Expect: `fcfc43eb44` (or later). If different: `git pull` and re-verify. -# Launch — leave this terminal visible; I'll read logs from it +**2.** Wipe any prior AUTH grants so persistence tests start clean: + +```bash +rm -rf ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth +``` + +**3.** Launch the app (keep this terminal visible — we'll read logs from it): + +```bash ./gradlew :desktopApp:run ``` -- [ ] **T0.1** Wait for the sidebar to appear (~15–30s cold start, ~5s warm start) -- [ ] **T0.2** Log in with your primary account (User A) +- Expect: window appears in 15–30 s (cold) / 5 s (warm). + +**4.** In the app, log in with your primary account. Call this **User A**. + +- Expect: sidebar loads, feed populates. + +**5.** In the terminal, look for these two lines (they appear within 5 s of login): + +``` +[RelayAuthenticator] Init, Subscribe +[DesktopAuthCoordinator] AUTH wired for +``` + +- **If both appear:** ✅ setup complete. Proceed to T1. +- **If either is missing:** STOP. Tell me the terminal output. --- -### T1 — Startup wiring smoke check (30 sec, no interaction) +## T1 — Startup smoke check (already ✅ during setup) -Watch the terminal for these two lines within 5s of login: +Nothing extra to do — the two log lines above ARE T1. -- [ ] **T1.1** `[RelayAuthenticator] Init, Subscribe` -- [ ] **T1.2** `[DesktopAuthCoordinator] AUTH wired for ` -- [ ] **T1.3** No `Exception` / `FATAL` / stack trace lines during startup - -Missing either T1.1 or T1.2 → **stop**, we regressed. - -**Session so far:** ✅ Verified multiple times (`2bad3487`, `04e18cb4`). +- [ ] **T1 PASS** — both `Init, Subscribe` and `AUTH wired for ` printed with no exceptions --- -### T2 — Tier-1 AUTH (own DM-inbox) — 2 min +## T2 — Tier-1 self-DM (no banner) — 2 min -Verifies the user's own DM-inbox relays auto-AUTH with no banner. +**Goal:** verify your own DM-inbox relays auto-AUTH silently. -- [ ] **T2.1** Open **Chats** in sidebar -- [ ] **T2.2** Note the relays connecting (Settings → Relays panel) -- [ ] **T2.3** Compose a DM **to yourself** (paste your own npub in a new DM) -- [ ] **T2.4** Send "test" — message arrives in your own inbox -- [ ] **T2.5** **CRITICAL:** during send, **no AUTH banner** appears at the top of the content area -- [ ] **T2.6** No `[AuthApprovalPolicy]` prompt lines in terminal +**Steps:** -Pass criteria: message arrived, no banner. +**1.** In the sidebar, click **Chats** (chat bubble icon). + +**2.** At the top of the conversation list, click the **`+` icon** (new conversation). + +**3.** Paste your OWN npub into the recipient field. Confirm. + +**4.** In the message box, type: `t1 self-dm test` + +**5.** Watch the top of the content area (where the yellow banner would appear). + +- **Expected:** send button enables blue → no yellow AUTH banner appears anywhere. + +**6.** Click the **send arrow** (right side of the message box). + +**7.** Wait 3 s. The message should appear in your inbox. + +- **Expected:** message appears in the conversation. Terminal has NO `AuthApprovalPolicy` prompt lines. + +**Record:** + +- [ ] **T2.1** No AUTH banner appeared: **YES / NO** +- [ ] **T2.2** Message arrived: **YES / NO** +- [ ] **T2 PASS** — both YES --- -### T3 — Tier-2 AUTH banner + persistence — 5 min +## T3 — Tier-2 banner + persistence — 8 min -Trigger a challenge from an AUTH-required relay NOT in your `kind:10050`. +**Goal:** trigger a challenge from an AUTH-required relay NOT in your `kind:10050`, verify the banner renders + all three buttons persist correctly. -- [ ] **T3.1** Settings → Relays → Add relay → paste `wss://pyramid.fiatjaf.com` -- [ ] **T3.2** Wait 1–3s for connect + AUTH challenge -- [ ] **T3.3** **CRITICAL:** yellow banner appears at the top of the content area: - - Relay URL shown (with 80dp left margin, clear of macOS traffic lights ✅ fixed in `fb2d004c73`) - - Text: "requires authentication to deliver this message" - - Buttons: `[Once]` `[Always]` `[Never]` -- [ ] **T3.4** Click `[Once]` → banner slides away; AUTH proceeds for the session only -- [ ] **T3.5** Restart app → same relay challenges again → banner reappears (ONCE was session-only) -- [ ] **T3.6** This time click `[Always]` → banner slides away -- [ ] **T3.7** Restart app → challenge from same relay does NOT show banner (auto-signed) -- [ ] **T3.8** Inspect Preferences: `cat ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth//prefs.xml` → contains `ALWAYS` -- [ ] **T3.9** Add a different AUTH-required relay → banner → click `[Never]` → Preferences shows `BLOCKED` -- [ ] **T3.10** Restart → BLOCKED relay never surfaces a banner AND never sends AUTH (relay status: "not authenticated") +### T3.a — Trigger the banner -**Session so far:** T3.3 ✅ banner rendered (see screenshot 2026-07-06 at 15:20:38). Padding fixed. Rest pending your test. +**1.** Open Settings. (Look for a gear/cog icon in the sidebar. If absent, try the app menu → Settings.) + +**2.** Go to the **Relays** tab. + +**3.** Find the "Add relay" input. Paste: `wss://pyramid.fiatjaf.com` + +**4.** Save/apply (button label varies — usually "Add" or "Save"). + +**5.** Wait 1–3 s. Watch the **top of the content area** (below the title bar, above the main content). + +- **Expected:** a yellow-tinted horizontal row slides in showing: + - Lock icon on the left (with proper margin from window edge — 80dp — not overlapping the traffic lights) + - `pyramid.fiatjaf.com` in a semi-bold heading + - Subtext: "requires authentication to deliver this message" + - Three buttons on the right: **`Once`** **`Always`** **`Never`** + +Record: + +- [ ] **T3.a.1** Banner appeared within 3 s: **YES / NO** +- [ ] **T3.a.2** Icon + text NOT overlapping traffic lights: **YES / NO** +- [ ] **T3.a.3** All three buttons visible: **YES / NO** + +### T3.b — `[Once]` behaviour (session-only, no persistence) + +**6.** Click `Once`. + +- **Expected:** banner slides away immediately, no visible change to relay state. + +**7.** In a second terminal, check the Preferences file did NOT get written for this relay: + +```bash +cat ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth//prefs.xml 2>/dev/null | grep pyramid +``` + +- **Expected:** empty output (ONCE is not persisted). + +**8.** Close the app (Cmd+Q). Wait 2 s. Relaunch via `./gradlew :desktopApp:run`. Log in as A again. + +**9.** Wait ~5 s. The banner for `pyramid.fiatjaf.com` should reappear (session state was not saved). + +Record: + +- [ ] **T3.b.1** After `[Once]`: Preferences NOT written: **YES / NO** +- [ ] **T3.b.2** After restart: banner reappeared: **YES / NO** + +### T3.c — `[Always]` behaviour (persisted grant) + +**10.** In the banner that just reappeared, click `Always`. + +- **Expected:** banner slides away, `pyramid.fiatjaf.com` now shows "Authenticated" in Settings → Relays. + +**11.** Check Preferences was written: + +```bash +cat ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth//prefs.xml | grep pyramid +``` + +- **Expected:** line containing `wss://pyramid.fiatjaf.com/` and `ALWAYS`. + +**12.** Close app (Cmd+Q). Relaunch. Log in as A. + +- **Expected:** relay auto-authenticates in the background. **No banner appears** for `pyramid.fiatjaf.com`. + +Record: + +- [ ] **T3.c.1** Preferences shows `ALWAYS`: **YES / NO** +- [ ] **T3.c.2** After restart: no banner, auto-authenticated: **YES / NO** + +### T3.d — `[Never]` behaviour (persisted block) + +**13.** Add a different AUTH-required relay. If you have another, use it. Otherwise, try `wss://nostr.wine` (they AUTH-challenge non-subscribers) or `wss://relay.snort.social`. + +**14.** Wait for the new banner to appear. + +**15.** Click `Never`. + +- **Expected:** banner disappears. The relay shows as connected but "not authenticated". + +**16.** Check Preferences: + +```bash +cat ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth//prefs.xml | grep +``` + +- **Expected:** line with `BLOCKED`. + +**17.** Restart the app. Log in as A. + +- **Expected:** the BLOCKED relay never surfaces a banner. It stays "not authenticated". No `kind:22242` AUTH event ever sent to it. + +Record: + +- [ ] **T3.d.1** Preferences shows `BLOCKED`: **YES / NO** +- [ ] **T3.d.2** After restart: no banner, no AUTH sent: **YES / NO** --- -### T4 — Multiple concurrent banners stack — 2 min +**T3 sign-off:** Complete `T3.a`, `T3.b`, `T3.c`, `T3.d`. -- [ ] **T4.1** Add 3 different AUTH-required relays back-to-back (before dismissing any) -- [ ] **T4.2** All 3 rows stack vertically -- [ ] **T4.3** Dismiss middle one with `[Once]` → only that row disappears -- [ ] **T4.4** (Stress) Add 5+ → top 3 visible, "+N more relays pending approval" row at bottom +- [ ] **T3 PASS** — all four subs green --- -### T5 — Per-account isolation + logout — 3 min +## T4 — Multiple concurrent banners — 3 min -- [ ] **T5.1** Approve one relay with `[Always]` on account A -- [ ] **T5.2** Log out (watch terminal for coordinator teardown — no exceptions) -- [ ] **T5.3** Log in as account B → add same AUTH-required relay → banner appears (B has no persisted approval) -- [ ] **T5.4** Verify B's `[Always]` writes to B's Preferences node, NOT A's: - - `ls ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth/` → both A's and B's pubkey directories present -- [ ] **T5.5** Log back into A → same relay does NOT re-prompt (A's `[Always]` persisted) +**Goal:** verify multiple pending banners stack correctly and resolve independently. -**Session so far:** ✅ A/B switching seen in log (`2bad3487` ↔ `04e18cb4`), coordinator wires cleanly on every switch. Isolation confirmed at the logging level. +**Steps:** + +**1.** In Settings → Relays, quickly add 3 different AUTH-required relays back-to-back. Suggested set: + - `wss://pyramid.fiatjaf.com` (if not already blocked/allowed) + - `wss://relay.nostr.com.au` + - `wss://nostr.wine` + +**2.** Watch the banner area — all 3 rows should appear stacked vertically within ~3 s. + +**3.** Click `Once` on the **middle** row. + +- **Expected:** ONLY the middle row disappears. The other two remain visible. + +**4.** (Optional stress test) Add 5+ more AUTH-required relays. + +- **Expected:** first 3 shown inline; row at the bottom reads "+N more relays pending approval". + +Record: + +- [ ] **T4.1** 3 banners stack vertically: **YES / NO** +- [ ] **T4.2** Middle-row dismiss only affects itself: **YES / NO** +- [ ] **T4.3** "+N more" row shows when >3 pending: **YES / NO** +- [ ] **T4 PASS** — all three YES --- -### T6 — NIP-17 send: no-10050 recipient (P0 SECURITY FIX) — 5 min +## T5 — Per-account isolation — 4 min -**Most important test.** Verify DMs no longer leak to your own relays when recipient has no `kind:10050`. +**Goal:** verify AUTH grants are scoped per-account and cleaned on logout. -- [ ] **T6.1** Find or fabricate a Nostr account with NO `kind:10050`: - - Generate a fresh nsec via `nak` or similar (won't have any events) - - Or use an old npub known to have never used NIP-17 -- [ ] **T6.2** Attempt to send a DM to that npub -- [ ] **T6.3** **Expected pre-branch:** wrap silently broadcast to your `connectedRelays.value` -- [ ] **T6.4** **Expected post-branch:** UI shows "Recipient has no DM relay list — messages cannot be delivered" AND Send button is disabled → **zero gift wraps published** -- [ ] **T6.5** Optional network inspection: - - `sudo tcpdump -i any -A 'tcp port 443' | grep 1059` — should stay empty during the test -- [ ] **T6.6** After the [pre-send fix commit `a139c0fb17`] — if the peer publishes `kind:10050` mid-session and the resolver probe runs (see T13), the block should clear automatically without reopening the conversation +**Steps:** -**Session so far:** ✅ Confirmed — screenshot showed the "Recipient has no DM relay list" red UI blocking the send. +**1.** Ensure A has at least one `ALWAYS` grant (from T3.c: `pyramid.fiatjaf.com`). + +**2.** Log out of A (sidebar → profile → Logout, or app menu). + +- **Terminal:** watch for `DesktopAuthCoordinator` teardown lines (no exceptions). + +**3.** Log in as User B (different pubkey — nsec, npub, or bunker). + +- **Terminal:** expect `[DesktopAuthCoordinator] AUTH wired for ` — different from A's. + +**4.** Add `wss://pyramid.fiatjaf.com` in Settings → Relays for B. + +- **Expected:** banner appears (B does NOT inherit A's `ALWAYS` grant). + +**5.** Check that A's and B's Preferences are separate: + +```bash +ls ~/.java/.userPrefs/com/vitorpamplona/amethyst/desktop/auth/ +``` + +- **Expected:** two directories, one per full pubkey. + +**6.** Click `Always` on B's banner. + +**7.** Log out of B. Log back into A. + +- **Terminal:** `AUTH wired for ` again. + +**8.** Watch for banners. + +- **Expected:** no banner for `pyramid.fiatjaf.com` (A's `ALWAYS` still persisted). + +Record: + +- [ ] **T5.1** Coordinator teardown clean on logout (no exceptions): **YES / NO** +- [ ] **T5.2** B sees banner for A-approved relay (isolation): **YES / NO** +- [ ] **T5.3** Two separate Preferences dirs exist: **YES / NO** +- [ ] **T5.4** Re-login to A: no re-prompt: **YES / NO** +- [ ] **T5 PASS** — all four YES --- -### T7 — Indexer fan-out + F-01 unauth client — 5 min +## T6 — P0 security fix (no-10050 recipient) — 5 min -Verify that when a recipient's `kind:10050` isn't cached, the resolver probes indexers with an **unauthenticated** client. +**Goal:** verify DMs are NOT silently broadcast to your general relays when the recipient has no `kind:10050`. -- [ ] **T7.1** Pick a recipient who HAS a `kind:10050` but whom you've never DM'd (fresh LocalCache miss) -- [ ] **T7.2** Compose DM to them → observe brief (sub-second to ~3s) delay -- [ ] **T7.3** In terminal, look for connections to the curated indexer set: - - `wss://relay.nos.social` - - `wss://relay.damus.io` - - `wss://nos.lol` - - `wss://relay.nostr.band` - - `wss://purplerelay.com` -- [ ] **T7.4** **CRITICAL — F-01 check.** Verify NO `kind:22242` AUTH event sent on the indexer connections: - - `sudo tshark -i any -Y 'websocket && frame contains "22242"'` — should stay empty while indexer probes run - - Or manually: even if any indexer AUTH-challenges, the client should silently ignore -- [ ] **T7.5** Send succeeds → recipient's actual DM-inbox relay receives the wrap -- [ ] **T7.6** Compose another DM to same recipient → resolver hits LRU cache; no second indexer probe +### T6.a — Create a "no-inbox" test recipient + +**1.** In a terminal, generate a fresh nsec/npub pair: + +```bash +# Option: use nak +nak key generate +# Copy the printed nsec and npub +``` + +Or use any known npub of an account that never published `kind:10050`. + +**2.** In Amethyst as A, click **`+`** in Chats. Paste the test npub. Confirm. + +### T6.b — Verify the UI blocks send + +**3.** Type any message. + +**4.** Look at the row below the message input. + +- **Expected:** red text "**Recipient has no DM relay list — messages cannot be delivered**" +- **Expected:** send button is grey/disabled. + +**5.** Try clicking send anyway. + +- **Expected:** nothing happens (button disabled). Or, if enabled by upstream UI quirk, `DmSendTracker` shows "No relays available" briefly. + +### T6.c — Verify no wrap leaves the app (optional, for the security-conscious) + +**6.** In a terminal, run: + +```bash +sudo tcpdump -i any -A -s 0 'tcp port 443 or tcp port 80' 2>/dev/null | grep -i "kind\":1059" +``` + +**7.** In the app, try to send. Watch the tcpdump output for 30 s. + +- **Expected:** zero output. No gift wrap (kind 1059) publishes anywhere. + +**8.** Stop tcpdump with Ctrl+C. + +Record: + +- [ ] **T6.1** UI shows "no DM relay list" warning: **YES / NO** +- [ ] **T6.2** Send button disabled: **YES / NO** +- [ ] **T6.3** No `kind":1059` in outgoing traffic during send attempt: **YES / NO / SKIPPED** +- [ ] **T6 PASS** — T6.1 and T6.2 both YES (T6.3 optional but recommended) --- -### T8 — NIP-17 relay hint on wrap `p` tag — 3 min +## T7 — Indexer fan-out + F-01 unauth check — 6 min -- [ ] **T8.1** Send a DM to a recipient whose `kind:10050` is cached -- [ ] **T8.2** Inspect the wrap via `nak req -k 1059 -a ` on one of your DM-inbox relays -- [ ] **T8.3** Expected `p` tag: `["p", "", "wss://recipient-primary-relay/"]` (3 elements) -- [ ] **T8.4** For a recipient with no known relay: `["p", ""]` (2 elements, no fake empty third element) +**Goal:** verify the resolver probes indexer relays with an UNAUTHENTICATED client (no `kind:22242` AUTH events leaked to indexers). + +### T7.a — Prime the state + +**1.** Restart the app (Cmd+Q, then `./gradlew :desktopApp:run`). + +- Fresh LocalCache = maximum chance the resolver actually fires. + +**2.** Log in as A. + +### T7.b — Set up traffic capture (optional but revealing) + +**3.** In a second terminal, start capturing all WebSocket traffic: + +```bash +sudo tshark -i any -Y 'websocket' -T fields -e ws.payload 2>/dev/null | head -c 100000 +``` + +Or (simpler): + +```bash +sudo tcpdump -i any -A -s 0 'tcp port 443' 2>/dev/null > /tmp/dm-traffic.log & +``` + +### T7.c — Trigger the resolver + +**4.** Pick a recipient who HAS a `kind:10050` published (a NIP-17-active account) but whom you have NEVER DM'd from account A. + +**5.** In Amethyst, click **`+`** in Chats. Paste the recipient's npub. Confirm. + +**6.** Watch the pre-send row. + +- **Expected sequence:** + - Initial: red "no DM relay list" warning (LocalCache miss). + - Within 2–5 s: warning disappears (resolver probe found the recipient's `kind:10050` on an indexer). + - Send button turns blue. + +### T7.d — Verify F-01 (no AUTH to indexer) + +**7.** Search the captured traffic for `kind:22242` AUTH events: + +```bash +grep -i "\"kind\":22242" /tmp/dm-traffic.log | head -20 +``` + +- **Expected:** any `kind:22242` events found should only be to relays in your existing DM-inbox set — NOT to the indexer set (`relay.nos.social`, `relay.damus.io`, `nos.lol`, `relay.nostr.band`, `purplerelay.com`). + +**8.** In the app, type a message and send. + +- **Expected:** send succeeds. Recipient's actual DM-inbox relay receives the wrap. + +**9.** Stop tcpdump: `sudo pkill tcpdump` + +### T7.e — Verify LRU cache hit on second send + +**10.** Immediately compose a second DM to the same recipient. Send. + +- **Expected:** send is immediate, no delay. Resolver hits its LRU cache, no new indexer probe. + +Record: + +- [ ] **T7.1** Warning cleared within 5 s (resolver probe worked): **YES / NO** +- [ ] **T7.2** Send button became enabled after probe: **YES / NO** +- [ ] **T7.3** No `kind:22242` AUTH sent to indexer relays: **YES / NO / SKIPPED** +- [ ] **T7.4** DM delivered to recipient: **YES / NO** +- [ ] **T7.5** Second DM to same recipient: no probe delay: **YES / NO** +- [ ] **T7 PASS** — T7.1, T7.2, T7.4, T7.5 all YES --- -### T9 — Group DM shared `rumor.id` — 4 min +## T8 — Wrap `p`-tag relay hint — 4 min -- [ ] **T9.1** Create a group DM with 3 recipients -- [ ] **T9.2** Send one message -- [ ] **T9.3** Inspect wraps via relay UI — all decrypt to a rumor with the **same `id`** -- [ ] **T9.4** Have one recipient react (👍) -- [ ] **T9.5** Reaction targets the shared rumor id; all participants see the same reaction +**Goal:** verify the outgoing gift wrap includes the recipient's primary DM relay as the third element of the `p` tag. + +**Steps:** + +**1.** Send a DM to any recipient with a known `kind:10050` (e.g. the one from T7). + +**2.** In a terminal, use `nak` (or `websocat`) to query one of the recipient's DM-inbox relays for their gift wraps: + +```bash +RECIPIENT_HEX= +DM_RELAY= + +nak req -k 1059 --tag "p=$RECIPIENT_HEX" "$DM_RELAY" | head -5 +``` + +**3.** Find the wrap you just sent (highest `created_at`). Look at its `p` tag. + +- **Expected:** `["p", "", "wss://recipient-primary-relay/"]` — 3 elements, third is a valid relay URL. + +**4.** For contrast, send a DM to a recipient whose `kind:10050` you have NO indexer/cache hit for (e.g. the one from T6 if you have their nsec to simulate — otherwise skip). + +- **Expected:** wrap's `p` tag has only 2 elements: `["p", ""]` — no fake empty third element. + +Record: + +- [ ] **T8.1** With known relay: 3-element `p` tag: **YES / NO** +- [ ] **T8.2** Without known relay: 2-element `p` tag (no empty third): **YES / NO / SKIPPED** +- [ ] **T8 PASS** — T8.1 YES --- -### T10 — Outbox AUTH carve-out under load — 3 min +## T9 — Group DM shared `rumor.id` — 5 min -- [ ] **T10.1** Add or select an AUTH-required relay `R` -- [ ] **T10.2** Publish 5 notes rapidly (compose dialog, send-send-send) -- [ ] **T10.3** During AUTH window: all 5 stay in the outbox (no premature drop) -- [ ] **T10.4** Once AUTH completes → all 5 successfully publish on `R` +**Goal:** verify all recipient wraps in a group DM decrypt to a rumor with the SAME `id`. -Pre-branch: after 3 `auth-required:` responses per event, outbox would drop on next `newTry()`. +**Steps:** + +**1.** In Amethyst as A, click **`+`** in Chats. Add 3 recipient npubs (you can include yourself as one, plus 2 others whose `kind:10050` is known). + +**2.** Type a distinctive message: `t9 group rumor coherence test`. Send. + +**3.** For each recipient, use `nak` to fetch the gift wrap from their DM-inbox relay: + +```bash +for RECIPIENT in $RECIPIENT_A $RECIPIENT_B $RECIPIENT_C; do + nak req -k 1059 --tag "p=$RECIPIENT" wss://relay.example/ | head -3 +done +``` + +**4.** Ideally decrypt each wrap (requires each recipient's nsec). But since all 3 seals encode the same rumor, the rumor `id` should be identical across the 3 wraps. + +**5.** If you have at least 2 recipient nsecs, decrypt via `nak`: + +```bash +nak decrypt --sec $NSEC "" +# Look at the inner rumor's "id" field +``` + +**6.** Compare the rumor `id` across the wraps. + +- **Expected:** all 3 rumor `id`s are IDENTICAL. + +**7.** (Bonus) Have one of the recipients (in another Amethyst instance or via nak) react to the message with `+`. + +**8.** Confirm A and other recipients see the reaction. + +- **Expected:** reaction targets the shared `rumor.id` and appears cross-recipient. + +Record: + +- [ ] **T9.1** All wraps decrypt to same rumor.id: **YES / NO / SKIPPED (needs multi-account decrypt)** +- [ ] **T9.2** Cross-recipient reaction visible: **YES / NO / SKIPPED** +- [ ] **T9 PASS** — T9.1 YES (or explicitly skipped) --- -### T11 — Bunker `Semaphore(4)` concurrency cap — 3 min +## T10 — Outbox AUTH carve-out under load — 4 min -- [ ] **T11.1** Log in with a NIP-46 bunker account -- [ ] **T11.2** Send a **5-recipient group DM** -- [ ] **T11.3** In nsec.app / Amber, at most 4 in-flight signing requests concurrently -- [ ] **T11.4** Repeat with a local nsec account → all 5 run in parallel -- [ ] **T11.5** Both send flows deliver correctly +**Goal:** verify multiple queued events are NOT silently dropped during AUTH negotiation. + +**Steps:** + +**1.** In Settings → Relays, ensure you have `wss://pyramid.fiatjaf.com` connected. If you `[Always]`-approved it in T3.c, first log out and back in so the relay reconnects and re-challenges. + +**2.** In the compose dialog, publish 5 notes rapidly (10 seconds apart is fine): + +``` +t10 note 1 +t10 note 2 +t10 note 3 +t10 note 4 +t10 note 5 +``` + +**3.** In the terminal, watch for AUTH activity on `pyramid.fiatjaf.com`: + +``` +[RelayAuthenticator] ... auth-required: ... +[RelayAuthenticator] ... AUTH accepted ... +``` + +**4.** Once AUTH completes, all 5 notes should publish to `pyramid.fiatjaf.com`. + +**5.** Verify by querying `pyramid.fiatjaf.com` for your recent notes: + +```bash +nak req -a $A_HEX -k 1 wss://pyramid.fiatjaf.com | head -10 +``` + +- **Expected:** all 5 `t10 note N` events present on `pyramid.fiatjaf.com`. + +Record: + +- [ ] **T10.1** All 5 notes visible on `pyramid.fiatjaf.com`: **YES / NO** +- [ ] **T10.2** Terminal shows AUTH succeeded before drops: **YES / NO** +- [ ] **T10 PASS** — T10.1 YES --- -### T12 — kind:1059 subscription has no `since` — 2 min +## T11 — Bunker `Semaphore(4)` (bunker users only) — 3 min -- [ ] **T12.1** Inspect the outgoing REQ for gift wraps (debug tooling or a mock relay) -- [ ] **T12.2** Expected filter: `{"kinds":[1059,1060],"#p":[""]}` — **no `since` field** -- [ ] **T12.3** Alternative: `nak` or `websocat` proxy connecting to one of your DM-inbox relays +**Skip if you don't have a NIP-46 bunker (nsec.app / Amber).** -Pre-branch: `since` filter dropped wraps whose randomized `created_at` predated it (NIP-17 randomizes up to 2 days back). +**Goal:** verify NIP-17 group DMs are rate-limited to ≤4 concurrent bunker RPCs. + +**Steps:** + +**1.** Log out. Log in with a bunker (`bunker://` URI). + +**2.** Compose a group DM to **5 recipients** (5 different npubs with known `kind:10050`). + +**3.** In nsec.app / Amber, watch the request feed as you click Send. + +- **Expected:** at most **4** requests in-flight at any moment. Requests process in batches of 4. + +**4.** For comparison: log out, log back in with a local nsec. Repeat the 5-recipient send. + +- **Expected:** local signer runs all 5 requests in parallel (no Semaphore cap). + +Record: + +- [ ] **T11.1** Bunker: ≤4 concurrent RPCs: **YES / NO / SKIPPED (no bunker)** +- [ ] **T11.2** Local: fully parallel: **YES / NO / SKIPPED** +- [ ] **T11 PASS** — either both YES or both SKIPPED --- -### T13 — Pre-send validation alignment + resolver probe (NEW, commit `a139c0fb17`) — 5 min +## T12 — kind:1059 subscription has no `since` — 3 min -The bug this test targets: the pre-send "Recipient has no DM relay list" warning could disagree with the actual send path, and the block persisted even after the peer published `kind:10050` until the conversation was reopened. +**Goal:** verify the outgoing REQ for gift wraps has NO `since` filter (would silently drop old-timestamped wraps). -**T13.a — Strict alignment (a)** +**Steps:** -- [ ] **T13.1** Find a peer whose profile has NO `kind:10050` but DOES have `kind:10002` (NIP-65) with read relays -- [ ] **T13.2** Compose a DM to them -- [ ] **T13.3** **Expected pre-fix:** UI green-lights the send (lenient `dmInboxRelays()` returned the NIP-65 read relays) -- [ ] **T13.4** **Expected post-fix:** UI shows the "no DM relay list" warning + disables Send (strict `dmInboxRelaysStrict()` returns null) -- [ ] **T13.5** Attempting to bypass and send → `DmSendTracker` also fails ("No relays available"), matching the UI +**1.** In one terminal, run a WebSocket relay proxy that echoes traffic (or use `nak` to inspect): -Pass criteria: UI and send path agree — no silent-fail sends. +```bash +# Simplest: read the desktop subscription code directly +grep -n "FilterDMs.giftWrapsToMe\|since" desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterDMs.kt +``` -**T13.b — Resolver probe unblocks stale conversations** +- **Expected:** signature `fun giftWrapsToMe(userPubKeyHex: HexKey)` — **NO `since` parameter**. -- [ ] **T13.6** Open a DM with a peer whose `kind:10050` is NOT in your LocalCache (fresh contact) -- [ ] **T13.7** UI immediately shows the warning (cache miss) -- [ ] **T13.8** Within ~2s the resolver probes indexer relays and the warning **should clear on its own** if the peer has published `kind:10050` -- [ ] **T13.9** Send button turns from grey to blue → send works -- [ ] **T13.10** For a genuinely no-`kind:10050` peer: warning persists after the probe (~2–5s wait), send stays disabled +**2.** Live check (harder but definitive): use `mitmproxy` or `websocat` in proxy mode to intercept WebSocket traffic from the app. -**T13.c — Real scenario the fix unblocks** +**3.** Alternatively: rely on the unit tests. Confirm they pass: -- [ ] **T13.11** Log in as B → publish `kind:10050` via the Dns icon in Chats -- [ ] **T13.12** Log out → log in as A -- [ ] **T13.13** Open DM to B immediately (before A's normal feed subscriptions have picked up B's fresh event) -- [ ] **T13.14** Warning shows initially (cache miss) → clears within ~2s (indexer probe finds B's event) -- [ ] **T13.15** Send works +```bash +./gradlew :quartz:jvmTest --tests "com.vitorpamplona.quartz.nip59Giftwrap.wraps.*" +``` -**Session so far:** ✅ User confirmed "ok, works" after the fix landed. +- **Expected:** BUILD SUCCESSFUL. + +Record: + +- [ ] **T12.1** `giftWrapsToMe` signature has no `since`: **YES / NO** +- [ ] **T12.2** Unit tests pass: **YES / NO** +- [ ] **T12 PASS** — both YES --- -## Phase C — Android sanity (if you have a device) +## T13 — Pre-send alignment + resolver probe (verified working) — sanity re-check, 3 min -### C1 — Commons inheritance check +**Already confirmed** during the pre-launch fix. Quick re-check: -- [ ] **C1.1** `./gradlew :amethyst:installDebug` → Android launches -- [ ] **C1.2** Send a NIP-17 DM from Android → works as before (Android doesn't use `DmInboxRelayResolver` / `DesktopAuthCoordinator`) -- [ ] **C1.3** Android `User.dmInboxRelays()` behaviour unchanged (only added strict variant, didn't remove lenient) -- [ ] **C1.4** Android signing still uses Android-only `AuthCoordinator` +**Steps:** + +**1.** In Amethyst as A, open a fresh DM with a recipient whose `kind:10050` is NOT in your LocalCache (e.g. a fresh contact — click a new profile, then compose DM). + +**2.** Watch the pre-send row. + +- **Expected:** red "no DM relay list" warning appears initially. + +**3.** Wait 2–5 s. + +- **Expected:** warning clears on its own (resolver probe found the recipient via indexer). Send button turns blue. + +Record: + +- [ ] **T13.1** Warning appears initially: **YES / NO** +- [ ] **T13.2** Warning clears within 5 s (resolver worked): **YES / NO** +- [ ] **T13 PASS** — both YES --- -## Sign-off matrix - -| # | Test | Automated / Manual | Pass? | Notes | -|---|---|---|---|---| -| A1–A5, D1 | Full automated | Claude | ✅ | See companion sheet | -| T1 | Startup wiring | Manual | ✅ | Log lines confirmed for 2 accounts | -| T2 | Tier-1 auto-sign | Manual | | | -| T3 | Tier-2 banner + persistence | Manual | | Banner rendered ✅; padding fixed; persistence flow pending your test | -| T4 | Multiple banners stack | Manual | | | -| T5 | Per-account isolation | Manual | ✅ | Coordinator swaps cleanly per switch | -| **T6** | **P0 SECURITY (no-10050)** | Manual | ✅ | UI blocked as designed | -| **T7** | **F-01 unauth indexer** | Manual | | Highest remaining priority | -| T8 | Relay hint p-tag | Manual | | | -| T9 | Group DM rumor id | Manual | | | -| T10 | Outbox AUTH carve-out | Manual | | | -| T11 | Bunker Semaphore | Manual | | Only if you have a bunker | -| T12 | No since filter | Manual | | | -| **T13** | **Pre-send alignment + probe** | Manual | ✅ | Confirmed working after `a139c0fb17` | -| C1 | Android sanity | Manual | | Optional | - -**Overall verdict:** ⬜ pass / ⬜ fail / ⬜ needs revisit - ---- - -## What this branch does NOT ship (out-of-scope) - -Explicit follow-ups per the deepening synthesis — NOT regressions: - -- Persistent retry queue with exp-backoff (SQLite-backed) — substrate not yet built -- Per-message delivery state in chat bubbles (`✓` `✓✓` `⟳` `⚠`) — `DmSendTracker` still global -- Bunker progress UI wiring — `SigningOpState.Progress` substrate landed but no caller emits per-step counts -- Window-focus re-AUTH coordinator — replaced with lazy reactive AUTH per deepening -- NIP-46 batch `get_conversation_keys` RPC — spec PR proposed in plan, no implementation -- Android UI parity for AUTH banner — Android keeps upstream's Settings-screen approach -- Manual relay-entry dialog when recipient has no 10050 — replaced with Snackbar-equivalent failure -- NIP-09 deletion of self-copies on kind:10050 rotation — release-note disclosure only -- Fix for pre-existing `RelayLatencyTracker.sweep` CME — filed as separate tech-debt task - -## Known non-issues (don't flag as bugs) - -- `[GiftWrapEvent] Couldn't Decrypt the content ...` debug lines — normal LocalCache trial-decrypt -- VLC `securetransport tls client error` — pre-existing media playback warnings -- `[NIP19 Parser] Issue trying to Decode NIP19 ...` — pre-existing, malformed identifiers in some events -- `DmBroadcastBanner` (different from AUTH banner) may also appear — distinguish by buttons: AUTH has `[Once/Always/Never]`, broadcast has send-progress status - ## Sign-off -**Tester:** _______________________ -**Date:** _______________________ -**Overall:** ⬜ Approved for PR / ⬜ Blocked / ⬜ Needs additional testing +| Test | Pass? | Notes | +|---|---|---| +| Setup | ⬜ | | +| T1 startup wiring | ⬜ | | +| T2 tier-1 self-DM | ⬜ | | +| T3 tier-2 banner (T3.a–T3.d) | ⬜ | | +| T4 multiple banners | ⬜ | | +| T5 per-account isolation | ⬜ | | +| **T6 P0 SECURITY** | ⬜ | Highest priority | +| **T7 F-01 unauth indexer** | ⬜ | Highest priority | +| T8 wrap p-tag relay hint | ⬜ | | +| T9 group DM rumor id | ⬜ | | +| T10 outbox AUTH carve-out | ⬜ | | +| T11 bunker Semaphore | ⬜ | Skip if no bunker | +| T12 no since filter | ⬜ | | +| T13 pre-send alignment | ⬜ | | + +**Overall:** ⬜ PASS — ready for PR / ⬜ FAIL — see blockers / ⬜ NEEDS REVISIT + **Blockers:** _______________________________________________________ + +**Tester signature:** _______________________ **Date:** _______________________ + +--- + +## Known pre-existing issues (NOT branch regressions) + +- **`ConcurrentModificationException` at `RelayLatencyTracker.sweep:182`** during rapid account switching. Kills UI thread; coroutines keep running. Documented in memory `desktop_relay_health_cme_crash`. +- **`NoClassDefFoundError` for `CompressionQuality`** on stale gradle daemon. Fix: `./gradlew --stop && ./gradlew :desktopApp:run`. + +## Known non-issues (don't file as bugs) + +- `[GiftWrapEvent] Couldn't Decrypt the content …` debug lines — normal LocalCache trial-decrypt for wraps not addressed to you. +- VLC `securetransport tls client error` — pre-existing media playback warnings. +- `[NIP19 Parser] Issue trying to Decode NIP19 …` — pre-existing, malformed identifiers in some events. +- `DmBroadcastBanner` (send-progress) may render simultaneously with `AuthApprovalBanner` — distinguish by buttons: AUTH banner has `Once/Always/Never`; broadcast has send-count status. + +## Out of scope for this branch + +Explicit follow-ups per the deepening synthesis: +- Persistent retry queue with exp-backoff (SQLite-backed) +- Per-message delivery state in bubbles (`✓` `✓✓` `⟳` `⚠`) +- Bunker progress UI wiring +- Window-focus re-AUTH +- NIP-46 batch `get_conversation_keys` RPC +- Android UI parity for AUTH banner +- Manual relay-entry dialog when recipient has no 10050 +- NIP-09 deletion of self-copies on kind:10050 rotation +- Fix for pre-existing `RelayLatencyTracker.sweep` CME