feat(project): Segregate discussion history into sibling TOML file
This commit is contained in:
@@ -121,15 +121,60 @@ def default_project(name: str = "unnamed") -> dict:
|
|||||||
|
|
||||||
# ── load / save ──────────────────────────────────────────────────────────────
|
# ── load / save ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_history_path(project_path: str | Path) -> Path:
|
||||||
|
p = Path(project_path)
|
||||||
|
return p.parent / f"{p.stem}_history.toml"
|
||||||
|
|
||||||
|
|
||||||
def load_project(path) -> dict:
|
def load_project(path) -> dict:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
|
proj = tomllib.load(f)
|
||||||
|
|
||||||
|
# Automatic Migration: move legacy 'discussion' to sibling file
|
||||||
|
hist_path = get_history_path(path)
|
||||||
|
if "discussion" in proj:
|
||||||
|
disc = proj.pop("discussion")
|
||||||
|
# Save to history file if it doesn't exist yet (or overwrite to migrate)
|
||||||
|
with open(hist_path, "wb") as f:
|
||||||
|
tomli_w.dump(disc, f)
|
||||||
|
# Save the stripped project file
|
||||||
|
save_project(proj, path)
|
||||||
|
# Restore for the returned dict so GUI works as before
|
||||||
|
proj["discussion"] = disc
|
||||||
|
else:
|
||||||
|
# Load from sibling if it exists
|
||||||
|
if hist_path.exists():
|
||||||
|
proj["discussion"] = load_history(path)
|
||||||
|
|
||||||
|
return proj
|
||||||
|
|
||||||
|
|
||||||
|
def load_history(project_path: str | Path) -> dict:
|
||||||
|
hist_path = get_history_path(project_path)
|
||||||
|
if hist_path.exists():
|
||||||
|
with open(hist_path, "rb") as f:
|
||||||
return tomllib.load(f)
|
return tomllib.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def save_project(proj: dict, path):
|
def save_project(proj: dict, path, disc_data: dict | None = None):
|
||||||
|
# Ensure 'discussion' is NOT in the main project dict
|
||||||
|
if "discussion" in proj:
|
||||||
|
# If disc_data wasn't provided, use the one from proj
|
||||||
|
if disc_data is None:
|
||||||
|
disc_data = proj["discussion"]
|
||||||
|
# Remove it so it doesn't get saved to the main file
|
||||||
|
proj = dict(proj) # shallow copy to avoid mutating caller's dict
|
||||||
|
del proj["discussion"]
|
||||||
|
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
tomli_w.dump(proj, f)
|
tomli_w.dump(proj, f)
|
||||||
|
|
||||||
|
if disc_data:
|
||||||
|
hist_path = get_history_path(path)
|
||||||
|
with open(hist_path, "wb") as f:
|
||||||
|
tomli_w.dump(disc_data, f)
|
||||||
|
|
||||||
|
|
||||||
# ── migration helper ─────────────────────────────────────────────────────────
|
# ── migration helper ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
56
tests/test_history_migration.py
Normal file
56
tests/test_history_migration.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import pytest
|
||||||
|
import tomli_w
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
from project_manager import load_project, save_project, default_project
|
||||||
|
|
||||||
|
def test_migration_on_load(tmp_path):
|
||||||
|
# Setup legacy project file with discussion
|
||||||
|
proj_path = tmp_path / "manual_slop.toml"
|
||||||
|
hist_path = tmp_path / "manual_slop_history.toml"
|
||||||
|
|
||||||
|
legacy_data = default_project("test-project")
|
||||||
|
legacy_data["discussion"]["discussions"]["main"]["history"] = ["Hello", "World"]
|
||||||
|
|
||||||
|
with open(proj_path, "wb") as f:
|
||||||
|
tomli_w.dump(legacy_data, f)
|
||||||
|
|
||||||
|
# Load project - should trigger migration
|
||||||
|
loaded_data = load_project(proj_path)
|
||||||
|
|
||||||
|
# Assertions
|
||||||
|
assert "discussion" in loaded_data
|
||||||
|
assert loaded_data["discussion"]["discussions"]["main"]["history"] == ["Hello", "World"]
|
||||||
|
|
||||||
|
# Check that it's NOT in the main file on disk anymore
|
||||||
|
with open(proj_path, "rb") as f:
|
||||||
|
on_disk = tomllib.load(f)
|
||||||
|
assert "discussion" not in on_disk
|
||||||
|
|
||||||
|
# Check history file
|
||||||
|
assert hist_path.exists()
|
||||||
|
with open(hist_path, "rb") as f:
|
||||||
|
hist_data = tomllib.load(f)
|
||||||
|
assert hist_data["discussions"]["main"]["history"] == ["Hello", "World"]
|
||||||
|
|
||||||
|
def test_save_separation(tmp_path):
|
||||||
|
# Setup fresh project data
|
||||||
|
proj_path = tmp_path / "manual_slop.toml"
|
||||||
|
hist_path = tmp_path / "manual_slop_history.toml"
|
||||||
|
|
||||||
|
proj_data = default_project("test-project")
|
||||||
|
proj_data["discussion"]["discussions"]["main"]["history"] = ["Saved", "Separately"]
|
||||||
|
|
||||||
|
# Save project - should save both files
|
||||||
|
save_project(proj_data, proj_path)
|
||||||
|
|
||||||
|
assert proj_path.exists()
|
||||||
|
assert hist_path.exists()
|
||||||
|
|
||||||
|
with open(proj_path, "rb") as f:
|
||||||
|
p = tomllib.load(f)
|
||||||
|
assert "discussion" not in p
|
||||||
|
|
||||||
|
with open(hist_path, "rb") as f:
|
||||||
|
h = tomllib.load(f)
|
||||||
|
assert h["discussions"]["main"]["history"] == ["Saved", "Separately"]
|
||||||
Reference in New Issue
Block a user