Nostrautica: End-to-End Testing Guide (Multi-Participant)
This guide drives a full browser-based test of Nostrautica with several simultaneous participants, and produces three artifacts:
- A filled-in test report (pass/fail per scenario, bugs found).
ORGANIZER-GUIDE.md, completed with real screenshots.PARTICIPANT-GUIDE.md, completed with real screenshots.
Observations about awkward UX go into a local docs/internal/UI-SUGGESTIONS.md
(gitignored, not published; append, don't rewrite others' findings).
Resilience note for the tester (human or model). This guide describes intent, not pixels. Button labels, exact copy, and layout will drift. If a step says “click Create my identity” and the button now says “Get started”, that's the same step, follow the UI, note the difference, and update the user guides to match what you actually saw. Only report a failure when the capability is missing or broken, not when wording moved.
1. Test environment
1.1 The orchestrator owns the stack
Do not hand-start the relay, Blossom, TLS proxy, and preview server. That is
exactly how the old quick-start ended up telling you to bind port 3000 twice (the
docker Blossom AND blossom.mjs). One command per tier brings up exactly the
infrastructure that tier needs, health-probes it, runs the specs, and tears
everything down with no orphans (e2e/orchestrator.mjs, audit §13.7):
# From the repo root:
pnpm e2e:smoke # preview only — the static PWA loads, no relay/blossom
pnpm e2e:integration # + relay (nak or in-repo) + Blossom + HTTPS proxy
pnpm e2e:chat # + a coordinator with the real Marmot admin bot (a double)
pnpm e2e:full # everything, all specs
The orchestrator builds the app itself (relay tiers get
VITE_NOSTRAUTICA_RELAYS/VITE_NOSTRAUTICA_BLOSSOM pointed at the local stack),
so you never manage that by hand. Pass extra Playwright args after --:
pnpm e2e:integration -- tests/integration/walking-skeleton.spec.ts --headed
Knobs (env): E2E_SKIP_BUILD=1 reuses an existing build (must already point at
the local stack); E2E_RELAY_IMPL=local|nak forces the relay implementation
(default: nak if on PATH (CLAUDE.md gotcha #3) else the in-repo relay).
A selected tier FAILS its setup loudly if its infrastructure can't start,
it never silently skips (audit D-11). The exit code is non-zero on any setup or
test failure, so CI/scripts can gate on it. Ports: preview 4173, relay 7777,
Blossom 3000, HTTPS proxy 8443: each bound by exactly ONE process (the
orchestrator reuses an already-healthy instance instead of double-binding).
To drive the app manually (headed, for the guide's screenshot scenarios), run a
tier headed against a spec you can watch, e.g.
pnpm e2e:integration -- tests/integration/walking-skeleton.spec.ts --headed.
For a free-form manual session, start the pieces from e2e/local-infra/ by hand
(relay, blossom + https-proxy) and a preview with PUBLIC_CSP_EXTRA_CONNECT set,
then open http://127.0.0.1:4173, but the orchestrator is the supported path.
1.1.1 Gotcha: HTTPS Blossom (why the proxy exists)
The media descriptor schema accepts only https:// blob URLs (audit C3. SSRF
hardening). e2e/local-infra/blossom.mjs is plain HTTP, so recording an
intro/talk and submitting it fails ("must be an https URL") unless its upload
responses are https. The orchestrator fronts Blossom (:3000) with a
self-signed TLS proxy (:8443) and sets BLOSSOM_PUBLIC_BASE_URL to the proxy
origin: it generates the throwaway cert under /tmp/nostrautica-tls if absent.
http://localhost:3000 is useful only for text-only diagnostics; its media URLs
are rejected. Chromium accepts the self-signed cert via --ignore-certificate-errors
ignoreHTTPSErrors: true, already wired intoe2e/playwright.config.ts.
1.1.2 Gotcha: CSP is set at RUN time, not just build time
vite preview re-renders the CSP shell (%sveltekit.env.PUBLIC_CSP_EXTRA_CONNECT%)
per request, so the value must be on the RUNNING preview process, not only the
build. Preview is owned by Playwright's webServer (e2e/playwright.config.ts),
whose command sets PUBLIC_CSP_EXTRA_CONNECT at run time, a single preview
owner, so it is never double-bound whether launched by a tier or a bare
playwright test. Without the run-time CSP, every local ws/http connection is
blocked with only "Relay not connected." to show for it.
1.2 Two test tiers
| Tier | Orchestrator tier | Infrastructure | What is testable |
|---|---|---|---|
| Tier 1: no coordinator | e2e:integration |
relay + Blossom | identity, sign-in, event creation, invites, join, manual approval, roster/directory, record & playback, favorites/notes, follows, settings, i18n, theme, outsider privacy |
| Tier 2: with coordinator | e2e:chat / e2e:full |
+ coordinator double | Marmot group chat, and (with a real coordinator + VENICE_API_KEY, ffmpeg) invite-code auto-approval, AI summaries, the matches screen, "Recompute all matches" |
The chat/full tiers start a coordinator DOUBLE (mock-coordinator-chat.mjs /
mock-coordinator.mjs) that uses MockStt/MockLlm, no API money. For a
Tier-2 pass against the REAL coordinator (real STT/LLM, costs money and minutes),
build and run @nostrautica/coordinator per docs/COORDINATOR-OPERATOR-GUIDE.md,
point its relays at ws://127.0.0.1:7777, and attach its npub in Admin.
1.3 Fake camera (required for the record flow)
Headless Chromium can supply a synthetic camera/mic so the intro-video flow works without hardware and without permission prompts:
--use-fake-device-for-media-stream
--use-fake-ui-for-media-stream
(optionally --use-file-for-fake-video-capture=<file>.y4m for a recognizable
clip). In Playwright also grant permissions: ["camera", "microphone"] on the
context.
2. Recommended test infrastructure
One isolated browser session per persona. Nostrautica keeps identity in IndexedDB/localStorage per origin, so session isolation = separate browser context or profile directory. Never share a profile between personas.
Option A: Claude-driven interactive testing (recommended for this guide)
Run one Playwright MCP server per persona, each headless with its own profile dir; desktop personas get a desktop viewport, attendee personas get a phone viewport (§2.1). Example registration:
claude mcp add organizer -- npx @playwright/mcp@latest --browser chromium --headless \
--user-data-dir /tmp/nostrautica-e2e/organizer
claude mcp add attendee-nina -- npx @playwright/mcp@latest --browser chromium --headless \
--user-data-dir /tmp/nostrautica-e2e/nina --device "Pixel 7"
claude mcp add attendee-ivan -- npx @playwright/mcp@latest --browser chromium --headless \
--user-data-dir /tmp/nostrautica-e2e/ivan --device "Pixel 7"
claude mcp add nostr-nadia -- npx @playwright/mcp@latest --browser chromium --headless \
--user-data-dir /tmp/nostrautica-e2e/nadia
claude mcp add outsider-otto -- npx @playwright/mcp@latest --browser chromium --headless \
--user-data-dir /tmp/nostrautica-e2e/otto
Pass the fake-camera flags through to Chromium for the personas that record
video. The model then drives each persona by name (navigate, click, fill,
browser_take_screenshot → save into docs/images/…), which is exactly the
mixed “test + observe + document” mode this guide needs. Persistent
--user-data-dir also lets you stop and resume a session (identity
survives), which mirrors real attendee behavior across days.
An equivalent alternative is a small Node driver script using Playwright's
API with named browser.newContext() per persona, same isolation, useful if
MCP is unavailable.
Option B: scripted regression (the tiers)
e2e/tests/ holds the Playwright suite organized by tier: tests/smoke/
(preview-only) and tests/integration/ (multi-BrowserContext create → join →
approve → roster/directory/record loops): the right home for anything from this
guide worth keeping as an automated regression. Run a tier via the orchestrator
(§1.1), e.g. pnpm e2e:integration. The orchestrator sets NOSTRAUTICA_E2E_RELAY
for you once the relay is confirmed up, so the integration specs RUN rather than
self-skip; running playwright test directly (without the orchestrator) leaves
that env unset and the relay-dependent specs skip. Always go through a tier.
2.1 Mobile viewports are part of the test matrix
The primary device at a real event is a phone. Run attendee personas on a
phone-sized viewport (Playwright device "Pixel 7" ≈ 412×915, or iPhone
14) and the organizer on desktop. For the scripted suite, add a second
project:
projects: [
{ name: "desktop", use: { ...devices["Desktop Chrome"] } },
{ name: "mobile", use: { ...devices["Pixel 7"] } },
],
At minimum, verify on mobile: no horizontal scrolling anywhere, bottom nav
reachable and not overlapping content, join form usable, video recording UI
fits portrait, long naddr/npub strings truncate instead of overflowing,
QR codes fit the screen, tap targets are comfortably tappable. File anything
off into docs/internal/UI-SUGGESTIONS.md with a screenshot.
2.2 Simulating the sign-in methods
Paste key (nsec): fully testable headless. Seed a complete “existing Nostr user” persona with
nak(github.com/fiatjaf/nak; verified with v0.15.2) against the local relay, then paste the nsec into the app:R=ws://localhost:7777 SK=$(nak key generate) echo $SK | nak encode nsec # ← paste this into "Paste a key" nak key public $SK # pubkey, for other personas' follow lists nak event -k 0 --sec $SK -c '{"name":"Nadia","about":"…","picture":"https://api.dicebear.com/9.x/avataaars/png?seed=Nadia"}' $R nak event -k 3 --sec $SK -p <followed-pubkey-hex> $R # non-empty follows(An https picture URL keeps the shipped
img-srcCSP happy.)NIP-07 extension: no real extension in headless Chromium. Either skip (note as untested), or inject a
window.nostrshim via an init script that signs with a fixed test key; then the “Log in with extension” button appears and can be exercised.NIP-46 remote signer,
bunker://paste: fully testable headless withnak bunkeras an auto-signing daemon (no phone needed):nak bunker --sec <hex-or-nsec> ws://localhost:7777 # prints: bunker://<pubkey>?relay=…&secret=XXXXPaste the printed URI into the app's “Paste a key” field → Connect bunker. The
secretin the URI auto-authorizes the connecting client and every subsequent request (sign_event,nip44_encrypt/decrypt) is answered without any prompt. ⚠ nak rotates the secret after each successful connect: each printed URI is single-use, so copy the latest printed URI per client (or pre-authorize a known client key with-k <client-pubkey-hex>). This exercises the full remote-sign surface: join gift-wraps, grant unwrapping, roster decrypt, follows, NIP-17 DMs, measured results are logged locally underdocs/internal/testing/.NIP-46
nostrconnect://QR flow:nak bunker connect <uri>is a stub in nak ≤ 0.15.2 (“this is not implemented yet”), so nak cannot consume the app's QR. Verify the option renders a QR + copyable URI + waiting state; to drive the handshake itself use a phone with Amber, or a small scripted signer (nostr-tools: subscribe kind-24133 for your pubkey, publish the URI'ssecretas the first response, then serveget_public_key/sign_event/nip44_*).
3. Personas
| Persona | Who they are | Viewport | Identity |
|---|---|---|---|
| Olga | Organizer. Comfortable with tech, new to Nostrautica. Creates the event, manages approvals. | Desktop | New, created in-app |
| Nina | Newcomer attendee. Knows nothing about Nostr; got a link from a friend. Joins without an invite code → manual approval. | Phone | New, created during join |
| Ivan | Invited attendee. New user arriving via an invite-code link → auto-approval (Tier 2) or manual (Tier 1). | Phone | New, created during join |
| Nadia | Nostr-native attendee. Has an existing identity (pre-made nsec with a kind-0 profile: name, bio, picture) and existing follows. | Desktop | Existing, pasted key |
| Otto | Outsider. Has an identity but is not approved into the event. Used for privacy checks. | Desktop | New, created in-app |
Seed Nadia before the run: create a keypair, publish a kind-0 profile (name “Nadia”, a bio, a picture) and a kind-3 follow list that includes Nina's pubkey (captured after Nina joins, or pre-generate Nina's key too): this is what makes the “following” social-overlay badges testable.
4. Test scenarios
Run in order, later scenarios depend on earlier state. 📸 marks a required screenshot; names refer to the checklist in §5.
S1: First contact & app chrome (any fresh persona)
- Open the app root. Expect the home/landing screen with the value
proposition and a way to get started. 📸
participant/01-home - Open Settings: switch theme Light↔Dark (persists across reload), switch
language to Slovenčina and back (UI re-translates; note untranslated
strings for
docs/internal/UI-SUGGESTIONS.md). 📸app/settings - Confirm the PWA manifest and service worker are served (installable app).
- Deep-link to a nonsense event URL (
#/e/naddr1invalid), the app shell must render an error state, not a blank page or server 404.
S2: Olga: create identity & event
- Olga logs in via “create identity” (name + optional photo). Expect a
success state with a key backup card (copy secret key; more options
like email link / password-protected file). 📸
participant/03-backup - Create the event: title, summary, start date, location, an approval mode
that allows both invites and manual review, AI matchmaking on, default
video length. 📸
organizer/01-create-form - Expect a success state with a shareable event link (and/or QR).
Capture the link: every other persona uses it. 📸
organizer/02-created - Open the organizer admin. Expect sections for pending requests, invite
codes, coordinator, co-organizers. 📸
organizer/03-admin-empty - Generate a batch of invite codes (e.g. 5). Expect one link + QR per code.
Capture one invite link for Ivan. 📸
organizer/04-invites - (Tier 2) Attach the coordinator by its npub; expect an “attached”
confirmation. 📸
organizer/05-coordinator - Verify the event also appears on Olga's home list marked as one she organizes.
S3: Nina: newcomer joins via plain link (phone)
- Open the plain event link (no code) in Nina's session. Expect the
event page with a clear “join” call to action. 📸
participant/02-event-page - Join as a brand-new user: photo, name, bio, skills, “looking for”. Note
for
docs/internal/UI-SUGGESTIONS.md: is it obvious which fields are public? Is any Nostr jargon leaking? 📸participant/04-join-form - Submit. Expect a “request sent / waiting for the organizer” state.
📸
participant/05-request-sent - Verify Nina is not able to see the attendee roster yet.
S4: Ivan: invited newcomer joins via code link (phone)
- Open the invite-code link in Ivan's session. Join as a new user.
- Tier 2: expect auto-approval within ~15–30 s → a “you're in” state
without organizer action. 📸
participant/06-approvedTier 1: expect the request to land in Olga's pending queue flagged as invite-backed; Olga approves it there. - Confirm Ivan's fresh identity got a backup prompt too.
S5: Nadia: existing Nostr user joins (desktop)
- Open the event link logged out; choose the “already a Nostr user” path;
paste Nadia's nsec. 📸
participant/07-signin-options - On the join form, expect her existing profile shown read-only (name/bio/photo prefilled from Nostr, with a note it won't be changed); only event-specific fields (skills, looking-for) editable.
- Send the join request.
- (Optional variant) Re-run Nadia via the
bunker://remote-signer path using thenak bunkerrecipe in §2.2, same expectations, but every signature/encryption round-trips through the signer daemon.
S6: Olga: approvals & roster
- In admin, expect Nina's and Nadia's pending requests (name, message,
skills, invite badge where applicable). 📸
organizer/06-pending - Approve both. Expect them to move to the approved section.
📸
organizer/07-approved - Within ~15 s, Nina's and Nadia's sessions should flip to approved (may need a revisit/refresh of the event page: note how discoverable that is).
- Everyone approved opens the attendee list and sees the same roster.
📸
participant/08-attendees(from Nina's phone)
S7: Recording intros (each approved attendee)
- From the event page, go to record the intro. Enable camera (fake device
feeds a test pattern), record a few seconds, stop, review the playback,
accept it. 📸
participant/09-record(mid-recording, phone) - Expect an upload-success state and, afterwards, the video playable from that attendee's directory entry (verify from another attendee's session, this proves event-encryption works end to end).
- Re-record path: record again, discard, keep the original, no duplicate entries.
- (Nadia, second event only / optional) If a video library exists, verify “reuse previous video” is offered.
S8: Directory, social overlay & private actions
- From Nina's session: browse attendees, open Nadia's detail page. Expect
profile, skills, intro video, recent Nostr posts (Nadia has real history).
📸
participant/10-attendee-detail - Follow Nadia. From Nadia's session, Nina should show a “follows you” / “following” badge (Nadia already followed Nina via her kind-3).
- Toggle ★ favorite, “want to meet”, “met ✓”, and write a private note on someone. Reload: they persist. Privacy check: from any other persona, confirm none of that is visible.
- Sort tabs (by matches / follows / name) reorder the list sensibly.
S9: Matches (Tier 2 only)
- Wait for the coordinator to process intros (watch its logs; minutes).
- Each attendee opens “people you should meet”. Expect ranked match cards
with a percentage, similar/complementary breakdown, and plain-language
reasoning. 📸
participant/11-matches(phone) - Tap through a match card to the attendee detail.
- Olga triggers “recompute all matches” in admin; expect no errors and eventually refreshed lists.
- Check the AI summary shows up on attendee detail pages.
S10: Otto: outsider privacy checks
- Otto (logged in, never joined) opens the event link: sees only public
info (title, date, summary) and a join prompt, no roster, no videos, no
matches. 📸
participant/12-outsider - Otto navigates directly to the attendees/matches URLs: expect empty/denied states, not decrypted content.
S11: Organizer lifecycle extras
- Co-organizer: Olga adds Nadia's npub as co-organizer. Nadia reopens the event and should now reach the admin screen with working approvals.
- Revoke: Olga revokes Ivan. Expect a consequence-explaining
confirmation. After it: Ivan no longer decrypts new roster/content;
remaining attendees still see the roster (minus Ivan); observe re-grant
flow completes. 📸
organizer/08-revoke - Re-process an attendee, no errors, directory entry republished.
S12: Persistence, hand-off & logout
- Reload each persona's browser: sessions and event access must survive (identity in IndexedDB, no re-login).
- Nina opens her profile page (“Me”): expect the Nostr hand-off, her npub,
copyable, links to other Nostr apps, backup options. 📸
participant/13-me - Log out and back in with a copied nsec, same identity, same event access.
S13: Mobile sweep
Re-check S3, S6.4, S7–S9 screens on the phone personas per §2.1's checklist
(no horizontal scroll, nav reachable, truncation, tap targets). Screenshot
anything broken for docs/internal/UI-SUGGESTIONS.md.
5. Screenshot checklist
Save under docs/images/organizer/ and docs/images/participant/ (plus
docs/images/app/ for shared chrome). PNG, light theme unless noted, phone
screenshots from a phone-viewport persona. Use these exact stems (add
NN- ordering as shown) so guide references resolve; recapture > reuse when
the UI changed.
| File stem | Screen / moment | Persona · viewport | Used in |
|---|---|---|---|
organizer/01-create-form |
event creation form, filled | Olga · desktop | Organizer guide |
organizer/02-created |
success + share link/QR | Olga · desktop | Organizer guide |
organizer/03-admin-empty |
admin overview, no requests | Olga · desktop | Organizer guide |
organizer/04-invites |
generated invite codes w/ QR | Olga · desktop | Organizer guide |
organizer/05-coordinator |
coordinator attached | Olga · desktop | Organizer guide |
organizer/06-pending |
pending join requests | Olga · desktop | Organizer guide |
organizer/07-approved |
approved attendees section | Olga · desktop | Organizer guide |
organizer/08-revoke |
revoke confirmation | Olga · desktop | Organizer guide |
participant/01-home |
landing screen | any · phone | Participant guide |
participant/02-event-page |
event page w/ join CTA | Nina · phone | Both guides |
participant/03-backup |
key backup card | Olga or Nina | Participant guide |
participant/04-join-form |
join form, filled | Nina · phone | Participant guide |
participant/05-request-sent |
waiting-for-approval state | Nina · phone | Participant guide |
participant/06-approved |
“you're in” state | Ivan · phone | Participant guide |
participant/07-signin-options |
existing-user sign-in methods | Nadia · desktop | Participant guide |
participant/08-attendees |
attendee roster | Nina · phone | Participant guide |
participant/09-record |
recording UI mid-capture | Ivan · phone | Participant guide |
participant/10-attendee-detail |
attendee detail w/ video + private actions | Nina · phone | Participant guide |
participant/11-matches |
match list w/ reasoning (Tier 2) | Nina · phone | Participant guide |
participant/12-outsider |
what a non-attendee sees | Otto · desktop | Participant guide |
participant/13-me |
Nostr hand-off / Me page | Nina · phone | Participant guide |
app/settings |
settings (theme + language) | any | Both guides |
6. Producing the two user guides
ORGANIZER-GUIDE.md and PARTICIPANT-GUIDE.md already exist as skeletons
with placeholder screenshots and `` markers. After (not
during) the test run:
- Drop in the screenshots you captured at the checklist stems; delete placeholder comments for images you added.
- Rewrite any step whose wording no longer matches the UI you saw. The skeleton's step text is a best guess; the UI is the truth. Keep the section structure unless a whole feature moved.
- Honor each guide's voice (stated at the top of each skeleton): the participant guide is for someone who has never heard of Nostr, no protocol jargon, no kind numbers, analogies over precision; the organizer guide may assume light technical comfort but still explains why (e.g. what attaching a coordinator gets you).
- Fill the FAQ/troubleshooting sections with real friction you hit during testing: those are the questions real users will have.
- Remove every remaining
TODOmarker; a guide with TODOs is not done. - Cross-check: every image referenced exists in
docs/images/, and every captured screenshot is referenced somewhere (or deleted).
7. Reporting results
Write docs/internal/testing/TEST-REPORT-<YYYY-MM-DD>.md (gitignored, local
only) containing: environment (commit, tier, browser), a table of scenarios
S1–S13 with pass / fail / skipped and one-line notes, a Bugs section
(repro steps, expected vs. actual, console errors, screenshot), and a pointer
to the UI observations you appended to docs/internal/UI-SUGGESTIONS.md.
Bugs block guide-writing only if the flow is
impossible to complete, otherwise document the workaround in the guide's
troubleshooting section and keep going.