Skip to content

Cover the localize text pipeline; raise the coverage gate to 88 - #26

Merged
thequantumfalcon merged 8 commits into
masterfrom
coverage-phase1-localize
Aug 15, 2026
Merged

Cover the localize text pipeline; raise the coverage gate to 88#26
thequantumfalcon merged 8 commits into
masterfrom
coverage-phase1-localize

Conversation

@thequantumfalcon

@thequantumfalcon thequantumfalcon commented Aug 15, 2026

Copy link
Copy Markdown
Owner

What

sts2/localize.py was 12% covered in CI and accounted for 39% of all uncovered code in the repo. The only test that exercised it ran against a real game install and skipped everywhere else, including CI.

That is the worst place in the app to have a hole. English descriptions never pass through this pipeline, so a game patch that reshapes a template silently ships half-rendered markup or a wrong number to every non-English player while the suite stays green.

It needs no game to test: of 970 lines only 16 touch the filesystem, and the parser, the English alignment pass and the renderer are pure text → text. tests/test_localize_pipeline.py drives all of them from fixture strings, including the full overlay build.

Template shapes in the fixtures were taken from the shipped English card, relic and potion descriptions of build 0.107.1, so they exercise forms that actually occur (NUM 881, PLURAL 173, ENERGY 106, SHOW 65, STAR 27, ENCH 3, COND 1) rather than invented ones.

Coverage: 74% → 90.48% in CI

Module Before After
localize.py 12% 94%
behavior.py 52% 100%
graveyard.py 61% 100%
config.py 69% 98%
__main__.py 71% 97%
fetcher.py 74% 88%
sources.py 77% 94%
app.py 81% 92%
updater.py 82% 91%
analytics.py 88% 96%
knowledge.py 86% 89%
ghost / rivalry / persist 71 / 79 / 81% 100%
prophecy.py 80% 97%

Most of the above was previously exercised only by mocks asserting mock.called, so each module could have returned a constant and stayed green.

--cov-fail-under moves 72 → 88, raised in three steps and each time from a measured CI run rather than an estimate.

Three bugs fixed, all found while covering the code

1. The overlay invented upgraded card descriptions. _build_all gave every no-alignment card an up variant whether or not English had upgraded text. card_detail.html renders an "Upgraded:" block whenever the field is present, so six cards — Apotheosis, Despair, Lantern Key, Sharp Edge, Wish, Reserves — showed translated players a section English readers never see, restating the base description back at them. Thirteen languages, so 78 entries.

Verified against a real German build off an installed game: all six clean, the 545 legitimate upgraded translations untouched, and nothing in the 608-card overlay claims an upgrade English does not have.

2. A duplicate "Upgraded:" block. Separately, 98 English and 120 German cards rendered an "Upgraded:" section identical to the description above it.

The obvious fix — dropping the field from the overlay — was measured and is wrong: knowledge.py skips absent overlay values, so the card falls back to the English upgraded string under a translated description. CARD.ANOINTED came out as a German description with 'Retain. Put every Rare card from your Draw Pile into your Hand. Exhaust.' beneath it, across 148 fields.

Fixed in the template instead, with the guard deck.js already applies to its popover. Measured: 490 English and 425 German cards keep their block, and no card whose upgraded text genuinely differs is hidden.

3. log.debug(" ", mm) passed an argument to a format string with no placeholder, which raises inside logging whenever DEBUG is on and the token-name check has anything to report.

Also removed an unreachable guard in detect_tilt (len(runs) >= 3 is checked above, and _group_sessions always returns at least one non-empty session for non-empty input).

The measurement gap has closed

This repo carried a ~7-point laptop-vs-CI coverage gap. It was almost entirely localize.py reading the installed game — 87% where the game exists, 12% in CI. Now that it runs on fixtures, both environments agree to within a point. The only remaining local-only difference is risk.py + diagnosis.py, which are gitignored and absent from a CI checkout. The stale comment in ci.yml documenting the old gap has been rewritten.

