atjamalpha

Test plan

What needs to work, and what’s actually been confirmed.

atjam is in alpha. The lexicon is shipped end-to-end, most read paths render, and a few write paths exist (round creation, signup, submission). This page is the manual run-through that confirms each piece works.

The list is hand-maintained. Status markers go stale fast — they reflect the most recent walk-through, not the live deployment. Re-verify before any production-affecting decision.

Status legend

  • Verified end-to-end at least once on production.
  • Implemented; not yet manually verified.
  • Not yet implemented in the web app.

Site infrastructure

  • /healthz.txt returns 200 with body “ok”.
  • /oauth-client-metadata.json returns 200 with the five-collection scope including at.atjam.invitation.
  • Static pages render: /lexicon, /privacy, /terms, /test-plan, /robots.txt, /favicon.svg.
  • 404 catch-all ($.tsx) renders on unknown paths.
  • NSID DNS: dig +short TXT _lexicon.atjam.at returns the authority DID.
  • Each of the five at.atjam.* schemas resolves at at://<authority-did>/com.atproto.lexicon.schema/<nsid>.
  • Security headers present on document responses: Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy.

Authentication

  • Sign in: type a handle in the AuthBar → redirect to the handle’s PDS → consent → return to /oauth/callback → land on home with handle in header.
  • Resume: reload after sign-in; header still shows handle without re-auth.
  • Sign out: header reverts to handle input; atjam:did cleared from localStorage.
  • Sign in with a did:web account, not just did:plc.
  • Loopback dev: signing in at http://127.0.0.1:3000 uses the inline client_id pattern; http://localhost:3000 should fail per OAuth spec.
  • Stale session: invalidate the OAuth session on the PDS side; next page load gracefully returns to signed-out state rather than crashing.

Organizer flows

Today the only organizer write path is /new — create a round, optionally creating a new jam in the same submit.

  • One-shot: create a brand-new jam and its first round in one submit. Verify both records on the organizer’s PDS; verify redirect lands on the new round’s detail page.
  • Attach to existing: when the organizer already has jams, the form defaults to “attach to existing”; the round attaches to the selected jam.
  • Validation: empty assignment, missing deadline, and empty accepted-types are each blocked with an inline error.
  • Accepted submission types: comma-separated NSIDs are parsed and trimmed; the cleaned array is what lands in the record.
  • Default joinMode: rounds created via /new are written without a joinMode field; readers resolve to “open”.
  • Choose joinMode (hosted or network) at round-creation time.
  • Choose networkGate (signup vs contributed) at round-creation time.
  • Add a signup-deadline milestone (currently only submission-deadline is exposed).
  • Write an at.atjam.invitation record (no “invite a DID” form yet).
  • Edit a round’s milestones after creation.
  • Delete a round you’ve created.

Participant flows

  • Sign up to an open round: button on the round page writes at.atjam.signup on the participant’s PDS, strong-reffing the round.
  • Idempotent signup: clicking sign-up twice — does the UI block the second attempt, or does it write a duplicate record?
  • Submit work: paste an AT URI for a record on another app (e.g. a Bluesky post). Writes at.atjam.submission with the payload strong-ref.
  • Submit a record whose $type isn’t in the round’s acceptedSubmissionTypes: the form should warn or block (the lexicon’s SHOULD).
  • Submit to a round after its submission-deadline: the write succeeds at the protocol level but the UI should signal “late.”
  • See invitations targeting your DID (no inbox view).
  • Sign up to a hosted round with a valid invitation strong-ref attached to your signup.
  • In a network round, invite someone else from your own UI.

Read flows

  • Home feed (/): rounds from VITE_KNOWN_ORGANIZERS plus the signed-in user’s own appear, sorted as expected.
  • Jam detail (/jam/$did/$rkey): renders jam name, description, and the list of its rounds.
  • Round detail (/round/$did/$rkey): renders assignment, milestones, signups, submissions; signup button for non-organizers; submit form for signed-up participants.
  • OG/Twitter meta: view-source a jam page and a round page; confirm og:title and og:description include the actual jam and round names.
  • Round with no signups: shows an empty-state, not a crash.
  • Round with no submissions: shows an empty-state, not a crash.
  • Round whose owning PDS is offline / unreachable: page degrades gracefully with an error, not a blank screen.

Round state derivation

deriveState() turns milestones into a state string for the badge. Today /new only sets submission-deadline, so all UI-created rounds follow the two-state path (open → closed). The three-state path requires hand-crafted records until the UI exposes signup-deadline.

  • No milestones at all → “open” forever.
  • Submission-deadline in future → badge shows “open” (emerald).
  • Submission-deadline in past → badge shows “closed” (stone).
  • Both signup-deadline + submission-deadline, between them → badge shows “in-progress” (amber).
  • Only signup-deadline (no submission-deadline), after it → “in-progress” indefinitely.

Invitation validation (consumer-side)

The lexicon defines who counts as a valid inviter; readers MUST validate the relationship. None of this is enforced in the web app yet — these tests target the validation logic when it lands.

  • Hosted round + signup without an invitation strong-ref → reader treats as invalid.
  • Hosted round + signup with an invitation whose creator DID matches the jam organizer → valid.
  • Hosted round + signup with an invitation from a DID that is NOT the jam organizer → invalid.
  • Invitation invitee DID does not match the signup’s creator DID → invalid (impostor signup).
  • Network round + invitation chain: organizer invites A; A invites B; B’s signup is valid.
  • Network round + forged chain: someone fakes an invitation pretending to be from a participant who hasn’t actually signed up → invalid.
  • Network round with networkGate: "contributed": inviter has signed up but NOT submitted → their invitations are invalid.
  • Network round with networkGate: "contributed": inviter has both signed up AND submitted → their invitations are valid.

Smoke tests after every deploy

The minimum to confirm a deploy didn’t break anything:

  • curl -I https://atjam.at/healthz.txt → 200.
  • curl -I https://atjam.at/oauth-client-metadata.json → 200.
  • curl -s https://atjam.at/oauth-client-metadata.json | grep at.atjam.invitation → one match.
  • curl -s https://atjam.at/ | head -c 200 → SSR’d HTML with the homepage hero.
  • flyctl status → machine in started state.

What this page isn’t

Not automated tests. There’s no test runner that ticks these off — each marker is the result of a human walk-through. Building an actual integration suite is its own piece of work, and the lexicon shape needs to stabilize first. Until then, this is the checklist.