1
0
Эх сурвалжийг харах

fix(校验): 修复测试 mock 方式 + 补充边界测试

Mochocyang 2 сар өмнө
parent
commit
0f7192a8ac

+ 102 - 37
src/lib/agent/skills/draft-review-skill.spec.ts

@@ -7,8 +7,14 @@ import {
   type ReviewEvidence,
 } from "./draft-review-skill";
 
+const mockCognition = vi.hoisted(() => vi.fn());
+const mockCharacterStates = vi.hoisted(() => vi.fn());
+const mockForeshadowing = vi.hoisted(() => vi.fn());
+const mockListSnapshots = vi.hoisted(() => vi.fn());
+const mockLoadSnapshot = vi.hoisted(() => vi.fn());
+
 vi.mock("@/lib/novel/character-cognition", () => ({
-  loadCognitionState: vi.fn(),
+  loadCognitionState: mockCognition,
   emptyCognitionState: vi.fn(() => ({
     characters: [],
     readerKnows: [],
@@ -17,7 +23,7 @@ vi.mock("@/lib/novel/character-cognition", () => ({
 }));
 
 vi.mock("@/lib/novel/character-state", () => ({
-  loadCharacterStates: vi.fn(),
+  loadCharacterStates: mockCharacterStates,
   createEmptyCharacterStateStore: vi.fn(() => ({
     characters: [],
     lastUpdated: "2026-07-05T00:00:00.000Z",
@@ -25,7 +31,7 @@ vi.mock("@/lib/novel/character-state", () => ({
 }));
 
 vi.mock("@/lib/novel/foreshadowing-tracker", () => ({
-  loadForeshadowingTracker: vi.fn(),
+  loadForeshadowingTracker: mockForeshadowing,
   createEmptyForeshadowingStore: vi.fn(() => ({
     items: [],
     lastUpdated: "2026-07-05T00:00:00.000Z",
@@ -33,40 +39,43 @@ vi.mock("@/lib/novel/foreshadowing-tracker", () => ({
 }));
 
 vi.mock("@/lib/novel/chapter-ingest", () => ({
-  listSnapshots: vi.fn(() => Promise.resolve([])),
-  loadSnapshot: vi.fn(),
+  listSnapshots: mockListSnapshots,
+  loadSnapshot: mockLoadSnapshot,
 }));
 
-vi.mock("@/commands/fs", () => ({
-  readFile: vi.fn(),
-  fileExists: vi.fn(),
-  writeFileAtomic: vi.fn(),
-  createDirectory: vi.fn(),
-}));
+const aSnapshot = (chapterNumber: number) => ({
+  chapterId: `ch${chapterNumber}`,
+  chapterNumber,
+  characters: [],
+  locations: [],
+  organizations: [],
+  items: [],
+  events: [],
+  characterStateChanges: [],
+  relationshipChanges: [],
+  knowledgeChanges: [],
+  foreshadowingChanges: [],
+  newCanonFacts: [],
+  timelineEvents: [],
+  conflicts: [],
+  summary: "",
+  endingHook: "",
+  snapshotPath: "",
+  memorySynced: true,
+  memorySyncedAt: "",
+});
 
 describe("loadReviewEvidence", () => {
-  beforeEach(() => vi.clearAllMocks());
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockCognition.mockResolvedValue(null);
+    mockCharacterStates.mockResolvedValue({ characters: [], lastUpdated: "" });
+    mockForeshadowing.mockResolvedValue({ items: [], lastUpdated: "" });
+    mockListSnapshots.mockResolvedValue([]);
+    mockLoadSnapshot.mockResolvedValue(null);
+  });
 
   it("返回空真源当记忆中心没有任何派生数据", async () => {
-    const { loadCognitionState } =
-      await import("@/lib/novel/character-cognition");
-    const { loadCharacterStates } = await import("@/lib/novel/character-state");
-    const { loadForeshadowingTracker } =
-      await import("@/lib/novel/foreshadowing-tracker");
-    const { listSnapshots, loadSnapshot } =
-      await import("@/lib/novel/chapter-ingest");
-    vi.mocked(loadCognitionState).mockResolvedValueOnce(null);
-    vi.mocked(loadCharacterStates).mockResolvedValueOnce({
-      characters: [],
-      lastUpdated: "",
-    });
-    vi.mocked(loadForeshadowingTracker).mockResolvedValueOnce({
-      items: [],
-      lastUpdated: "",
-    });
-    vi.mocked(listSnapshots).mockResolvedValueOnce([]);
-    vi.mocked(loadSnapshot).mockResolvedValueOnce(null);
-
     const evidence = await loadReviewEvidence("/proj");
     expect(evidence.cognition).toBeNull();
     expect(evidence.characterStates.characters).toHaveLength(0);
@@ -77,22 +86,78 @@ describe("loadReviewEvidence", () => {
   });
 
   it("当全部读取成功时聚合返回", async () => {
-    const { loadCognitionState } =
-      await import("@/lib/novel/character-cognition");
-    vi.mocked(loadCognitionState).mockResolvedValueOnce({
+    mockCognition.mockResolvedValueOnce({
       characters: [{ character: "李雷", knows: ["暗杀计划"], doesNotKnow: [] }],
       readerKnows: [],
       lastUpdatedChapter: 3,
     });
+    mockCharacterStates.mockResolvedValueOnce({
+      characters: [
+        {
+          characterName: "李雷",
+          currentLocation: "客栈",
+          status: "正常",
+          equipment: [],
+          abilities: [],
+          relationships: {},
+          lastUpdatedChapter: 3,
+          lastUpdatedAt: "",
+        },
+      ],
+      lastUpdated: "",
+    });
+    mockForeshadowing.mockResolvedValueOnce({
+      items: [
+        {
+          id: "fs1",
+          name: "黑令符",
+          description: "暗杀信物",
+          status: "planted",
+          plantedChapter: 2,
+          advancedChapters: [],
+          relatedCharacters: [],
+          relatedEvents: [],
+          notes: "",
+        },
+      ],
+      lastUpdated: "",
+    });
+    mockListSnapshots.mockResolvedValueOnce([3]);
+    mockLoadSnapshot.mockResolvedValueOnce(aSnapshot(3));
+
     const evidence = await loadReviewEvidence("/proj");
     expect(evidence.cognition?.characters[0].character).toBe("李雷");
+    expect(evidence.characterStates.characters[0].characterName).toBe("李雷");
+    expect(evidence.foreshadowing.items[0].name).toBe("黑令符");
+    expect(evidence.previousSnapshot?.chapterNumber).toBe(3);
     expect(evidence.rawLoadError).toBe(false);
   });
 
   it("读取异常时 rawLoadError=true 但不抛出", async () => {
-    const { loadCognitionState } =
-      await import("@/lib/novel/character-cognition");
-    vi.mocked(loadCognitionState).mockRejectedValueOnce(new Error("文件损坏"));
+    mockCognition.mockRejectedValueOnce(new Error("文件损坏"));
+
+    const evidence = await loadReviewEvidence("/proj");
+    expect(evidence.rawLoadError).toBe(true);
+    expect(evidence.cognition).toBeNull();
+    expect(evidence.characterStates.characters).toHaveLength(0);
+    expect(evidence.foreshadowing.items).toHaveLength(0);
+  });
+
+  it("listSnapshots 返回负数时只取正数章节快照", async () => {
+    mockCognition.mockResolvedValueOnce(null);
+    mockListSnapshots.mockResolvedValueOnce([-2, -1, 3]);
+    mockLoadSnapshot.mockResolvedValueOnce(aSnapshot(3));
+
+    const evidence = await loadReviewEvidence("/proj");
+    expect(evidence.previousSnapshot?.chapterNumber).toBe(3);
+    expect(evidence.rawLoadError).toBe(false);
+  });
+
+  it("多个模块同时失败时 rawLoadError 仍为 true", async () => {
+    mockCognition.mockRejectedValueOnce(new Error("损坏1"));
+    mockCharacterStates.mockRejectedValueOnce(new Error("损坏2"));
+    mockForeshadowing.mockRejectedValueOnce(new Error("损坏3"));
+
     const evidence = await loadReviewEvidence("/proj");
     expect(evidence.rawLoadError).toBe(true);
     expect(evidence.cognition).toBeNull();

+ 86 - 44
src/lib/agent/skills/draft-review-skill.ts

@@ -1,62 +1,104 @@
-import { loadCognitionState, type CognitionState } from "@/lib/novel/character-cognition"
-import { loadCharacterStates, type CharacterStateStore } from "@/lib/novel/character-state"
-import { loadForeshadowingTracker, type ForeshadowingStore } from "@/lib/novel/foreshadowing-tracker"
-import { listSnapshots, loadSnapshot, type ChapterSnapshot } from "@/lib/novel/chapter-ingest"
+import {
+  loadCognitionState,
+  type CognitionState,
+} from "@/lib/novel/character-cognition";
+import {
+  loadCharacterStates,
+  type CharacterStateStore,
+} from "@/lib/novel/character-state";
+import {
+  loadForeshadowingTracker,
+  type ForeshadowingStore,
+} from "@/lib/novel/foreshadowing-tracker";
+import {
+  listSnapshots,
+  loadSnapshot,
+  type ChapterSnapshot,
+} from "@/lib/novel/chapter-ingest";
 
-export type DeviationType = "cognition" | "state" | "continuity" | "foreshadowing"
-export type DeviationSeverity = "high" | "mid" | "low"
+export type DeviationType =
+  "cognition" | "state" | "continuity" | "foreshadowing";
+export type DeviationSeverity = "high" | "mid" | "low";
 
 export interface Deviation {
-  id: string
-  type: DeviationType
-  location: string
-  originalText: string
-  expected: string
-  memoryEvidence: string
-  severity: DeviationSeverity
-  repairAction?: string
+  id: string;
+  type: DeviationType;
+  location: string;
+  originalText: string;
+  expected: string;
+  memoryEvidence: string;
+  severity: DeviationSeverity;
+  repairAction?: string;
 }
 
 export interface DraftReviewInput {
-  projectPath: string
-  draftChapterText: string
-  draftChapterNumber: number
-  mode: "full" | "incremental"
-  previousRound?: DraftReviewResult
+  projectPath: string;
+  draftChapterText: string;
+  draftChapterNumber: number;
+  mode: "full" | "incremental";
+  previousRound?: DraftReviewResult;
 }
 
 export interface DraftReviewResult {
-  deviations: Deviation[]
-  revisedDraft: string
-  repairSummary: string
-  retryRound: number
-  truncated: boolean
+  deviations: Deviation[];
+  revisedDraft: string;
+  repairSummary: string;
+  retryRound: number;
+  truncated: boolean;
 }
 
 export interface ReviewEvidence {
-  cognition: CognitionState | null
-  characterStates: CharacterStateStore
-  foreshadowing: ForeshadowingStore
-  previousSnapshot: ChapterSnapshot | null
-  internalConflict: boolean
-  rawLoadError: boolean
+  cognition: CognitionState | null;
+  characterStates: CharacterStateStore;
+  foreshadowing: ForeshadowingStore;
+  previousSnapshot: ChapterSnapshot | null;
+  /** 派生数据内部矛盾标志,Task 3/4 会扩展检测逻辑 */
+  internalConflict: boolean;
+  rawLoadError: boolean;
 }
 
-export async function loadReviewEvidence(projectPath: string): Promise<ReviewEvidence> {
-  let cognition: CognitionState | null = null
-  let characterStates: CharacterStateStore = { characters: [], lastUpdated: "" }
-  let foreshadowing: ForeshadowingStore = { items: [], lastUpdated: "" }
-  let previousSnapshot: ChapterSnapshot | null = null
-  let rawLoadError = false
+export async function loadReviewEvidence(
+  projectPath: string,
+): Promise<ReviewEvidence> {
+  let cognition: CognitionState | null = null;
+  let characterStates: CharacterStateStore = {
+    characters: [],
+    lastUpdated: "",
+  };
+  let foreshadowing: ForeshadowingStore = { items: [], lastUpdated: "" };
+  let previousSnapshot: ChapterSnapshot | null = null;
+  let rawLoadError = false;
 
-  try { cognition = await loadCognitionState(projectPath) } catch { rawLoadError = true }
-  try { characterStates = await loadCharacterStates(projectPath) } catch { rawLoadError = true }
-  try { foreshadowing = await loadForeshadowingTracker(projectPath) } catch { rawLoadError = true }
   try {
-    const numbers = await listSnapshots(projectPath)
-    const valid = numbers.filter((n) => n > 0)
-    if (valid.length > 0) previousSnapshot = await loadSnapshot(projectPath, Math.max(...valid))
-  } catch { rawLoadError = true }
+    cognition = await loadCognitionState(projectPath);
+  } catch {
+    rawLoadError = true;
+  }
+  try {
+    characterStates = await loadCharacterStates(projectPath);
+  } catch {
+    rawLoadError = true;
+  }
+  try {
+    foreshadowing = await loadForeshadowingTracker(projectPath);
+  } catch {
+    rawLoadError = true;
+  }
+  try {
+    const numbers = await listSnapshots(projectPath);
+    const valid = numbers.filter((n) => n > 0);
+    if (valid.length > 0)
+      previousSnapshot = await loadSnapshot(projectPath, Math.max(...valid));
+  } catch {
+    rawLoadError = true;
+  }
 
-  return { cognition, characterStates, foreshadowing, previousSnapshot, internalConflict: false, rawLoadError }
+  return {
+    cognition,
+    characterStates,
+    foreshadowing,
+    previousSnapshot,
+    internalConflict: false,
+    rawLoadError,
+  };
 }