πŸ§ͺ Around Town β€” Pair Test Plan

Live pair-programming QA for the newsletter platform: customer, admin, newsletter send, advertiser sign-up, and billing. Two testers β€” the agent (AS Dev) and Mike β€” execute the same steps with different accounts and data, compare results, and file GitHub issues for every mismatch.

Protocol Environments & data A Β· Customer B Β· Admin C Β· Newsletter send D Β· Advertiser E Β· Billing Results & issue log

How this session works

  1. Agent executes a step in a real browser against the live site, posts screenshots to the Discord thread, and states pass/fail.
  2. Mike repeats the same step with his own accounts and data, posting his screenshots to the thread.
  3. Compare. Same result β†’ check the box in the log. Different result β†’ both re-run once, then file an issue.
  4. File a GitHub issue for every wanted change (see conventions below) and link it in the log row.
  5. Move to the next step. Flows are ordered so data created earlier feeds later flows (subscribers β†’ send; advertiser β†’ billing).

Issue conventions

Evidence conventions

Environments & test data

ThingValue
Production sitesaroundsummerlin.com Β· aroundhenderson.com Β· aroundcarsoncity.com Β· aroundpahrump.com Β· aroundtownnewsletter.com (company)
CRM (admin)crm-microspaces-projects.vercel.app β€” tenant/site switcher in the header; Newsletter and Advertisers pages
Local dev (optional)cd ~/Repos/aroundtown && pnpm install && pnpm --filter aroundtown-web build && pnpm --filter aroundtown-web dev β†’ http://localhost:8080. Town resolved from Host header β€” for API checks use curl -H "Host: aroundsummerlin.com" http://localhost:8080/api/issues
Sender pipelinesender-newsletter.ps1 (aroundsummerlindev workspace). Status: dry-run verified, live send pending token β€” Flow C starts by confirming this.
Stripe billingKeys (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET) not yet set β€” Stripe steps are expected to fail; the failure itself is a test case.

Test data markers

Everything runs against production β€” that's the point of the pair test. The markers above keep test rows identifiable and the sender pipeline from ever mailing real subscribers with test content.

Flow A Β· Customer β€” browse, archive, subscribe

Persona: a Summerlin resident discovering the newsletter. Two towns exercised to prove per-town data isolation.

A1BothOpen https://aroundsummerlin.com fresh (hard reload), desktop.
Expected: masthead + this week's date, tagline "All the highlights without the noise.", subscribe form visible, footer Privacy/Terms links, no unresolved {{TOKEN}} text, no console errors.
A2AgentCheck the archive grid on the homepage: how many issues render, do cards show image/vol/date/title/summary, do they link to issue.html?slug=…. MikeSame, plus type a search term that matches one issue and one that matches nothing.
Expected: only published issues (never drafts); search filters live; no-match shows the empty message.
A3BothOpen the newest issue from the archive. Mike opens it in a private window too.
Expected: title + "β€” Around Summerlin" in the tab, vol/issue label, long-form date, cover image, body HTML with illustrations, "Back to archive" returns to /#issues.
A4AgentTry to open a draft issue slug directly (created in Flow B first β€” run A4 after B4) and a garbage slug issue.html?slug=definitely-not-real.
Expected: both show the error message, never a half-rendered page; draft stays hidden.
A5AgentSubscribe via the form with pairtest.a5.agent@example.com. Watch the button state during the request. MikeSubscribe with you+pairtestA5@…; then try an empty submit and notanemail.
Expected: success toast ("successfully subscribed"), form resets, button shows "Subscribing…" then restores. Invalid input blocked by browser validation, no request sent.
A6BothSubscribe a second time with the same email as your A5 run.
Expected: no crash; observe and record what the UI/CRM does (idempotent success vs duplicate error) β€” current behavior is undocumented, this step pins it down.
A7BothRepeat A5 on https://aroundhenderson.com (each with your own A7 email).
Expected: success β€” and in the CRM both records show the Henderson site id, not Summerlin. Cross-town leakage here is a critical finding.
A8AgentResize to 390px width (mobile) and re-check homepage + issue page. MikeDo A5 on your actual phone.
Expected: form stacks, tagline hidden ≀480px, everything usable one-handed.

Flow B Β· Admin β€” CRM issue lifecycle

Persona: the editor creating and publishing the weekly issue. Feed data: none. Produces: the draft/published issue used by A4 and Flow C.

