Quellcode durchsuchen

fix: degraded backup covers manuscript files

lingfengQAQ vor 1 Woche
Ursprung
Commit
668c2b6113

+ 3 - 3
docs/superpowers/plans/2026-06-10-audit-fix-plan.md

@@ -75,9 +75,9 @@ def rollback(self, chapter: int) -> bool:
 - Modify: `webnovel-writer/scripts/backup_manager.py:175-195`
 - Test: `webnovel-writer/scripts/tests/test_backup_manager.py`
 
-- [ ] **Step 1: 写测试**:模拟 git 不可用(monkeypatch `_git_available` 为 False),项目含 `正文/第0001章-x.md`,调用 `backup()` 后断言备份目录里存在该正文文件副本。
-- [ ] **Step 2: 实现**:降级路径把 `正文/`、`大纲/`、`设定集/`、`.webnovel/state.json` 全部 `shutil.copytree/copy2` 进 `.webnovel/backups/snapshot_ch{N}_{ts}/`;输出明确列出备份了什么。保留按数量滚动清理(最多 10 个 snapshot)。
-- [ ] **Step 3: 提交** `fix: degraded backup covers manuscript files`。
+- [x] **Step 1: 写测试**:模拟 git 不可用(monkeypatch `_git_available` 为 False),项目含 `正文/第0001章-x.md`,调用 `backup()` 后断言备份目录里存在该正文文件副本。
+- [x] **Step 2: 实现**:降级路径把 `正文/`、`大纲/`、`设定集/`、`.webnovel/state.json` 全部 `shutil.copytree/copy2` 进 `.webnovel/backups/snapshot_ch{N}_{ts}/`;输出明确列出备份了什么。保留按数量滚动清理(最多 10 个 snapshot)。
+- [x] **Step 3: 提交** `fix: degraded backup covers manuscript files`。
 
 ### Task 4: init 重跑不得静默覆盖损坏的 state.json
 

+ 26 - 5
webnovel-writer/scripts/backup_manager.py

@@ -190,18 +190,39 @@ __pycache__/
         backup_dir = self.project_root / ".webnovel" / "backups"
         backup_dir.mkdir(parents=True, exist_ok=True)
 
-        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-        backup_name = f"ch{chapter_num:04d}_{timestamp}"
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
+        backup_name = f"snapshot_ch{chapter_num:04d}_{timestamp}"
         backup_path = backup_dir / backup_name
 
         try:
-            # 备份 state.json
+            backup_path.mkdir(parents=True, exist_ok=True)
+            copied = []
+
+            for folder_name in ("正文", "大纲", "设定集"):
+                source_dir = self.project_root / folder_name
+                if source_dir.exists():
+                    shutil.copytree(source_dir, backup_path / folder_name)
+                    copied.append(folder_name)
+
             state_file = self.project_root / ".webnovel" / "state.json"
             if state_file.exists():
-                backup_path.mkdir(parents=True, exist_ok=True)
-                shutil.copy2(state_file, backup_path / "state.json")
+                target_state_dir = backup_path / ".webnovel"
+                target_state_dir.mkdir(parents=True, exist_ok=True)
+                shutil.copy2(state_file, target_state_dir / "state.json")
+                copied.append(".webnovel/state.json")
+
+            snapshots = sorted(
+                (path for path in backup_dir.glob("snapshot_ch*") if path.is_dir()),
+                key=lambda path: path.name,
+            )
+            for old_snapshot in snapshots[:-10]:
+                shutil.rmtree(old_snapshot)
 
             print(f"✅ 本地备份完成: {backup_path}")
+            if copied:
+                print(f"📦 已备份: {', '.join(copied)}")
+            else:
+                print("⚠️  未找到正文/大纲/设定集或 state.json 可备份")
             return True
         except OSError as e:
             print(f"❌ 本地备份失败: {e}")

+ 36 - 0
webnovel-writer/scripts/tests/test_backup_manager.py

@@ -97,3 +97,39 @@ def test_rollback_restores_files_on_current_branch_with_new_commit(tmp_path):
     after_count = int(_run_git(project_root, "rev-list", "--count", "HEAD").stdout.strip())
     assert after_count == before_count + 1
     assert "rollback: 恢复到 ch0001 备份点" in _run_git(project_root, "log", "-1", "--format=%s").stdout
+
+
+def test_local_backup_copies_manuscript_when_git_unavailable(tmp_path, monkeypatch):
+    monkeypatch.setattr(backup_manager, "is_git_available", lambda: False)
+
+    webnovel_dir = tmp_path / ".webnovel"
+    manuscript_dir = tmp_path / "正文"
+    outline_dir = tmp_path / "大纲"
+    settings_dir = tmp_path / "设定集"
+    webnovel_dir.mkdir()
+    manuscript_dir.mkdir()
+    outline_dir.mkdir()
+    settings_dir.mkdir()
+    (webnovel_dir / "state.json").write_text('{"current_chapter": 1}', encoding="utf-8")
+    (manuscript_dir / "第0001章-x.md").write_text("正文内容", encoding="utf-8")
+    (outline_dir / "第0001章.md").write_text("大纲内容", encoding="utf-8")
+    (settings_dir / "人物.md").write_text("设定内容", encoding="utf-8")
+
+    manager = GitBackupManager(str(tmp_path))
+
+    assert manager.backup(1) is True
+
+    snapshots = sorted((webnovel_dir / "backups").glob("snapshot_ch0001_*"))
+    assert len(snapshots) == 1
+    snapshot = snapshots[0]
+    assert (snapshot / "正文" / "第0001章-x.md").read_text(encoding="utf-8") == "正文内容"
+    assert (snapshot / "大纲" / "第0001章.md").read_text(encoding="utf-8") == "大纲内容"
+    assert (snapshot / "设定集" / "人物.md").read_text(encoding="utf-8") == "设定内容"
+    assert (snapshot / ".webnovel" / "state.json").read_text(encoding="utf-8") == '{"current_chapter": 1}'
+
+    for chapter in range(2, 13):
+        assert manager.backup(chapter) is True
+
+    snapshots = sorted((webnovel_dir / "backups").glob("snapshot_ch*"))
+    assert len(snapshots) == 10
+    assert snapshot not in snapshots