The rollup parity case writes over half a million readings and folds them
into DAY buckets: about 35 s on a CI runner alone. Run beside two other heavy
files on the same Postgres it took 1.7 times as long and crossed the 60 s
default. Nothing in the case waits on a lock: its advisory locks are scoped
to the worker's own database, and the measurement with the database capped
at two CPUs (14.6 s alone, 25.3 s beside the sharing matrix and the restore
memory test) shows plain contention. The case gets a 150 s budget, twice the
contended estimate. HEALTHLOG_INTEGRATION_PG_CPUS caps the database
container so a CI runner's contention can be reproduced locally.
The summary's all-time aggregate grouped every live reading of the
account by type: one index entry per reading, millions on an account
with a watch, two to four seconds whenever they were not in memory and
the slowest read of the request on every cache miss. The count now sums
the DAY buckets that already hold a per-day count of every source,
counts live only the readings before the fold window and any type not
folded yet, and takes lastSeenAt from the newest reading the
latest-reading read already returns. Parity with the live count is
pinned against a real Postgres.
Neither read is a bug a behavioural test can see: both shapes return the
right rows, and only the plan differs. So the guards split by what each
can actually answer.
The unit guard asserts on the SQL the readers build, with comments
stripped so it matches the query text and never the prose describing it:
no DISTINCT ON on this path, a LATERAL driven from an unnested type list
with a LIMIT on the per-type branch, a bound type predicate on the rollup
read sitting alongside the user and granularity filters, and the route
still calling the shared readers rather than growing a second copy. Four
of its five cases fail against the pre-change SQL.
The integration companion runs the shipped readers and the pre-change SQL
side by side over one fixture on the real Postgres and requires identical
rows. The fixture carries the cases the rewrite could have broken: a type
whose only rows are tombstoned, a type with no rows, a type whose newest
row is a tombstone over a surviving one, and coarser rollup granularities
the DAY read must ignore. Dropping the soft-delete filter or shortening
the type list makes both cases fail.
Plan assertions are deliberately not in the integration file: a
testcontainer fixture is too small for the planner to choose the access
paths a real account produces, so an EXPLAIN there would pin nothing.