Files
odysseus/tests/test_preset_atomic_save.py
T
Mazen Tamer Salah d58202d10e fix(presets): persist presets atomically to avoid corruption on crash (#2169)
PresetManager.save() used a plain open("w") + json.dump, which truncates
presets.json before writing the new content. A crash, power loss, or
serialization error mid-write leaves the file truncated/empty and every
saved preset is lost.

Route the write through core.atomic_io.atomic_write_json (tmp file +
os.replace), matching how the rest of the codebase persists JSON state.
The helper is imported lazily so this module stays free of the heavy core
package import graph at module load time.

Adds tests/test_preset_atomic_save.py covering the source contract, a
failed-write leaving the existing file intact, and a round trip.
2026-06-08 19:16:37 +02:00

44 lines
1.6 KiB
Python

"""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"