- 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
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
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
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
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