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
+10 -1
View File
@@ -94,9 +94,16 @@ class TestStaleFindingsAreDroppedByTheEndpoint:
def test_a_finding_that_still_holds_is_kept(self, client):
c = client({"done": True, "unresolved": [
{"plugin_id": "ghost-plugin", "type": ON_DISK}]})
{"plugin_id": "ghost-plugin", "type": ON_DISK}]},
config={"ghost-plugin": {"enabled": True}})
assert [e["plugin_id"] for e in _get(c)["unresolved"]] == ["ghost-plugin"]
def test_a_plugin_removed_from_config_is_no_longer_reported(self, client):
c = client({"done": True, "unresolved": [
{"plugin_id": "ghost-plugin", "type": ON_DISK}]},
config={"other-plugin": {"enabled": True}})
assert _get(c)["unresolved"] == []
def test_the_reported_device_verdict_clears(self, client):
"""The five findings the live device served, against its real state."""
installed = ["football-scoreboard", "ledmatrix-weather",
@@ -157,11 +164,13 @@ class TestTheFilterUsesTheReconcilersOwnRules:
# finding on the strength of something nothing can read.
c = client({"done": True, "unresolved": [
{"plugin_id": "broken-plugin", "type": ON_DISK}]},
config={"broken-plugin": {"enabled": True}},
corrupt=["broken-plugin"])
assert [e["plugin_id"] for e in _get(c)["unresolved"]] == ["broken-plugin"]
def test_a_standalone_backup_dir_does_not_count_as_installed(self, client):
c = client({"done": True, "unresolved": [
{"plugin_id": "weather.standalone-backup-20260101", "type": ON_DISK}]},
config={"weather.standalone-backup-20260101": {"enabled": True}},
installed=["weather.standalone-backup-20260101"])
assert len(_get(c)["unresolved"]) == 1