import json from pathlib import Path import pytest import app.persistence as persistence from app.config.store import ( COLOR_PALETTE_LIMIT, CONFIG_SCHEMA_VERSION, CONFIG_V1_FIELDS, CONFIG_V2_FIELDS, CONFIG_V3_FIELDS, CONFIG_V4_FIELDS, CONFIG_V5_FIELDS, CONFIG_V6_FIELDS, CONFIG_V7_FIELDS, CONFIG_V8_FIELDS, CONFIG_V9_FIELDS, CONFIG_V10_FIELDS, DEFAULT_ANIMATION_PREVIEW_MAX_CONCURRENT, DEFAULT_COLOR_PALETTE, DEFAULT_DISPLAY, DEFAULT_WORKSPACE_ORDER, DEFAULT_MATRIX_REFRESH_RATE_LIMIT_HZ, DEFAULT_PREVIEW_REFRESH_INTERVAL_MS, MATRIX_REFRESH_RATE_LIMITS, ConfigError, ConfigStore, resolve_data_dir, resolve_runtime_dir, ) def test_config_store_creates_and_updates(tmp_path): store = ConfigStore(project_root=tmp_path) assert store.config["orientation"] == 0 assert store.config["color_palette"] == DEFAULT_COLOR_PALETTE assert store.config["voltage_calibration_factor"] == 1.0 assert store.config["voltage_calibrated_at"] is None assert store.config["preview_refresh_interval_ms"] == DEFAULT_PREVIEW_REFRESH_INTERVAL_MS assert store.config["low_voltage_protection_enabled"] is False assert store.config["matrix_refresh_rate_limit_hz"] == 100 assert store.config["custom_test_color"] == "#40A0FF" assert store.config["default_display"] == DEFAULT_DISPLAY assert store.config["workspace_order"] == DEFAULT_WORKSPACE_ORDER assert store.config["performance_mode_enabled"] is False assert store.config["animation_preview_max_concurrent"] == DEFAULT_ANIMATION_PREVIEW_MAX_CONCURRENT assert (tmp_path / "data" / "config.json").exists() assert "schema_version" not in store.config persisted = json.loads((tmp_path / "data" / "config.json").read_text(encoding="utf-8")) assert persisted["schema_version"] == CONFIG_SCHEMA_VERSION updated = store.update({"orientation": 180, "brightness": 35}) assert updated["orientation"] == 180 assert updated["brightness"] == 35 reloaded = ConfigStore(project_root=tmp_path) assert reloaded.config["orientation"] == 180 assert reloaded.config["brightness"] == 35 def test_v1_config_migrates_preview_interval_without_losing_device_values(tmp_path): original_store = ConfigStore(project_root=tmp_path) original_store.update({ "orientation": 270, "brightness": 31, "color_palette": ["#010203", "#AABBCC"], "voltage_calibration_factor": 1.01, "voltage_calibrated_at": "2026-07-20T12:34:56.000Z", "voltage_calibration_reference": 5.0, "voltage_calibration_uncalibrated": 4.950495, }) v1_document = json.loads(original_store.path.read_text(encoding="utf-8")) v1_document["schema_version"] = 1 v1_document.pop("preview_refresh_interval_ms") v1_document.pop("low_voltage_protection_enabled") v1_document.pop("matrix_refresh_rate_limit_hz") v1_document.pop("custom_test_color") v1_document.pop("default_display") v1_document.pop("workspace_order") v1_document.pop("performance_mode_enabled") v1_document.pop("animation_preview_max_concurrent") original_store.path.write_text( json.dumps(v1_document, ensure_ascii=False), encoding="utf-8", ) migrated = ConfigStore(project_root=tmp_path) persisted = json.loads(migrated.path.read_text(encoding="utf-8")) assert migrated.config["preview_refresh_interval_ms"] == DEFAULT_PREVIEW_REFRESH_INTERVAL_MS assert migrated.config["orientation"] == 270 assert migrated.config["brightness"] == 31 assert migrated.config["color_palette"] == ["#010203", "#AABBCC"] assert migrated.config["voltage_calibration_factor"] == 1.01 assert migrated.config["voltage_calibrated_at"] == "2026-07-20T12:34:56.000Z" assert persisted["schema_version"] == CONFIG_SCHEMA_VERSION assert migrated.config["low_voltage_protection_enabled"] is False assert migrated.config["matrix_refresh_rate_limit_hz"] == 100 def test_v2_config_migrates_protection_switch_without_losing_values(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({ "orientation": 90, "brightness": 27, "preview_refresh_interval_ms": 321, "color_palette": ["#123456"], }) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 2 document.pop("low_voltage_protection_enabled") document.pop("matrix_refresh_rate_limit_hz") document.pop("custom_test_color") document.pop("default_display") document.pop("workspace_order") document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["low_voltage_protection_enabled"] is False assert migrated.config["orientation"] == 90 assert migrated.config["brightness"] == 27 assert migrated.config["preview_refresh_interval_ms"] == 321 assert migrated.config["color_palette"] == ["#123456"] assert migrated.config["matrix_refresh_rate_limit_hz"] == 100 assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v3_config_migrates_matrix_refresh_limit_without_losing_values(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({ "orientation": 180, "brightness": 29, "preview_refresh_interval_ms": 444, "low_voltage_protection_enabled": True, }) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 3 document.pop("matrix_refresh_rate_limit_hz") document.pop("custom_test_color") document.pop("default_display") document.pop("workspace_order") document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["matrix_refresh_rate_limit_hz"] == DEFAULT_MATRIX_REFRESH_RATE_LIMIT_HZ assert migrated.config["orientation"] == 180 assert migrated.config["brightness"] == 29 assert migrated.config["preview_refresh_interval_ms"] == 444 assert migrated.config["low_voltage_protection_enabled"] is True assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v4_config_migrates_custom_test_color_without_losing_values(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({"orientation": 270, "brightness": 37}) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 4 document.pop("custom_test_color") document.pop("default_display") document.pop("workspace_order") document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["custom_test_color"] == "#40A0FF" assert migrated.config["orientation"] == 270 assert migrated.config["brightness"] == 37 assert migrated.config["default_display"] == DEFAULT_DISPLAY assert migrated.config["workspace_order"] == DEFAULT_WORKSPACE_ORDER assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v5_config_migrates_default_display_without_losing_values(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({"orientation": 90, "brightness": 33, "custom_test_color": "#123456"}) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 5 document.pop("default_display") document.pop("workspace_order") document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["default_display"] == DEFAULT_DISPLAY assert migrated.config["orientation"] == 90 assert migrated.config["brightness"] == 33 assert migrated.config["custom_test_color"] == "#123456" assert migrated.config["workspace_order"] == DEFAULT_WORKSPACE_ORDER assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v6_config_migrates_workspace_order_without_losing_values(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({"orientation": 180, "brightness": 26}) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 6 document.pop("workspace_order") document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["workspace_order"] == DEFAULT_WORKSPACE_ORDER assert migrated.config["orientation"] == 180 assert migrated.config["brightness"] == 26 assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v7_config_inserts_media_workspace_without_reordering_existing_items(tmp_path): store = ConfigStore(project_root=tmp_path) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 7 document["workspace_order"] = ["settings", "text", "templates", "animations", "device", "canvas"] document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["workspace_order"] == [ "settings", "text", "templates", "media-import", "animations", "device", "canvas", ] assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v8_config_migrates_performance_mode_disabled(tmp_path): store = ConfigStore(project_root=tmp_path) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 8 document.pop("performance_mode_enabled") document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["performance_mode_enabled"] is False assert migrated.config["animation_preview_max_concurrent"] == 2 assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == CONFIG_SCHEMA_VERSION def test_v9_config_migrates_animation_preview_concurrency_without_losing_values(tmp_path): store = ConfigStore(project_root=tmp_path) document = json.loads(store.path.read_text(encoding="utf-8")) document["schema_version"] = 9 document["performance_mode_enabled"] = True document.pop("animation_preview_max_concurrent") store.path.write_text(json.dumps(document), encoding="utf-8") migrated = ConfigStore(project_root=tmp_path) assert migrated.config["performance_mode_enabled"] is True assert migrated.config["animation_preview_max_concurrent"] == 2 assert json.loads(migrated.path.read_text(encoding="utf-8"))["schema_version"] == 10 def test_config_schema_field_sets_are_explicit_and_incremental(): assert "preview_refresh_interval_ms" not in CONFIG_V1_FIELDS assert "low_voltage_protection_enabled" not in CONFIG_V1_FIELDS assert CONFIG_V2_FIELDS == CONFIG_V1_FIELDS | {"preview_refresh_interval_ms"} assert CONFIG_V3_FIELDS == CONFIG_V2_FIELDS | {"low_voltage_protection_enabled"} assert CONFIG_V4_FIELDS == CONFIG_V3_FIELDS | {"matrix_refresh_rate_limit_hz"} assert CONFIG_V5_FIELDS == CONFIG_V4_FIELDS | {"custom_test_color"} assert CONFIG_V6_FIELDS == CONFIG_V5_FIELDS | {"default_display"} assert CONFIG_V7_FIELDS == CONFIG_V6_FIELDS | {"workspace_order"} assert CONFIG_V8_FIELDS == CONFIG_V7_FIELDS assert CONFIG_V9_FIELDS == CONFIG_V8_FIELDS | {"performance_mode_enabled"} assert CONFIG_V10_FIELDS == CONFIG_V9_FIELDS | {"animation_preview_max_concurrent"} def test_animation_preview_concurrency_is_strict_and_persistent(tmp_path): store = ConfigStore(project_root=tmp_path) for value in (1, 2, 50): assert store.update({"animation_preview_max_concurrent": value})[ "animation_preview_max_concurrent" ] == value assert ConfigStore(project_root=tmp_path).config[ "animation_preview_max_concurrent" ] == value for value in (0, 51, True, 2.0, "2", None): with pytest.raises(ConfigError): store.update({"animation_preview_max_concurrent": value}) def test_workspace_order_is_strict_unique_and_persistent(tmp_path): store = ConfigStore(project_root=tmp_path) selected = ["templates", "device", "settings", "canvas", "text", "animations"] assert store.update({"workspace_order": selected})["workspace_order"] == selected assert ConfigStore(project_root=tmp_path).config["workspace_order"] == selected for value in ( None, [], ["device", "device"], ["bad id"], ["_bad"], [1, "device"], "device", ): with pytest.raises(ConfigError): store.update({"workspace_order": value}) assert store.config["workspace_order"] == selected @pytest.mark.parametrize("value", [["device", "device"], ["bad id"], [], [1]]) def test_invalid_persisted_workspace_order_is_preserved_and_blocks_startup(tmp_path, value): store = ConfigStore(project_root=tmp_path) document = json.loads(store.path.read_text(encoding="utf-8")) document["workspace_order"] = value original = json.dumps(document, ensure_ascii=False).encode("utf-8") store.path.write_bytes(original) with pytest.raises(ConfigError): ConfigStore(project_root=tmp_path) assert store.path.read_bytes() == original def test_default_display_is_strict_normalized_and_persistent(tmp_path): store = ConfigStore(project_root=tmp_path) selected = { "type": "template", "id": "AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA", } assert store.update({"default_display": selected})["default_display"] == { "type": "template", "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", } assert ConfigStore(project_root=tmp_path).config["default_display"]["id"] == ( "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" ) for value in ( None, {}, {"type": "template"}, {"type": "image", "id": selected["id"]}, {"type": "template", "id": "not-a-uuid"}, {"type": "template", "id": selected["id"], "name": "extra"}, ): with pytest.raises(ConfigError): store.update({"default_display": value}) def test_custom_test_color_is_normalized_strict_and_persistent(tmp_path): store = ConfigStore(project_root=tmp_path) assert store.update({"custom_test_color": "#a1b2c3"})["custom_test_color"] == "#A1B2C3" assert ConfigStore(project_root=tmp_path).config["custom_test_color"] == "#A1B2C3" for value in (None, True, 123, "red", "#12345", "#1234567"): with pytest.raises(ConfigError): store.update({"custom_test_color": value}) assert store.config["custom_test_color"] == "#A1B2C3" def test_matrix_refresh_rate_limit_requires_a_fixed_integer_preset(tmp_path): store = ConfigStore(project_root=tmp_path) for value in sorted(MATRIX_REFRESH_RATE_LIMITS): assert store.update({"matrix_refresh_rate_limit_hz": value})[ "matrix_refresh_rate_limit_hz" ] == value assert ConfigStore(project_root=tmp_path).config[ "matrix_refresh_rate_limit_hz" ] == value for value in (0, 14, 16, 60.0, True, "60", None): with pytest.raises(ConfigError): store.update({"matrix_refresh_rate_limit_hz": value}) @pytest.mark.parametrize("value", [0, 1, None, "true", 1.0]) def test_low_voltage_protection_switch_requires_a_strict_boolean(tmp_path, value): store = ConfigStore(project_root=tmp_path) with pytest.raises(ConfigError, match="must be a boolean"): store.update({"low_voltage_protection_enabled": value}) assert store.config["low_voltage_protection_enabled"] is False def test_low_voltage_protection_switch_persists_across_restart(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({"low_voltage_protection_enabled": True}) assert ConfigStore(project_root=tmp_path).config[ "low_voltage_protection_enabled" ] is True def test_preview_refresh_interval_range_type_and_restart_persistence(tmp_path): store = ConfigStore(project_root=tmp_path) for value in (1, 99, 100, 60000): assert store.update({"preview_refresh_interval_ms": value})[ "preview_refresh_interval_ms" ] == value assert ConfigStore(project_root=tmp_path).config[ "preview_refresh_interval_ms" ] == value for value in (0, 60001, True, 1.5, "100"): with pytest.raises(ConfigError): store.update({"preview_refresh_interval_ms": value}) def test_palette_normalizes_persists_and_updates_atomically(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({"color_palette": []}) store.add_palette_color("#aabbcc") store.add_palette_color("#AABBCC") store.add_palette_color("#010203") store.remove_palette_color("#aabbcc") store.remove_palette_color("#ffffff") assert store.config["color_palette"] == ["#010203"] assert ConfigStore(project_root=tmp_path).config["color_palette"] == ["#010203"] def test_legacy_palette_repairs_entries_without_losing_other_config(tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() raw_palette = ["#aabbcc", "bad", "#AABBCC"] + [f"#{index:06X}" for index in range(30)] (data_dir / "config.json").write_text(json.dumps({ "orientation": 180, "brightness": 35, "color_palette": raw_palette, }), encoding="utf-8") store = ConfigStore(project_root=tmp_path) assert store.config["orientation"] == 180 assert store.config["brightness"] == 35 assert store.config["color_palette"][0] == "#AABBCC" assert len(store.config["color_palette"]) == COLOR_PALETTE_LIMIT persisted = json.loads((data_dir / "config.json").read_text(encoding="utf-8")) assert persisted["schema_version"] == CONFIG_SCHEMA_VERSION assert persisted["color_palette"] == store.config["color_palette"] def test_config_without_palette_is_migrated_to_defaults(tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() path = data_dir / "config.json" path.write_text(json.dumps({"orientation": 90, "brightness": 50}), encoding="utf-8") store = ConfigStore(project_root=tmp_path) assert store.config["orientation"] == 90 assert store.config["color_palette"] == DEFAULT_COLOR_PALETTE persisted = json.loads(path.read_text(encoding="utf-8")) assert persisted["schema_version"] == CONFIG_SCHEMA_VERSION assert persisted["color_palette"] == DEFAULT_COLOR_PALETTE def test_config_store_rejects_corrupt_file_without_modifying_it(tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() path = data_dir / "config.json" original = b"{bad json" path.write_bytes(original) with pytest.raises(ConfigError, match="unreadable"): ConfigStore(project_root=tmp_path) assert path.read_bytes() == original def test_voltage_calibration_migrates_and_persists_as_one_group(tmp_path): store = ConfigStore(project_root=tmp_path) store.update({ "voltage_calibration_factor": 1.01, "voltage_calibrated_at": "2026-07-20T12:34:56.000Z", "voltage_calibration_reference": 5.0, "voltage_calibration_uncalibrated": 4.950495, }) reloaded = ConfigStore(project_root=tmp_path) assert reloaded.config["voltage_calibration_factor"] == 1.01 assert reloaded.config["voltage_calibrated_at"] == "2026-07-20T12:34:56.000Z" def test_legacy_config_migration_preserves_valid_voltage_calibration(tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() path = data_dir / "config.json" calibration = { "voltage_calibration_factor": 1.01, "voltage_calibrated_at": "2026-07-20T12:34:56.000Z", "voltage_calibration_reference": 5.0, "voltage_calibration_uncalibrated": 4.950495, } path.write_text( json.dumps({"orientation": 180, "brightness": 35, **calibration}), encoding="utf-8", ) store = ConfigStore(project_root=tmp_path) assert {key: store.config[key] for key in calibration} == calibration persisted = json.loads(path.read_text(encoding="utf-8")) assert {key: persisted[key] for key in calibration} == calibration assert persisted["schema_version"] == CONFIG_SCHEMA_VERSION def test_invalid_or_partial_voltage_calibration_is_preserved_and_blocks_startup(tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() path = data_dir / "config.json" path.write_text(json.dumps({ "orientation": 180, "brightness": 35, "voltage_calibration_factor": 9, "voltage_calibrated_at": "bad", "voltage_calibration_reference": 5.0, }), encoding="utf-8") original = path.read_bytes() with pytest.raises(ConfigError, match="calibration is incomplete"): ConfigStore(project_root=tmp_path) assert path.read_bytes() == original @pytest.mark.parametrize( "document, message", [ ({"schema_version": CONFIG_SCHEMA_VERSION + 1}, "newer than supported"), ({"orientation": 0, "unexpected": True}, "unknown persisted config fields"), ], ) def test_unmigratable_config_is_preserved_and_blocks_startup( tmp_path, document, message, ): data_dir = tmp_path / "data" data_dir.mkdir() path = data_dir / "config.json" original = (json.dumps(document, separators=(",", ":")) + "\n").encode("utf-8") path.write_bytes(original) with pytest.raises(ConfigError, match=message): ConfigStore(project_root=tmp_path) assert path.read_bytes() == original def test_config_migration_is_idempotent(tmp_path): data_dir = tmp_path / "data" data_dir.mkdir() path = data_dir / "config.json" path.write_text(json.dumps({"orientation": 270, "brightness": 42}), encoding="utf-8") first = ConfigStore(project_root=tmp_path) migrated = path.read_bytes() second = ConfigStore(project_root=tmp_path) assert first.config == second.config assert path.read_bytes() == migrated assert json.loads(migrated)["schema_version"] == CONFIG_SCHEMA_VERSION def test_atomic_write_failure_keeps_previous_config_and_cleans_temp_files( tmp_path, monkeypatch, ): store = ConfigStore(project_root=tmp_path) store.update({"orientation": 90}) original = store.path.read_bytes() def fail_replace(_source, _target): raise OSError("replace failed") monkeypatch.setattr(persistence.os, "replace", fail_replace) with pytest.raises(OSError, match="replace failed"): store.update({"orientation": 180}) assert store.path.read_bytes() == original assert store.config["orientation"] == 90 assert list(store.data_dir.glob("*.tmp")) == [] def test_directory_sync_failure_restores_previous_config_and_cleans_temp_files( tmp_path, monkeypatch, ): store = ConfigStore(project_root=tmp_path) store.update({"orientation": 90}) original = store.path.read_bytes() sync_calls = 0 def fail_second_sync(_directory): nonlocal sync_calls sync_calls += 1 if sync_calls == 2: raise OSError("directory sync failed") monkeypatch.setattr(persistence, "_fsync_directory", fail_second_sync) with pytest.raises(OSError, match="directory sync failed"): store.update({"orientation": 180}) assert store.path.read_bytes() == original assert store.config["orientation"] == 90 assert list(store.data_dir.glob(".*.tmp")) == [] @pytest.mark.parametrize( ("field", "value"), [ ("brightness", True), ("orientation", 0.0), ("default_text_size", "12"), ("low_voltage_protection_enabled", 1), ("performance_mode_enabled", 1), ("animation_preview_max_concurrent", True), ], ) def test_current_config_schema_rejects_noncanonical_json_types( tmp_path, field, value, ): store = ConfigStore(project_root=tmp_path) document = json.loads(store.path.read_text(encoding="utf-8")) document[field] = value original = (json.dumps(document, separators=(",", ":")) + "\n").encode("utf-8") store.path.write_bytes(original) with pytest.raises(ConfigError, match="must be"): ConfigStore(project_root=tmp_path) assert store.path.read_bytes() == original def test_explicit_project_root_overrides_storage_environment(tmp_path, monkeypatch): environment_data = tmp_path / "environment-data" environment_runtime = tmp_path / "environment-runtime" monkeypatch.setenv("MATRIX_DATA_DIR", str(environment_data)) monkeypatch.setenv("STATE_DIRECTORY", str(tmp_path / "state")) monkeypatch.setenv("MATRIX_RUNTIME_DIR", str(environment_runtime)) monkeypatch.setenv("RUNTIME_DIRECTORY", str(tmp_path / "runtime")) explicit = tmp_path / "explicit" store = ConfigStore(project_root=explicit) assert store.data_dir == explicit / "data" assert store.runtime_dir == explicit / "data" / "runtime" assert not environment_data.exists() assert not environment_runtime.exists() def test_environment_storage_path_precedence_and_runtime_fallback(tmp_path, monkeypatch): matrix_data = tmp_path / "matrix-data" state_data = tmp_path / "state-data" matrix_runtime = tmp_path / "matrix-runtime" systemd_runtime = tmp_path / "systemd-runtime" monkeypatch.setenv("MATRIX_DATA_DIR", str(matrix_data)) monkeypatch.setenv("STATE_DIRECTORY", str(state_data)) monkeypatch.setenv("MATRIX_RUNTIME_DIR", str(matrix_runtime)) monkeypatch.setenv("RUNTIME_DIRECTORY", str(systemd_runtime)) assert resolve_data_dir() == matrix_data assert resolve_runtime_dir(data_dir=matrix_data) == matrix_runtime monkeypatch.delenv("MATRIX_DATA_DIR") monkeypatch.delenv("MATRIX_RUNTIME_DIR") assert resolve_data_dir() == state_data assert resolve_runtime_dir(data_dir=state_data) == systemd_runtime monkeypatch.delenv("STATE_DIRECTORY") monkeypatch.delenv("RUNTIME_DIRECTORY") configured_data = tmp_path / "configured-only" monkeypatch.setenv("MATRIX_DATA_DIR", str(configured_data)) assert ( resolve_runtime_dir(data_dir=resolve_data_dir()) == Path(__file__).resolve().parents[1] / "data" / "runtime" ) monkeypatch.delenv("MATRIX_DATA_DIR") assert resolve_data_dir() == Path(__file__).resolve().parents[1] / "data"