import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { chatBridge } from './chatBridgeService'; import { CodexRuntime, type CodexProviderSpec } from './codexRuntime'; /** * 真机复现(集成冒烟,不进 npm test):生产同一条链路打真实自部署端点。 * chatBridge(Responses→Chat 翻译) → 真实 codex app-server → 局域网 llama.cpp * * 存在的理由:客户端日志里每 ~15 秒一条 `request timed out` 重试、5 次后回落 HTTP, * 而桥接/超时/WebSocket/排队四个假设都用受控 mock 否掉了 —— 剩下的只能在这台真端点上验。 * 端点不可达时整组跳过,不让 smoke 变成网络探测。 * * 断言的是「健康的一轮应当是什么样」:上游只被问一次、Codex 一条重试都不产生、能拿到回复。 * * 跑法:npm run smoke:codex -- electron/service/codex/chatBridge.live.itest.ts */ const UPSTREAM_BASE = process.env.ZSJZ_LIVE_UPSTREAM ?? 'http://10.66.66.66:8080/v1'; const MODEL = process.env.ZSJZ_LIVE_MODEL ?? '/home/rdd/llama/model/Ornith-1.5-35B-Q5_K_M.gguf'; const HEALTH_URL = UPSTREAM_BASE.replace(/\/v\d+$/u, '') + '/health'; let home = ''; let cwd = ''; const bridgeLines: string[] = []; const codexLines: string[] = []; async function probe(url: string, init?: RequestInit): Promise<{ ok: boolean; ms: number; status: number | null; error: string }> { const startedAt = Date.now(); try { const res = await fetch(url, { ...init, signal: AbortSignal.timeout(5_000) }); return { ok: res.ok, ms: Date.now() - startedAt, status: res.status, error: '' }; } catch (error) { return { ok: false, ms: Date.now() - startedAt, status: null, error: error instanceof Error ? error.message : String(error) }; } } /** * 可达性必须在模块顶层测:describe.skipIf 在收集阶段就求值, * 放进 beforeAll 里会永远是初始值、整组被静默跳过。 */ const health = await probe(HEALTH_URL); const reachable = health.ok; beforeAll(async () => { home = await mkdtemp(join(tmpdir(), 'zsjz-live-home-')); cwd = await mkdtemp(join(tmpdir(), 'zsjz-live-cwd-')); chatBridge.setDiagnostic((line) => bridgeLines.push(line)); }); afterAll(async () => { await chatBridge.stop().catch(() => undefined); await rm(home, { recursive: true, force: true }).catch(() => undefined); await rm(cwd, { recursive: true, force: true }).catch(() => undefined); }); const retryLines = (): string[] => codexLines.filter((line) => /retrying sampling request/u.test(line)); const disconnectLines = (): string[] => bridgeLines.filter((line) => /提前断开|转发完成|上游 HTTP|不可达/u.test(line)); describe.skipIf(!reachable)('真机:自部署 Chat 端点跑一整轮', () => { it('上游只被问一次、Codex 不重试、拿到回复', async () => { const bridge = await chatBridge.start({ upstreamBaseUrl: UPSTREAM_BASE, headers: null }); const spec: CodexProviderSpec = { id: 'zsjz', name: '真机冒烟', baseUrl: bridge.url, model: MODEL }; const runtime = new CodexRuntime({ codexHome: home }); runtime.on('diagnostic', (line: string) => codexLines.push(line)); const startedAt = Date.now(); let text = ''; let failure = ''; try { await runtime.applyProvider(spec); const threadId = await runtime.startThread({ cwd }); const result = await runtime.runTurn({ threadId, prompt: '用一句话说明什么是资金流向分析', cwd, timeoutMs: 420_000 }); text = result.text; } catch (error) { failure = error instanceof Error ? error.message : String(error); } finally { await runtime.dispose(); } const elapsed = (Date.now() - startedAt) / 1000; console.log(`[真机] 耗时=${elapsed.toFixed(1)}s 失败=${failure || '无'}`); console.log(`[真机] 回复=${text.slice(0, 80)}`); console.log(`[真机] Codex 重试行=${retryLines().length}`); console.log('[真机] 桥接:\n ' + bridgeLines.join('\n ')); console.log('[真机] Codex 关键诊断:\n ' + disconnectLines().join('\n ')); expect(failure).toBe(''); expect(text.length).toBeGreaterThan(0); expect(retryLines().length).toBe(0); // 一次正常的轮次不该让桥接把同一份请求重发给上游 expect(bridgeLines.filter((line) => /起 round/u.test(line)).length).toBe(1); }, 480_000); });