fix(api-v3): re-check reconciliation findings by the reconciler's own rules

Both CodeRabbit findings on the merge commit, verified against the code first.

Major, plugins.py: the stale-findings filter derived its own notion of "in
config" and "on disk", and both were looser than the reconciliation module's.
set(load_config()) also contains system keys, the secrets-file keys load_config()
merges in, and non-dict values; and any directory holding a manifest.json
counted as installed even when that manifest does not parse. Either looseness
clears a finding that is still true -- and a secrets key read as a plugin is the
precise bug the filter exists to stop reporting, so reintroducing that asymmetry
while re-checking was the wrong way round.

The two extractions now live in state_reconciliation.py as config_plugin_ids()
and disk_plugin_ids(), with ignored_config_keys() and secrets_top_level_keys()
alongside. _get_config_state() and _get_disk_state() use them too, so there is
one definition rather than two that can drift. _get_disk_state() re-reads each
manifest for version/name after taking membership from the shared extractor;
that costs one extra small read per plugin on a path that runs once per boot.

Minor, the new test: the fixture assigned api_v3.config_manager and
api_v3.plugin_manager directly. Those live on a module-level blueprint
singleton, so the mocks leaked into every later test that imports api_v3 --
pointing at a tmp_path already deleted. Both now go through monkeypatch.setattr,
which restores them. This is the same pollution class that made an earlier test
in this session break seven unrelated ones, so it is worth getting right.

Five cases added for the parity itself: a secrets key, a system key and a
non-dict value must not clear an "installed but missing from config" finding,
and neither an unparseable manifest nor a .standalone-backup- directory may
count as installed. All five fail against the looser version.

Linux CI on the preceding commit: Core unit tests, plugin harness, CodeQL and
CodeRabbit all pass. Codacy reads action_required on every commit of this
branch including the first, so it is pre-existing and not from this work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-11 09:59:33 -04:00
co-authored by Claude Opus 5
parent 05cb9dc31f
commit a6039f11ef
3 changed files with 158 additions and 50 deletions
+15 -11
View File
@@ -715,21 +715,25 @@ def _drop_stale_reconciliation_findings(unresolved):
indefinitely. Best-effort: any failure here returns the list untouched,
because showing a stale warning beats failing the endpoint.
The disk set is enumerated directly rather than through
_installed_plugin_ids(): this mirrors StateReconciliation._get_disk_state(),
whose verdict we are re-checking, and avoids triggering a discovery scan on
an endpoint the overview page polls.
Both sets come from the reconciliation module's own extractors rather than
being re-derived here. That matters for correctness, not tidiness: a plain
set(load_config()) also contains system keys, the secrets-file keys merged
in by load_config(), and non-dict values, and any directory holding a
manifest.json would count as installed even if that manifest does not
parse. Either looseness clears findings that are still true -- and a secrets
key read as a plugin is the very bug the filter exists to stop reporting.
"""
try:
from src.plugin_system.state_reconciliation import still_unresolved
from src.plugin_system.state_reconciliation import (
config_plugin_ids, disk_plugin_ids, ignored_config_keys,
still_unresolved,
)
config_keys = set(api_v3.config_manager.load_config() or {})
installed = set()
cm = api_v3.config_manager
config_keys = config_plugin_ids(cm.load_config() or {},
ignored_config_keys(cm))
plugins_dir = getattr(api_v3.plugin_manager, 'plugins_dir', None)
if plugins_dir:
for entry in Path(plugins_dir).iterdir():
if entry.is_dir() and (entry / 'manifest.json').exists():
installed.add(entry.name)
installed = disk_plugin_ids(plugins_dir) if plugins_dir else set()
return still_unresolved(unresolved, config_keys, installed)
except Exception:
logger.debug("[Reconciliation] Could not re-check stored findings", exc_info=True)