Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions novelforge/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,14 @@ def keys(self) -> list[str]:
return list(self._store.keys())

def snapshot(self) -> dict[str, ProgressState]:
"""Return shallow copies of every entry, keyed by token.
"""Return deep copies of every entry, keyed by token.

Primarily intended for diagnostics and test assertions.
Callers can read or mutate the returned dicts freely without
corrupting shared state. Primarily intended for diagnostics and
test assertions.
"""
with self._lock:
return {k: dict(v) for k, v in self._store.items()} # type: ignore[misc]
return {k: copy.deepcopy(v) for k, v in self._store.items()} # type: ignore[misc]
Comment on lines 191 to +192
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snapshot() now uses copy.deepcopy, which should generally preserve the ProgressState type. Consider removing the broad # type: ignore[misc] (or replacing it with a narrower cast(...) / correct ignore code) so mypy can validate the return type instead of suppressing it.

Copilot uses AI. Check for mistakes.

def clear(self) -> None:
"""Remove all entries (primarily for test teardown)."""
Expand Down
9 changes: 6 additions & 3 deletions tests/test_progress_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,12 +217,15 @@ def test_keys_returns_all_tokens(self):
progress_manager.create(f"k{i}", _base_state())
assert set(progress_manager.keys()) == {"k0", "k1", "k2", "k3"}

def test_snapshot_returns_shallow_copies(self):
progress_manager.create("s1", _base_state(step="init"))
def test_snapshot_returns_deep_copies(self):
progress_manager.create("s1", _base_state(step="init", chapters_done=[{"num": 1}]))
snap = progress_manager.snapshot()
# Top-level field mutation must not affect the store
snap["s1"]["step"] = "MUTATED"
# Original in the store must be unchanged
assert progress_manager.get("s1")["step"] == "init"
# Nested structure mutation must not affect the store either
snap["s1"]["chapters_done"][0]["num"] = 999
assert progress_manager.get("s1")["chapters_done"][0]["num"] == 1

def test_clear_removes_all_entries(self):
for i in range(3):
Expand Down
Loading