refactor: daemon collectors, thin webui proxies, pure config reads
- move state collectors from lib/state.py to daemon/collectors/ (7 modules, registration side-effect; daemon/server.py imports the package before the first populate()) - webui/api: new daemon_route() decorator factory in common.py collapses the try/except daemon-proxy boilerplate in all 8 blueprints (rules/params/body/transform keep responses identical) - firewall: interface-coverage invariant — config is the source of truth for zone interfaces (absent key = empty, no hands-off zones); pure validate_coverage() enforced at save (400) and apply (409, force: true overrides), top-level `unmanaged` exemption - lib: get_config() reads are now pure (no dir creation or writes); new lib/bootstrap.py creates runtime dirs and persists the one-shot nginx legacy migration at daemon start, after system_import (lib.nginx.migrate_config_file) - lib/common: compute_pending() apply-bookkeeping helper - daemon: emit_and_refresh() handler helper; refresh_state(bump=) so /status/refresh no longer bumps versions (poll/mutation only) - acme: move --log last so acme.sh never treats a real arg as the log-file argument - docs: AGENTS.md, config.md, state-model.md, api.md updated; HARDEN.md dropped (plan implemented); apply-confirm force wording Tests: 917 passed; ruff check + format clean.
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from lib.common import (
|
||||
_APPLY_HASH_KEY,
|
||||
_LAST_APPLIED_CONFIG_KEY,
|
||||
compute_pending,
|
||||
config_hash,
|
||||
deep_diff,
|
||||
load_json,
|
||||
@@ -78,6 +79,56 @@ class TestDeepDiff:
|
||||
assert not any(p.startswith("z.ranges[0].n") for p in paths)
|
||||
|
||||
|
||||
class TestComputePending:
|
||||
def test_hash_match_no_pending(self):
|
||||
cfg = {"a": 1}
|
||||
stamp_applied(cfg)
|
||||
pending, diff = compute_pending(cfg)
|
||||
assert pending is False
|
||||
assert diff == []
|
||||
|
||||
def test_never_applied_pending_no_snapshot(self):
|
||||
pending, diff = compute_pending({"a": 1})
|
||||
assert pending is True
|
||||
assert diff == []
|
||||
|
||||
def test_never_applied_pending_with_foreign_snapshot(self):
|
||||
# A recorded snapshot that does not match the current hash is still
|
||||
# used for the diff.
|
||||
cfg = {"a": 2, _LAST_APPLIED_CONFIG_KEY: {"a": 1}}
|
||||
pending, diff = compute_pending(cfg)
|
||||
assert pending is True
|
||||
assert diff == [{"path": "a", "action": "changed", "old": 1, "new": 2}]
|
||||
|
||||
def test_hash_mismatch_with_snapshot_diffs(self):
|
||||
applied = {"zones": {"lan": {"services": ["http"]}}}
|
||||
stamped = dict(applied)
|
||||
stamp_applied(stamped)
|
||||
drifted = {"zones": {"lan": {"services": ["http", "ssh"]}}}
|
||||
drifted[_LAST_APPLIED_CONFIG_KEY] = applied
|
||||
drifted[_APPLY_HASH_KEY] = stamped[_APPLY_HASH_KEY]
|
||||
pending, diff = compute_pending(drifted)
|
||||
assert pending is True
|
||||
paths = {d["path"] for d in diff}
|
||||
assert "zones.lan.services" in paths
|
||||
|
||||
def test_hash_mismatch_snapshot_not_dict(self):
|
||||
cfg = {"a": 1, _LAST_APPLIED_CONFIG_KEY: "not-a-dict"}
|
||||
pending, diff = compute_pending(cfg)
|
||||
assert pending is True
|
||||
assert diff == []
|
||||
|
||||
def test_meta_keys_excluded_from_diff(self):
|
||||
cfg = {"a": 1}
|
||||
stamp_applied(cfg)
|
||||
cfg["a"] = 2 # drift
|
||||
pending, diff = compute_pending(cfg)
|
||||
assert pending is True
|
||||
assert not any(
|
||||
p.startswith(("_last_applied",)) for d in diff for p in [d["path"]]
|
||||
)
|
||||
|
||||
|
||||
class TestDashboardFallback:
|
||||
def test_hash_subsystem_unchanged_generic(self):
|
||||
# Guards that a pending status without a snapshot still yields a
|
||||
|
||||
Reference in New Issue
Block a user