Skip to content

Follow-ups from #441: secret-helper migration, ten more bug fixes, and coverage for every remaining untested module - #444

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

Follow-ups from #441: secret-helper migration, ten more bug fixes, and coverage for every remaining untested module#444
ChuckBuilds merged 6 commits into
mainfrom
claude/ledmatrix-test-coverage-c8vkg8

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Pull Request

Summary

Follow-up to #441, delivering its deferred work list — and fixing what that work uncovered. Writing coverage for the ten remaining untested modules surfaced ten more live bugs (two crashing methods, an always-blank ticker render, a silent no-op cache clear, an AttributeError on ESPN nulls, a double-prefixing log formatter, a broken "shared" rankings cache, a URL-mangling cleaner, a config-merge aliasing leak, and a non-idempotent validator). All are fixed here, with tests asserting the corrected behavior. The PR also completes the api_v3 secret-logic migration onto the canonical secret_helpers module — which required making ConfigManager's secret strip/merge round-trip array-aware, since the canonical array-item secret format was not survivable by the old code — plus the get_display_duration bool fix, real schedule/dim coverage for DisplayController, and a coverage ratchet to 48%.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor (no functional change) — the api_v3 inline-copy removal
  • Build / CI
  • Plugin work (link to the plugin)

Related issues

Follow-up work list from #441. Bugs fixed (each was pinned by a new test before the fix, then the test flipped):

  1. api_v3 secret handling unified — the three drifted inline copies of find_secret_fields/separate_secrets now import from src/web_interface/secret_helpers; ConfigManager._strip_secrets_recursive/_deep_merge are array-aware so accounts[].token-style secrets round-trip (strip keeps list indices; merge realigns by index; the regular list's length is authoritative in both directions, so deleted items are never resurrected).
  2. get_display_duration no longer reads True as a 1-second slot (bool excluded from both numeric branches).
  3. display_helper: draw_error_message/draw_no_data_message crashed with AttributeError; the scorebug overprinted status and clock at the same y; draw_ticker_layout drew fully off-canvas (every frame blank).
  4. api_helper.clear_cache guarded on a method CacheManager doesn't have — it never cleared anything; now uses the real surface.
  5. base_odds_manager: explicit ESPN nulls ("homeTeamOdds": null) raised AttributeError; moneyline-only odds formatted as "No odds available".
  6. logging_config: ContextualFormatter mutated the record, double-prefixing with two handlers; log_error(exc_info=...) raised TypeError.
  7. dynamic_team_resolver: the "shared" class cache was instance-shadowed — every scoreboard refetched AP rankings; writes now go through the class.
  8. saved_repositories: unanchored .replace('.git','') mangled URLs (e.g. my.github.io); failed saves left phantom in-memory entries.
  9. config_helper.merge_configs aliased un-overridden nested dicts into its result.
  10. startup_validator.validate_all accumulated errors across calls.

Test plan

  • Ran on a real Raspberry Pi with hardware
  • Ran in emulator mode (EMULATOR=true python3 run.py) — via the plugin-safety harness (fixture plugin at all 8 panel sizes vs goldens): 65 passed
  • Ran the dev preview server (scripts/dev_server.py)
  • Ran the test suite (pytest) — full CI mirror: 2,574 passed, 0 failed (pytest -m "not hardware" test/ --ignore=test/plugins --cov=src --cov=web_interface), coverage 50%. ~330 new tests across 12 new/extended suites, including endpoint-level secret round-trips through a real ConfigManager+SchemaManager on tmp files.
  • 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 — SavedRepositoriesManager._clean_url, ConfigManager._is_parallel_secrets_list, and every new test module documents the contract it pins
  • 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

Plugin-facing changes are strictly enabling: array-item secrets (x-secret inside array items) now actually work end to end; display_duration: true in a hand-edited config no longer produces 1-second slots; the shared rankings cache stops per-scoreboard refetching.

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 — N/A, no config keys added

Notes for reviewer

  • Commit order tells the story: (1) secret migration + array-aware round-trip, (2) the ten small fixes, (3) the new module suites, (4) schedule/dim coverage + two more vacuous tests repaired, (5) ratchet 45→48.
  • Deliberately pinned, not changed: api_v3's deep_merge still replaces lists wholesale (form posts are complete arrays — index-merging would resurrect deleted items; now documented in-code); is_odds_available stays moneyline-blind (its render-gating contract) while format_odds_summary now formats ML-only data; config_helper's {id}_config key convention and default-enabled-True divergence.
  • The parity guard flipped: test_secret_separation_parity.py now asserts zero inline copies and that the canonical import exists.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved resilience when sports odds contain missing or partial data, including money-line-only results.
    • Fixed score and ticker display rendering to prevent overlapping or off-screen text.
    • Prevented repeated validation messages and corrected schedule-related display behavior.
    • Improved repository URL handling and rollback when saving changes fails.
    • Prevented boolean duration values from being treated as valid timings.
  • Improvements

    • Enhanced secret handling for arrays and nested configuration items while preserving placeholders.
    • Improved cache clearing compatibility and shared ranking-cache behavior.
    • Prevented configuration merges and log formatting from unintentionally modifying source data.
    • Increased automated test coverage requirements.

claude added 5 commits August 7, 2026 17:33
…ager 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
… 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
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
…us 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
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
@coderabbitai

coderabbitai Bot commented Aug 7, 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: 16 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: a3aba9d9-5ae6-4dd1-a89c-933d21f6cf8e

📥 Commits

Reviewing files that changed from the base of the PR and between c9239d0 and 41f91a4.

📒 Files selected for processing (8)
  • src/common/config_helper.py
  • src/config_manager.py
  • src/plugin_system/base_plugin.py
  • src/plugin_system/saved_repositories.py
  • test/test_base_plugin_duration.py
  • test/test_config_helper.py
  • test/test_config_manager_secrets.py
  • test/test_saved_repositories.py
📝 Walkthrough

Walkthrough

This PR expands regression coverage and corrects behavior for secret arrays, cache clearing, configuration merging, rendering, odds extraction, plugin state, repository persistence, logging, validation, schedules, skin loading, and sports celebrations.

Changes

Behavior corrections and regression coverage

Layer / File(s) Summary
Shared secret handling and round trips
src/config_manager.py, web_interface/blueprints/api_v3.py, test/test_config_manager_secrets.py, test/web_interface/*
Secret helpers now support nested array items, parallel secret lists, and complete API round trips.
Cache, configuration, logging, and validation utilities
src/common/*, src/startup_validator.py, test/test_api_helper.py, test/test_config_helper.py, test/test_logging_config.py, test/test_startup_validator.py
Cache-manager compatibility, deep-copy merging, log-record copying, exception overrides, and validation reset behavior are covered.
Odds and display rendering corrections
src/base_odds_manager.py, src/common/display_helper.py, test/test_base_odds_manager.py, test/test_display_helper.py
Odds extraction handles null values and money-line-only data. Scorebug, ticker, and message rendering return corrected images.
Plugin duration, repository, and ranking state
src/plugin_system/*, src/dynamic_team_resolver.py, related tests
Boolean durations are rejected, repository mutations roll back after save failures, URLs are normalized, and ranking caches are shared across resolver instances.
Runtime, schedule, celebration, and coverage validation
test/test_display_controller*.py, test/test_skin_runtime_cache.py, test/test_sports_capabilities.py, .github/workflows/test.yml
Tests cover schedule transitions, skin runtime behavior, celebration edge cases, and the workflow now requires 48% coverage.

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 10.73% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the secret-helper migration, bug fixes, and expanded test coverage described in the pull request.
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.
✨ 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 7, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 35 complexity · 0 duplication

Metric Results
Complexity 35
Duplication 0

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.

Comment thread src/config_manager.py Fixed

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web_interface/blueprints/api_v3.py (1)

5349-5367: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Orphaned array-item secrets stay in config_secrets.json after the user removes items.

separate_secrets omits the accounts key from the secrets output when no item carries a secret value, for example after the user clears the array. deep_merge then leaves the previous parallel list in current_secrets[plugin_id]. ConfigManager._deep_merge later ignores the extra entries and logs a warning, so no item is resurrected, but the secret values remain on disk indefinitely.

Consider deleting the stored secrets entry for a key when the newly separated config contains that key with no secrets.

🤖 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 `@web_interface/blueprints/api_v3.py` around lines 5349 - 5367, Update the
secret merge flow around separate_secrets and current_secrets so keys present in
the newly separated configuration but containing no secrets remove the
corresponding stored entry before or instead of deep_merge. Ensure cleared
array-item secrets, such as accounts, are deleted from
current_secrets[plugin_id] while preserving unrelated secret keys and existing
merge behavior for keys that still contain values.
🧹 Nitpick comments (5)
test/test_skin_runtime_cache.py (1)

52-60: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix the implicit Optional annotations flagged by Ruff (RUF013).

body, extra_files, entry_point, manifest_id, and manifest_extra are annotated as non-optional but default to None. Ruff reports RUF013 on lines 55-59. Use explicit optional types.

🔧 Proposed fix
-              body: str = None,
-              extra_files: dict = None,
-              entry_point: str = None,
-              manifest_id: str = None,
-              manifest_extra: dict = None,
+              body: Optional[str] = None,
+              extra_files: Optional[dict] = None,
+              entry_point: Optional[str] = None,
+              manifest_id: Optional[str] = None,
+              manifest_extra: Optional[dict] = None,

Add the import:

from typing import Optional
🤖 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_skin_runtime_cache.py` around lines 52 - 60, Update the make_skin
parameters body, extra_files, entry_point, manifest_id, and manifest_extra to
use explicit Optional annotations, adding the typing.Optional import if needed,
while preserving their existing None defaults and behavior.

Source: Linters/SAST tools

test/test_sports_capabilities.py (1)

1021-1034: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Encode the expected value for "-4" in the table instead of a branch in the test body.

The table declares expected as None for "-4", but the body overrides it and asserts -4. The declared value is dead data and contradicts the assertion. The trailing comment on line 1026 also leaves an open question in the test source.

♻️ Proposed cleanup
     `@pytest.mark.parametrize`("value,expected", [
         ({"value": None}, None),      # int(float(None)) TypeError -> caught
         ({"value": "abc"}, None),
         ({"other": 1}, 0),            # neither key -> default 0
         ([3], None),                  # list -> TypeError -> caught
-        ("-4", None),                 # regex fallback finds digits -> 4? No:
+        ("-4", -4),                   # int(float("-4")) parses directly
     ])
     def test_score_to_int_edges(self, value, expected):
-        result = CelebrationMixin._score_to_int(value)
-        if value == "-4":
-            # int(float("-4")) parses directly: -4.
-            assert result == -4
-        else:
-            assert result == expected
+        assert CelebrationMixin._score_to_int(value) == expected
🤖 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_sports_capabilities.py` around lines 1021 - 1034, Update
test_score_to_int_edges so the "-4" parameter row declares expected as -4,
remove the special if value == "-4" branch, and assert result against expected
uniformly. Clean up the trailing comment on that row so the table no longer
contains contradictory or unresolved information.
test/test_display_controller_schedule.py (1)

56-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused clock fixture and the unused check_at parameter.

No test requests the clock fixture. check_at also accepts clock=None and never uses it. Both helpers start and stop their own patcher through at(). Dead scaffolding in a new file invites incorrect use later.

♻️ Proposed cleanup
-@pytest.fixture
-def clock():
-    patchers = []
-
-    def _at(time_str, day="monday"):
-        patchers.append(p := at(time_str, day))
-        return p
-
-    yield _at
-    for p in patchers:
-        p.stop()
-
-
-def check_at(dc, time_str, day="monday", clock=None):
+def check_at(dc, time_str, day="monday"):
     """Run _check_schedule at a mocked wall time, resetting the minute gate."""
🤖 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_display_controller_schedule.py` around lines 56 - 77, Remove the
unused clock fixture and delete the unused clock parameter from check_at,
keeping check_at’s existing at() patching and schedule-reset behavior unchanged.
test/test_logging_config.py (1)

132-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the capture handlers after each test.

_capture attaches a handler to a named logger and changes its level, but never restores either. The conftest reset_logging fixture restores the root logger only, so these handlers stay attached to test.adapter* and test.lwc* for the rest of the session. TestLogWithContext._capture at Line 231 has the same problem.

Use a fixture or try/finally to call logger.removeHandler(handler) and restore the previous level.

🤖 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_logging_config.py` around lines 132 - 138, Update both _capture
helpers, including TestLogWithContext._capture, to clean up the attached handler
and restore the logger’s previous level after each test. Use a fixture or
try/finally so cleanup runs even when assertions fail, and preserve each
logger’s original configuration.
test/web_interface/test_api_v3_secret_roundtrip.py (1)

84-122: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the api_v3 blueprint attributes after each test.

The fixture assigns manager attributes directly on the shared api_v3 blueprint object and never restores them. The objects persist after tmp_path is removed. Other test modules that rely on the blueprint state, or that run after this module, can then observe a real ConfigManager pointing at a deleted directory. The failure depends on test order, which makes it hard to diagnose.

Convert the fixture to a yield fixture and restore the previous values.

♻️ Proposed teardown
+    _saved = {name: getattr(api_v3, name, None) for name in (
+        "config_manager", "schema_manager", "plugin_manager",
+        "plugin_store_manager", "saved_repositories_manager",
+        "operation_queue", "plugin_state_manager", "operation_history",
+        "cache_manager")}
     api_v3.config_manager = config_manager
@@
     e.fresh_load = fresh_load
-    return e
+    yield e
+    for name, value in _saved.items():
+        setattr(api_v3, name, value)
🤖 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_secret_roundtrip.py` around lines 84 - 122,
Convert the fixture that configures the shared api_v3 blueprint into a
yield-based fixture, saving each blueprint manager attribute’s original value
before assignment and restoring all of them after yield. Ensure teardown
restores the complete set of attributes modified in the fixture, including
config_manager, schema_manager, plugin_manager, plugin_store_manager,
saved_repositories_manager, operation_queue, plugin_state_manager,
operation_history, and cache_manager.
🤖 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/common/config_helper.py`:
- Around line 164-169: Update merge_configs in
src/common/config_helper.py:164-169 to deep-copy value in the non-recursive
override branch, preserving full independence from override_config. Add a
regression test in test/test_config_helper.py:126-140 that mutates an overridden
nested list or dictionary and verifies override_config remains unchanged.

In `@src/config_manager.py`:
- Around line 296-312: Update _is_parallel_secrets_list and its use in
_strip_secrets_recursive to recognize parallel lists only when each secret item
is a placeholder (known-empty dicts or dictionaries whose values are empty
placeholders such as {"id": ""}); do not classify nested secret-field data as
parallel. For non-placeholder lists, preserve the whole-key secret behavior so
the key is dropped and cannot be merged back by _deep_merge.

In `@src/plugin_system/base_plugin.py`:
- Around line 408-410: Update the numeric validation in validate_config() to
exclude bool values alongside the existing int/float type check, matching
get_display_duration() behavior. Ensure boolean durations are rejected before
display_duration is read while valid numeric durations remain accepted.

In `@src/plugin_system/saved_repositories.py`:
- Around line 110-114: Make repository persistence atomic in
saved_repositories.py: at lines 110-114, retain the prior repositories list
until _save_repositories() completes successfully, and at lines 134-137, retain
the removed entry until the save succeeds; update _save_repositories() to write
and fsync a temporary file in the target directory, then atomically replace
config_path, ensuring failed writes cannot truncate or partially overwrite the
existing file.

---

Outside diff comments:
In `@web_interface/blueprints/api_v3.py`:
- Around line 5349-5367: Update the secret merge flow around separate_secrets
and current_secrets so keys present in the newly separated configuration but
containing no secrets remove the corresponding stored entry before or instead of
deep_merge. Ensure cleared array-item secrets, such as accounts, are deleted
from current_secrets[plugin_id] while preserving unrelated secret keys and
existing merge behavior for keys that still contain values.

---

Nitpick comments:
In `@test/test_display_controller_schedule.py`:
- Around line 56-77: Remove the unused clock fixture and delete the unused clock
parameter from check_at, keeping check_at’s existing at() patching and
schedule-reset behavior unchanged.

In `@test/test_logging_config.py`:
- Around line 132-138: Update both _capture helpers, including
TestLogWithContext._capture, to clean up the attached handler and restore the
logger’s previous level after each test. Use a fixture or try/finally so cleanup
runs even when assertions fail, and preserve each logger’s original
configuration.

In `@test/test_skin_runtime_cache.py`:
- Around line 52-60: Update the make_skin parameters body, extra_files,
entry_point, manifest_id, and manifest_extra to use explicit Optional
annotations, adding the typing.Optional import if needed, while preserving their
existing None defaults and behavior.

In `@test/test_sports_capabilities.py`:
- Around line 1021-1034: Update test_score_to_int_edges so the "-4" parameter
row declares expected as -4, remove the special if value == "-4" branch, and
assert result against expected uniformly. Clean up the trailing comment on that
row so the table no longer contains contradictory or unresolved information.

In `@test/web_interface/test_api_v3_secret_roundtrip.py`:
- Around line 84-122: Convert the fixture that configures the shared api_v3
blueprint into a yield-based fixture, saving each blueprint manager attribute’s
original value before assignment and restoring all of them after yield. Ensure
teardown restores the complete set of attributes modified in the fixture,
including config_manager, schema_manager, plugin_manager, plugin_store_manager,
saved_repositories_manager, operation_queue, plugin_state_manager,
operation_history, and cache_manager.
🪄 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: ef111d06-130e-447d-834a-bcce50fbc893

📥 Commits

Reviewing files that changed from the base of the PR and between fc25a70 and c9239d0.

📒 Files selected for processing (28)
  • .github/workflows/test.yml
  • src/base_odds_manager.py
  • src/common/api_helper.py
  • src/common/config_helper.py
  • src/common/display_helper.py
  • src/config_manager.py
  • src/dynamic_team_resolver.py
  • src/logging_config.py
  • src/plugin_system/base_plugin.py
  • src/plugin_system/saved_repositories.py
  • src/startup_validator.py
  • test/test_api_helper.py
  • test/test_base_odds_manager.py
  • test/test_base_plugin_duration.py
  • test/test_config_helper.py
  • test/test_config_manager_secrets.py
  • test/test_display_controller.py
  • test/test_display_controller_schedule.py
  • test/test_display_helper.py
  • test/test_dynamic_team_resolver.py
  • test/test_logging_config.py
  • test/test_saved_repositories.py
  • test/test_skin_runtime_cache.py
  • test/test_sports_capabilities.py
  • test/test_startup_validator.py
  • test/web_interface/test_api_v3_secret_roundtrip.py
  • test/web_interface/test_secret_separation_parity.py
  • web_interface/blueprints/api_v3.py

Comment thread src/common/config_helper.py
Comment thread src/config_manager.py
Comment thread src/plugin_system/base_plugin.py
Comment thread src/plugin_system/saved_repositories.py
- 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
@ChuckBuilds
ChuckBuilds merged commit ee59caa into main Aug 7, 2026
27 checks passed
@ChuckBuilds
ChuckBuilds deleted the claude/ledmatrix-test-coverage-c8vkg8 branch August 7, 2026 20:17
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.

3 participants