The vault's layout is account state. documents.spec read the default
while the layout journey in v142-qol turned it on the shared account,
so which one failed depended on the order they ran in. Each now runs on
a vault account of its own; the vault fixture seeds the same corpus
under ids tagged per account, so two vaults never claim one row.
Global setup clears every rate-limit bucket keyed on a fixture account
or on the loopback addresses the suite calls from, not only the login
and share buckets: the check-in, write, export and import buckets
outlive a run by up to an hour, and a second run on the same database
met the first run's counts. A developer's own accounts are left alone.
The layout journey waits for its own write rather than whichever layout
write answers first.
Every bundle value that chained facts with " · " now reads as a phrase,
a comma list or a parenthesis, in all seven locales, and the em-dash
separators in the day, documents, correlation, sharing, environment,
vaccination and Apple Health import strings become sentences or clauses.
The day view's running list names the record first, with the dose in
the name and a life event's category as a chip, and puts a quiet day
count below: "Day 4, since 31 December" instead of a dot chain. Document
meta, location periods, vaccination rows and mood crosstab rows drop
their composed dots too.
A guard walks every bundle and fails on any " · " outside a reasoned
allowlist, and on an allowlist entry that no longer needs its place.
The vault runs its months side by side by default now, so a month is
marked above its first document and no longer has a heading row. The
grid check reads the arrangement and the month marker instead of an h2.
The wrench beside Upload opens Settings, Appearance, Documents, the way
the illness journal offers its display options: preview cards or a
compact list, and months stacked as blocks or flowing across the full
width with the month name inline above the document that opens it. The
choice is stored per account in `users.documents_layout_json` behind
GET/PUT /api/documents/inbound/layout (preserve-when-absent, module
gated, the read delegable like the rest of the vault), carried in the
account-settings backup and cleared by the data wipe.
Deleting from the vault now always asks first. The bulk bar's Delete and
a focused card's Delete key both go through one confirmation on the page,
and the detail sheet's Delete opens its own; before, all three deleted on
the spot with only an undo toast behind them.
The selection bar is two tight lines: the count with an X at its right
edge, then one action row with Delete alone at the right in the
destructive style. Below 1024 px the verbs are icon-only at the 44 px tap
floor, so the row stays one line at 360 px. The bar carries the
selection-bar slot the Coach launcher already steps aside for.
- Load each settings section on its own: the section route imported all
twenty-one statically. A preload starts the chunk beside the record
gate's read, and the loading state paints the settings card shell.
/settings/[section] goes from 611 to 403 KB gz.
- Take Zod out of the shell every route shares: the account-access parse
run on each boot is plain TypeScript now, held to the published schema
by an equivalence test over good and broken blocks. The canonical unit
table moves to its own module, so the display transform no longer
pulls Zod in either. Layout entry 199 to 136 KB gz; /insights 369 to
304, /medications 428 to 365, /labs 400 to 337.
- Open a saved Coach conversation at its last answer: an instant jump
instead of the smooth scroll that lost the end, then follow late
layout (charts, tables) for a few seconds until the reader scrolls.
- Show the document archive as a month grid of preview tiles, two
across on a phone and three or four on a desktop, with the month
headings kept.
- Add the admin card for the measurement table maintenance: a status
read on the maintenance route, one calm confirmation, and the run's
state while it is queued, running, finished or stopped.
- Re-base the bundle budget: /insights at 304, /settings/[section]
watched at 403, aggregate cap 4335 with the reason written down.
Adds the AI-optional journeys: the overview with AI switched off by the
operator and with no provider (no AI requests, no refusals, one setup
hint, remembered dismissal), and on an account of its own, withdrawing AI
consent deletes the stored briefing, and a hidden Coach's conversation is
read and deleted from Settings, AI.
Specs that drive AI surfaces against mocked routes now serve an available
capability map through /api/auth/me, since the web no longer assumes AI
is on until told otherwise.
The protected shell renders its hydration gate for every child until
/api/auth/me resolves, so after a goto, a reload or an exit click nothing
exists yet: no nav, no top bar, no banner, no page body. Fifteen absence
assertions across twelve specs were asserted inside that window. Each was
true because the surface had not rendered, and each would have stayed green
if the thing it forbids came back.
Every site now waits for a positive marker of the loaded state first: the
document sheet's footer download action, the settled integrations card
rather than the family attribute its skeleton shares, the top bar after an
exit, the page's own heading, the delegate's own marker, the Coach FAB. Two
skeleton-absence waits are replaced by waits on the content that follows.
delegated-writes had a different cause: it asserted the bulk selection bar
absent although no row was ever selected, so it passed for an owner too. The
gate is one level earlier - below MANAGE no row carries a checkbox at all -
so the test asserts that instead, with the bar and the row delete kept as
backstops.
No sleeps added, no timeouts raised, no claim weakened.
The maximize case clicked the drawer's maximize control as soon as the drawer
was on screen. Those are not the same event: the vault records `?doc=` as the
way back only once the detail sheet has actually closed with the drawer owning
the same document, and maximizing before that sends the close down the other
branch, which calls `history.back()` and races the push to `/coach`. The URL
then stays on `/documents` and nothing is raised. The sheet's own close control
leaves the tree with the sheet, so its absence is that hand-off, observed.
The delete case is addressed to the toast's attributes rather than its copy.
The literal there has already been wrong once — a trailing full stop moved and
the assertion spent its whole timeout hunting a sentence that no longer
existed, on every attempt, on both projects.
- account-sharing polls the offline persister's own IndexedDB store for
the flushed snapshot instead of sleeping past the debounce.
- The record-session-fence cases poll for the observable halves of their
setups (the switch round-trip landing, the exit switch being issued);
the one remaining fixed window is a documented absence-assertion grace
with nothing to poll on.
- The documents suite's two remaining sleeps are named for what they
are: a focus-dance grace the outer document cannot observe, and the
long-press gesture duration itself.
- The inventory-date fixtures derive their printed expiry from now
instead of a fixed 2027 date that would silently flip the fixture to
expired when the calendar catches up.
Closing a card-opened document asks the browser to go back, so the entry
the selection pushed is consumed and the reader's next Back reaches the
page before it. A traversal is a request, not a guarantee: one asked for
while another is still settling can be dropped, and nothing raises when
it is. The sheet closes, React stays consistent, and the address bar goes
on naming a document nobody has open, so a reload or a shared link
re-opens the sheet that was just closed.
Pressing Escape straight after the browser's Forward button is exactly
that shape, and it reproduces: ten repeats across both viewports failed
two of twenty with the URL still carrying the id, and none of twenty once
the close verifies itself. The check is guarded by the document id, so a
traversal that landed on a different selection is left alone, and it is
cancelled the moment the sheet re-opens.
The browser test that found it could not say so. It asserted the sheet
was closed through a locator matched on the document TITLE, and the sheet
falls back to an untitled label while its detail read is pending, so the
assertion also passed on an open sheet showing a skeleton, resolved
immediately instead of waiting, and pushed the failure into a URL poll
ten seconds later. It now asserts the dialog role, which is what the
neighbouring deep-link test already does.
A spec that arms `page.route(...)` in a `beforeEach` and never drops the
handler races its own teardown. The context closes, an async handler is
still awaiting, its next call rejects with "Test ended.", and Playwright
attributes that to the test that had just passed. With `retries: 2` plus
`failOnFlakyTests`, which spec bites is a matter of timing.
`e2e/setup/test.ts` re-exports Playwright's `test` extended with an auto
fixture that unroutes every page and the context after each test, and
clears the page event listeners a spec subscribes to; `expect` rides
along so a spec needs one import. Auto means a spec author cannot forget
it and does not have to know it exists. Every spec now takes `test` from
there, and `e2e-route-cleanup-guard.test.ts` keeps the import boundary
closed with an allowlist that carries its reason.
The per-file `afterEach` on the cycle spec goes away — leaving both would
read as not trusting the fixture.
Not covered: a page inside a context a spec builds itself. Only
`share-documents.spec.ts` does that, and it owns those contexts.
Give each facet trigger a leading icon and show the text label from sm up
(and on phones only while the facet is active, truncated), so three facets
plus the search never overflow a 360-430px row — the bar stays one line
without a horizontal scroller. Drop the now-dead filter.groupLabel key and
point the vault e2e flows at the dropdown menu items instead of the removed
inline chips.
Pins the doctor-flow script (type chip, condition chip, text search —
three independent routes to one document), the URL deep-link contract
(?kind / ?episode / ?year / ?doc), delete with undo, the bulk flows
(50-item kind change, per-id partial failure, client-side chunking above
100 ids), keyboard-only navigation over the roving grid, page-wide drop
and clipboard-paste intake, the duplicate-upload contract, the admin
per-file cap (commit-on-Enter, rejection copy naming the limit), the
touch long-press selection on the mobile project, and axe scans over the
timeline, the open detail sheet, and bulk mode.
The fixture seeds the 60-document corpus straight through Postgres —
the upload endpoint's 60/hour bucket cannot carry API seeding across
two Playwright projects — with blobs encrypted in the same binary2
layout the server writes, so previews exercise the real decrypt path.
Idempotent throughout; mutating tests use disposable namespaces.