mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-18 04:49:13 +00:00
Merge main into the api_v3 split, carrying the three fixes it would have dropped
main modified web_interface/blueprints/api_v3.py while this branch deletes it, so git reports a modify/delete conflict. Resolving that by accepting the deletion - the obvious move - would silently discard every api_v3.py change merged since this branch forked. Three were: #560 the sudo diagnosis on /system/action -> api_v3/system.py #557 the stale-reconciliation-findings filter -> api_v3/plugins.py #550 the deletion of dead _validate_time_range -> api_v3/__init__.py All three were verified absent from the split beforehand: _sudo_hint_for, _SUDO_NEEDS_PASSWORD and _drop_stale_reconciliation_findings appeared in zero files, while _validate_time_range was still sitting at __init__.py:343 after #550 removed it from main. Each is now ported to the module that owns the affected route, and the dead function is gone. Two further changes this surfaced: - api_v3/__init__.py imported fcntl at module scope. fcntl is POSIX-only, the monolithic blueprint never imported it, and on this branch it made the whole api_v3 package unimportable off POSIX: the pristine branch cannot run its test suite on Windows at all - 14 collection errors interrupt the run. The import now happens inside _starlark_manifest_lock(), the only thing that uses it. Deliberately NOT degraded to a no-op lock off POSIX; that function's docstring describes a real lost-update race, and silently not locking would be worse than failing loudly on a platform that cannot drive the display. - test/test_reconciliation_status_endpoint.py is new. #557's endpoint wiring had no test - only the pure still_unresolved() helper in src/ was covered, and that lives outside web_interface so it survived the split untouched. Losing the filter during this merge would therefore have been completely silent. Removing the filter call now fails three of its seven cases. Suite on this merge: 115 failed / 4317 passed. main today: 105 / 4252. All 11 extra failures are in test_starlark_pixlet_routes.py and need POSIX file locks, so they are Windows-only; on that same file the pristine branch shows 19 failed plus 39 errors, so this is a large net improvement rather than a regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""The reconciliation-status endpoint must re-check its stored verdict.
|
||||
|
||||
#557 added _drop_stale_reconciliation_findings() so a resolved condition stops
|
||||
being reported: the verdict is a snapshot written once per run, and a run that
|
||||
fails to apply a fix also declines to retry, so a device whose plugins were all
|
||||
present in config kept being told for hours that four of them were missing.
|
||||
|
||||
That wiring had no test. Only the pure still_unresolved() helper in
|
||||
src/plugin_system/ was covered, which lives outside web_interface and therefore
|
||||
survived the api_v3 blueprint split untouched -- so when #553 moved this
|
||||
endpoint into api_v3/plugins.py, losing the filter would have been completely
|
||||
silent. This test exists so that cannot happen again.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from web_interface.blueprints.api_v3 import api_v3 # noqa: E402
|
||||
|
||||
IN_CONFIG = "plugin_missing_in_config"
|
||||
ON_DISK = "plugin_missing_on_disk"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path, monkeypatch):
|
||||
"""App whose status file, config and plugins dir are all under our control."""
|
||||
monkeypatch.setattr(tempfile, "gettempdir", lambda: str(tmp_path))
|
||||
|
||||
plugins_dir = tmp_path / "plugin-repos"
|
||||
plugins_dir.mkdir()
|
||||
|
||||
def _install(plugin_id):
|
||||
d = plugins_dir / plugin_id
|
||||
d.mkdir()
|
||||
(d / "manifest.json").write_text(json.dumps({"id": plugin_id}), encoding="utf-8")
|
||||
|
||||
def _setup(verdict, config=None, installed=()):
|
||||
(tmp_path / "ledmatrix_reconciliation.json").write_text(
|
||||
json.dumps(verdict), encoding="utf-8")
|
||||
for pid in installed:
|
||||
_install(pid)
|
||||
api_v3.config_manager = MagicMock()
|
||||
api_v3.config_manager.load_config.return_value = dict(config or {})
|
||||
api_v3.plugin_manager = MagicMock()
|
||||
api_v3.plugin_manager.plugins_dir = str(plugins_dir)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(api_v3, url_prefix="/api/v3")
|
||||
return app.test_client()
|
||||
|
||||
return _setup
|
||||
|
||||
|
||||
def _get(c):
|
||||
return c.get("/api/v3/plugins/reconciliation-status").get_json()["data"]
|
||||
|
||||
|
||||
class TestStaleFindingsAreDroppedByTheEndpoint:
|
||||
def test_a_plugin_now_in_config_is_no_longer_reported(self, client):
|
||||
c = client({"done": True, "unresolved": [
|
||||
{"plugin_id": "football-scoreboard", "type": IN_CONFIG}]},
|
||||
config={"football-scoreboard": {"enabled": True}})
|
||||
# The regression: this kept being reported for hours after it resolved.
|
||||
assert _get(c)["unresolved"] == []
|
||||
|
||||
def test_a_plugin_now_installed_is_no_longer_reported(self, client):
|
||||
c = client({"done": True, "unresolved": [
|
||||
{"plugin_id": "odds-ticker", "type": ON_DISK}]},
|
||||
installed=["odds-ticker"])
|
||||
assert _get(c)["unresolved"] == []
|
||||
|
||||
def test_a_finding_that_still_holds_is_kept(self, client):
|
||||
c = client({"done": True, "unresolved": [
|
||||
{"plugin_id": "ghost-plugin", "type": ON_DISK}]})
|
||||
assert [e["plugin_id"] for e in _get(c)["unresolved"]] == ["ghost-plugin"]
|
||||
|
||||
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",
|
||||
"odds-ticker", "starlark-apps"]
|
||||
c = client({"done": True, "unresolved": [
|
||||
{"plugin_id": p, "type": IN_CONFIG} for p in installed]},
|
||||
config={p: {"enabled": True} for p in installed},
|
||||
installed=installed)
|
||||
assert _get(c)["unresolved"] == []
|
||||
|
||||
|
||||
class TestTheEndpointStaysRobust:
|
||||
def test_an_empty_verdict_is_passed_through(self, client):
|
||||
c = client({"done": True, "unresolved": []})
|
||||
assert _get(c) == {"done": True, "unresolved": []}
|
||||
|
||||
def test_a_broken_config_manager_leaves_findings_untouched(self, client):
|
||||
"""Best-effort: a stale warning beats a failed endpoint."""
|
||||
c = client({"done": True, "unresolved": [
|
||||
{"plugin_id": "football-scoreboard", "type": IN_CONFIG}]})
|
||||
api_v3.config_manager.load_config.side_effect = OSError("config unreadable")
|
||||
body = _get(c)
|
||||
assert [e["plugin_id"] for e in body["unresolved"]] == ["football-scoreboard"]
|
||||
|
||||
def test_a_run_still_in_progress_is_reported_as_such(self, client):
|
||||
c = client({"done": False, "unresolved": []})
|
||||
assert _get(c)["done"] is False
|
||||
Reference in New Issue
Block a user