File size: 5,971 Bytes
097fb32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/**
 * test/unit-openai-log-summary.mjs
 *
 * 回归测试:summary 落盘模式仅保留问答摘要与少量元数据。
 * 运行方式:npm run build && node test/unit-openai-log-summary.mjs
 */

import fs from 'fs';
import path from 'path';

const LOG_DIR = '/tmp/cursor2api-openai-log-summary';
process.env.LOG_FILE_ENABLED = '1';
process.env.LOG_DIR = LOG_DIR;
process.env.LOG_PERSIST_MODE = 'summary';

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 latestPersistedRecord() {
    const files = fs.readdirSync(LOG_DIR).filter(name => name.endsWith('.jsonl')).sort();
    assert(files.length > 0, '应生成 JSONL 文件');
    const file = path.join(LOG_DIR, files[files.length - 1]);
    const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
    assert(lines.length > 0, 'JSONL 不应为空');
    return JSON.parse(lines[lines.length - 1]);
}

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] summary 落盘模式回归\n');

await runTest('Chat Completions summary 模式只保留 question / answer', 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: 'Please say hello in English.' }],
            },
        };
        const res = new MockResponse();
        await handleOpenAIChatCompletions(req, res);

        const summary = latestSummary();
        assert(summary, '应生成 summary');
        assertEqual(summary.status, 'success');

        const persisted = latestPersistedRecord();
        assertEqual(persisted.summary.path, '/v1/chat/completions');
        assert(persisted.payload.question.includes('Please say hello'), '应保留用户问题摘要');
        assert(persisted.payload.answer.includes('Hello world'), '应保留模型回答摘要');
        assertEqual(persisted.payload.answerType, 'text');
        assertEqual(persisted.payload.messages, undefined, 'summary 模式不应保留 messages');
        assertEqual(persisted.payload.finalResponse, undefined, 'summary 模式不应保留 finalResponse');
        assertEqual(persisted.payload.rawResponse, undefined, 'summary 模式不应保留 rawResponse');
    });
});

await runTest('Responses summary 模式也能提取 question / answer', async () => {
    await withMockCursor(['Hello', ' world'], async () => {
        const req = {
            method: 'POST',
            path: '/v1/responses',
            body: {
                model: 'gpt-4.1',
                stream: false,
                input: 'Please answer with a short hello.',
            },
        };
        const res = new MockResponse();
        await handleOpenAIResponses(req, res);

        const persisted = latestPersistedRecord();
        assertEqual(persisted.summary.path, '/v1/responses');
        assert(persisted.payload.question.includes('short hello'), 'Responses summary 模式应保留问题摘要');
        assert(persisted.payload.answer.includes('Hello world'), 'Responses summary 模式应保留回答摘要');
        assertEqual(persisted.payload.answerType, 'text');
        assertEqual(persisted.payload.originalRequest, undefined, 'summary 模式不应保留 originalRequest');
        assertEqual(persisted.payload.cursorMessages, undefined, 'summary 模式不应保留 cursorMessages');
    });
});

console.log('\n' + '═'.repeat(55));
console.log(`  结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计`);
console.log('═'.repeat(55) + '\n');

if (failed > 0) process.exit(1);