From dc9b1fb8d1adcac5b7dbbd69abbc9bfbc6cb7412 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:43:04 +0000 Subject: [PATCH 1/2] Initial plan From 2901f892b3083d43fbcbfc7663ce06f44c839718 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:45:18 +0000 Subject: [PATCH 2/2] fix: snapshot() returns deep copies instead of shallow copies Agent-Logs-Url: https://github.com/CyberSecDef/NovelForge/sessions/c1d734a6-6751-497f-aa1d-83a76d319ffd Co-authored-by: CyberSecDef <17597068+CyberSecDef@users.noreply.github.com> --- novelforge/progress.py | 8 +++++--- tests/test_progress_manager.py | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/novelforge/progress.py b/novelforge/progress.py index 38a672e..a3f5e81 100644 --- a/novelforge/progress.py +++ b/novelforge/progress.py @@ -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] def clear(self) -> None: """Remove all entries (primarily for test teardown).""" diff --git a/tests/test_progress_manager.py b/tests/test_progress_manager.py index 8390b15..1e9186b 100644 --- a/tests/test_progress_manager.py +++ b/tests/test_progress_manager.py @@ -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):