Two statistics validated against their definitions

Rather than against a recorded output, which would only pin whatever the code happened to do:

  • _consistency_index against the closed form of the R/S statistic for a linear ramp: cumulative deviation is S_k = k(k−n)/2, so R = n²/8 exactly and S = √((n²−1)/12). Checked at n = 10, 20, 40, 100. (An initial guess that it approaches 1 was wrong — it is 0.773 at n=40 — and deriving it exactly is what caught that.)
  • plural_index_ru against real Russian agreement, including the 11–14 exception: 1→0, 21→0, 101→0, 2/3/4→1, 22→1, 11/12/13/14→2, 111→2.

Verification

  • pytest -q — 1570 passed
  • pytest -q -m browser — 15 passed (required check, invisible to a plain run)
  • mypy sts2 — clean
  • ruff check sts2/ tests/ — clean
  • All 15 CI checks green on Linux (3.11/3.12/3.13), Windows and macOS

Known, not addressed

The root cause behind bug 2 is upstream: the game's description templates omit the keyword prefixes (Retain., Innate., Exhaust.) that the wiki-derived English carries, because those live in card metadata. That affects base descriptions too, not only upgraded ones, and reconstructing them across 13 languages is a feature rather than a fix.

routes.py (87%) is the largest remaining coverage target.

sts2/localize.py was 12% in CI and 39% of all uncovered code in the repo,
because the only test that exercised it ran against a real game install and
skipped everywhere else. That is the worst place in the app to have a hole:
English descriptions never pass through this pipeline, so a game patch that
reshapes a template silently ships half-rendered markup to every non-English
player while the suite stays green.

It needs no game to test. Of 970 lines only 16 touch the filesystem; the
parser, the English alignment pass and the renderer are pure text -> text.
tests/test_localize_pipeline.py drives all of them from fixture strings,
including the full overlay build, and takes localize.py from 12% to 94%.

Template shapes in the fixtures were taken from the shipped English card,
relic and potion descriptions of build 0.107.1, so they match the forms that
actually occur rather than invented ones.

Also covered, all previously exercised only by mocks asserting `mock.called`:
behavior (52% -> 99%), graveyard (61% -> 100%), config path detection and
state migration (69% -> 98%), the CLI localize/serve/network-bind paths
(71% -> 97%), the fetcher's three fallback extraction strategies
(74% -> 88%), ghost/rivalry/persist (100%), prophecy (97%) and the wiki.gg
API batching.

Two statistics are validated against their definitions rather than against a
recorded output: the R/S consistency index against its closed form for a
linear ramp (R = n^2/8, S = sqrt((n^2-1)/12)), and plural_index_ru against
real Russian agreement including the 11-14 exception.

