"""``resolve_config_source`` and ``load_config`` must agree on what is read. ``SensorContext.config_source`` promises the JSON file that was actually loaded. That promise only holds while the recorded lookup and the loading lookup are the same implementation, so this module checks it against every installed config name, the alias table, an explicit path and the legacy ``"name"`` field, and pins the preserved error types/messages for a missing or non-JSON input. """ from __future__ import annotations import json from pathlib import Path import pytest from orisys import configs REPO_ROOT = Path(__file__).resolve().parents[1] SENSORS = REPO_ROOT / "sensors" def _read(path: str | Path) -> dict: return json.loads(Path(path).read_text(encoding="utf-8-sig")) def test_every_listed_name_resolves_to_the_file_load_config_reads() -> None: names = configs.list_config() assert names for name in names: source = configs.resolve_config_source(name) assert source is not None, name assert _read(source) == configs.load_config(name), name # The recorded source can be loaded directly with the same result. assert configs.load_config(source) == configs.load_config(name), name def test_aliases_resolve_like_loading() -> None: aliases = dict(configs.CONFIG_ALIASES) assert aliases for alias, canonical in aliases.items(): source = configs.resolve_config_source(alias) assert source is not None, alias assert _read(source) == configs.load_config(alias), alias canonical_source = configs.resolve_config_source(canonical) if canonical_source is not None: assert source == canonical_source, alias def test_explicit_paths_resolve_to_themselves(tmp_path: Path) -> None: bundle_config = SENSORS / "box" / "sensor.json" assert configs.resolve_config_source(bundle_config) == bundle_config.resolve() assert configs.load_config(bundle_config) == configs.load_config("box") copied = tmp_path / "copied-config.json" copied.write_text(bundle_config.read_text(encoding="utf-8"), encoding="utf-8") assert configs.resolve_config_source(copied) == copied.resolve() assert configs.load_config(copied) == configs.load_config(bundle_config) def test_legacy_name_field_scan_agrees_with_loading() -> None: """A config's JSON ``"name"`` may differ from its file stem (``prototype``).""" found = False for name in configs.list_config(): source = configs.resolve_config_source(name) if source is None: continue field = _read(source).get("name") if ( isinstance(field, str) and field and field not in configs.list_config() ): resolved = configs.resolve_config_source(field) assert resolved is not None, field assert _read(resolved) == configs.load_config(field), field found = True assert found, "expected at least one legacy name field in the installed configs" def test_missing_and_non_json_inputs_keep_their_error_contract(tmp_path: Path) -> None: assert configs.resolve_config_source("no-such-config-name") is None with pytest.raises(FileNotFoundError, match="Input config file not found"): configs.load_config("no-such-config-name") absolute_missing = tmp_path / "missing.json" assert configs.resolve_config_source(absolute_missing) is None with pytest.raises(FileNotFoundError, match="not found at absolute path"): configs.load_config(absolute_missing) not_json = tmp_path / "config.txt" not_json.write_text("{}", encoding="utf-8") # The source lookup names the existing file; loading still refuses a non-JSON # file with the historical ValueError instead of silently reading it. assert configs.resolve_config_source(not_json) == not_json.resolve() with pytest.raises(ValueError, match="must be a JSON file"): configs.load_config(not_json)