|
|
@@ -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();
|
|
|
+ });
|
|
|
+});
|