test(analytics): guard the three unguarded Priority 0 behaviours (#155)

docs/ui-information-architecture-review.md claimed Priority 0 was guarded by
analytics-sql.test.ts. That file only ever covered the three SQL *fragments*
(EXCLUDE_RECONCILED_SOURCE, NATIVE_CURRENCY, INVESTMENT_SIGNED). Items 2, 3 and
6 of the Priority 0 list had nothing, and all three fail silently — no error,
no empty screen, just a number that quietly disagrees with the one beside it.

New: src/__tests__/integration/analytics-priority0.test.ts, 18 tests.

- **Item 2, pace/headline parity.** /api/analytics/daily summed over a month
  must equal /api/analytics/monthly's headline to the cent, per category, and
  across every month in the window. Cents rather than exact equality is the
  assertion because the two routes group at different grains (month+category
  vs month+day+category) and both carry 4dp — the displayed grain is what has
  to agree, which is precisely the bug (the chart ending the month a few cents
  off the headline above it). Fixtures are only shapes where a naive gross
  debit sum disagrees: a 50% split, a refund, fee and interest rows, an
  itemised loan repayment, a reconciled source row, family-tagged spend,
  transfers/investment/income, and a row someone else paid that I hold a split
  on. A fourth test asserts the naive sum does NOT match — without it, parity
  between two queries identical by construction would pass on fixtures too
  plain to discriminate.
- **Item 3, fees date range.** Default 12 months with the window reported
  (from=2025-09-01, not 08-31 — toDateStr, not toISOString); the older tail
  excluded; months=0 all-time with a null range rather than a period claim the
  number does not make; a narrower months=3; the transaction-level fee lines
  bounded by the same window as the statement totals, since a bounded total
  beside an unbounded list is the same defect wearing the fix; and a nonsense
  months param clamped rather than producing an Invalid Date window.
- **Item 6, partial-month comparisons.** The in-progress month never enters a
  baseline; a partial month is compared through the same day of each prior
  month; a complete month keeps full totals; top movers truncate both sides;
  year boundary and leap February.

Item 6 was untestable by construction, which is why it had no test: the rules
were closures in budget/page.tsx reading `new Date()` inline. Extracted to
src/lib/partial-month.ts with an injectable clock, and budget/page.tsx now
imports them. The extraction also de-duplicates the baseline filter, which was
written out identically in two places.

The file freezes the clock (vi.useFakeTimers, toFake: ["Date"] only — pg needs
real timers for connection handling). Without it the fixtures drift out of the
routes' rolling windows over time and the assertions go vacuous, which is the
same class of silent failure the file exists to catch.

Mutation-checked, all three: breaking daily's spend semantics fails 3 of the
item-2 tests (the placebo correctly still passes); forcing the fees window to
all-time fails 5; letting the current month into the baseline fails 1; and
dropping the elapsed-day truncation fails 3.
This commit is contained in:
2026-08-21 00:52:36 +10:00
parent fc9ffaba25
commit eaf5d5ac66
4 changed files with 708 additions and 49 deletions
+17 -7
View File
@@ -47,13 +47,23 @@ Also fixed while in here, both found by checking rather than by proposal:
category-day, so the pace chart ended the month a few cents off its own
headline. Both now carry 4dp and round once, at display.
Partially guarded by `src/__tests__/integration/analytics-sql.test.ts`, which
covers the SQL fragments — `EXCLUDE_RECONCILED_SOURCE`, `NATIVE_CURRENCY` and
`INVESTMENT_SIGNED` — and nothing else. **Items 2, 3 and 6 have no test.** Those
are the three where a regression is silent rather than loud: the pace chart would
simply go back to disagreeing with the headline printed directly above it, the
fees figure back to growing forever, and a partial month back to being measured
against complete ones — all without anything failing. Tracked as #155.
Guarded in two files. `src/__tests__/integration/analytics-sql.test.ts` covers
the SQL fragments — `EXCLUDE_RECONCILED_SOURCE`, `NATIVE_CURRENCY` and
`INVESTMENT_SIGNED`. Since 2026-08-21 `analytics-priority0.test.ts` covers items
2, 3 and 6 (#155): the three where a regression is silent rather than loud — the
pace chart simply going back to disagreeing with the headline printed directly
above it, the fees figure back to growing forever, and a partial month back to
being measured against complete ones, none of which fails anything.
Two things were needed to make item 6 testable at all, and both are the point
rather than incidental. The rules lived as closures inside `budget/page.tsx`
reading `new Date()` inline, so they now live in `src/lib/partial-month.ts` with
an injectable clock; and the tests freeze that clock, because a fixture defined
against the real "now" drifts out of the routes' rolling windows and the
assertion goes vacuous — the same class of silent failure, one level up. Item
2's suite carries a placebo assertion for the same reason: parity between two
queries that are identical by construction proves nothing unless some fixture
can actually tell the old and new spend definitions apart.
### Landed after this doc, in the same family (2026-08-02)