Просмотр исходного кода

refactor(test): 新测试里移除硬编码的 maxTokens / temperature 数字

此前 Phase 5 专项测试和 revise-foundation 测试按仓库历史惯例在 client.defaults
里写了 temperature: 0.7 / maxTokens: 4096 / thinkingBudget: 0 等数字。这些值:
- 对测试本身无意义——chat 方法被 vi.spyOn 拦截,defaults 运行时不被读取
- 会误导阅读者以为这是生产推荐配置,尤其 maxTokens: 4096 真被抄到生产会导致
  一章写到一半被截断(CLAUDE.md 明令禁止的 maxTokens 回归)

改成 `as unknown as LLMClient` 的 stub cast + 注释说明,保留类型满足度,
去掉所有具体数字。现有 architect.test.ts 其它测试里的老硬编码块不动,这次
只收敛新加的代码。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fanghanjun 6 месяцев назад
Родитель
Сommit
cf5a38d17a

+ 26 - 53
packages/core/src/__tests__/architect.test.ts

@@ -1,6 +1,7 @@
 import { afterEach, describe, expect, it, vi } from "vitest";
 import { ArchitectAgent } from "../agents/architect.js";
 import type { BookConfig } from "../models/book.js";
+import type { LLMClient } from "../llm/provider.js";
 
 const ZERO_USAGE = {
   promptTokens: 0,
@@ -708,35 +709,37 @@ describe("ArchitectAgent", () => {
 
   // ---- Phase 5 段落式架构稿专项 ----
 
-  it("generateFoundation parses story_frame / volume_map / roles sections", async () => {
-    const agent = new ArchitectAgent({
+  // 测试 stub:chat 会被 vi.spyOn 拦截,client.defaults 运行时不会被读取。
+  // 故意不填 temperature / maxTokens 等数字——避免在测试里留下"推荐配置"的
+  // 错误示范(maxTokens 填错会误导后续抄到生产,触发 CLAUDE.md 禁止的
+  // maxTokens 回归)。只保留类型要求的身份字段。
+  const buildPhase5Agent = (): ArchitectAgent =>
+    new ArchitectAgent({
       client: {
         provider: "openai",
         apiFormat: "chat",
         stream: false,
-        defaults: {
-          temperature: 0.7,
-          maxTokens: 4096,
-          thinkingBudget: 0, maxTokensCap: null,
-          extra: {},
-        },
-      },
+      } as unknown as LLMClient,
       model: "test-model",
       projectRoot: process.cwd(),
     });
 
-    const book: BookConfig = {
-      id: "phase5-book",
-      title: "测试书",
-      platform: "qidian",
-      genre: "xuanhuan",
-      status: "active",
-      targetChapters: 50,
-      chapterWordCount: 3000,
-      language: "zh",
-      createdAt: "2026-04-19T00:00:00.000Z",
-      updatedAt: "2026-04-19T00:00:00.000Z",
-    };
+  const phase5Book = (): BookConfig => ({
+    id: "phase5-book",
+    title: "测试书",
+    platform: "qidian",
+    genre: "xuanhuan",
+    status: "active",
+    targetChapters: 50,
+    chapterWordCount: 3000,
+    language: "zh",
+    createdAt: "2026-04-19T00:00:00.000Z",
+    updatedAt: "2026-04-19T00:00:00.000Z",
+  });
+
+  it("generateFoundation parses story_frame / volume_map / roles sections", async () => {
+    const agent = buildPhase5Agent();
+    const book = phase5Book();
 
     vi.spyOn(agent as unknown as { chat: (...args: unknown[]) => Promise<unknown> }, "chat")
       .mockResolvedValue({
@@ -796,22 +799,7 @@ describe("ArchitectAgent", () => {
     const { tmpdir } = await import("node:os");
     const { join } = await import("node:path");
 
-    const agent = new ArchitectAgent({
-      client: {
-        provider: "openai",
-        apiFormat: "chat",
-        stream: false,
-        defaults: {
-          temperature: 0.7,
-          maxTokens: 4096,
-          thinkingBudget: 0, maxTokensCap: null,
-          extra: {},
-        },
-      },
-      model: "test-model",
-      projectRoot: process.cwd(),
-    });
-
+    const agent = buildPhase5Agent();
     const tmpDir = await mkdtemp(join(tmpdir(), "inkos-arch-test-"));
     try {
       await agent.writeFoundationFiles(tmpDir, {
@@ -846,22 +834,7 @@ describe("ArchitectAgent", () => {
     const { tmpdir } = await import("node:os");
     const { join } = await import("node:path");
 
-    const agent = new ArchitectAgent({
-      client: {
-        provider: "openai",
-        apiFormat: "chat",
-        stream: false,
-        defaults: {
-          temperature: 0.7,
-          maxTokens: 4096,
-          thinkingBudget: 0, maxTokensCap: null,
-          extra: {},
-        },
-      },
-      model: "test-model",
-      projectRoot: process.cwd(),
-    });
-
+    const agent = buildPhase5Agent();
     const tmpDir = await mkdtemp(join(tmpdir(), "inkos-arch-legacy-test-"));
     try {
       await agent.writeFoundationFiles(tmpDir, {

+ 38 - 66
packages/core/src/__tests__/revise-foundation.test.ts

@@ -2,6 +2,31 @@ import { describe, it, expect, vi, afterEach } from "vitest";
 import { ArchitectAgent } from "../agents/architect.js";
 import { FoundationReviewerAgent } from "../agents/foundation-reviewer.js";
 import type { ArchitectOutput } from "../agents/architect.js";
+import type { BookConfig } from "../models/book.js";
+import type { LLMClient } from "../llm/provider.js";
+
+// 测试 stub:chat 会被 vi.spyOn 拦截,client.defaults 运行时根本不会被读取。
+// 故意不填 temperature / maxTokens 等数字,避免在测试里留下"推荐配置"的错误
+// 示范(尤其 maxTokens —— 填错会误导后续抄到生产,触发 CLAUDE.md 禁止的
+// maxTokens 回归)。只保留类型要求的身份字段。
+const TEST_CLIENT: LLMClient = {
+  provider: "openai",
+  apiFormat: "chat",
+  stream: false,
+} as unknown as LLMClient;
+
+const buildArchitect = (): ArchitectAgent =>
+  new ArchitectAgent({
+    client: TEST_CLIENT,
+    model: "test-model",
+    projectRoot: process.cwd(),
+  });
+
+const testBook = (): BookConfig => ({
+  id: "test-book", title: "测试书", platform: "qidian", genre: "xuanhuan",
+  status: "active", targetChapters: 50, chapterWordCount: 3000, language: "zh",
+  createdAt: "2026-04-19T00:00:00.000Z", updatedAt: "2026-04-19T00:00:00.000Z",
+});
 
 describe("architect generateFoundation with reviseFrom option", () => {
   afterEach(() => {
@@ -9,22 +34,7 @@ describe("architect generateFoundation with reviseFrom option", () => {
   });
 
   it("injects legacy content into the system prompt when reviseFrom is supplied", async () => {
-    const agent = new ArchitectAgent({
-      client: {
-        provider: "openai",
-        apiFormat: "chat",
-        stream: false,
-        defaults: {
-          temperature: 0.7,
-          maxTokens: 4096,
-          thinkingBudget: 0, maxTokensCap: null,
-          extra: {},
-        },
-      },
-      model: "test-model",
-      projectRoot: process.cwd(),
-    });
-
+    const agent = buildArchitect();
     const chatSpy = vi.spyOn(agent as unknown as { chat: (...args: unknown[]) => Promise<unknown> }, "chat")
       .mockResolvedValue({
         content: [
@@ -54,24 +64,15 @@ describe("architect generateFoundation with reviseFrom option", () => {
         usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
       });
 
-    await agent.generateFoundation(
-      {
-        id: "test-book", title: "测试书", platform: "qidian", genre: "xuanhuan",
-        status: "active", targetChapters: 50, chapterWordCount: 3000, language: "zh",
-        createdAt: "2026-04-19T00:00:00.000Z", updatedAt: "2026-04-19T00:00:00.000Z",
+    await agent.generateFoundation(testBook(), undefined, undefined, {
+      reviseFrom: {
+        storyBible: "- 旧世界观:架空唐代\n- 旧主角:林辞",
+        volumeOutline: "## 第一卷\n- 1. 主角登场",
+        bookRules: "## 规则\n- 禁现代词",
+        characterMatrix: "林辞 - 主角",
+        userFeedback: "升级到段落式架构稿",
       },
-      undefined,
-      undefined,
-      {
-        reviseFrom: {
-          storyBible: "- 旧世界观:架空唐代\n- 旧主角:林辞",
-          volumeOutline: "## 第一卷\n- 1. 主角登场",
-          bookRules: "## 规则\n- 禁现代词",
-          characterMatrix: "林辞 - 主角",
-          userFeedback: "升级到段落式架构稿",
-        },
-      },
-    );
+    });
 
     const systemMsg = (chatSpy.mock.calls[0]?.[0] as Array<{ role: string; content: string }>)[0]!;
     expect(systemMsg.content).toContain("把一本已有书的架构稿从条目式升级");
@@ -80,22 +81,7 @@ describe("architect generateFoundation with reviseFrom option", () => {
   });
 
   it("does not inject revisePrompt when reviseFrom is absent", async () => {
-    const agent = new ArchitectAgent({
-      client: {
-        provider: "openai",
-        apiFormat: "chat",
-        stream: false,
-        defaults: {
-          temperature: 0.7,
-          maxTokens: 4096,
-          thinkingBudget: 0, maxTokensCap: null,
-          extra: {},
-        },
-      },
-      model: "test-model",
-      projectRoot: process.cwd(),
-    });
-
+    const agent = buildArchitect();
     const chatSpy = vi.spyOn(agent as unknown as { chat: (...args: unknown[]) => Promise<unknown> }, "chat")
       .mockResolvedValue({
         content: [
@@ -108,11 +94,7 @@ describe("architect generateFoundation with reviseFrom option", () => {
         usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
       });
 
-    await agent.generateFoundation({
-      id: "test-book", title: "测试", platform: "qidian", genre: "xuanhuan",
-      status: "active", targetChapters: 50, chapterWordCount: 3000, language: "zh",
-      createdAt: "2026-04-19T00:00:00.000Z", updatedAt: "2026-04-19T00:00:00.000Z",
-    });
+    await agent.generateFoundation(testBook());
 
     const systemMsg = (chatSpy.mock.calls[0]?.[0] as Array<{ role: string; content: string }>)[0]!;
     expect(systemMsg.content).not.toContain("把一本已有书的架构稿从条目式升级");
@@ -163,22 +145,12 @@ describe("pipeline.reviseFoundation", () => {
         passed: true, totalScore: 90, dimensions: [], overallFeedback: "ok",
       } as unknown as Awaited<ReturnType<FoundationReviewerAgent["review"]>>);
 
-      // Minimal config for PipelineRunner
+      // Minimal config for PipelineRunner — 共用 TEST_CLIENT 避免重复。
       const state = new StateManager(root);
       const runner = new PipelineRunner({
         state,
         projectRoot: root,
-        client: {
-          provider: "openai",
-          apiFormat: "chat",
-          stream: false,
-          defaults: {
-            temperature: 0.7,
-            maxTokens: 4096,
-            thinkingBudget: 0, maxTokensCap: null,
-            extra: {},
-          },
-        },
+        client: TEST_CLIENT,
         model: "test-model",
       } as unknown as ConstructorParameters<typeof PipelineRunner>[0]);