mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-07 11:48:06 +00:00
fix: unify version comparison, refuse secret-leaking saves, reset skin 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
This commit is contained in:
@@ -4,8 +4,9 @@ Tests for the ConfigManager secrets round-trip and the load_config fast path.
|
||||
The contract under test: config_secrets.json values are deep-merged INTO the
|
||||
in-memory config at load time, and stripped back OUT before anything is
|
||||
written to config.json — so secrets live in exactly one file on disk. This
|
||||
suite pins that round-trip plus its known sharp edges (some marked as
|
||||
SUSPECTED BUG and characterized rather than fixed).
|
||||
suite pins that round-trip plus its sharp edges, including the guard that a
|
||||
save REFUSES (ConfigError) when the secrets file exists but can't be loaded,
|
||||
rather than leaking merged secrets into config.json in plaintext.
|
||||
|
||||
Complements test_config_manager.py, which covers loading/migration/validation.
|
||||
"""
|
||||
@@ -16,6 +17,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from src.config_manager import ConfigManager
|
||||
from src.exceptions import ConfigError
|
||||
|
||||
|
||||
def make_manager(tmp_path, config=None, secrets=None):
|
||||
@@ -99,13 +101,11 @@ class TestSaveStripsSecrets:
|
||||
on_disk = json.loads((tmp_path / "config.json").read_text())
|
||||
assert on_disk == {"timezone": "UTC"}
|
||||
|
||||
def test_corrupt_secrets_file_writes_secrets_to_config_json(self, tmp_path):
|
||||
# SUSPECTED BUG (characterized, not fixed): when the secrets file is
|
||||
# corrupt (or otherwise unloadable) at save time, save_config proceeds without
|
||||
# stripping — writing the merged secrets into config.json in
|
||||
# plaintext. The code comments acknowledge the tradeoff (it prevents
|
||||
# data loss); this test pins the behavior so any future change to it
|
||||
# is deliberate.
|
||||
def test_corrupt_secrets_file_refuses_save_no_plaintext_leak(self, tmp_path):
|
||||
# Regression guard: when the secrets file exists but is corrupt at
|
||||
# save time, stripping is impossible — the save must raise instead of
|
||||
# writing the merged secrets into config.json in plaintext (the
|
||||
# historical behavior).
|
||||
manager = make_manager(
|
||||
tmp_path,
|
||||
config={"weather": {"city": "Austin"}},
|
||||
@@ -114,10 +114,28 @@ class TestSaveStripsSecrets:
|
||||
loaded = manager.load_config()
|
||||
(tmp_path / "config_secrets.json").write_text("{corrupt")
|
||||
|
||||
manager.save_config(loaded)
|
||||
with pytest.raises(ConfigError):
|
||||
manager.save_config(loaded)
|
||||
|
||||
# On-disk config untouched: no secret leaked.
|
||||
on_disk = json.loads((tmp_path / "config.json").read_text())
|
||||
assert "api_key" not in on_disk.get("weather", {})
|
||||
|
||||
def test_corrupt_secrets_file_refuses_atomic_save_too(self, tmp_path):
|
||||
# Same refusal on the atomic save path, which shared the leak.
|
||||
manager = make_manager(
|
||||
tmp_path,
|
||||
config={"weather": {"city": "Austin"}},
|
||||
secrets={"weather": {"api_key": "s3cret"}},
|
||||
)
|
||||
loaded = manager.load_config()
|
||||
(tmp_path / "config_secrets.json").write_text("{corrupt")
|
||||
|
||||
with pytest.raises(ConfigError):
|
||||
manager.save_config_atomic(loaded)
|
||||
|
||||
on_disk = json.loads((tmp_path / "config.json").read_text())
|
||||
assert on_disk["weather"].get("api_key") == "s3cret" # leaked
|
||||
assert "api_key" not in on_disk.get("weather", {})
|
||||
|
||||
|
||||
class TestLoadFastPath:
|
||||
|
||||
Reference in New Issue
Block a user