| #!/usr/bin/env node |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import Database from 'better-sqlite3'; |
| import { existsSync, unlinkSync, mkdirSync } from 'fs'; |
| import { dirname } from 'path'; |
|
|
| |
|
|
| let passed = 0, failed = 0; |
| const errors = []; |
|
|
| function assert(condition, msg) { |
| if (condition) { |
| passed++; |
| console.log(` ✓ ${msg}`); |
| } else { |
| failed++; |
| const err = ` ✗ ${msg}`; |
| errors.push(err); |
| console.error(err); |
| } |
| } |
|
|
| function assertEq(actual, expected, msg) { |
| const ok = JSON.stringify(actual) === JSON.stringify(expected); |
| if (!ok) { |
| console.error(` actual: ${JSON.stringify(actual)}`); |
| console.error(` expected: ${JSON.stringify(expected)}`); |
| } |
| assert(ok, msg); |
| } |
|
|
| |
| |
|
|
| const TEST_DB_PATH = '/tmp/cursor2api-test.db'; |
|
|
| |
| if (existsSync(TEST_DB_PATH)) unlinkSync(TEST_DB_PATH); |
|
|
| let db; |
|
|
| function initDb(dbPath) { |
| const dir = dirname(dbPath); |
| if (dir && dir !== '.' && !existsSync(dir)) mkdirSync(dir, { recursive: true }); |
| db = new Database(dbPath); |
| db.pragma('journal_mode = WAL'); |
| db.pragma('synchronous = NORMAL'); |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS requests ( |
| request_id TEXT PRIMARY KEY, |
| timestamp INTEGER NOT NULL, |
| summary_json TEXT NOT NULL, |
| payload_json TEXT |
| ); |
| CREATE INDEX IF NOT EXISTS idx_timestamp ON requests(timestamp); |
| `); |
| } |
|
|
| function dbInsertRequest(summary, payload) { |
| db.prepare( |
| 'INSERT OR REPLACE INTO requests (request_id, timestamp, summary_json, payload_json) VALUES (?, ?, ?, ?)' |
| ).run(summary.requestId, summary.startTime, JSON.stringify(summary), JSON.stringify(payload)); |
| } |
|
|
| function dbGetPayload(requestId) { |
| const row = db.prepare('SELECT payload_json FROM requests WHERE request_id = ?').get(requestId); |
| if (!row?.payload_json) return undefined; |
| try { return JSON.parse(row.payload_json); } catch { return undefined; } |
| } |
|
|
| function dbGetSummaries({ limit, before }) { |
| let rows; |
| if (before !== undefined) { |
| rows = db.prepare('SELECT summary_json FROM requests WHERE timestamp < ? ORDER BY timestamp DESC LIMIT ?').all(before, limit); |
| } else { |
| rows = db.prepare('SELECT summary_json FROM requests ORDER BY timestamp DESC LIMIT ?').all(limit); |
| } |
| return rows.map(r => { try { return JSON.parse(r.summary_json); } catch { return null; } }).filter(Boolean); |
| } |
|
|
| function dbGetSummaryCount() { |
| return db.prepare('SELECT COUNT(*) as cnt FROM requests').get().cnt; |
| } |
|
|
| function dbGetSummariesSince(cutoff) { |
| const rows = db.prepare('SELECT summary_json FROM requests WHERE timestamp >= ? ORDER BY timestamp ASC').all(cutoff); |
| return rows.map(r => { try { return JSON.parse(r.summary_json); } catch { return null; } }).filter(Boolean); |
| } |
|
|
| function dbClear() { |
| db.prepare('DELETE FROM requests').run(); |
| } |
|
|
| |
|
|
| function makeSummary(id, startTime, extra = {}) { |
| return { |
| requestId: id, |
| startTime, |
| endTime: startTime + 1000, |
| method: 'POST', |
| path: '/v1/messages', |
| model: 'claude-sonnet-4-6', |
| stream: true, |
| apiFormat: 'anthropic', |
| hasTools: false, |
| toolCount: 0, |
| messageCount: 3, |
| status: 'success', |
| responseChars: 500, |
| retryCount: 0, |
| continuationCount: 0, |
| toolCallsDetected: 0, |
| thinkingChars: 0, |
| systemPromptLength: 100, |
| phaseTimings: [], |
| title: `测试请求 ${id}`, |
| ...extra, |
| }; |
| } |
|
|
| function makePayload(id) { |
| return { |
| question: `用户问题 ${id}`, |
| answer: `模型回答 ${id}`, |
| answerType: 'text', |
| }; |
| } |
|
|
| |
| const BASE_TS = Date.now() - 10000; |
| const records = [ |
| { summary: makeSummary('req-001', BASE_TS + 1000), payload: makePayload('req-001') }, |
| { summary: makeSummary('req-002', BASE_TS + 2000), payload: makePayload('req-002') }, |
| { summary: makeSummary('req-003', BASE_TS + 3000), payload: makePayload('req-003') }, |
| { summary: makeSummary('req-004', BASE_TS + 4000, { status: 'error', error: '超时' }), payload: makePayload('req-004') }, |
| { summary: makeSummary('req-005', BASE_TS + 5000), payload: makePayload('req-005') }, |
| ]; |
|
|
| |
|
|
| console.log('=== unit-logger-db: SQLite 接口功能测试 ===\n'); |
|
|
| |
| console.log('【1】initDb'); |
| try { |
| initDb(TEST_DB_PATH); |
| assert(existsSync(TEST_DB_PATH), '数据库文件已创建'); |
| const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name); |
| assert(tables.includes('requests'), '表 requests 已创建'); |
| const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index'").all().map(r => r.name); |
| assert(indexes.includes('idx_timestamp'), '索引 idx_timestamp 已创建'); |
| } catch (e) { |
| assert(false, `initDb 抛出异常: ${e.message}`); |
| } |
|
|
| |
| console.log('\n【2】dbInsertRequest'); |
| for (const { summary, payload } of records) { |
| dbInsertRequest(summary, payload); |
| } |
| assertEq(dbGetSummaryCount(), 5, '插入 5 条后总数为 5'); |
|
|
| |
| console.log('\n【3】dbGetPayload'); |
| const p2 = dbGetPayload('req-002'); |
| assert(p2 !== undefined, 'req-002 payload 可读取'); |
| assertEq(p2.question, '用户问题 req-002', 'payload.question 正确'); |
| assertEq(p2.answer, '模型回答 req-002', 'payload.answer 正确'); |
| assert(dbGetPayload('req-999') === undefined, '不存在的 requestId 返回 undefined'); |
|
|
| |
| console.log('\n【4】dbGetSummaries(无游标)'); |
| const all = dbGetSummaries({ limit: 10 }); |
| assertEq(all.length, 5, '返回全部 5 条'); |
| assertEq(all[0].requestId, 'req-005', '第一条是最新的 req-005'); |
| assertEq(all[4].requestId, 'req-001', '最后一条是最旧的 req-001'); |
|
|
| |
| console.log('\n【5】dbGetSummaries(limit=3)'); |
| const top3 = dbGetSummaries({ limit: 3 }); |
| assertEq(top3.length, 3, '返回 3 条'); |
| assertEq(top3[0].requestId, 'req-005', '第一条是 req-005'); |
| assertEq(top3[2].requestId, 'req-003', '第三条是 req-003'); |
|
|
| |
| console.log('\n【6】dbGetSummaries(游标分页)'); |
| |
| const page1 = dbGetSummaries({ limit: 3 }); |
| assertEq(page1.length, 3, '第一页 3 条'); |
| assertEq(page1[0].requestId, 'req-005', '第一页第一条 req-005'); |
|
|
| |
| const beforeTs = page1[page1.length - 1].startTime; |
| const page2 = dbGetSummaries({ limit: 3, before: beforeTs }); |
| assertEq(page2.length, 2, '第二页 2 条(剩余 req-002, req-001)'); |
| assertEq(page2[0].requestId, 'req-002', '第二页第一条 req-002'); |
| assertEq(page2[1].requestId, 'req-001', '第二页第二条 req-001'); |
|
|
| |
| console.log('\n【7】dbGetSummaryCount'); |
| assertEq(dbGetSummaryCount(), 5, '总数为 5'); |
|
|
| |
| console.log('\n【8】dbGetSummariesSince'); |
| |
| const since = dbGetSummariesSince(BASE_TS + 3000); |
| assertEq(since.length, 3, 'since 返回 3 条'); |
| assertEq(since[0].requestId, 'req-003', '第一条 req-003(ASC 顺序)'); |
| assertEq(since[2].requestId, 'req-005', '最后一条 req-005'); |
|
|
| |
| const sinceEmpty = dbGetSummariesSince(Date.now() + 99999); |
| assertEq(sinceEmpty.length, 0, '未来 cutoff 返回空数组'); |
|
|
| |
| console.log('\n【9】INSERT OR REPLACE 幂等性'); |
| const updatedSummary = { ...records[0].summary, status: 'error', title: '已更新' }; |
| dbInsertRequest(updatedSummary, records[0].payload); |
| assertEq(dbGetSummaryCount(), 5, '重复插入后总数不变(仍 5 条)'); |
| const allAfter = dbGetSummaries({ limit: 10 }); |
| const updated = allAfter.find(s => s.requestId === 'req-001'); |
| assertEq(updated?.title, '已更新', 'REPLACE 更新了 summary 内容'); |
|
|
| |
| console.log('\n【10】dbClear'); |
| dbClear(); |
| assertEq(dbGetSummaryCount(), 0, '清空后总数为 0'); |
| const afterClear = dbGetSummaries({ limit: 10 }); |
| assertEq(afterClear.length, 0, '清空后查询返回空数组'); |
| assert(dbGetPayload('req-001') === undefined, '清空后 payload 也不可读取'); |
|
|
| |
|
|
| console.log(`\n${'='.repeat(40)}`); |
| console.log(`测试结果: ${passed} 通过 / ${failed} 失败`); |
| if (errors.length > 0) { |
| console.error('\n失败项:'); |
| for (const e of errors) console.error(e); |
| } |
|
|
| |
| db.close(); |
| try { unlinkSync(TEST_DB_PATH); } catch { } |
|
|
| process.exit(failed > 0 ? 1 : 0); |
|
|