Kaynağa Gözat

fix(review): preserve committed snapshots on approval

Ma 7 ay önce
ebeveyn
işleme
48ea3e353d

+ 69 - 0
packages/cli/src/__tests__/cli-integration.test.ts

@@ -620,6 +620,75 @@ describe("CLI integration", () => {
     });
     });
   });
   });
 
 
+  describe("inkos review", () => {
+    it("preserves the original chapter snapshot when approving review", async () => {
+      const configPath = join(projectDir, "inkos.json");
+      const initialized = await stat(configPath).then(() => true).catch(() => false);
+      if (!initialized) run(["init"]);
+
+      const state = new StateManager(projectDir);
+      const bookId = "review-approve-cli";
+      const bookDir = join(projectDir, "books", bookId);
+      const storyDir = join(bookDir, "story");
+      const chaptersDir = join(bookDir, "chapters");
+      await mkdir(chaptersDir, { recursive: true });
+      await mkdir(storyDir, { recursive: true });
+
+      await writeFile(
+        join(bookDir, "book.json"),
+        JSON.stringify({
+          id: bookId,
+          title: "Review Approve CLI",
+          platform: "other",
+          genre: "other",
+          status: "active",
+          targetChapters: 10,
+          chapterWordCount: 2200,
+          createdAt: "2026-03-22T00:00:00.000Z",
+          updatedAt: "2026-03-22T00:00:00.000Z",
+        }, null, 2),
+        "utf-8",
+      );
+      await writeFile(join(storyDir, "current_state.md"), "State at ch1", "utf-8");
+      await writeFile(join(storyDir, "pending_hooks.md"), "Hooks at ch1", "utf-8");
+      await writeFile(join(chaptersDir, "0001_ch1.md"), "# Chapter 1\n\nContent 1", "utf-8");
+      await writeFile(
+        join(chaptersDir, "index.json"),
+        JSON.stringify([
+          {
+            number: 1,
+            title: "Ch1",
+            status: "ready-for-review",
+            wordCount: 100,
+            createdAt: "",
+            updatedAt: "",
+            auditIssues: [],
+            lengthWarnings: [],
+          },
+        ], null, 2),
+        "utf-8",
+      );
+
+      await state.snapshotState(bookId, 1);
+
+      await writeFile(join(storyDir, "current_state.md"), "State at ch3", "utf-8");
+      await writeFile(join(storyDir, "pending_hooks.md"), "Hooks at ch3", "utf-8");
+
+      const output = run(["review", "approve", bookId, "1"]);
+      expect(output).toContain("Chapter 1 approved");
+
+      await expect(
+        readFile(join(storyDir, "snapshots", "1", "current_state.md"), "utf-8"),
+      ).resolves.toBe("State at ch1");
+      await expect(
+        readFile(join(storyDir, "snapshots", "1", "pending_hooks.md"), "utf-8"),
+      ).resolves.toBe("Hooks at ch1");
+
+      const index = await state.loadChapterIndex(bookId);
+      expect(index[0]?.status).toBe("approved");
+    });
+  });
+
   describe("inkos plan/compose", () => {
   describe("inkos plan/compose", () => {
     beforeAll(async () => {
     beforeAll(async () => {
       const configPath = join(projectDir, "inkos.json");
       const configPath = join(projectDir, "inkos.json");

+ 0 - 4
packages/cli/src/commands/review.ts

@@ -129,10 +129,6 @@ reviewCommand
       };
       };
       await state.saveChapterIndex(bookId, index);
       await state.saveChapterIndex(bookId, index);
 
 
-      // Ensure a snapshot exists at the approved chapter so reject can
-      // roll back to this committed state later.
-      await state.snapshotState(bookId, chapterNum);
-
       if (opts.json) {
       if (opts.json) {
         log(JSON.stringify({ bookId, chapter: chapterNum, status: "approved" }));
         log(JSON.stringify({ bookId, chapter: chapterNum, status: "approved" }));
       } else {
       } else {

+ 17 - 0
packages/core/src/__tests__/state-manager.test.ts

@@ -1084,5 +1084,22 @@ describe("StateManager", () => {
 
 
       await expect(manager.rollbackToChapter(bookId, 99)).rejects.toThrow("Cannot restore snapshot");
       await expect(manager.rollbackToChapter(bookId, 99)).rejects.toThrow("Cannot restore snapshot");
     });
     });
+
+    it("removes sqlite memory files when rolling back", async () => {
+      await setupRollbackBook();
+
+      const storyDir = join(manager.bookDir(bookId), "story");
+      await Promise.all([
+        writeFile(join(storyDir, "memory.db"), "stale db", "utf-8"),
+        writeFile(join(storyDir, "memory.db-shm"), "stale shm", "utf-8"),
+        writeFile(join(storyDir, "memory.db-wal"), "stale wal", "utf-8"),
+      ]);
+
+      await manager.rollbackToChapter(bookId, 1);
+
+      await expect(stat(join(storyDir, "memory.db"))).rejects.toThrow();
+      await expect(stat(join(storyDir, "memory.db-shm"))).rejects.toThrow();
+      await expect(stat(join(storyDir, "memory.db-wal"))).rejects.toThrow();
+    });
   });
   });
 });
 });

+ 8 - 0
packages/core/src/state/manager.ts

@@ -485,6 +485,14 @@ export class StateManager {
       // drafts directory missing
       // drafts directory missing
     }
     }
 
 
+    // Drop any persisted sqlite acceleration index so discarded chapters
+    // cannot leak back into retrieval after the markdown/state rollback.
+    await Promise.all([
+      rm(join(bookDir, "story", "memory.db"), { force: true }),
+      rm(join(bookDir, "story", "memory.db-shm"), { force: true }),
+      rm(join(bookDir, "story", "memory.db-wal"), { force: true }),
+    ]);
+
     await this.saveChapterIndex(bookId, kept);
     await this.saveChapterIndex(bookId, kept);
     return discarded;
     return discarded;
   }
   }