Stop the net-worth range offering more history than exists - #127
Conversation
1e30ddf to
aeff6a1
Compare
Follow-up to #130, which did not fix the race. Part of #125. #130 added `standalone: true` on the theory that the node_modules layout was what raced. It is not. pnpm/action-setup clears its install directory before writing to it regardless of install mode, so with the default ~/setup-pnpm that `rmdir` is shared by every job on a self-hosted box. #130's own run passed only because nothing else was running beside it; the moment two jobs started together again, both #127 and #129 reproduced the identical error on branches that already contained the fix: ENOTEMPTY: directory not empty, rmdir '~/setup-pnpm/node_modules/.bin/store/v11/files/03' `dest: ${{ runner.temp }}/setup-pnpm` is the actual fix: runner.temp is per-job and cleaned up by the runner, so no two jobs share a directory to race on. That alone would regress something worth keeping. The pnpm store lived under the install dir, so making the install dir disposable would make the store disposable too, and every job would re-download ~1100 packages — contradicting the reason this workflow deliberately skips `cache: pnpm`. So the store is pinned to a stable path with `--store-dir ~/.pnpm-store`. That separation is also what makes the original failure impossible to repeat: a shared content-addressed store is pnpm's normal mode and is safe for concurrent readers. It was only fragile here because it happened to sit inside the one directory the action deletes.
…xists #107 — 6M was always selectable, so a household whose balance history begins two days ago got six months of chart holding two days of net worth: roughly 95% of the plot area hatched fill and dashed line, and a y-axis stretched to -$30K to accommodate a region that is not net worth. The coverage logic itself was already right and is untouched. coveredTrendDelta still measures over the fully covered span only — the comment recording why (the "+2430.7% past 6 months" it replaced) still applies. This changes the frame around it, not the maths. getFullCoverageSince computes the date every account first had a balance, as MAX(MIN(date)) over balance_history rather than by reconstructing a series — the range control needs it on every dashboard load. Ranges reaching back before that are disabled and say why, on the control itself rather than only in a tooltip, so the reason reaches keyboard and screen-reader users. The page then loads the range the hero will actually open on instead of a hard-coded 6M. When coverage began too recently for even the narrowest range, the covered span is plotted on its own with a note naming what is omitted and why. That is the case the issue was filed about, and gating alone did not fix it — it only shrank the hatching from six months to one. Crucially this drops pre-coverage points rather than un-marking them: nothing partial is ever drawn without the dashed-and-hatched treatment, which is the condition this work was filed under. Selecting a range that does include partial data still renders it exactly as before. Also fixes an artifact this introduced: with a two-day window the y-axis showed "$52.9K" four times, because centsToCompact resolves to $100 and the domain was narrower than that. axisTickFormatter picks whole dollars with separators when the spread is small. Deferred deliberately: the assets/debts split (mode="multi"). The chart already supports it, but distinguishing the two series by red/green line colour alone is the textbook CVD failure case, and stroke dashing is unavailable because dashing already means "partial coverage" here. It needs direct end-labels to be done properly, and belongs in its own change rather than bolted onto this one. Mutation testing found the tests were weaker than they looked. rangeSupport was called at describe scope, so those calls ran during collection and Stryker's per-test coverage could not attribute them — three mutants read as survived while the assertions did cover them. Moved into the tests. It also showed the boundary case was never exercised: with a non-midnight asOf the window start and the coverage date differ by hours, so `>=` and `>` both passed. Pinned with a midnight asOf plus the one-day-before case. 100% of mutants killed. Closes #107
aeff6a1 to
eb8f327
Compare
The mutation run on this PR reported money.ts at 64.46% with 19 NoCoverage mutants. Comparing against an earlier run — 76.47% covered, same 24 survivors, 0 NoCoverage — shows the 24 are pre-existing and the 19 are new: the axisTickFormatter this branch added had no tests at all. Adds 13, covering both branches of the threshold, the boundary between them at exactly $100 of spread, sign and rounding on the whole-dollar path, and the degenerate inputs (empty series, single point, unordered values, a domain spanning zero). money.ts is now 80.17% with 0 NoCoverage. The remaining 24 survivors predate this branch and belong to #34.
|
Note on the The run reports 39.13%, under the 60 threshold. Per file:
Two different things in there, and only one was mine to fix: Mine, now fixed. Structural, and not fixable here. So the score is arithmetic over a denominator that includes 111 mutants no unit test could ever reach. That is exactly #103, and it is why the workflow sets Blocking checks — typecheck · lint · test, CodeQL, Analyze, GitGuardian — all pass. |
The range control always offered 6M, so a household whose balance history begins two days ago got six months of chart holding two days of net worth — roughly 95% of the plot area hatched fill and dashed line, and a y-axis stretched to −$30K to accommodate a region that is not net worth.
What this does not change
The coverage logic is untouched.
coveredTrendDeltastill measures over the fully covered span only, and the comment recording why — the "+2430.7% past 6 months" it replaced — still stands. This changes the frame around the number, not the maths.What it changes
getFullCoverageSincecomputes the date every account first had a balance, asMAX(MIN(date))overbalance_historyrather than by reconstructing a series — the range control needs it on every dashboard load.Ranges reaching back before that are disabled and say why on the control itself, not only in a tooltip, so the reason reaches keyboard and screen-reader users. A range that goes grey without explanation reads as a bug. The page then loads the range the hero will actually open on, instead of a hard-coded 6M.
When coverage began too recently for even the narrowest range, the covered span is plotted on its own with a note naming what is omitted and why. That case is what the issue was actually about, and gating alone did not fix it — it only shrank the hatching from six months to one. I checked in the browser rather than assuming the gating was enough.
The honesty condition holds: pre-coverage points are dropped, not un-marked. Nothing partial is ever drawn without the dashed-and-hatched treatment. Selecting a range that does include partial data renders it exactly as before.
An artifact this introduced, and fixed
With a two-day window the y-axis showed
$52.9Kfour times —centsToCompactresolves to $100 and the domain was narrower than that.axisTickFormatternow picks whole dollars with separators when the spread is small.Deliberately deferred
The assets/debts split (
mode="multi"). The chart already supports it, but distinguishing the two series by red/green line colour alone is the textbook CVD failure case — and stroke dashing is unavailable as a fallback because dashing already means "partial coverage" in this chart. It needs direct end-labels to be done properly, and belongs in its own change rather than bolted onto this one.What mutation testing caught
My tests were weaker than they looked.
rangeSupportwas called atdescribescope, so those calls ran during collection and Stryker's per-test coverage could not attribute them — three mutants read as survived while the assertions genuinely did cover them. Moved inside the tests.It also showed the boundary case was never actually exercised: with a non-midnight
asOf, the window start and the coverage date differ by hours, so>=and>both passed. Pinned with a midnightasOfplus the one-day-before case.net-worth-range.ts: 100% (21 killed, 0 survived).Verification
tsc --noEmitandeslint src/cleanCloses #107.
🤖 Generated with Claude Code