浏览代码

fix(cli,core): align interact and minimax regressions

Ma 6 月之前
父节点
当前提交
ffa0872b5e

+ 64 - 41
packages/cli/src/__tests__/cli-integration.test.ts

@@ -12,16 +12,29 @@ const cliEntry = resolve(cliDir, "dist", "index.js");
 
 
 let projectDir: string;
 let projectDir: string;
 
 
+function buildTestEnv(overrides?: Record<string, string>) {
+  const baseEnv = Object.fromEntries(
+    Object.entries(process.env).filter(([key]) =>
+      !key.startsWith("INKOS_")
+      && !key.startsWith("OPENAI_")
+      && !key.startsWith("ANTHROPIC_")
+      && key !== "TAVILY_API_KEY",
+    ),
+  );
+
+  return {
+    ...baseEnv,
+    // Prevent global config from leaking into tests
+    HOME: projectDir,
+    ...overrides,
+  };
+}
+
 function run(args: string[], options?: { env?: Record<string, string> }): string {
 function run(args: string[], options?: { env?: Record<string, string> }): string {
   return execFileSync("node", [cliEntry, ...args], {
   return execFileSync("node", [cliEntry, ...args], {
     cwd: projectDir,
     cwd: projectDir,
     encoding: "utf-8",
     encoding: "utf-8",
-    env: {
-      ...process.env,
-      // Prevent global config from leaking into tests
-      HOME: projectDir,
-      ...options?.env,
-    },
+    env: buildTestEnv(options?.env),
     timeout: 10_000,
     timeout: 10_000,
   });
   });
 }
 }
@@ -31,7 +44,7 @@ function runStderr(args: string[], options?: { env?: Record<string, string> }):
     const stdout = execFileSync("node", [cliEntry, ...args], {
     const stdout = execFileSync("node", [cliEntry, ...args], {
       cwd: projectDir,
       cwd: projectDir,
       encoding: "utf-8",
       encoding: "utf-8",
-      env: { ...process.env, HOME: projectDir, ...options?.env },
+      env: buildTestEnv(options?.env),
       timeout: 10_000,
       timeout: 10_000,
     });
     });
     return { stdout, stderr: "", exitCode: 0 };
     return { stdout, stderr: "", exitCode: 0 };
@@ -195,48 +208,58 @@ describe("CLI integration", () => {
     it("returns structured JSON for shared interaction mode switches", async () => {
     it("returns structured JSON for shared interaction mode switches", async () => {
       const initialized = await stat(join(projectDir, "inkos.json")).then(() => true).catch(() => false);
       const initialized = await stat(join(projectDir, "inkos.json")).then(() => true).catch(() => false);
       if (!initialized) run(["init"]);
       if (!initialized) run(["init"]);
-      await writeFile(
-        join(projectDir, ".env"),
-        Object.entries(failingLlmEnv).map(([key, value]) => `${key}=${value}`).join("\n"),
-        "utf-8",
-      );
-      const output = run(["interact", "--json", "--message", "切换到全自动"]);
-      const data = JSON.parse(output);
+      const envPath = join(projectDir, ".env");
+      const originalEnv = await readFile(envPath, "utf-8");
+      try {
+        await writeFile(
+          envPath,
+          Object.entries(failingLlmEnv).map(([key, value]) => `${key}=${value}`).join("\n"),
+          "utf-8",
+        );
+        const output = run(["interact", "--json", "--message", "切换到全自动"]);
+        const data = JSON.parse(output);
 
 
-      expect(data.request.intent).toBe("switch_mode");
-      expect(data.request.mode).toBe("auto");
-      expect(data.session.automationMode).toBe("auto");
+        expect(data.request.intent).toBe("switch_mode");
+        expect(data.request.mode).toBe("auto");
+        expect(data.session.automationMode).toBe("auto");
+      } finally {
+        await writeFile(envPath, originalEnv, "utf-8");
+      }
     });
     });
 
 
     it("binds the requested book when interact is called with --book", async () => {
     it("binds the requested book when interact is called with --book", async () => {
       const initialized = await stat(join(projectDir, "inkos.json")).then(() => true).catch(() => false);
       const initialized = await stat(join(projectDir, "inkos.json")).then(() => true).catch(() => false);
       if (!initialized) run(["init"]);
       if (!initialized) run(["init"]);
-      await writeFile(
-        join(projectDir, ".env"),
-        Object.entries(failingLlmEnv).map(([key, value]) => `${key}=${value}`).join("\n"),
-        "utf-8",
-      );
-      const state = new StateManager(projectDir);
-      await state.saveBookConfig("harbor", {
-        id: "harbor",
-        title: "Harbor",
-        platform: "tomato",
-        genre: "other",
-        status: "active",
-        targetChapters: 20,
-        chapterWordCount: 3000,
-        createdAt: "2026-04-07T00:00:00.000Z",
-        updatedAt: "2026-04-07T00:00:00.000Z",
-      });
-
-      const output = run(["interact", "--json", "--book", "harbor", "--message", "/books"]);
-      const data = JSON.parse(output);
+      const envPath = join(projectDir, ".env");
+      const originalEnv = await readFile(envPath, "utf-8");
+      try {
+        await writeFile(
+          envPath,
+          Object.entries(failingLlmEnv).map(([key, value]) => `${key}=${value}`).join("\n"),
+          "utf-8",
+        );
+        const state = new StateManager(projectDir);
+        await state.saveBookConfig("harbor", {
+          id: "harbor",
+          title: "Harbor",
+          platform: "tomato",
+          genre: "other",
+          status: "active",
+          targetChapters: 20,
+          chapterWordCount: 3000,
+          createdAt: "2026-04-07T00:00:00.000Z",
+          updatedAt: "2026-04-07T00:00:00.000Z",
+        });
 
 
-      expect(data.session.activeBookId).toBe("harbor");
+        const output = run(["interact", "--json", "--book", "harbor", "--message", "/books"]);
+        const data = JSON.parse(output);
 
 
-      // Clean up harbor book and session so subsequent tests start with an empty project
-      await rm(join(projectDir, "books", "harbor"), { recursive: true, force: true });
-      await rm(join(projectDir, ".inkos-session.json"), { force: true }).catch(() => {});
+        expect(data.session.activeBookId).toBe("harbor");
+      } finally {
+        await writeFile(envPath, originalEnv, "utf-8");
+        await rm(join(projectDir, "books", "harbor"), { recursive: true, force: true });
+        await rm(join(projectDir, ".inkos-session.json"), { force: true }).catch(() => {});
+      }
     });
     });
   });
   });
 
 