Reported coverage goes 74% -> ~87% in CI, so --cov-fail-under moves 72 -> 84.
The number is derived from a CI-equivalent local harness that reproduced the
old baseline exactly (7334 statements, 1577 missed, 76% against CI's 74%);
it is calibrated to ~2 points, not measured, so confirm against the real CI
run and raise the floor further if it lands higher.

Side effect worth noting: the laptop-vs-CI measurement gap this repo has
carried was almost entirely localize.py, and it is now gone -- both
environments report 89%.
_build_all gave every no-alignment card an "up" variant regardless of
whether English had upgraded text to translate. card_detail.html renders an
"Upgraded:" block whenever description_upgraded is present, so six cards --
Apotheosis, Despair, Lantern Key, Sharp Edge, Wish and Reserves -- showed
translated players a section English readers never see, restating the base
description back at them. Thirteen languages, so 78 entries.

Checking for upgraded English first fixes it without changing any card that
genuinely has one. Verified against a real German build off the installed
game: all six are now clean, the 545 legitimate upgraded translations are
untouched, and nothing in the 608-card overlay claims an upgrade English
does not have.

Also in this pass, both found while covering the module:

- log.debug("   ", mm) passed an argument to a format string with no
  placeholder, which raises inside logging whenever DEBUG is enabled and the
  token-name check has anything to report.
- detect_tilt's empty-session guard was unreachable: len(runs) >= 3 is
  checked above and _group_sessions always returns at least one non-empty
  session for non-empty input, so neither the runs[-5:] fallback nor the
  guard could ever run.
_program_name uses os.path.basename, which only splits on the host
separator, so the hardcoded "C:\Games\...\Spirescope.exe" literal came back
whole on Linux and macOS and the assertion failed there while passing on
Windows.

The drive-letter test in test_config.py is left as it is and now says why:
_find_game_dir builds the same literal, so the two Paths compare equal
however the host parses the separators.
The 84 in the previous commit was derived from a local CI-equivalent harness
and deliberately left slack for the calibration error. This leg has since
measured 88.32%, so 86 restores the ~2 points of headroom the old
72-against-74 gate had. A gate with four points of slack is most of the way
to not being a gate.
@thequantumfalcon thequantumfalcon changed the title Cover the localize text pipeline; raise the coverage gate to 84 Cover the localize text pipeline; raise the coverage gate to 86 Aug 15, 2026
deck.js already guarded its popover with `descUp !== desc`; the card detail
page did not, so it printed an "Upgraded:" section identical to the text
directly above it. 98 cards in English, where the wiki records the same text
for both, and 120 in German, where the difference is a keyword prefix
(Retain./Innate./Exhaust.) that lives in card metadata rather than in the
game's description template, so the localized template cannot express it.

Deliberately fixed in the template rather than in the localize overlay.
knowledge.py skips absent overlay fields, so an overlay that dropped the
field would leave the *English* upgrade string sitting under a translated
description -- mixed-language output on 148 fields, which is worse than the
duplicate it set out to remove.

Checked against real data: 490 English and 425 German cards keep their
upgrade block, and no card whose upgraded text genuinely differs is hidden.
Three code paths that degrade quietly rather than raising, so a break shows
up as a wrong number or a missing entity on a page instead of a failing
request:

- compute_records feeds the hall-of-fame panel and is entirely max()/min()
  over run history; picking the wrong extreme is invisible without asserting
  the value. analytics 88% -> 96%.
- _load_mods runs at KnowledgeBase construction, which happens at application
  import, so anything escaping it takes the server down before a page can
  render. Covers namespacing, base-id conflicts, and every malformed-input
  shape a hand-authored mod file can arrive in. knowledge 86% -> 89%.
- _merge_local_build_ids protects hand-assigned patch mappings from being
  discarded by a bundle install, which silently changes which runs count as
  current-patch and therefore the win rates shown. updater 82% -> 91%.

Total 89% -> 91% under the CI-equivalent harness.

Note for anyone writing mod fixtures: CARD.MODDED is a real Defect card in
the shipped data, so a mod using that id is a genuine base conflict rather
than a free synthetic name.
None of this is reachable through the test client's normal request path: the
lifespan only runs when a real server starts, the watcher and log poller are
background coroutines, and the global exception handler exists precisely for
requests that never reach a route.

All four swallow exceptions by design, so a bad save file or a locked game
log cannot take the dashboard down -- which is also why a break in them is
invisible. The shutdown half matters most: nothing used to run after the
lifespan's yield, so the watcher task was never cancelled and the observer
threads were never stopped or joined.

app.py 81% -> 92%, total 91% -> 92% under the CI-equivalent harness.
This leg now reports 90.48%, so 88 restores the ~2 points of headroom the
old 72-against-74 gate had rather than leaving four and a half.
@thequantumfalcon thequantumfalcon changed the title Cover the localize text pipeline; raise the coverage gate to 86 Cover the localize text pipeline; raise the coverage gate to 88 Aug 15, 2026
@thequantumfalcon
thequantumfalcon merged commit 0f68832 into master Aug 15, 2026
15 checks passed
@thequantumfalcon
thequantumfalcon deleted the coverage-phase1-localize branch August 15, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant