Cover the localize text pipeline; raise the coverage gate to 88 - #26
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
sts2/localize.pywas 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.pydrives 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
localize.pybehavior.pygraveyard.pyconfig.py__main__.pyfetcher.pysources.pyapp.pyupdater.pyanalytics.pyknowledge.pyghost/rivalry/persistprophecy.pyMost 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-undermoves 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_allgave every no-alignment card anupvariant whether or not English had upgraded text.card_detail.htmlrenders 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.pyskips absent overlay values, so the card falls back to the English upgraded string under a translated description.CARD.ANOINTEDcame 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.jsalready 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 insideloggingwhenever DEBUG is on and the token-name check has anything to report.Also removed an unreachable guard in
detect_tilt(len(runs) >= 3is checked above, and_group_sessionsalways 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.pyreading 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 isrisk.py+diagnosis.py, which are gitignored and absent from a CI checkout. The stale comment inci.ymldocumenting 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_indexagainst the closed form of the R/S statistic for a linear ramp: cumulative deviation isS_k = k(k−n)/2, soR = n²/8exactly andS = √((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_ruagainst 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 passedpytest -q -m browser— 15 passed (required check, invisible to a plain run)mypy sts2— cleanruff check sts2/ tests/— cleanKnown, 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.