Skip to content

Test suite overhaul + fixes for the three bugs it uncovered - #441

Merged
ChuckBuilds merged 8 commits into
mainfrom
claude/ledmatrix-test-coverage-c8vkg8
Aug 7, 2026
Merged

Test suite overhaul + fixes for the three bugs it uncovered#441
ChuckBuilds merged 8 commits into
mainfrom
claude/ledmatrix-test-coverage-c8vkg8

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Pull Request

Summary

An audit of the test suite found that CI was giving false confidence: only 24 of 90 test files ran on PRs, the plugin-safety job silently skipped everything (no plugins present in CI), several enrolled tests were assert True shells that could not fail, and the declared coverage gate had never executed. This PR makes CI honest (full-tree run, a bundled fixture plugin with goldens, one real coverage gate at 45%), replaces the can't-fail tests with real assertions, adds ~180 new tests covering the riskiest untested logic, and adds drift guards that pin cross-file contracts. The new tests surfaced three real production bugs, which are fixed in the final commit (see below).

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor (no functional change)
  • Build / CI
  • Plugin work (link to the plugin)

Related issues

None pre-existing — the three bugs below were discovered by this PR's new tests and are fixed in commit 206eca0:

  1. Version comparison unified. store_manager.update_plugin used raw string equality while the UI's update badge used packaging — so v1.2.0 vs 1.2.0 triggered a full reinstall the UI called unnecessary, and a locally-ahead plugin got silently downgraded. Both now share one comparator: compatibility.is_update_available() (PEP 440). Equivalent spellings skip the reinstall; locally-ahead versions are never downgraded; unparseable versions still reconcile via reinstall.
  2. Secrets can no longer leak into config.json. If config_secrets.json existed but couldn't be parsed at save time, both save_config and save_config_atomic proceeded without stripping — writing merged secrets to config.json in plaintext. Both now raise ConfigError with an actionable message instead. A missing secrets file is still fine, and _migrate_config's catch-all keeps boot resilient.
  3. render_skin_card now resets the shared 3-strike counter on success (both the vegas-card and mode-renderer paths), mirroring _render_game — transient card failures no longer accumulate across a session until they permanently disable a working skin.

Also documented executably (not changed): the three inline find_secret_fields/separate_secrets copies in api_v3.py lack the canonical module's array-item secret support (test_secret_separation_parity.py guards the copy count).

Test plan

  • Ran on a real Raspberry Pi with hardware
  • Ran in emulator mode (EMULATOR=true python3 run.py) — via the plugin-safety harness, which renders the new fixture plugin at all 8 default panel sizes under RGBMatrixEmulator and compares against committed goldens
  • Ran the dev preview server (scripts/dev_server.py)
  • Ran the test suite (pytest) — full mirror of both new CI jobs locally: pytest -m "not hardware" test/ --ignore=test/plugins --cov=src --cov=web_interface --cov-fail-under=45 (2,200+ tests) and LEDMATRIX_PLUGINS_DIR=test/fixtures/plugins LEDMATRIX_REQUIRE_PLUGINS=1 pytest test/plugins/ (65 passed). Enabling the 63 previously-unenrolled files surfaced exactly 4 rotted tests, all fixed here. The three production fixes flip their characterization tests to assert the corrected behavior.
  • Manually verified the affected code path in the web UI
  • N/A — documentation-only change

Documentation

  • I updated README.md if user-facing behavior changed
  • I updated the relevant doc in docs/ if developer behavior changed
  • I added/updated docstrings on new public functions — compatibility.is_update_available, ConfigManager._load_secrets_for_save, and every new test module carries a docstring explaining what contract it pins and why
  • N/A — no docs needed

Plugin compatibility

  • No plugin breakage expected
  • Some plugins will need updates — listed below
  • N/A — change doesn't touch the plugin system

The production changes are strictly less destructive for plugins: fewer unnecessary reinstalls, no downgrades of locally-ahead versions, and working skins stop getting disabled by stale strike counts. The new test/fixtures/plugins/ci-fixture-plugin is a test fixture only — it lives outside the configured plugins directory and is invisible to normal discovery.

Checklist

  • My commits follow the message convention in CONTRIBUTING.md
  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • I've not committed any secrets or hardcoded API keys
  • If this adds a new config key, the form in the web UI was verified (the form is generated from config_schema.json) — N/A, no config keys added

