'use strict'; const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { ChatLog, ChatKind, sanitize } = require('../src/core/chatLog'); /** 建一个使用临时目录的公屏日志 */ function setup(options) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'autostore-chatlog-')); return { log: new ChatLog(Object.assign({ dir }, options || {})), dir }; } test('append 记录公屏消息并保留发言人与类型', () => { const { log } = setup(); const entry = log.append({ botId: 'BotA', bot: 'BotA', message: '大家好', kind: ChatKind.CHAT, username: 'Steve' }); assert.strictEqual(entry.botId, 'BotA'); assert.strictEqual(entry.message, '大家好'); assert.strictEqual(entry.username, 'Steve'); assert.strictEqual(entry.kind, 'chat'); assert.ok(entry.t > 0); }); test('空消息不入库,直接返回 null', () => { const { log } = setup(); assert.strictEqual(log.append({ botId: 'A', message: '' }), null); assert.strictEqual(log.append({ botId: 'A' }), null); assert.strictEqual(log.snapshot().total, 0); }); test('list 按机器人隔离,并支持关键字与类型过滤', () => { const { log } = setup(); log.append({ botId: 'A', message: '服务器将在 5 分钟后重启', kind: ChatKind.SYSTEM }); log.append({ botId: 'A', message: '你好', kind: ChatKind.CHAT, username: 'Steve' }); log.append({ botId: 'B', message: '另一个机器人的消息' }); assert.strictEqual(log.list('A').length, 2); assert.strictEqual(log.list('B').length, 1); assert.strictEqual(log.list('A', { kind: ChatKind.SYSTEM }).length, 1); assert.strictEqual(log.list('A', { keyword: '重启' }).length, 1); assert.strictEqual(log.list('A', { keyword: '不存在' }).length, 0); }); test('list 限制条数时保留最新的若干条且保持时间升序', () => { const { log } = setup(); for (let i = 0; i < 10; i++) log.append({ botId: 'A', message: 'm' + i, time: 1000 + i }); const rows = log.list('A', { limit: 3 }); assert.deepStrictEqual(rows.map((r) => r.message), ['m7', 'm8', 'm9']); }); test('每个机器人的内存缓冲独立限长', () => { const { log } = setup({ maxEntriesPerBot: 2 }); for (let i = 0; i < 5; i++) log.append({ botId: 'A', message: 'a' + i }); log.append({ botId: 'B', message: 'b0' }); assert.strictEqual(log.list('A').length, 2); assert.strictEqual(log.list('B').length, 1); const snap = log.snapshot(); const a = snap.bots.find((b) => b.botId === 'A'); assert.strictEqual(a.count, 5, '计数应统计全部写入量而非缓冲量'); assert.strictEqual(a.buffered, 2); }); test('按机器人分文件落盘(JSONL),重启后可读回文件', () => { const { log, dir } = setup(); log.append({ botId: 'BotA', bot: 'BotA', message: 'hello' }); log.append({ botId: 'BotA', bot: 'BotA', message: 'world' }); log.append({ botId: 'BotB', message: 'other' }); const fileA = path.join(dir, 'BotA.jsonl'); const fileB = path.join(dir, 'BotB.jsonl'); assert.strictEqual(fs.readFileSync(fileA, 'utf8').trim().split('\n').length, 2); assert.strictEqual(fs.readFileSync(fileB, 'utf8').trim().split('\n').length, 1); }); test('机器人名含路径分隔符时落盘文件名被安全化', () => { const { log, dir } = setup(); log.append({ botId: 'a/b:c', message: 'x' }); const files = fs.readdirSync(dir); assert.strictEqual(files.length, 1); assert.strictEqual(files[0], 'a_b_c.jsonl'); assert.strictEqual(sanitize('a/b:c'), 'a_b_c'); }); test('exportTo 导出纯文本,含类型标记与发言人', () => { const { log, dir } = setup(); log.append({ botId: 'BotA', message: '你好', kind: ChatKind.CHAT, username: 'Steve' }); log.append({ botId: 'BotA', message: 'u claimmode', kind: ChatKind.COMMAND }); const file = path.join(dir, 'out.log'); assert.strictEqual(log.exportTo('BotA', file), file); const text = fs.readFileSync(file, 'utf8'); assert.ok(text.includes('Steve: 你好')); assert.ok(text.includes('【指令】u claimmode')); }); test('onEntry 回调收到新消息,回调抛异常不影响写入', () => { const { log } = setup(); const seen = []; log.onEntry = (entry) => { seen.push(entry.message); throw new Error('订阅者异常'); }; assert.doesNotThrow(() => log.append({ botId: 'A', message: 'hi' })); assert.deepStrictEqual(seen, ['hi']); assert.strictEqual(log.list('A').length, 1); });