| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- 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<void>((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<void>((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<string, Record<string, unknown>> | 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,
- );
- });
|