| |
| |
| |
| |
| |
| |
|
|
| import fs from 'fs'; |
| import path from 'path'; |
|
|
| const LOG_DIR = '/tmp/cursor2api-openai-log-persistence'; |
| process.env.LOG_FILE_ENABLED = '1'; |
| process.env.LOG_DIR = LOG_DIR; |
|
|
| const { handleOpenAIChatCompletions, handleOpenAIResponses } = await import('../dist/openai-handler.js'); |
| const { clearAllLogs, getRequestSummaries } = await import('../dist/logger.js'); |
|
|
| let passed = 0; |
| let failed = 0; |
|
|
| function assert(condition, msg) { |
| if (!condition) throw new Error(msg || 'Assertion failed'); |
| } |
|
|
| function assertEqual(a, b, msg) { |
| const as = JSON.stringify(a); |
| const bs = JSON.stringify(b); |
| if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`); |
| } |
|
|
| function createCursorSseResponse(deltas) { |
| const encoder = new TextEncoder(); |
| const stream = new ReadableStream({ |
| start(controller) { |
| for (const delta of deltas) { |
| controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'text-delta', delta })}\n\n`)); |
| } |
| controller.close(); |
| }, |
| }); |
|
|
| return new Response(stream, { |
| status: 200, |
| headers: { 'Content-Type': 'text/event-stream' }, |
| }); |
| } |
|
|
| class MockResponse { |
| constructor() { |
| this.statusCode = 200; |
| this.headers = {}; |
| this.body = ''; |
| this.ended = false; |
| } |
|
|
| writeHead(statusCode, headers) { |
| this.statusCode = statusCode; |
| this.headers = { ...this.headers, ...headers }; |
| } |
|
|
| write(chunk) { |
| this.body += String(chunk); |
| return true; |
| } |
|
|
| end(chunk = '') { |
| this.body += String(chunk); |
| this.ended = true; |
| } |
|
|
| json(obj) { |
| this.writeHead(this.statusCode, { 'Content-Type': 'application/json' }); |
| this.end(JSON.stringify(obj)); |
| } |
|
|
| status(code) { |
| this.statusCode = code; |
| return this; |
| } |
| } |
|
|
| function resetLogs() { |
| clearAllLogs(); |
| fs.rmSync(LOG_DIR, { recursive: true, force: true }); |
| } |
|
|
| function readPersistedRecords() { |
| if (!fs.existsSync(LOG_DIR)) return []; |
| const files = fs.readdirSync(LOG_DIR) |
| .filter(name => name.endsWith('.jsonl')) |
| .sort(); |
| const rows = []; |
| for (const file of files) { |
| const lines = fs.readFileSync(path.join(LOG_DIR, file), 'utf8') |
| .split('\n') |
| .filter(Boolean); |
| for (const line of lines) { |
| rows.push(JSON.parse(line)); |
| } |
| } |
| return rows; |
| } |
|
|
| function latestSummary() { |
| return getRequestSummaries(10)[0]; |
| } |
|
|
| async function withMockCursor(deltas, fn) { |
| const originalFetch = global.fetch; |
| global.fetch = async () => createCursorSseResponse(deltas); |
| try { |
| await fn(); |
| } finally { |
| global.fetch = originalFetch; |
| } |
| } |
|
|
| async function runTest(name, fn) { |
| try { |
| resetLogs(); |
| await fn(); |
| console.log(` ✅ ${name}`); |
| passed++; |
| } catch (e) { |
| console.error(` ❌ ${name}`); |
| console.error(` ${e.message}`); |
| failed++; |
| } |
| } |
|
|
| console.log('\n📦 [1] OpenAI 成功请求日志持久化回归\n'); |
|
|
| await runTest('Chat Completions stream=true 会完成 summary 并落盘', async () => { |
| await withMockCursor(['Hello', ' world'], async () => { |
| const req = { |
| method: 'POST', |
| path: '/v1/chat/completions', |
| body: { |
| model: 'gpt-4.1', |
| stream: true, |
| messages: [{ role: 'user', content: 'Say hello' }], |
| }, |
| }; |
| const res = new MockResponse(); |
| await handleOpenAIChatCompletions(req, res); |
|
|
| assert(res.ended, '响应应结束'); |
| const summary = latestSummary(); |
| assert(summary, '应生成 summary'); |
| assertEqual(summary.path, '/v1/chat/completions'); |
| assertEqual(summary.stream, true); |
| assertEqual(summary.status, 'success'); |
| assert(summary.responseChars > 0, 'responseChars 应大于 0'); |
|
|
| const records = readPersistedRecords(); |
| const persisted = records.find(r => r.summary?.requestId === summary.requestId); |
| assert(persisted, '应写入 JSONL'); |
| assertEqual(persisted.summary.status, 'success'); |
| assertEqual(persisted.summary.stream, true); |
| }); |
| }); |
|
|
| await runTest('Chat Completions stream=false 会完成 summary 并落盘', async () => { |
| await withMockCursor(['Hello', ' world'], async () => { |
| const req = { |
| method: 'POST', |
| path: '/v1/chat/completions', |
| body: { |
| model: 'gpt-4.1', |
| stream: false, |
| messages: [{ role: 'user', content: 'Say hello' }], |
| }, |
| }; |
| const res = new MockResponse(); |
| await handleOpenAIChatCompletions(req, res); |
|
|
| assert(res.ended, '响应应结束'); |
| const summary = latestSummary(); |
| assert(summary, '应生成 summary'); |
| assertEqual(summary.path, '/v1/chat/completions'); |
| assertEqual(summary.stream, false); |
| assertEqual(summary.status, 'success'); |
| assert(summary.responseChars > 0, 'responseChars 应大于 0'); |
|
|
| const records = readPersistedRecords(); |
| const persisted = records.find(r => r.summary?.requestId === summary.requestId); |
| assert(persisted, '应写入 JSONL'); |
| assertEqual(persisted.summary.status, 'success'); |
| assertEqual(persisted.summary.stream, false); |
| }); |
| }); |
|
|
| await runTest('Responses stream=true 会完成 summary 并落盘', async () => { |
| await withMockCursor(['Hello', ' world'], async () => { |
| const req = { |
| method: 'POST', |
| path: '/v1/responses', |
| body: { |
| model: 'gpt-4.1', |
| stream: true, |
| input: 'Say hello', |
| }, |
| }; |
| const res = new MockResponse(); |
| await handleOpenAIResponses(req, res); |
|
|
| assert(res.ended, '响应应结束'); |
| const summary = latestSummary(); |
| assert(summary, '应生成 summary'); |
| assertEqual(summary.path, '/v1/responses'); |
| assertEqual(summary.stream, true); |
| assertEqual(summary.apiFormat, 'responses'); |
| assertEqual(summary.status, 'success'); |
| assert(summary.responseChars > 0, 'responseChars 应大于 0'); |
|
|
| const records = readPersistedRecords(); |
| const persisted = records.find(r => r.summary?.requestId === summary.requestId); |
| assert(persisted, '应写入 JSONL'); |
| assertEqual(persisted.summary.status, 'success'); |
| assertEqual(persisted.summary.stream, true); |
| }); |
| }); |
|
|
| await runTest('Responses stream=false 会完成 summary 并落盘', async () => { |
| await withMockCursor(['Hello', ' world'], async () => { |
| const req = { |
| method: 'POST', |
| path: '/v1/responses', |
| body: { |
| model: 'gpt-4.1', |
| stream: false, |
| input: 'Say hello', |
| }, |
| }; |
| const res = new MockResponse(); |
| await handleOpenAIResponses(req, res); |
|
|
| assert(res.ended, '响应应结束'); |
| const summary = latestSummary(); |
| assert(summary, '应生成 summary'); |
| assertEqual(summary.path, '/v1/responses'); |
| assertEqual(summary.stream, false); |
| assertEqual(summary.apiFormat, 'responses'); |
| assertEqual(summary.status, 'success'); |
| assert(summary.responseChars > 0, 'responseChars 应大于 0'); |
|
|
| const records = readPersistedRecords(); |
| const persisted = records.find(r => r.summary?.requestId === summary.requestId); |
| assert(persisted, '应写入 JSONL'); |
| assertEqual(persisted.summary.status, 'success'); |
| assertEqual(persisted.summary.stream, false); |
| }); |
| }); |
|
|
| console.log('\n' + '═'.repeat(55)); |
| console.log(` 结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计`); |
| console.log('═'.repeat(55) + '\n'); |
|
|
| if (failed > 0) process.exit(1); |
|
|