Notes for reviewer

  • The one behavior change users could notice: a save with a corrupt-but-present secrets file now fails loudly (ConfigError) instead of silently writing secrets into config.json. Web routes surface it as a 500 with the actionable message; boot is unaffected (_migrate_config catches it).
  • The coverage gate is 45% — measured baseline is 47%, minus a 2-point buffer. Ratcheting it up as coverage grows is intentional follow-up.
  • pytest.ini no longer carries --cov* flags; coverage lives in exactly one place, the CI unit-tests step. The unit-tests job runs test/ wholesale instead of an allowlist (the allowlist rotted once — 63 files silently unenrolled); the current ignore list is empty.
  • Future work (deliberately out of scope): migrating the api_v3 inline secret copies onto src/web_interface/secret_helpers, skin_runtime discovery-cache tests, _check_schedule/_check_dim_schedule under freezegun, unit tests for logging_config, startup_validator, common/*_helper, dynamic_team_resolver, base_odds_manager, saved_repositories, the get_display_duration bool-as-int quirk, and a coverage ratchet.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

Summary by CodeRabbit

  • Bug Fixes

    • Configuration saves now stop safely when secret data is unreadable or invalid.
    • Successful skin renders recover correctly after temporary rendering failures.
    • Plugin updates now recognize equivalent versions and avoid unnecessary reinstalls or downgrades.
  • Tests

    • Expanded automated coverage for compatibility, configuration secrets, discovery, skins, fonts, version handling, API helpers, and schema merging.
    • Added a fixture plugin for reliable discovery and safety validation.
  • Chores

    • CI runs broader test suites with enforced coverage reporting.
    • Local test configuration is streamlined.

claude added 4 commits August 6, 2026 22:30
…ething real

The unit-tests CI job ran an explicit 24-file allowlist that had rotted:
63 of 90 test files (display, vegas, store manager, web API, web_interface)
never ran on a PR. The job now runs all of test/ (minus test/plugins, which
the plugin-safety job owns) so new test files are enrolled by default and
any exclusion needs a visible, commented --ignore.

The plugin-safety job was a green no-op: plugins/ is empty in CI, so every
test skipped with 'Manifest not found'. It now renders a bundled
deterministic fixture plugin (test/fixtures/plugins/ci-fixture-plugin,
golden images included for all 8 default sizes) via LEDMATRIX_PLUGINS_DIR,
and sets LEDMATRIX_REQUIRE_PLUGINS=1 so discovering zero plugins fails
loudly instead of skipping green. The per-plugin suites document that they
target dev machines with real plugins installed.

Coverage is now measured and enforced in exactly one place — the CI
unit-tests step (--cov=src --cov=web_interface --cov-fail-under=45, from a
measured 47% baseline). pytest.ini previously declared --cov-fail-under=30
but CI always passed --no-cov, so the gate had never run anywhere; local
pytest is now coverage-free and fast.

Enabling the 63 unenrolled files surfaced three cases of test rot, fixed
here: test_display_controller_vegas_tick.py could not collect without the
hardware rgbmatrix module (now uses the emulator convention), the
state-reconciliation unrecoverable-cache tests broke when production added
the is_plugin_uninstalled tombstone check (bare Mock returned truthy),
and test_get_system_status assumed the optional psutil dependency
(now installed via requirements-test.txt and guarded by importorskip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
test_font_manager.py was 5 of 6 tests shaped as 'try: call(); assert True /
except: assert True' — running in CI while unable to fail on any
regression. Rewritten against the real FontManager API and the bundled
assets/fonts: returned font types, cache-hit identity, distinct entries per
size, default-font fallback for unknown families and corrupt files
(recorded in failed_loads), BDF native-size reading, text measurement, and
cache lifecycle.

test_display_manager.py's test_draw_text ended in 'assert True'; it now
renders onto a known-black canvas and asserts pixels were actually lit —
which required un-breaking the fixture's freetype MagicMock so draw_text's
isinstance check doesn't silently swallow the draw.

test_display_controller.py carried a permanently-skipped test whose skip
reason already declared it redundant; deleted.

Both display test files now set EMULATOR=true before importing
display_manager (the same convention as test_display_dirty_tracking.py) so
they collect standalone instead of depending on which test module imports
display_manager first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
…config merges, durations, skin cards)

New unit tests for pure or filesystem-only logic that previously had zero
direct coverage:

- test_compatibility.py: the semver install gate (parse_semver suffix
  handling, every range operator, TRUSTWORTHY_FLOOR behavior for cores
  reporting untrustworthy versions, 'more restrictive wins', and the
  malformed-manifest shapes that used to raise).
- test/web_interface/test_secret_helpers.py: the canonical x-secret
  helpers — find/separate/mask/remove, array-item secrets, no input
  mutation, and a separate->recombine round-trip.
- test/web_interface/test_api_v3_helpers.py: the module-level helpers
  behind the plugin config save endpoint (_is_plugin_update_available,
  _coerce_to_bool including the int==1 quirk, deep_merge including its
  shared-subtree shallowness, _parse_form_value, dotted-key-aware
  _get_schema_property/_set_nested_value).
- test_base_plugin_duration.py: get_display_duration's full coercion
  ladder (instance attr -> config -> 15.0), including the bool-is-int
  quirk where display_duration=True means one second.
- test_config_manager_secrets.py: the secrets round-trip — deep-merge on
  load, strip on save, group pruning, the load fast path — and two
  characterized sharp edges marked SUSPECTED BUG: an unreadable secrets
  file at save time writes secrets into config.json in plaintext, and a
  same-mtime-same-size content swap is served stale.
- test_schema_manager_merge.py: merge_with_defaults branch behavior (None
  replacement vs falsey preservation, dict-vs-scalar mismatches, arrays
  replaced wholesale, defaults never mutated).
- test_skin_system.py (extended): render_skin_card shares _render_game's
  3-strike counter but never resets it on success — the asymmetry is
  pinned in both directions, along with card fallthrough and the disable
  interaction between the two paths.

Suspected bugs are characterized, not fixed — each carries a comment so a
future behavior change is deliberate rather than accidental.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
Three guard suites that pin contracts spanning multiple files, where one
side changing unilaterally breaks the other silently:

- test_version_comparison_consistency.py: the repo's four version
  comparators (compatibility.parse_semver, api_v3's packaging-based
  _is_plugin_update_available, store_manager update_plugin's raw string
  equality, skin_runtime._major) answer differently on the same inputs.
  A table pins each one's verdict; update_plugin is driven through its
  real code path to show the SUSPECTED BUGs: 'v1.2.0' vs '1.2.0'
  triggers a full reinstall the UI calls unnecessary, and a locally-ahead
  plugin gets downgraded. A pairwise-ordering check keeps parse_semver
  agreeing with packaging on plain X.Y.Z.
- test/web_interface/test_secret_separation_parity.py: api_v3.py carries
  three inline copies of find_secret_fields/separate_secrets that lack
  the canonical module's array-item support. The copy count is asserted
  exact (it may only go down; new copies must import
  src/web_interface/secret_helpers), the missing-array-support gap is
  asserted so it can't grow silently, and the canonical behavior that
  migration will adopt is documented executably.
- test_discovery_path_contract.py: the three 'where is plugin X'
  resolvers (PluginManager discovery, StoreManager._find_plugin_path,
  SchemaManager.get_schema_path) agree on the configured directory, and
  their divergent fallback chains are characterized. Also pins the
  .standalone-backup- naming contract shared by store rollback and
  discovery, and _resolve_skin_target's path-traversal rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b6e8ee1-4889-4542-ad8e-01623dc37c6d

📥 Commits

Reviewing files that changed from the base of the PR and between 206eca0 and d97a299.

📒 Files selected for processing (4)
  • src/config_manager.py
  • src/plugin_system/compatibility.py
  • src/plugin_system/store_manager.py
  • test/test_version_comparison_consistency.py
📝 Walkthrough

Walkthrough

The pull request expands CI test execution, adds a deterministic fixture plugin, centralizes plugin version comparison, hardens configuration secret saves, resets skin-render failure counts, and adds broad core and web-interface test coverage.

Changes

Test validation

Layer / File(s) Summary
CI plugin matrix and fixture setup
.github/workflows/test.yml, pytest.ini, requirements-test.txt, test/fixtures/plugins/..., test/plugins/...
CI runs plugin tests and non-plugin tests with a 45% coverage minimum. The fixture plugin and discovery override provide deterministic plugin testing.
Shared version comparison and update behavior
src/plugin_system/compatibility.py, src/plugin_system/store_manager.py, web_interface/blueprints/api_v3.py, test/test_compatibility.py, test/test_version_comparison_consistency.py, test/web_interface/test_api_v3_helpers.py
Plugin update decisions use shared semantic comparison. Tests cover normalized, newer, older, prerelease, malformed, and missing versions.
Configuration secret save safety
src/config_manager.py, test/test_config_manager_secrets.py
Regular and atomic saves reject unreadable or invalid existing secret files. Tests cover merging, stripping, corruption, cache invalidation, and stale reads.
Core contracts and rendering behavior
src/base_classes/sports/core.py, test/test_base_plugin_duration.py, test/test_discovery_path_contract.py, test/test_display_*.py, test/test_font_manager.py, test/test_schema_manager_merge.py, test/test_skin_system.py, test/web_interface/test_state_reconciliation.py
Tests cover plugin duration, discovery paths, schema merging, emulator collection, display rendering, fonts, skin failure resets, and reconciliation setup.
Web interface and secret contracts
test/test_web_api.py, test/web_interface/test_api_v3_helpers.py, test/web_interface/test_secret_helpers.py, test/web_interface/test_secret_separation_parity.py
Tests cover helper parsing, nested updates, secret masking and separation, array-item handling, inline-copy parity, and optional psutil support.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the test suite overhaul and the production fixes included in the changeset.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ledmatrix-test-coverage-c8vkg8

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Aug 6, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 3 complexity · -2 duplication

Metric Results
Complexity 3
Duplication -2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
test/test_discovery_path_contract.py (1)

36-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add type hints to the affected test helpers, fixtures, and test methods.

  • Type annotate helper/fixture functions and test function parameters/returns in test_discovery_path_contract.py, test_font_manager.py, test_schema_manager_merge.py, test_skin_system.py, test_version_comparison_consistency.py, test_secret_helpers.py, and test_secret_separation_parity.py.
  • Use str | None for _write_plugin(..., dir_name=None) on this Python target.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_discovery_path_contract.py` around lines 36 - 167, Apply type hints
to all affected helpers, fixtures, and test methods in
test/test_discovery_path_contract.py:36-167, test/test_font_manager.py:18-126,
test/test_schema_manager_merge.py:14-104, test/test_skin_system.py:464-561,
test/test_version_comparison_consistency.py:49-148,
test/web_interface/test_secret_helpers.py:51-241, and
test/web_interface/test_secret_separation_parity.py:30-119. Annotate parameters
and return types using the concrete types implied by each helper, fixture, and
test; specifically update _write_plugin’s dir_name parameter to str | None and
add appropriate return annotations throughout.

Sources: Coding guidelines, Linters/SAST tools

test/web_interface/test_api_v3_helpers.py (1)

156-171: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark SCHEMA as a class variable.

Ruff reports RUF012 because SCHEMA is mutable at class scope. Add a ClassVar annotation or move the schema into a fixture. This prevents accidental shared-state mutation between tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/web_interface/test_api_v3_helpers.py` around lines 156 - 171, Annotate
the class-level mutable SCHEMA definition with typing.ClassVar in the test
class, preserving its existing schema contents and behavior while resolving Ruff
RUF012.

Sources: Coding guidelines, Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/fixtures/plugins/ci-fixture-plugin/manager.py`:
- Line 18: Add a class docstring to CIFixturePlugin stating that it is a
deterministic, CI-only fixture plugin, while leaving the class behavior
unchanged.
- Around line 22-38: Update display() to call self.display_manager.clear()
before accessing self.display_manager.image and performing any drawing. Preserve
the existing rendering operations and ensure
self.display_manager.update_display() remains after rendering.

In `@test/fixtures/plugins/ci-fixture-plugin/requirements.txt`:
- Line 1: Add the existing LEDMatrix/Pillow-compatible pinned Pillow dependency
to the fixture’s requirements.txt, preserving the explanatory comment and
ensuring the plugin loader detects and installs it for manager.py’s
PIL.ImageDraw import.

In `@test/test_base_plugin_duration.py`:
- Around line 16-110: Add repository-required type hints to all affected test
callables: in test/test_base_plugin_duration.py lines 16-110, annotate
_MinimalPlugin methods, make_plugin(), and every test method; in
test/test_compatibility.py lines 25-271, annotate parameterized test parameters
and test method returns; in test/test_config_manager_secrets.py lines 21-161,
annotate make_manager(), tmp_path, and test method returns; and in
test/web_interface/test_api_v3_helpers.py lines 30-231, annotate test methods
and helper inputs with concrete types, using -> None for tests and preserving
existing behavior.
- Around line 81-84: Update BasePlugin.get_display_duration to exclude bool
values from numeric duration handling, including both True and False, so they
use the configured duration or 15.0 default. Replace
test_bool_true_is_one_second with an assertion covering the expected fallback
behavior for instance_duration=True.

In `@test/test_compatibility.py`:
- Around line 219-227: Rename test_unparseable_core_version_is_compatible in
test/test_compatibility.py:219-227 to describe that an untrustworthy core with a
high declared floor is blocked; rename the corresponding secrets test in
test/test_config_manager_secrets.py:102-120 to state that the secrets file is
corrupt rather than unreadable. No assertion or behavior changes are needed.

In `@test/test_config_manager_secrets.py`:
- Around line 102-120: Update save_config() to detect parsing failures in
config_secrets.json and raise a clear ConfigError before writing config.json.
Ensure the failure path performs no plaintext secret write, and preserve the
regression test’s expectation that the secret is not saved in the main
configuration.

In `@test/test_skin_system.py`:
- Around line 521-550: Update SportsCore.render_skin_card and its successful
card-render path so _skin_failures resets to zero when render_vegas_card returns
an image and when the mode renderer returns True. Revise
test_card_success_does_not_reset_strikes to assert successes clear the counter
and that a later failure starts from the reset state rather than accumulated
strikes.

In `@test/test_version_comparison_consistency.py`:
- Around line 101-115: Update PluginStoreManager.update_plugin() to use one
shared semantic version comparator that normalizes equivalent forms such as a
leading “v”, and reinstall only when the registry version is newer than the
installed version. Change
test_v_prefix_triggers_reinstall_despite_semantic_equality and
test_locally_ahead_version_triggers_downgrade_reinstall to assert no reinstall
and preserve the successful update result.

---

Nitpick comments:
In `@test/test_discovery_path_contract.py`:
- Around line 36-167: Apply type hints to all affected helpers, fixtures, and
test methods in test/test_discovery_path_contract.py:36-167,
test/test_font_manager.py:18-126, test/test_schema_manager_merge.py:14-104,
test/test_skin_system.py:464-561,
test/test_version_comparison_consistency.py:49-148,
test/web_interface/test_secret_helpers.py:51-241, and
test/web_interface/test_secret_separation_parity.py:30-119. Annotate parameters
and return types using the concrete types implied by each helper, fixture, and
test; specifically update _write_plugin’s dir_name parameter to str | None and
add appropriate return annotations throughout.

In `@test/web_interface/test_api_v3_helpers.py`:
- Around line 156-171: Annotate the class-level mutable SCHEMA definition with
typing.ClassVar in the test class, preserving its existing schema contents and
behavior while resolving Ruff RUF012.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d9d9b8d-e215-4d9c-a0c3-5fb01494f887

📥 Commits

Reviewing files that changed from the base of the PR and between d9683e2 and b8b0c8e.

⛔ Files ignored due to path filters (8)
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/128x32/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/128x64/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/128x96/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/256x128/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/256x32/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/64x32/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/64x64/ci-fixture.png is excluded by !**/*.png
  • test/fixtures/plugins/ci-fixture-plugin/test/golden/96x48/ci-fixture.png is excluded by !**/*.png
📒 Files selected for processing (30)
  • .github/workflows/test.yml
  • pytest.ini
  • requirements-test.txt
  • test/fixtures/plugins/ci-fixture-plugin/config_schema.json
  • test/fixtures/plugins/ci-fixture-plugin/manager.py
  • test/fixtures/plugins/ci-fixture-plugin/manifest.json
  • test/fixtures/plugins/ci-fixture-plugin/requirements.txt
  • test/plugins/conftest.py
  • test/plugins/test_basketball_scoreboard.py
  • test/plugins/test_calendar.py
  • test/plugins/test_clock_simple.py
  • test/plugins/test_odds_ticker.py
  • test/plugins/test_soccer_scoreboard.py
  • test/plugins/test_text_display.py
  • test/test_base_plugin_duration.py
  • test/test_compatibility.py
  • test/test_config_manager_secrets.py
  • test/test_discovery_path_contract.py
  • test/test_display_controller.py
  • test/test_display_controller_vegas_tick.py
  • test/test_display_manager.py
  • test/test_font_manager.py
  • test/test_schema_manager_merge.py
  • test/test_skin_system.py
  • test/test_version_comparison_consistency.py
  • test/test_web_api.py
  • test/web_interface/test_api_v3_helpers.py
  • test/web_interface/test_secret_helpers.py
  • test/web_interface/test_secret_separation_parity.py
  • test/web_interface/test_state_reconciliation.py
💤 Files with no reviewable changes (1)
  • test/test_display_controller.py

Comment thread test/fixtures/plugins/ci-fixture-plugin/manager.py
Comment thread test/fixtures/plugins/ci-fixture-plugin/manager.py
Comment thread test/fixtures/plugins/ci-fixture-plugin/requirements.txt
Comment thread test/test_base_plugin_duration.py
Comment thread test/test_base_plugin_duration.py
Comment thread test/test_compatibility.py Outdated
Comment thread test/test_config_manager_secrets.py Outdated
Comment thread test/test_skin_system.py Outdated
Comment thread test/test_version_comparison_consistency.py Outdated
- ci-fixture-plugin: call display_manager.clear() before rendering (per
  plugin guidelines — the fixture should model a well-behaved plugin),
  add a class docstring, and document why Pillow is deliberately not
  pinned in its requirements.txt (core dependency; harness installs
  nothing).
- Rename two tests whose names contradicted their assertions:
  test_unparseable_core_version_is_compatible ->
  test_unparseable_core_with_high_floor_is_blocked, and
  test_unreadable_secrets_file... -> test_corrupt_secrets_file...
- Annotate TestGetSchemaProperty.SCHEMA as ClassVar (RUF012).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

Copy link
Copy Markdown
Owner Author

Addressed the CodeRabbit review in 71b665e (or latest head):

Applied

  • ci-fixture-plugin/manager.py: display_manager.clear() before rendering (per plugin guidelines), plus a class docstring. Goldens verified unchanged.
  • Renamed the two misleadingly-named tests (test_unparseable_core_with_high_floor_is_blocked, test_corrupt_secrets_file_writes_secrets_to_config_json).
  • TestGetSchemaProperty.SCHEMA annotated as ClassVar (RUF012).

Declined, with reasons

  • Fix get_display_duration bool handling, save_config corrupt-secrets leak, render_skin_card counter reset, update_plugin semantic version comparison — these are the four suspected production bugs this PR deliberately characterizes without fixing (see PR description, "Related issues"). The tests pin current behavior with # SUSPECTED BUG markers precisely so the fixes can land as small, reviewable follow-up PRs where each behavior change is the headline, not a side effect of a test-infrastructure PR. Changing the tests now to assert the desired behavior would make this PR red against main.
  • Pin Pillow in the fixture's requirements.txt — Pillow is a core LEDMatrix dependency (root requirements.txt) so it's always present wherever the harness runs, and the harness loads plugins with install_deps=False. A pin would only invite a needless pip install; the file now documents this.
  • Type hints across all new test files — the existing test suite is unannotated and mypy.ini scopes checking to ^src/; annotating only the new files would be inconsistent churn for no checker benefit.

Generated by Claude Code

claude added 2 commits August 6, 2026 22:41
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
…n strikes on card success

Fixes the three suspected bugs this PR's characterization tests pinned,
flipping those tests to assert the corrected behavior:

- plugins/store: ONE shared update comparator. New
  compatibility.is_update_available() (PEP 440 via packaging) is now used
  by both the web UI's update badge (api_v3._is_plugin_update_available
  is a thin alias) and store_manager.update_plugin's reinstall decision.
  Previously update_plugin used raw string equality: 'v1.2.0' vs '1.2.0'
  triggered a full reinstall the UI called unnecessary, and a locally-
  ahead plugin (2.0.0 installed, registry 1.9.0) was silently DOWNGRADED.
  Now equivalent spellings skip the reinstall and locally-ahead versions
  are never downgraded; unparseable versions still reconcile by
  reinstalling from the registry.

- config: save_config and save_config_atomic now refuse (ConfigError)
  when config_secrets.json exists but cannot be loaded. Both previously
  proceeded without stripping, writing the merged secrets into
  config.json in plaintext. The shared _load_secrets_for_save() helper
  raises with an actionable message instead; a missing secrets file is
  still fine (nothing to strip), and _migrate_config's catch-all keeps
  boot resilient.

- skins: render_skin_card resets _skin_failures on both success paths
  (vegas card returned, or mode renderer handled), mirroring
  _render_game. Transient card failures no longer accumulate across a
  session until they permanently disable a working skin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
@ChuckBuilds ChuckBuilds changed the title Test suite overhaul: honest CI, real assertions, and coverage for the fragile paths Test suite overhaul + fixes for the three bugs it uncovered Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/config_manager.py (1)

288-312: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Catch expected load failures and log the traceback.

Line 304 catches all Exception values. This can hide an implementation defect as a secrets-file error. Catch the expected file and JSON parsing exceptions instead.

Line 311 logs an error without exc_info=True. Add the traceback and the [Config] context prefix for remote diagnosis.

Proposed fix
-        except Exception as e:
+        except (OSError, UnicodeDecodeError, json.JSONDecodeError, RecursionError) as e:
             error_msg = (
                 f"Refusing to save config: secrets file {self.secrets_path} exists "
                 f"but could not be loaded ({e}). Saving without it would write "
                 f"merged secret values into config.json in plaintext. Fix or "
                 f"remove the secrets file, then retry."
             )
-            self.logger.error(error_msg)
+            self.logger.error("[Config] %s", error_msg, exc_info=True)
             raise ConfigError(error_msg, config_path=self.secrets_path) from e
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config_manager.py` around lines 288 - 312, Update _load_secrets_for_save
to catch only the expected file-access and JSON-parsing exceptions instead of
all Exception values, allowing unexpected implementation errors to propagate.
Change the corresponding logger.error call to include the “[Config]” context
prefix and enable traceback logging with exc_info=True, while preserving the
existing ConfigError and secret-protection behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/plugin_system/compatibility.py`:
- Around line 199-215: Update is_update_available() to reject truthy non-string
installed_version or latest_version values immediately after the missing-value
check, returning True for these malformed inputs. Keep valid string comparison
unchanged and retain the existing InvalidVersion handling for unparseable
strings.

In `@src/plugin_system/store_manager.py`:
- Around line 2981-2988: Update the version comparison in the store manager’s
manifest-present path to call is_update_available(local_version, remote_version)
without requiring either version to be truthy, while preserving the
manifest-exists check so missing manifests still use _reinstall_with_rollback().
Add store integration cases covering an empty local version and an empty
registry version, confirming both follow the shared comparator result.

---

Nitpick comments:
In `@src/config_manager.py`:
- Around line 288-312: Update _load_secrets_for_save to catch only the expected
file-access and JSON-parsing exceptions instead of all Exception values,
allowing unexpected implementation errors to propagate. Change the corresponding
logger.error call to include the “[Config]” context prefix and enable traceback
logging with exc_info=True, while preserving the existing ConfigError and
secret-protection behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f439bb1-f2b8-4e9b-8aaa-b762ba40d4b5

📥 Commits

Reviewing files that changed from the base of the PR and between b8b0c8e and 206eca0.

📒 Files selected for processing (13)
  • .github/workflows/test.yml
  • src/base_classes/sports/core.py
  • src/config_manager.py
  • src/plugin_system/compatibility.py
  • src/plugin_system/store_manager.py
  • test/fixtures/plugins/ci-fixture-plugin/manager.py
  • test/fixtures/plugins/ci-fixture-plugin/requirements.txt
  • test/test_compatibility.py
  • test/test_config_manager_secrets.py
  • test/test_skin_system.py
  • test/test_version_comparison_consistency.py
  • test/web_interface/test_api_v3_helpers.py
  • web_interface/blueprints/api_v3.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • test/fixtures/plugins/ci-fixture-plugin/requirements.txt
  • .github/workflows/test.yml
  • test/fixtures/plugins/ci-fixture-plugin/manager.py
  • test/test_version_comparison_consistency.py
  • test/test_skin_system.py
  • test/test_compatibility.py
  • test/web_interface/test_api_v3_helpers.py

Comment thread src/plugin_system/compatibility.py
Comment thread src/plugin_system/store_manager.py
- is_update_available: reject truthy non-string versions (a malformed
  manifest can carry a number; packaging raises TypeError on those) by
  surfacing the mismatch instead of raising.
- store_manager.update_plugin: drop the truthiness gate around the
  comparator so a missing version on either side follows the shared
  'no update' verdict, keeping the store consistent with the UI badge;
  a missing manifest still uses the reinstall recovery path.
- config_manager._load_secrets_for_save: catch only expected read/parse
  failures (OSError/ValueError/RecursionError) so implementation bugs
  propagate as themselves, and log with traceback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
@ChuckBuilds
ChuckBuilds merged commit d6c5f97 into main Aug 7, 2026
17 checks passed
@ChuckBuilds
ChuckBuilds deleted the claude/ledmatrix-test-coverage-c8vkg8 branch August 7, 2026 14:17
ChuckBuilds added a commit that referenced this pull request Aug 7, 2026
…d coverage for every remaining untested module (#444)

* refactor(web): use canonical secret helpers in api_v3; make ConfigManager secret strip/merge array-aware

api_v3.py carried three inline nested copies of find_secret_fields/
separate_secrets (main-config save, plugin-config save, plugin-config
reset). They drifted from each other (one lacked isinstance guards) and
none supported the canonical module's array-item secrets
(accounts[].token). All three endpoints now import from
src/web_interface/secret_helpers.

Adopting the canonical behavior makes array-item secrets reachable, and
their parallel-placeholder shape ([{'token': ...}, {}] alongside the
regular list) was not survivable by ConfigManager's round-trip:
_strip_secrets_recursive dropped the whole key (losing the regular
fields from config.json) and _deep_merge replaced the regular list
wholesale on load. Both are now array-aware:

- strip removes the secret fields from each item and ALWAYS keeps the
  list so indices survive for merge-on-load; whole-key secrets (scalar
  lists, shape mismatches) still drop the key entirely — never leak.
- merge folds each secrets item into the config item at the same index,
  skipping {} placeholders. The regular list's length is authoritative
  in both directions: a user deleting an array item never has it
  resurrected from a stale secrets entry (extras warn and are ignored).

api_v3's own deep_merge intentionally still replaces lists wholesale —
form posts carry complete arrays and index-merging would resurrect
deleted items; a comment now documents that.

Tests: the parity guard flips from 'exactly 3 inline copies' to 'zero,
and the canonical import must exist'; TestArraySecretStripAndMerge
covers the new strip/merge semantics incl. length-mismatch contracts;
new test_api_v3_secret_roundtrip.py drives all three endpoints through
a Flask client with a REAL ConfigManager+SchemaManager over tmp_path,
proving secrets land in config_secrets.json, config.json stays clean,
and a fresh load merges them back into the right array items.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

* fix: repair broken helper paths across display, cache, odds, logging, resolver, repos, config, validator

Nine fixes for bugs surfaced while writing coverage for previously
untested modules (plus the bool-duration quirk pinned in PR #441):

- base_plugin.get_display_duration: exclude bools from both numeric
  branches — display_duration=True no longer reads as a 1-second slot;
  it falls through to config, then the 15.0 default.
- display_helper: draw_error_message/draw_no_data_message called
  _draw_centered_text with the wrong arguments and crashed with
  AttributeError — both now delegate to draw_centered_text.
  draw_scorebug_layout drew status and clock at the same y, overprinting
  each other — they now share one combined top line.
  draw_ticker_layout drew its text starting at x=display_width (fully
  off-canvas), returning a blank frame every time — now draws at x=0;
  scroll_speed stays accepted-but-unused and is documented as such.
- api_helper.clear_cache guarded on a nonexistent CacheManager.clear()
  method, silently never clearing anything; it now uses the real surface
  (clear_cache/delete/list_cache_files) and no-ops safely otherwise.
- base_odds_manager._extract_espn_data raised AttributeError when ESPN
  sent explicit JSON nulls ("homeTeamOdds": null) — every level now
  null-safes with 'or {}'. format_odds_summary gated on
  is_odds_available, which deliberately ignores money lines, so
  ML-only odds formatted as "No odds available" — it now gates only on
  empty/no_odds data and formats money lines.
- logging_config.ContextualFormatter mutated record.msg in place, so a
  second handler prepended the context prefix twice; it now formats a
  copy. log_error hardcoded exc_info=True and raised TypeError when the
  caller passed exc_info — now kwargs.setdefault.
- dynamic_team_resolver wrote its "shared" class cache through self,
  creating instance shadows — the cache was per-instance and every
  scoreboard refetched rankings. Writes now go through the class.
- saved_repositories cleaned URLs with an unanchored .replace('.git','')
  that mangled URLs merely containing '.git' (my.github.io -> myhub.io);
  now strips only a trailing suffix. add/remove also roll back the
  in-memory list when the save fails, so memory always matches disk.
- config_helper.merge_configs shallow-copied the base, aliasing every
  un-overridden nested dict into the result — now deep-copies.
- startup_validator.validate_all accumulated errors/warnings across
  calls — now resets both lists per run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

* test: cover the previously untested modules

Nine new suites plus an extension, asserting the Phase-1b fixed behavior
and pinning the quirks deliberately left alone:

- test_logging_config.py: formatters (JSON shape, no record mutation,
  single prefix through two handlers), PluginLoggerAdapter precedence,
  setup_logging handler hygiene and LEDMATRIX_DEBUG, log_error exc_info.
- test_startup_validator.py: exact messages, error-vs-warning split,
  accessor split (load_config vs get_config), cache-dir branches with
  os.access monkeypatched (root can write anything in CI), idempotence,
  raise_on_errors classification precedence.
- test_config_helper.py (full): load/save round trips, dot-notation
  get/set incl. silent-failure contract, post-fix no-aliasing merge,
  schema validation branches, the '{id}_config' key pin, default-enabled
  pin.
- test_saved_repositories.py: three load shapes, bare-list rewrite pin,
  trailing-only .git strip (my.github.io regression), save-failure
  rollback, type-classification case-sensitivity pin.
- test_api_helper.py: rate-limit math, cache-hit short circuit, ESPN
  URL/key formats, exact User-Agent guard, retry adapter, post-fix
  clear_cache against the real CacheManager surface, ttl-dropped pin.
- test_base_odds_manager.py: cache-key/URL construction, no_odds
  sentinel round trip, stale-cache fallback, null-safe extraction,
  ML-only formatting, is_odds_available truth table (ML-blind by
  contract), config key/attr mismatch pin.
- test_dynamic_team_resolver.py: expansion/dedup/slicing, dropped
  unknown-dynamic names (TOP_ substring hazard pinned), genuinely
  shared class cache (second instance: zero HTTP), TTL expiry,
  failure degradation without raising.
- test_display_helper.py (full): the fixed error/no-data renders,
  combined scorebug top line, non-blank ticker with scroll_speed
  no-op pin, composite upconversion, logo bleed positions, square
  orientation pin.
- test_skin_runtime_cache.py: discovery-cache hit/invalidation
  semantics (manifest mtime, .py edits pinned as non-invalidating),
  sys.modules namespacing contract incl. bare-name restore and stdlib
  shadowing, entry-module execute-once, API minor-version tolerance,
  skin_matches_target table.
- test_sports_capabilities.py (extended): _draw_celebration_layout
  executed for real (flash window, matrix-dims fallback, highlight
  alternation, logo-failure isolation), _should_celebrate_for direct,
  strict duration boundary, score_to_int edges, both-teams-score
  precedence, expired-coalesce refire, disabled-win baseline
  preservation, id-less prune.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

* test: real schedule/dim coverage for DisplayController; fix two vacuous schedule tests

New test_display_controller_schedule.py drives _check_schedule and
_check_dim_schedule on a bare controller stub: same-day and
midnight-crossing windows with inclusive boundaries, global vs per-day vs
legacy-inferred modes (and dim's global-only default — no legacy
inference), per-day disabled days, invalid %H:%M fallbacks, unknown
timezone -> UTC, dim_brightness default 30, inactive-display short
circuit, and the _was_display_active/_was_dimmed transition flags.

test_display_controller.py's test_schedule_disabled and
test_active_hours patched config_service.get_config — which
_check_schedule never reads — so both asserted the init-default value
and could not fail. Rewritten on the test_inactive_hours pattern
(inject controller.config['schedule'], reset the minute gate, flip the
flag to the opposite state first so the assertion has teeth).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

* ci: raise coverage floor to 48%

Measured 50% with the new suites in place (was 47% baseline when the
gate was introduced at 45); floor stays two points under measured.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

* fix: address CodeQL alert and review findings

- config_manager: the "secrets list longer than config list" warning now
  interpolates only config-side data (no key name or secrets-derived
  values), resolving the CodeQL clear-text-logging alert.
- base_plugin: validate_config rejects bool display_duration, matching
  get_display_duration (bool is an int subclass and would otherwise pass
  as a positive number).
- config_helper: merge_configs deep-copies override values in the
  non-recursive branch so mutating the merged result cannot reach back
  into override_config.
- saved_repositories: saves are atomic (temp file + fsync + os.replace),
  so a failed write can no longer truncate saved_repositories.json.
- tests: regression cases for each fix, plus a pin that whole-item
  array secrets (key[] + key[].field both marked) strip to empty {}
  skeletons — no secret values can reach config.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants