fix(web): auto_update and other core settings are not orphaned plugins (#589)

v3.4.0 shows "Plugin Config Warning - In config but not installed:
auto_update. Reinstall via the Plugin Store, or remove these entries from
config.json." auto_update is the core weekly-update setting from #581.
Reconciliation treated every top-level dict not in its private
_SYSTEM_CONFIG_KEYS list as a plugin id, and #581 could not know to extend
that list.

- Move core top-level keys into src/core_config_keys.py (CORE_CONFIG_KEYS)
  and use it in reconciliation. Tests fail if a config.template.json key or
  a key written by the general-settings save is missing from it.
- A secrets-file key only counts as a non-plugin when no installed plugin
  has that id. Plugin secrets are namespaced by id, so installed plugins
  with secrets were reported as missing from config on every run.
- still_unresolved() drops "not on disk" findings whose id is no longer a
  plugin entry in config, so a stored verdict clears without a restart.
- A plugin whose id is a core key is skipped with a warning, and the fix
  never writes a plugin stub over or in place of a core setting.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-15 18:19:39 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent c200b5837d
commit 9616a5a054
7 changed files with 392 additions and 41 deletions
+11 -6
View File
@@ -142,8 +142,14 @@ class TestStaleFindingsAreDropped:
assert still_unresolved(entries, set(), {"odds-ticker"}) == []
def test_genuinely_missing_on_disk_is_kept(self):
entries = [{"plugin_id": "data", "type": self.ON_DISK}]
assert still_unresolved(entries, set(), {"odds-ticker"}) == entries
entries = [{"plugin_id": "ghost-plugin", "type": self.ON_DISK}]
assert still_unresolved(entries, {"ghost-plugin"}, {"odds-ticker"}) == entries
def test_missing_on_disk_no_longer_in_config_is_dropped(self):
# Removed from config.json (or never a plugin entry at all, like a
# core key an older build misread): nothing left to reinstall.
entries = [{"plugin_id": "ghost-plugin", "type": self.ON_DISK}]
assert still_unresolved(entries, set(), {"odds-ticker"}) == []
def test_unrecheckable_kinds_are_kept(self):
# Filtering must only ever remove what it can prove stale.
@@ -164,7 +170,6 @@ class TestStaleFindingsAreDropped:
live = still_unresolved(entries, installed, installed)
# All four "installed but not in config" findings were false and clear.
# 'data' legitimately is not installed, so this filter keeps it; it stops
# being reported because _get_config_state() no longer invents it from the
# secrets file, which takes effect on the next reconciliation run.
assert [e["plugin_id"] for e in live] == ["data"]
# 'data' is not a plugin entry in config (it came from the secrets
# file), so its "not on disk" finding is provably stale and clears too.
assert live == []