+ 1 - 1
packages/cli/src/commands/interact.ts

@@ -77,7 +77,7 @@ export function createInteractCommand(hooks: InteractCommandHooks = {}): Command
         ? await hooks.createTools(projectRoot)
         ? await hooks.createTools(projectRoot)
         : hooks.runInteraction
         : hooks.runInteraction
           ? ({} as InteractionRuntimeTools)
           ? ({} as InteractionRuntimeTools)
-          : await createInteractionTools(projectRoot);
+          : await createInteractionTools(projectRoot, undefined, { requireApiKey: false });
       const runInteraction = hooks.runInteraction ?? processProjectInteractionInput;
       const runInteraction = hooks.runInteraction ?? processProjectInteractionInput;
       const result = await runInteraction({
       const result = await runInteraction({
         projectRoot,
         projectRoot,

+ 2 - 1
packages/cli/src/tui/tools.ts

@@ -40,8 +40,9 @@ export { createInteractionToolsFromDepsCompat as createInteractionToolsFromDeps
 export async function createInteractionTools(
 export async function createInteractionTools(
   projectRoot: string,
   projectRoot: string,
   hooks?: CliInteractionToolHooks,
   hooks?: CliInteractionToolHooks,
+  options?: { readonly requireApiKey?: boolean },
 ): Promise<InteractionRuntimeTools> {
 ): Promise<InteractionRuntimeTools> {
-  const config = await loadConfig({ projectRoot });
+  const config = await loadConfig({ projectRoot, requireApiKey: options?.requireApiKey });
   const pipeline = new PipelineRunner(buildPipelineConfig(config, projectRoot));
   const pipeline = new PipelineRunner(buildPipelineConfig(config, projectRoot));
   const state = new StateManager(projectRoot);
   const state = new StateManager(projectRoot);
   return createInteractionToolsFromDeps(pipeline, state, hooks);
   return createInteractionToolsFromDeps(pipeline, state, hooks);

+ 2 - 2
packages/core/src/__tests__/service-presets-regression.test.ts

@@ -9,9 +9,9 @@ describe("service-presets regression", () => {
       expect(preset!.baseUrl).toBe("https://api.minimaxi.com/anthropic");
       expect(preset!.baseUrl).toBe("https://api.minimaxi.com/anthropic");
     });
     });
 
 
-    it("uses openai-completions api format", () => {
+    it("uses anthropic-messages api format", () => {
       const preset = resolveServicePreset("minimax");
       const preset = resolveServicePreset("minimax");
-      expect(preset!.api).toBe("openai-completions");
+      expect(preset!.api).toBe("anthropic-messages");
     });
     });
 
 
     it("has knownModels with all 7 MiniMax models", () => {
     it("has knownModels with all 7 MiniMax models", () => {

+ 4 - 4
packages/core/src/__tests__/service-resolver-regression.test.ts

@@ -47,15 +47,15 @@ describe("resolveServiceModel regression — preset baseUrl override", () => {
   it("uses preset baseUrl, not pi-ai built-in baseUrl", async () => {
   it("uses preset baseUrl, not pi-ai built-in baseUrl", async () => {
     const result = await resolveServiceModel("minimax", "MiniMax-M2.7", root);
     const result = await resolveServiceModel("minimax", "MiniMax-M2.7", root);
 
 
-    // Must use our preset (api.minimaxi.com), NOT pi-ai's (api.minimax.io/anthropic)
-    expect(result.model.baseUrl).toBe("https://api.minimaxi.com/v1");
+    // Must use our preset (api.minimaxi.com/anthropic), NOT pi-ai's (api.minimax.io/anthropic)
+    expect(result.model.baseUrl).toBe("https://api.minimaxi.com/anthropic");
   });
   });
 
 
   it("uses preset api format, not pi-ai built-in api format", async () => {
   it("uses preset api format, not pi-ai built-in api format", async () => {
     const result = await resolveServiceModel("minimax", "MiniMax-M2.7", root);
     const result = await resolveServiceModel("minimax", "MiniMax-M2.7", root);
 
 
-    // Must use openai-completions (our preset), NOT anthropic-messages (pi-ai)
-    expect(result.model.api).toBe("openai-completions");
+    // Must use our preset semantics instead of pi-ai's built-in minimax route.
+    expect(result.model.api).toBe("anthropic-messages");
   });
   });
 
 
   it("inherits metadata from pi-ai (reasoning, cost, contextWindow)", async () => {
   it("inherits metadata from pi-ai (reasoning, cost, contextWindow)", async () => {