J1: the campaign set up and its first session — lane 3's acceptance, and two defects it found (#726, #729) #787

Merged
claude-bot merged 4 commits from build/lane3-j1 into main 2026-09-23 20:56:32 +00:00
Collaborator

The last piece of lane 3. #729's acceptance requires J1 and J6 green on desktop, phone and keyboard-only with axe clean. J6 landed in #770. J1's lane-3 half was a test.skip with a TODO addressed to lane 3 by name: time zone, invite, Discord, and the first session. It's written now.

sh scripts/journeys.sh, whole suite: 27 passed, 3 skipped (J2's lane-4 skip in each project), 0 failed. a11y-baseline.js is still {}, with no tolerated entries. vitest 1928/1928; lint 0 errors; test_sessions, test_pagination and test_series 88/88; ruff clean.

Two defects the journey found, fixed here

1. Settings → Discord wiped what you'd typed when you tested the channel, and its verdict never appeared. The same cause made "Saved" vanish the moment it showed, on Discord and on Recording, retention & AI.

  • After a test or a save, the section called useSettingsAccess().reload. That goes back through the loading state, so the campaign becomes null, SettingsSection draws Busy, and the form unmounts. It then remounts from what's stored: unsaved values gone, verdict gone.
  • The fix is a quiet refresh that keeps the campaign on screen while it re-reads. It's only honoured over the load it was made against, so a stale re-read can't overwrite a newer page load. reload stays as the retry after a failure, where there's nothing on screen to keep.
  • Why the unit tests missed it: a mock that resolves immediately is batched into one render, so the form never visibly unmounted. The new tests keep the re-read pending, and they fail on the old code.

2. The sessions list said "0 times" under every open vote. Sessions.jsx read time_slots, which SessionListItem has never carried; the unit fixture had invented it. The list response now has time_slot_count, counted in one grouped query and only for votes, so it's not an N+1. It's additive on the user API, not /api/bot/*, so there's no contract bump. "1 time" is no longer "1 times".

What J1 now walks

  1. Dana creates a campaign under a run-unique name and opens it from the Home card, landing on This week with the four-item checklist.
  2. Checklist → Settings → General. Set the zone (Asia/Tokyo) and add a reminder. The reminder offsets are read from the campaign and never hardcoded (repo CLAUDE.md); after a reload, the form must equal exactly what's stored.
  3. Checklist → Invites & joining. Make a labelled link.
  4. Contents → Discord.
    • The "Nothing is posted anywhere" banner (D67) shows, and "Send a test message" is aria-disabled with a reason. It's still focusable, and pressing it sends nothing (D36).
    • Fill in the channel and test: the fake bot's log grows by exactly one request with this run's guild and channel.
    • Queue a refusal on the fake. The test shows that exact message in role=alert, and This week shows the delivery banner (#369).
  5. Checklist → New session, in vote mode with two candidate times. The fake bot receives session_proposed for this session with 2 slots.
  6. D17, below.
  7. The invitation. Sign out, open the invite, sign in as Wren. Wren lands on the campaign, not Home.
  8. Wren leaves via Members & characters. The three projects share Wren, and J1's first test needs Wren in no campaign; a finally cleans up if a run dies between joining and leaving.

Axe is clean at WCAG 2.2 AA on every page reached, in all three projects. That covers This week in four states and as the invited player, the Settings sections, the session form, the Sessions list, the public invitation page, and Members & characters as a player.

Step 1 was written before D17 was corrected, and was reinterpreted

The TODO said to check that "every time shown afterwards is in [the campaign's zone]". That's the retired behaviour. D17 says one time, always the reader's own, with a line reconciling it to the table's day when the two differ.

  • The table is in Asia/Tokyo, set in Settings. The reader is in America/Chicago, set with Playwright's timezoneId on this test only, so no other journey's dates move.
  • A 10:00 AM Tokyo session is the previous evening in Chicago, with or without daylight saving, so the reconciling line is owed on every run whatever the date.
  • Asserted: the stored instant reads exactly <day> 10:00 in Tokyo (set on the table's clock), and the page shows the browser's Chicago reading with a zone marker that's non-empty and not the table's (shown on the reader's clock). The table's own reading is absent from main, and "The table calls this 's session — you are N hours behind." appears.
  • No abbreviation is spelled in the test. A zone's abbreviation is locale data, not a constant.

Found, not fixed: three items for the owner

  1. The Contents rail doesn't pick up a campaign made in the same tab. AppFrame's useCampaigns fetches once per app load, so creating, renaming, joining or leaving without a full reload leaves a stale list. That's lane 1's shell. Re-fetching on pathname change would be cheap, given the client's 15 s cache and invalidate-on-mutation. The spec enters through the Home card for now and says why.
  2. Sessions list, Past tab and search. SessionListItem has no summary or description, so a played row always says "No recap yet" and search only matches titles. The unit fixtures invent both fields. Summaries aren't gated on read, so returning them isn't a leak; it's a size choice between a server-side first sentence for the row and full text for search.
  3. Home's fresh-campaign card sends both "Schedule the first session" and "Invite players" to /campaigns/:id, beside a comment saying "until lane 3 moves it". Lane 3 has moved it. Repointing needs a matching spec change.

Not walked

  • Confirming a vote (lane 4, J2).
  • The session page and its scan (lane 4, already recorded against J6 in #775).
  • Nothing needed /campaigns/:id/manage.

🤖 Generated with Claude Code

**The last piece of lane 3.** #729's acceptance requires J1 and J6 green on desktop, phone and keyboard-only with axe clean. J6 landed in #770. J1's lane-3 half was a `test.skip` with a TODO addressed to lane 3 by name: time zone, invite, Discord, and the first session. It's written now. `sh scripts/journeys.sh`, whole suite: **27 passed, 3 skipped** (J2's lane-4 skip in each project), 0 failed. **`a11y-baseline.js` is still `{}`**, with no tolerated entries. vitest 1928/1928; lint 0 errors; `test_sessions`, `test_pagination` and `test_series` 88/88; ruff clean. ## Two defects the journey found, fixed here **1. Settings → Discord wiped what you'd typed when you tested the channel, and its verdict never appeared.** The same cause made "Saved" vanish the moment it showed, on Discord and on Recording, retention & AI. - After a test or a save, the section called `useSettingsAccess().reload`. That goes back through the loading state, so the campaign becomes null, `SettingsSection` draws Busy, and the form unmounts. It then remounts from what's *stored*: unsaved values gone, verdict gone. - The fix is a quiet `refresh` that keeps the campaign on screen while it re-reads. It's only honoured over the load it was made against, so a stale re-read can't overwrite a newer page load. `reload` stays as the retry after a failure, where there's nothing on screen to keep. - **Why the unit tests missed it:** a mock that resolves immediately is batched into one render, so the form never visibly unmounted. The new tests keep the re-read pending, and **they fail on the old code**. **2. The sessions list said "0 times" under every open vote.** `Sessions.jsx` read `time_slots`, which `SessionListItem` has never carried; the unit fixture had invented it. The list response now has `time_slot_count`, counted in one grouped query and only for votes, so it's not an N+1. It's additive on the user API, not `/api/bot/*`, so there's no contract bump. "1 time" is no longer "1 times". ## What J1 now walks 1. Dana creates a campaign under a run-unique name and opens it from the Home card, landing on This week with the four-item checklist. 2. **Checklist → Settings → General.** Set the zone (Asia/Tokyo) and add a reminder. The reminder offsets are **read from the campaign and never hardcoded** (repo `CLAUDE.md`); after a reload, the form must equal exactly what's stored. 3. **Checklist → Invites & joining.** Make a labelled link. 4. **Contents → Discord.** - The "Nothing is posted anywhere" banner (D67) shows, and "Send a test message" is `aria-disabled` with a reason. It's still focusable, and pressing it sends nothing (D36). - Fill in the channel and test: the fake bot's log grows by exactly one request with this run's guild and channel. - Queue a refusal on the fake. The test shows **that exact message** in `role=alert`, and This week shows the delivery banner (#369). 5. **Checklist → New session**, in vote mode with two candidate times. The fake bot receives `session_proposed` for *this* session with 2 slots. 6. **D17**, below. 7. **The invitation.** Sign out, open the invite, sign in as Wren. Wren lands on the campaign, not Home. 8. **Wren leaves** via Members & characters. The three projects share Wren, and J1's first test needs Wren in no campaign; a `finally` cleans up if a run dies between joining and leaving. **Axe is clean at WCAG 2.2 AA on every page reached, in all three projects.** That covers This week in four states and as the invited player, the Settings sections, the session form, the Sessions list, the public invitation page, and Members & characters as a player. ## Step 1 was written before D17 was corrected, and was reinterpreted The TODO said to check that "every time shown afterwards is in [the campaign's zone]". That's the retired behaviour. D17 says **one time, always the reader's own**, with a line reconciling it to the table's day when the two differ. - The **table** is in Asia/Tokyo, set in Settings. The **reader** is in America/Chicago, set with Playwright's `timezoneId` on this test only, so no other journey's dates move. - A 10:00 AM Tokyo session is the previous evening in Chicago, with or without daylight saving, so the reconciling line is owed on every run whatever the date. - Asserted: the stored instant reads exactly `<day> 10:00` in Tokyo (**set on the table's clock**), and the page shows the browser's Chicago reading with a zone marker that's non-empty and not the table's (**shown on the reader's clock**). The table's own reading is absent from `main`, and "The table calls this *<weekday>*'s session — you are N hours behind." appears. - **No abbreviation is spelled in the test.** A zone's abbreviation is locale data, not a constant. ## Found, not fixed: three items for the owner 1. **The Contents rail doesn't pick up a campaign made in the same tab.** `AppFrame`'s `useCampaigns` fetches once per app load, so creating, renaming, joining or leaving without a full reload leaves a stale list. That's lane 1's shell. Re-fetching on pathname change would be cheap, given the client's 15 s cache and invalidate-on-mutation. The spec enters through the Home card for now and says why. 2. **Sessions list, Past tab and search.** `SessionListItem` has no `summary` or `description`, so a played row always says "No recap yet" and search only matches titles. The unit fixtures invent both fields. Summaries aren't gated on read, so returning them isn't a leak; it's a size choice between a server-side first sentence for the row and full text for search. 3. **Home's fresh-campaign card** sends both "Schedule the first session" and "Invite players" to `/campaigns/:id`, beside a comment saying "until lane 3 moves it". Lane 3 has moved it. Repointing needs a matching spec change. ## Not walked - Confirming a vote (lane 4, J2). - The session page and its scan (lane 4, already recorded against J6 in #775). - Nothing needed `/campaigns/:id/manage`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Two defects in lane 3's own pages, both found by walking J1 in a real browser.

Settings: a Discord channel test, and a save on Discord or Recording, re-read
the campaign through useSettingsAccess's `reload` - the retry-after-failure
path, which goes back through "Loading...". That unmounted the section's form
and drew a fresh one from the stored campaign, so a test of an unsaved
channel wiped the channel and its verdict, and "Saved" vanished before it
could be read. useSettingsAccess now has a quiet `refresh` that keeps the
campaign on screen while it asks, honoured only over the load it was made
against; both pages use it. The unit tests missed it because a re-read that
resolves at once lands in the same render as its own start - the new ones
hold it on the wire, and fail on the old code.

Sessions list: a vote read `time_slots`, which the list endpoint never
carried, so every open vote said "0 times". SessionListItem gains an additive
`time_slot_count`, counted for the page's votes in one query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the test.skip lane 1 left for lane 3. Dana makes a campaign and walks
its This week checklist: the table's time zone and one more reminder on
Settings > General (the rows are checked against what the campaign stores,
never a hardcoded timing), an invite link on Invites & joining, Discord -
D36's test-with-nothing-to-test, a real test the fake bot records, a queued
refusal the page reports in the bot's own words and This week's #369 banner,
and a passing test that clears it - then a vote on the session form, whose
session_proposed reaches the bot. Wren opens the invite signed out, signs in
through its own door, is admitted to the campaign, and leaves again through
Members so the next project still has a stranger.

Step 1 is written as D17 decides, not as the TODO worded it: the table plays
on Asia/Tokyo and the reader (Playwright's timezoneId) is in America/Chicago,
so a ten o'clock morning session is the evening before for the reader. It
asserts the instant was set on the table's clock, shown on the reader's with
a zone marker that is not the table's, and reconciled by "The table calls
this ...'s session". Markers are asked of the browser, never spelled. A vote
has no time to show, so that check is on a second session booked directly.

axe runs clean on every page reached. OIDC_ORIGIN is exported from fixtures
for the invitation's own sign-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(changelog): the two defects J1 found (#729)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m7s
CI / Backend lint (ruff) (pull_request) Successful in 28s
CI / Feedback site tests (pull_request) Successful in 40s
CI / Bot/backend version sync (pull_request) Successful in 22s
CI / Does this change touch the recording path? (pull_request) Successful in 10s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Bot tests and audit (pull_request) Successful in 1m57s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m43s
CI / Docker image build (pull_request) Successful in 3m14s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 5m26s
CI / Backend migration, tests, and audit (pull_request) Successful in 14m3s
783231ff03
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.