import { createServer, type Server } from 'node:http'; import { mkdtemp, mkdir, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { CodexRuntime } from '../codexRuntime'; import { chatRouter } from './routerService'; import { applyProvider, clearProvider } from '../providerService'; /** * 集成冒烟(不进 npm test,跑法:npm run smoke:codex): * 真 codex.exe + 内置路由层 + 只说 Chat Completions 的本地 mock。 * 钉住三件事: * 1. applyProvider 对 chat-only 端点自动起路由,codex 的 base_url 指向本地路由; * 2. codex 发出的 Responses 请求经路由翻译后,上游收到合法 Chat 请求(含鉴权透传); * 3. 两轮对话第二轮的全量历史里带着第一轮提问与回复(store=false 的无状态语义经桥不丢)。 */ const API_KEY = 'sk-router-smoke-secret-1234567890'; let upstream: Server | null = null; let upstreamUrl = ''; let upstreamPosts: string[] = []; let root = ''; let homes = 0; beforeAll(async () => { upstream = createServer((req, res) => { const chunks: Buffer[] = []; req.on('data', (c: Buffer) => chunks.push(c)); req.on('end', () => { const route = req.url?.split('?')[0] ?? ''; if (req.method !== 'POST' || (route !== '/v1/chat/completions' && route !== '/chat/completions')) { res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'no route' } })); return; } upstreamPosts.push(Buffer.concat(chunks).toString('utf8')); res.writeHead(200, { 'content-type': 'text/event-stream' }); res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: '路由回复' } }] })}\n\n`); res.write(`data: ${JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] })}\n\n`); res.write(`data: ${JSON.stringify({ choices: [], usage: { prompt_tokens: 11, completion_tokens: 4, total_tokens: 15 } })}\n\n`); res.write('data: [DONE]\n\n'); res.end(); }); }); await new Promise((resolve) => upstream?.listen(0, '127.0.0.1', resolve)); const address = upstream.address(); const port = typeof address === 'object' && address ? address.port : 0; upstreamUrl = `http://127.0.0.1:${port}/v1`; root = await mkdtemp(join(tmpdir(), 'zsjz-router-')); }, 120_000); afterAll(async () => { await chatRouter.stop(); await new Promise((resolve) => (upstream ? upstream.close(() => resolve()) : resolve())); await rm(root, { recursive: true, force: true }).catch(() => undefined); }); describe('内置路由层:codex → 路由 → Chat 上游', () => { it( 'chat-only 端点自动桥接,两轮对话全量历史经路由不丢', async () => { const home = join(root, `home-${++homes}`); await mkdir(home, { recursive: true }); const runtime = new CodexRuntime({ codexHome: home }); try { // 走应用真实链路:探测判 chat-only → 起路由 → spec.baseUrl=路由地址 const applied = await applyProvider(runtime, { modelId: 'chat-model', name: '桥接模型', baseUrl: upstreamUrl, apiKey: API_KEY, modelRecordId: 7, }); expect(applied.bridged).toBe(true); // 落盘的是真实上游地址,路由地址不落盘 expect(applied.baseUrl).toBe(upstreamUrl); expect(runtime.status.state).toBe('ready'); expect(runtime.provider?.baseUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/v1$/u); expect(runtime.provider?.baseUrl).not.toBe(upstreamUrl); const config = await runtime.readConfig(); const entry = (config.model_providers as Record> | undefined)?.zsjz; expect(entry?.base_url).toBe(runtime.provider?.baseUrl); expect(entry?.wire_api).toBe('responses'); expect(JSON.stringify(config)).not.toContain(API_KEY); const threadId = await runtime.startThread({ cwd: root, approvalPolicy: 'never', sandbox: 'read-only' }); const first = await runtime.runTurn({ threadId, prompt: '第一句', approvalPolicy: 'never' }); expect(first.status).toBe('completed'); expect(first.text).toContain('路由回复'); const second = await runtime.runTurn({ threadId, prompt: '第二句', approvalPolicy: 'never' }); expect(second.status).toBe('completed'); // 应用时的探测会先直连上游打一发 /chat/completions(无 messages); // 桥接转发的请求必须恰好两轮,且都是合法 Chat 请求 const bridgePosts = upstreamPosts.filter((body) => body.includes('"messages"')); expect(bridgePosts.length).toBe(2); const firstRequest = JSON.parse(bridgePosts[0] ?? '{}') as { model?: string; stream?: boolean; messages?: Array<{ role: string; content: unknown }>; }; expect(firstRequest.model).toBe('chat-model'); expect(firstRequest.stream).toBe(true); expect(firstRequest.messages?.some((m) => m.role === 'developer')).toBe(true); expect(JSON.stringify(firstRequest.messages)).toContain('第一句'); const secondRequest = JSON.parse(bridgePosts[1] ?? '{}') as { messages?: Array<{ role: string; content: unknown }>; }; const serialized = JSON.stringify(secondRequest.messages); // store=false 的无状态语义:第二轮请求带全量历史(第一轮问、答 + 第二轮问) expect(serialized).toContain('第一句'); expect(serialized).toContain('路由回复'); expect(serialized).toContain('第二句'); // Responses 专有字段没有被透传给 Chat 上游 expect(serialized).not.toContain('prompt_cache_key'); expect(upstreamPosts.every((body) => !body.includes('ZSJZ_CODEX_API_KEY'))).toBe(true); } finally { await clearProvider(runtime); await runtime.dispose(); } }, 240_000, ); });