/** * SessionService 游标分页测试(v0.8.1 P2-3) * * 契约:无 limit 全量;limit 无游标 = 尾部窗口(最后 N 条,升序返回); * limit + beforeRowid = 游标之前的更早 N 条(升序返回);完整性判定 * (返回条数 < limit 即到会话开头)由调用方依据长度比较。 */ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); let dbAvailable = true; let Database: typeof import('better-sqlite3'); try { // eslint-disable-next-line @typescript-eslint/no-require-imports Database = require('better-sqlite3'); const probe = new Database(':memory:'); probe.close(); } catch { dbAvailable = false; } import { SessionService } from '../session.service'; describe.skipIf(!dbAvailable)('SessionService — 游标分页(v0.8.1 P2-3)', () => { let db: InstanceType; let svc: SessionService; const sessionId = 's_page'; beforeAll(() => { if (!dbAvailable) return; db = new Database(':memory:'); db.exec(` CREATE TABLE sessions ( id TEXT PRIMARY KEY, title TEXT NOT NULL DEFAULT '', created_at INTEGER, updated_at INTEGER, message_count INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, pinned INTEGER DEFAULT 0, archived INTEGER DEFAULT 0, deleted_at INTEGER, metadata TEXT DEFAULT '{}' ); CREATE TABLE messages ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, role TEXT, content TEXT, reasoning_content TEXT, tool_calls TEXT, tool_result TEXT, attachments TEXT, iteration INTEGER, created_at INTEGER ); CREATE TABLE session_summaries ( session_id TEXT PRIMARY KEY, summary TEXT, summarized_until_rowid INTEGER, updated_at INTEGER ); CREATE TABLE tasks ( id TEXT PRIMARY KEY, session_id TEXT, title TEXT, description TEXT, status TEXT, priority TEXT, parent_id TEXT, assigned_to TEXT, order_idx INTEGER, created_at INTEGER, updated_at INTEGER, completed_at INTEGER ); `); db.prepare('INSERT INTO sessions (id) VALUES (?)').run(sessionId); svc = new SessionService(() => db); for (let i = 1; i <= 10; i++) { svc.saveMessage({ sessionId, role: 'user', content: `msg-${i}` }); } }); afterAll(() => { if (db) db.close(); }); it('无 limit → 全量(向后兼容)', () => { expect(svc.getMessages(sessionId)).toHaveLength(10); }); it('limit 无游标 → 尾部窗口(最后 N 条,升序返回)', () => { const tail = svc.getMessages(sessionId, { limit: 3 }); expect(tail.map((m) => m.content)).toEqual(['msg-8', 'msg-9', 'msg-10']); }); it('limit + beforeRowid → 游标之前 N 条(升序返回)', () => { const full = svc.getMessages(sessionId); const cursorRowId = full[6].rowId!; // msg-7 的 rowid const older = svc.getMessages(sessionId, { limit: 3, beforeRowid: cursorRowId }); expect(older.map((m) => m.content)).toEqual(['msg-4', 'msg-5', 'msg-6']); }); it('游标翻到开头:返回条数 < limit 表示已完整', () => { const full = svc.getMessages(sessionId); const first = full[0].rowId!; const older = svc.getMessages(sessionId, { limit: 3, beforeRowid: first }); expect(older).toHaveLength(0); }); });