Files
houseplan-card/tests_backend/test_config_schema_manifest.py
T
Codex f43187218f fix: the schema dump must not match the HACS *manifest.json glob (#33)
repo-hygiene caught it: HACS globs *manifest.json over the whole clone
and rejects a repository with two. The dump is scripts/config-schema.json.

User-Visible: no
Issue: #33
2026-08-30 12:45:17 +03:00

83 lines
3.2 KiB
Python

"""#33: the committed schema manifest must stay fresh, and the lifecycle
fixtures must pass the real schema losslessly.
The dump script stubs the package parents itself, so this module runs both in
the sandbox (no homeassistant) and in CI.
"""
from __future__ import annotations
import copy
import importlib.util
import json
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
def _load_dump_module():
spec = importlib.util.spec_from_file_location(
"houseplan_dump_config_schema", REPO / "scripts" / "dump-config-schema.py")
module = importlib.util.module_from_spec(spec)
sys.modules["houseplan_dump_config_schema"] = module
spec.loader.exec_module(module)
return module
def _validation():
_load_dump_module() # its importer registers the stubbed packages
return sys.modules["custom_components.houseplan.validation"]
def test_issue_33_manifest_is_fresh_and_deterministic():
dump = _load_dump_module()
first = dump.build_manifest()
second = dump.build_manifest()
assert first == second, "the walker must be deterministic"
rendered = json.dumps(first, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
committed = (REPO / "scripts" / "config-schema.json").read_text(encoding="utf-8")
assert rendered == committed, (
"scripts/config-schema.json is stale — the schema changed; "
"run python3 scripts/dump-config-schema.py and commit the diff"
)
# AC1: the manifest is not a stub — it must cover the whole persisted shape.
assert len(first["fields"]) > 200
def _collect_paths(node, prefix=""):
paths = set()
if isinstance(node, dict):
for key, value in node.items():
paths |= _collect_paths(value, f"{prefix}.{key}" if prefix else key)
elif isinstance(node, list):
for item in node:
paths |= _collect_paths(item, f"{prefix}[]")
else:
paths.add(prefix)
return paths
def test_issue_33_lifecycle_fixtures_pass_the_schema_losslessly():
validation = _validation()
fixtures = REPO / "test" / "fixtures" / "config-lifecycle"
dropped_by_design = {"spaces[].aspect", "spaces[].segments"} # vol.Remove
for name in ("oldest-supported", "current", "future-fields"):
raw = json.loads((fixtures / f"{name}.json").read_text(encoding="utf-8"))
validated = validation.CONFIG_SCHEMA(copy.deepcopy(raw))
before = _collect_paths(raw)
after = _collect_paths(validated)
lost = before - after - dropped_by_design
assert not lost, f"{name}: validation silently dropped {sorted(lost)}"
def test_issue_33_future_fields_round_trip_exactly():
validation = _validation()
fixtures = REPO / "test" / "fixtures" / "config-lifecycle"
raw = json.loads((fixtures / "future-fields.json").read_text(encoding="utf-8"))
validated = validation.CONFIG_SCHEMA(copy.deepcopy(raw))
assert validated["future_root_field"] == {"kept": True}
assert validated["settings"]["future_setting"] == [1, 2, 3]
assert validated["spaces"][0]["future_space_field"] == "kept"
assert validated["spaces"][0]["settings"]["future_display"] is True
assert validated["markers"][0]["future_marker_field"] == {"nested": "kept"}