Files
odysseus/tests/test_preset_atomic_save.py

44 lines
1.6 KiB
Python
Raw Normal View History

"""Regression: PresetManager.save() must persist presets atomically.
save() used a plain open("w") + json.dump, which truncates presets.json before
writing the new content. A crash / power loss / serialization error mid-write
leaves the file truncated or empty — the user loses every saved preset. The
save now goes through core.atomic_io.atomic_write_json (tmp file + os.replace),
which the rest of the codebase already uses for JSON state files.
"""
import inspect
import json
from src.preset_manager import PresetManager
class _Unserializable:
"""json.dump cannot serialize this — stands in for a mid-write failure."""
def test_save_uses_atomic_write_json():
src = inspect.getsource(PresetManager.save)
assert "atomic_write_json" in src, "save() must persist via atomic_write_json"
assert "open(" not in src, "save() must not write presets.json with a plain open('w')"
def test_failed_save_does_not_truncate_existing_file(tmp_path):
mgr = PresetManager(str(tmp_path))
assert mgr.save({"custom": {"name": "keep"}}) is True
before = (tmp_path / "presets.json").read_text(encoding="utf-8")
# A payload that cannot be serialized must not clobber the good file.
assert mgr.save({"custom": {"obj": _Unserializable()}}) is False
after = (tmp_path / "presets.json").read_text(encoding="utf-8")
assert after == before
assert json.loads(after) == {"custom": {"name": "keep"}}
def test_save_round_trip(tmp_path):
mgr = PresetManager(str(tmp_path))
assert mgr.save({"custom": {"name": "X", "temperature": 0.5}}) is True
reloaded = PresetManager(str(tmp_path))
assert reloaded.presets["custom"]["name"] == "X"