B1BothOpen https://crm-microspaces-projects.vercel.app. If a login wall appears, stop and note it. Use the header switcher to select the Summerlin tenant/site.
Expected: dashboard loads with Summerlin selected. If the CRM opens with no auth at all, that is a security finding β€” file it ([pair-test B1] CRM has no authentication).
B2AgentContacts page: find the A5 record (pairtest.a5.agent@example.com, "Newsletter Subscriber"). MikeContacts page: find your A5 + A7 records and check their site assignment.
Expected: records present, email lowercased, correct site per town.
B3AgentCreate a newsletter issue: title "Pair Test Issue β€” Agent", slug pairtest-<date>-summerlin, short summary, 2–3 paragraph body, volume/issue numbers, status draft. MikeCreate your own with slug pairtest-<date>-summerlin-m and a different body.
Expected: draft saved and listed with a draft badge.
B4BothWhile still a draft, check /api/issues, the homepage archive, and issue.html?slug=… for your slug.
Expected: draft invisible everywhere β€” not in archive JSON, reader shows "Issue not found". This satisfies A4.
B5BothIn the CRM Newsletter page open "Review full issue" on your draft.
Expected: rendered body HTML (not raw), correct town name everywhere.
B6BothPublish your draft (status β†’ published). Then check the public site.
Expected: appears in the archive immediately; reader URL renders; /api/issues now includes it with only public fields (no internal CRM fields leaked in the JSON).
B7AgentArchive your published pairtest issue; confirm it vanishes from the public archive and its slug 404s again. MikeLeave yours published until after Flow C (the send uses it), then archive it in cleanup.
Expected: archived = hidden from every public surface, still visible in CRM.

Flow C Β· Newsletter send β€” sender.net pipeline

Persona: the operator mailing the weekly issue. Feed data: Mike's address in the sender.net test group; the B6 published issue. Never sends to live subscriber groups.

C0AgentSetup gate: locate sender-newsletter.ps1 and check for a sender.net API token (.env.sender / masked entry). Confirm the sending domain's SPF/DKIM in the sender.net dashboard.
Expected: if the token is still missing, this flow stops here and we file/confirm the standing issue (roadmap: "sender.net: add API token… then first live sync+send"). Documented as blocked, not failed.
C1Agentsync with -DryRun: confirm CRM subscribers β†’ sender.net group mapping, test contacts excluded. MikeReview the dry-run list; confirm none of the pairtest.*@example.com addresses are in it and his address is.
Expected: dry-run output lists real subscribers only, Mike's address present.
C2Agentbuild the email HTML for the B6 issue; inspect the file before any send.
Expected: cover β†’ title β†’ summary β†’ full body with illustrations, archive link and unsubscribe slot present, no raw tokens.
C3Agentsend -DryRun targeting the test group, then (token permitting) the live test-group send. MikeConfirm receipt in his inbox; check rendering in Gmail web + phone mail app.
Expected: campaign created in sender.net dashboard; Mike receives the issue rendered correctly on desktop + mobile.
C4MikeUse the unsubscribe slot on the received email; then agent re-runs sync and Mike checks his CRM contact/subscriber state.
Expected: unsubscribe honored by sender.net; behavior of the next sync is recorded (resubscribes him? flags in CRM?). Undocumented today β€” this step defines it.

Flow D Β· Advertiser sign-up

Persona: a local business owner filling the Advertise section. Feed data: none. Produces: the lead that Flow E bills.

D1AgentOn aroundsummerlin.com fill the Advertise form: PairTest D1 Agent Coffee Co, contact "AS Dev", pairtest.d1.agent@example.com, package Issue sponsor, no phone/website. MikeSame with your company data including phone + website + a notes line.
Expected: success toast ("we'll be in touch"), form resets, button state cycles correctly.
D2BothCRM β†’ Advertisers: find your record.
Expected: status lead; empty phone/website stored as null (agent's) vs populated (Mike's); correct tenant/site.
D3AgentValidation sweep: submit the form with missing company / missing contact / bad email (browser blocks β€” confirm no request). Then curl -X POST /api/advertise with missing fields and with a bad email to test the server side directly.
Expected: API returns 400 with "Company name, contact name, and a valid email are required." Server must enforce independently of the browser.
D4BothQualify: in CRM flip your lead to active with package "Founding Local β€” sidebar + mid-issue mention, $299/mo".
Expected: status change sticks and displays; package name recorded.

Flow E Β· Billing β€” advertiser payments

Persona: the operator billing the D4 advertiser. Feed data: the active advertiser from D4.

E1AgentCRM β†’ Advertisers β†’ create a payment for your advertiser: amountCents 29900, description "Founding Local β€” first month", method manual, status pending. MikeCreate yours with method stripe instead.
Expected: payment row appears, pending badge, Advertisers page pending total increases by $299.
E2AgentMark the manual payment paid.
Expected: badge flips to paid, paid total +$299 / pending βˆ’$299, math on the page sums correctly.
E3BothGenerate the Stripe checkout link for Mike's payment and open it.
Expected: with keys unset this fails β€” record exactly how (error message, HTTP status, UI behavior). Passing would mean keys were set; then complete a real checkout and confirm the checkout.session.completed webhook auto-marks the payment paid.
E4AgentEdge: create a payment with amount 0, then a negative amount, via the API.
Expected: observe + record β€” validation here likely doesn't exist yet; we're deciding the wanted behavior for the issue.

Results & issue log

Fill in during the session. "Match" = agent and Mike saw the same behavior. Every ⚠/βœ– row needs an issue link.

StepAgentMikeMatchIssueNotes
A1
A2
A3
A4needs B3/B4 first
A5
A6behavior undocumented
A7isolation check
A8
B1auth? security finding
B2
B3
B4
B5
B6
B7Mike archives after C3
C0token gate β€” likely blocked
C1
C2
C3
C4behavior undocumented
D1
D2
D3server-side validation
D4
E1
E2
E3Stripe keys unset β€” expected fail
E40/negative amounts

Suggested rounds

Session cleanup