Bladeren bron

test(codex): 钉住客户端应用模型时交给运行时的最终参数

真机 smoke 用的是手搓 spec,覆盖不到「客户端 → toProviderSpec → 桥接 → runtime.applyProvider」
这一段组合,正是之前反复出问题的地方。补两条:

- 走 Chat 端点时交给运行时的 spec 必须是自定义 zsjz provider、base_url 指向本地桥接(不是上游地址),
  并带上单槽端点该有的 request_max_retries=0 / stream_max_retries=0 / stream_idle_timeout_ms=300s /
  supports_websockets=false;落盘的 applied.baseUrl 仍是上游真实地址。
- 上游没有 Chat 路由时直接抛错,且一次都不碰运行时(不会留下半个生效的坏配置)。

applyProvider 会落 provider.json,用 vi.mock 把 getCodexDataDir 指到临时目录 —— 注意目录只能在
mock 工厂里算一次,providerFile() 每次都调它,返回新目录就写不进同一处。

验证:vitest 115 passed(该文件 15 passed);tsc --noEmit 0 报错。
cc 1 week geleden
bovenliggende
commit
b15a7416d6
1 gewijzigde bestanden met toevoegingen van 72 en 1 verwijderingen
  1. 72 1
      ai-electron/electron/service/codex/providerService.test.ts

+ 72 - 1
ai-electron/electron/service/codex/providerService.test.ts

@@ -1,6 +1,12 @@
 import { createServer, type Server } from 'node:http';
-import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { mkdtempSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
+import { chatBridge } from './chatBridgeService';
+import type { CodexProviderSpec, CodexRuntime } from './codexRuntime';
 import {
+  applyProvider,
   baseUrlHint,
   describeDroppedKeys,
   normalizeBaseUrl,
@@ -9,6 +15,14 @@ import {
 } from './providerService';
 import type { ApplyProviderInput } from './types';
 
+/** applyProvider 会往 getCodexDataDir() 落 provider.json;挪到临时目录,绝不碰客户端真实数据目录 */
+vi.mock('./codexHome', async (importOriginal) => {
+  const actual = await importOriginal<typeof import('./codexHome')>();
+  // 目录只能算一次:providerFile() 每次都会调 getCodexDataDir(),给出不同路径就写不进同一处
+  const dir = mkdtempSync(join(tmpdir(), 'zsjz-provider-test-'));
+  return { ...actual, getCodexDataDir: () => dir };
+});
+
 let server: Server;
 let port = 0;
 let chatStatus = 404;
@@ -169,3 +183,60 @@ describe('probeEndpoint', () => {
     expect(result.detail).toContain('http://127.0.0.1:1/v1/chat/completions');
   });
 });
+
+/**
+ * 客户端真正走的是 applyProvider:探测 → 起桥接 → 把 spec 交给运行时重启子进程。
+ * 这里用假运行时把「交给 Codex 的最终参数」钉死 —— 真机 smoke 用的是手搓 spec,覆盖不到这一段。
+ */
+describe('applyProvider 交给运行时的 spec', () => {
+  class StubRuntime {
+    spec: CodexProviderSpec | null | undefined;
+    calls = 0;
+
+    async applyProvider(spec: CodexProviderSpec | null): Promise<void> {
+      this.calls += 1;
+      this.spec = spec;
+    }
+  }
+
+  it('一律自定义 provider + 桥接地址,并带上单槽端点该有的重试/超时开关', async () => {
+    chatStatus = 200;
+    const stub = new StubRuntime();
+    const upstream = `http://127.0.0.1:${port}/v1`;
+    const applied = await applyProvider(stub as unknown as CodexRuntime, {
+      modelId: 'qwen',
+      name: '自部署 Qwen',
+      baseUrl: upstream,
+      modelRecordId: 7,
+    });
+    try {
+      const spec = stub.spec;
+      expect(spec).not.toBeNull();
+      expect(spec?.id).toBe('zsjz');
+      expect(spec?.model).toBe('qwen');
+      // Codex 连的是本地桥接,不是上游
+      expect(spec?.baseUrl).toBe(chatBridge.info?.url);
+      expect(spec?.baseUrl).not.toBe(upstream);
+      expect(spec?.extra).toMatchObject({
+        request_max_retries: 0,
+        stream_max_retries: 0,
+        stream_idle_timeout_ms: 300_000,
+        supports_websockets: false,
+      });
+      // 落盘的是上游真实地址,便于页面展示
+      expect(applied).toMatchObject({ model: 'qwen', modelRecordId: '7', baseUrl: upstream, bridged: true });
+    } finally {
+      await chatBridge.stop();
+    }
+  });
+
+  it('上游没有 Chat 路由时直接报错,且不碰运行时', async () => {
+    chatStatus = 404;
+    const stub = new StubRuntime();
+    await expect(
+      applyProvider(stub as unknown as CodexRuntime, { modelId: 'qwen', baseUrl: `http://127.0.0.1:${port}/v1` }),
+    ).rejects.toThrow(/没有 \/chat\/completions/);
+    expect(stub.calls).toBe(0);
+    expect(stub.spec).toBeUndefined();
+  });
+});