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