The persisted copy of a streamed reply used to mount as a new bubble, so
what the reader had done on the streamed one (the chart or table view, the
open steps list, the evidence panel) reset when the conversation refetch
landed. A turn now keeps one key from its first streamed frame to its
persisted copy, both render in the same wrapper, and the copy keeps the
live tables instead of refetching them, so React keeps the one bubble.
The wrapper stops being a live region once the turn is settled. The
150 ms twin-hiding window this replaces is gone.
The server offers no chips under a clarifying question. The second
streamed turn now carries the card alone, and axe runs once with the
chips under the first answer and once with the card.
Three streamed turns against a stubbed chat stream: steps under the answer, a result table with its chart/table toggle and copy menu, a follow-up chip and a clarification choice that each send which one was tapped, and axe over the answer, chips and card in both themes.
A table the answer referenced opens under the prose as its chart; the
others sit under Data used in the evidence disclosure and open as
tables. The plot is one image whose name points at the table view, two
series carry legend buttons, and the chart loads through the shared
chart runtime. A metric chart token for a metric a shown table already
covers is dropped.
The delegated-writes spec checked for the invitation form's level control
right after page load, before the form had painted, found nothing and
skipped every test on every run, CI included.
- Give the journey its own account pair, seeded and reset in global setup,
so it no longer invites the same pair as the account-sharing journey
(a second live grant between two accounts is refused with 409).
- Wait for the form through its data-slot and fail when the WRITE level
control is missing instead of skipping.
- Revoke the journey's grant in afterAll.
- Keep it out of the mobile project in the config rather than through a
runtime skip.
Add two checks so a skip cannot hide a spec again:
- e2e-conditional-skip-guard.test.ts requires every conditional
test.skip/test.fixme in e2e/ to carry a reason and refuses a bare
test.skip().
- scripts/check-e2e-skipped-specs.mjs reads the Playwright JSON reports of
both shards in the e2e gate job and fails when a spec file skipped all of
its tests across every project, apart from an allowlist with reasons.
The invitation journeys, the backup journey (archive, snapshot download
and restores) and the delegated doctor report acted on jars signed in once
in global setup, so they passed or met the re-proof gate depending on how
far into the run they started. Each now acts from a session of its own,
made stale before every gated press, and answers the dialog with the
password; the delegated report asserts the 401 before re-proving over the
wire. The helper gains expireRecentProof, reproveWithPassword and
actWithReproof.
The off-host card reads pendingDeletions and lifecycle, which the route
always sends. The mock predated them, so the card threw and the page never
painted its rows. The mock now carries a refused deletion and a bucket
without an expiry rule, so the scan reaches both new lines.
A report for a past window lists a medication only if its course had begun
by the window's end. The fixture's drug was entered on the day of the run,
so it was rightly absent from a report for last year and the control that
expected it failed. The course now starts before that window. Generating the
report also meets the re-proof dialog from a stale session every time,
rather than only when the run has been going for five minutes.
Connecting an assistant and making a share link ask for a sign-in or
re-proof within five minutes. The shared jar is signed in once in global
setup, so the specs passed early in a run and failed later. They now start
from a session of their own signed in ten minutes ago: the consent page
withholds Allow and links to the confirm page, where the password re-proof
returns to the consent request; the share form opens the re-proof dialog
and creates the link after the password is given.
The restore route answers 202 with a job id and leaves the work to the
backup-restore queue. The journey still expected the synchronous 200 and
read the record straight after the click. It now asserts the queued
answer and its status route, polls that route until the job reports
succeeded (a failed job ends the wait with its code), and only then
checks what the restore put back and dropped. The job runs on the
pg-boss worker inside the server under test, as the manual backup pass
earlier in the same file already relies on.
Three assertions in the AI-optional overview spec named slots that no
longer exist anywhere in the tree, so they held whatever the page did.
They now name the error primitives the overview paints on a refused read
and the Coach hand-offs that paint only while the Coach is available, and
the no-provider journey checks that /coach goes back to the overview.
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.
A journey on its own account switches mood and medications on, proves
the Mood nav entry, the Mood and Medications pills, the mood trends slot
and both add-menu entries are there, switches both off through the
module route and proves each is gone, the trends row fills from pulse,
/insights/mood shows the module notice at the same URL, and no chart on
the way paints its error state.
Data never depends on AI. The Insights data reads no longer ask an AI
switch or the `insights` module (now only the AI analysis opt-out): ECG
list, detail and upload, rhythm events, derived scores, rule cards,
correlations and patterns, the baseline strip, health status, intraday
pulse, and the Today digest with its dismiss. Breathing screening moves
to the sleep module and lab changes to the labs module. Stored Coach
facts, conversations, message ratings and the seen stamp stay readable
and erasable while the Coach is unavailable. Cards now say
`provider: "rules"`.
Reads that mix data with model text answer 200, null the model text
and publish an `ai` state from the capability resolver, without warming
anything: comprehensive (`ai.briefing`, `ai.statusText`, `hasProvider`
from the presence probe), the derived assessment override, the period
narrative, nudge status, the seeded opener, about-me questions, the
workout paragraph, the pregenerate warm (`queued: false`), the Today
digest (lead, reaction line, check-in) and the dashboard briefing,
which is now applied per read after the snapshot cache. Hiding the
Coach no longer hides the briefing.
`insights/generate` GET and POST ask the `briefing` capability and
`insights/chat` POST the `coach` capability, with the typed refusal
envelope; a missing provider keeps the chat's `coach.provider.none`
frame and adds `reason`. The /coach page, the MCP ECG and intraday
tools and `aiAvailable` on the provider route follow the same answer.
An inventory of AI and data routes replaces the Coach gate inventory:
every route that asks a capability names it, and no data route asks.
Refs #975
A connector signing in through the MCP OAuth consent screen gets stuck
after Allow in Chromium: the grant is issued, but the browser never
reaches the client's callback. Only a real browser enforces the page's
CSP on that redirect, so this is a browser journey. It registers a
client with a local loopback callback, clicks Allow and requires the
callback to receive a code, state and issuer. It runs against the second
server, which now carries APP_URL because the MCP surface fails closed
without a configured origin. Refs #983
The stub port was drawn from 49152-65535. Linux assigns local ports for
outgoing connections from 32768-60999, so the drawn number could already
be held by a client socket the runner had opened, and the notification
journey failed in beforeAll with EADDRINUSE although no other stub was
listening. The port now comes from 20000-32767, below the Linux and
macOS ephemeral ranges.
The mobile smoke test pressed Escape on the capture picker and tapped
the More button straight away. While the picker's close animation still
covers the bottom nav, that tap lands on the overlay and the hub never
opens, which failed the lucide-react update once. The test now waits
until the picker is hidden.
The malformed-payload journey installs its /api/auth/me route before it
clicks the switch. When the owner session read its own payload again in
that window, the helper threw inside the route handler: the throw came
before fulfill, so its catch never saw it, the test failed, and the
teardown's unroute then hit a closed page. It showed up on the Playwright
1.63 branch, whose timing opens that window more often. An unswitched
payload now passes through unchanged, and the test asserts that at least
one switched response was actually corrupted before trusting the refusal.
The setup flow specs answer questions through these helpers before their
first real navigation. That case still went through page.request, which
shares the runner's keep-alive agent, and a stale socket failed an answer
with read ECONNRESET on the dependency update branches. The page now opens
the public version endpoint first and sends the call with its own fetch.
The setup flow helpers read the account, the dashboard layout and wrote
answers through page.request, which shares the runner's keep-alive agent
with every other API context. A socket the server had already closed on
its idle timeout failed one answer with read ECONNRESET, and CI counts
the passing retry as a failure. The calls go through the page's fetch
now, as the record session specs already do; before the first navigation
there is no origin, so that case keeps the API context.
Switch into the managed record, follow the sidebar Settings entry to
the profile card and on to the Modules card, then repeat from the
phone-width user menu. Asserts the entry's target, the absent
Notifications entry and the record id on each card, after the shell
has loaded.
Refs #939
Brings in the truncate retry, the socket fix for the record-fence spec that
failed this branch's second e2e shard, and the corrected absence assertions.
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 offline fence spec already sends its account switch with the page's
own fetch, because every Playwright API context in the runner shares one
keep-alive agent and can be handed a socket the server closed on its idle
timeout. The read on the next line still went through that agent, and it
failed with read ECONNRESET on all three attempts, which fails the run
because CI treats a flaky test as a failure on purpose.
It reads through the page now, like the write above it. The two remaining
calls on the shared context sit in finally blocks that swallow their
errors, so they cannot fail a test.
The panel paints before its status query answers, and every variant inside it
is conditional, so an assertion that a variant is absent could pass on a
question that had simply not been answered yet — which is how the journey for
the unmeasured-provider case passed on a retry against the very defect it was
written to catch.
The panel now carries the variant it settled on, absent until it has settled,
and the three journeys wait on that instead of racing it. The third journey
covers the state a freshly deployed instance is in before its first AI call:
no offer, and no claim of an outage either.
Thirty-one files carried internal build vocabulary and anchors pointing at
notes that are not in the repository: a reader of `checklist.ts` in a year
would find its rationale keyed to a research id they cannot open, inside a
comment that also names an internal build stage. Two test titles carried the
same anchors.
The version tags and the whole of the reasoning stay; only the vocabulary and
the dangling references go, with the few sentences that were built around an
anchor rewritten to stand on their own.
The done screen's assistant panel stops claiming that the instance's
provider already serves the person. A tri-state read over the operator's
own provider ledger decides between a one-tap offer that mints the
consent receipt, a plain "not answering right now" note, and today's
setup link; anything unknown, any read error, a managed profile and an
operator who has switched the assistant off all fall to no offer.
# Conflicts:
# e2e/setup/global-setup.ts
A provider with no sync history reports "unknown" instead of "connected",
so a fresh account is no longer told a device it never linked is already
delivering; the connect step resolves the sync verdict into four honest
tiles, records the flow's result only for a connection that is actually
serving, points an unconfigured provider at its credentials, and steps
over a source the account already has.
Two structural guards caught the new gate. The error-code catalogue is derived
from the route tree on every run and fails in both directions, so a code the
server emits and does not publish is a client that cannot branch on it; the
session-surface guard freezes the short list of files under `src/app` that
resolve a caller without `requireAuth`, and the register route now belongs on
it — `requireAuth` is the wrong gate there twice over, since it would throw at
the anonymous caller the route exists for and this route wants the ABSENCE of a
session.
The code is `auth.already_authenticated` rather than the bare
`already_authenticated`: the catalogue's own header rules that new codes are
dot.case, and it sits beside `auth.mfa.code_invalid` in the same family. The
OpenAPI document renders the catalogue into the ErrorEnvelope description, so
it is regenerated.
Running the two cases in parallel let each walk the other's account state, so
they go serial like the sibling journeys. The second case also asserted the
wrong thing: with a Q2 area answered as well, an already-delivering source
correctly falls through to `log-reading` — that IS the fix. Split into three:
nothing connected, a connection with somewhere to fall to, and a connection
that is the only task the answers name.
Every layer of the invite chain had unit coverage and the chain was broken
anyway: the proxy test proved the edge admits the landing, the page test proved
it redirects onto the registration form, and neither could see that the
redirect arrives in an RSC flight payload after the shell has already flushed.
The operator's path — mint an invitation, open the URL in a private window,
reach a usable form — was exercised by no spec at any level.
The new spec mints a real invitation in the admin UI and opens it twice: once
in a genuinely cookie-less context, asserting both where the visitor lands and
that they never passed through the sign-in page, and once in the admin's own
window, asserting the explaining panel and that the admin's session survives. A
third case pins the server half through the wire. Against `origin/main` all
three fail — the anonymous visitor lands on `/auth/login`, the panel does not
exist, and the registration POST answers 201 over a live session.
The minted URL gains a test id so the spec reads it from its own element
rather than from the first paragraph in the ticket.
Two runs on one account, and the only difference between them is a row in
provider_health: a success inside the freshness window brings the offer and
the tap leaves an active ai_full receipt; a dead credential inside its
cooldown brings 'not answering' and no button at all.
The seed is instance-wide because the projection is - a fresh account's own
ledger is empty by construction - so the spec runs serially and puts the
operator key and the ledger rows back afterwards. The offer path asserts
that no provider was called: the flow reads a projection and never probes.
Verified as a real check, not a tautology: moving the seeded success from 30
minutes to 25 hours ago makes the offer case fail.
No journey reached the connect arm: bp answers `manual`, visit answers
`file`, and the child path ends at confirm. The a11y sweep opened the screen
and asserted the connect card was visible — which it was, for the few hundred
milliseconds before the status envelope resolved, so the sweep passed through
the defect rather than catching it.
The new journey answers Q4 with a wearable and asserts only after
`GET /api/integrations/status` has settled: nothing is claimed for an account
with no connection and no result is stamped on its behalf, and a connection
seeded as delivering is acknowledged rather than offered. The a11y sweep now
waits for the tile the screen settled on instead of the frame before it.
The string-headroom sweep went red on CI for French at phone width by
two pixels the Mac never showed: the runner's fallback fonts render a
touch wider. The box was the sex select on the confirm screen — its
value is whitespace-nowrap, and a grid item's automatic minimum is its
min-content, so the track was pinned at the padded option's width and
the shell scrolled sideways. Both cells of that row and the trigger
carry min-w-0 now: the track takes the free space and the value
ellipsises, which is the select primitive's own design. Reproduced and
verified locally by narrowing the viewport to 372 px, which stands in
for the wider fonts (336 px of scroll before, none after).
The sweep now names what overflowed: the boxes past the shell's edge,
with tag, slot, classes, text prefix and edges, ride the failure
message and the outside list, so the next red run says what rather
than only that.
A module page whose switch is off answers with the shared notice that
names the switch and offers the Modules settings, so a bookmark or a
report link explains itself instead of bouncing home. The vaccinations
journey pinned the old redirect and went red for the behaviour the setup
flow introduces on purpose.
"Finish without the profile" completed the flow with no record id, and
the route then derived the child's answers onto the guardian's own
record. The server now decides: answers given for somebody else with no
record to apply them to complete and latch the flow without deriving or
seeding anything, whatever the client sent. The checklist carries the
profile instead and reads none of those answers as the guardian's own.
A record id the caller does not guard answers 404 like every
id-addressed managed-profile route, one answer for missing, not managed
and not theirs alike; the contract documents the field, the 404 and the
422 text. Pinned by two integration cases and a second child journey.
"Someone I look after" derived the child's answers onto the guardian's
own record. The completion now takes the managed record the profile
step just created, checks the same access rule the guardian's modules
route applies, and lands the same derivation and dashboard seed on that
record through the same record-keyed write; the guardian's own flow is
stamped complete and latched without touching its map. Three
integration cases pin it, and the child journey reads the child's map
through a real switch — it asserted the guardian's map before, which
pinned the wrong record.
Also from the review: the checklist's visit row read a meta the visits
list never publishes and now counts its body; the first-result tile
read the measurements list as an array and now shows the reading; the
demo allowlist admits the disclaimer and tour writes, with a structural
test over every onboarding route file; restart clears the old first
result, and the screen trusts the ledger before painting a result as
done; the headroom sweep's raw-key check matches the flow's own key
shapes; the demo migration script stops copying the dropped column; the
questions-to-pass rule and the shell's onboarding-path rule are pure and
pinned; the baseline form composes the shared action row; the schema
diff loses the unrelated formatting churn.
At 390 px with labels a third longer the action row pushed "Confirm and
continue" past the viewport; both rows now wrap. The string-headroom
sweep lengthens every word by 30% rather than hanging the whole surplus
on the last one — that manufactured an unbreakable run no locale
produces — leaves screen-reader-only boxes alone, and accepts a select
value's ellipsis, which is the primitive's design app-wide.
Run against the standalone build, the four journeys and the axe sweep
surfaced five things the unit tier could not see. The completion route
derives only once every question is settled, and Q6 stays pending for
somebody with no unit-bearing area — the confirm screen now passes the
questions the flow never showed before it completes. The dashboard seed
matched a JSON null where the unset column is a SQL NULL, so it never
landed; the retired step route carried the same filter. The auth shell's
onboarding branch matched only the front door, so every question screen
rendered inside the full app chrome with the sidebar. Inline links
relied on colour alone, the step list sat outside any landmark, and the
baseline form brought a second h1 onto the confirm screen. The two
sweeps run serially, since each drives one account through the API.
The integration file gains the seed case that would have caught the
null filter.
The journey anchors one dose at the clock and its twin twelve hours on, and
the twin wrapped past midnight for every run that started after noon. The
skip then landed on tomorrow's slot and the dashboard tile read one taken
over two expected where the skip-parity figure is a hundred; the green runs
on main were all morning runs. The twin is twelve hours on before noon and
the last minute of the day after it, so the skip lands on the day the tile
reads whenever the suite runs.
Four journeys, one account each, from the welcome screen to the
dashboard: blood pressure and weight typed in, a daily medication added
inline, a visit within a month with labs, and a child's managed profile
created on the confirm screen. Each asserts what the walk wrote — the
module map, the tile order, the checklist, the settled ledger — and the
first proves that leaving after Q3 resumes at Q4.
Two sweeps on accounts of their own drive the state through the answers
route and visit every screen by URL: axe at both widths plus one
keyboard-only answer, and every locale with every text node lengthened
by 30% before the page, the shell, every clipping box and every control
are measured. All six run in one project because each mutates the
account it walks.
The medication wizard learns to stay where it was mounted after a
create, which the first-result screen needs and the medications list
does not.
On the release that adds the ledger the table is empty, so on a host that has
been uploading nightly for months every account read Never, under a sentence
saying no copy had ever reached the bucket. That sentence is untrue on exactly
the morning an operator opens the new card for the first time, and it is a
claim the host cannot make: what is empty is the record, not the bucket.
The run now records every account it walks, not only the ones an object came
of. A walk that produced nothing moves the attempt instant and leaves the
success columns alone, so the two cases separate: no row at all is no record
yet, a row without a success is never. Never now means what the word says —
a run looked at this account and has never put anything there.
The card names the last check on the never row, so the state is a fact with a
timestamp rather than an assertion. Runbook and all seven bundles carry the
same two sentences. The integration file proves both arms against real
Postgres, including that a failed run leaves the previous good copy on the
row.
The unit tests either side of this mock Prisma, so neither proves the
migration applies or that the writer and the reader agree on a shape. The
integration file runs the real nightly job against real Postgres with an
in-memory bucket, then reads the row back and asks the console's own
classifier what it says: fresh after a run, one row across two runs, stale
once it ages past two nightly periods, gone when the account is.
The component test pins the case an operator most needs stated: a host with
no bucket configured says so, rather than painting an empty list that reads
as "no problems". The accessibility scan's admin-backups response gains one
row in each state, because a card that paints only its header proves nothing
about the rows anybody reads.
Two small corrections ride along, both from writing the tests. The freshness
badge composes its classes through `cn` like every other surface, and the
error-reporting line waits for the settings to arrive instead of asserting
"nothing is sent" while the answer is still loading.
The adherence journey pinned the tile at the figure its engine produced before a skip left the denominator; with both engines held to one number the tile reads what the card reads.
The journey's isolation went one way only. `test.describe.configure` orders
the tests inside the file; the suite is fullyParallel with two workers, so
everything else runs beside it — and its own trigger, the admin reminder
sweep, walked the whole instance and dispatched for every account that had
an overdue dose. With the mail env pointed at the local responder, that
would have started sending real messages for accounts this file has never
heard of, and the exact-equality assertion on the accepted recipients would
have been reading them. The sweep now names the account it then counts, and
the comments that defended the opposite direction say what the exposure
actually is.
Its own server, too, and for two reasons. The `SMTP_*` vars decide whether
the Email card renders at all, so on the shared server they put a card the
axe scan and the card-anatomy sweep had never measured onto
`/settings/integrations` for every spec that opens the route. And unset,
`HEALTHLOG_PROCESS_TYPE` starts the pg-boss reminder scheduler inside the
process serving requests, whose every-fifteen-minutes tick either mints the
missed-dose row that makes the sweep skip its own dispatch or dispatches
beside it. A third server carries both: mail configured, `web`, and only
this spec talks to it.
The shared server keeps `all` on measured grounds rather than taste. Worker
status is per-process and in-memory, so a `web` process reports the worker
stopped, `/admin` renders that row destructive-red, and axe fails it at
3.97:1 against the muted tile — a real contrast defect any operator running
the documented web/worker split already sees, and not this branch's to fix.
That limit is written where the server is defined.
Also here: the stub draws its port per run instead of pinning one, and says
so by name when the port is taken; teardown ends its sockets and survives a
setup that never got as far as starting it; the ledger reset stops deleting
rows whose subject is this account but whose recipient is somebody else,
and clears intake events explicitly rather than through a cascade; the
attempt poll refuses a window larger than the twenty rows the diagnostic
returns; and the header names the precondition the file cannot enforce and
the internal reasons it pins on purpose.
Verified against a production build and a throwaway Postgres: the journey
three times over at one worker, then the a11y and settings geometry specs
at two. Both assertions broken once to see them fail.