/** * AuditService 单元测试(P1-14 测试基线) * 覆盖:链式哈希生成与验证、篡改检测 * * 注意:better-sqlite3 为原生模块——若本机安装的构建与系统 Node ABI 不匹配, * 测试自动跳过(skipIf),避免误报失败。 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import log from 'electron-log'; // better-sqlite3 加载失败时(ABI 不匹配)自动跳过整个套件 // 注意:require 成功不代表原生绑定可用(Electron ABI vs 系统 Node ABI), // 需实际构造一次内存库验证 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; } // electron-log 在纯 Node 环境静默(false = 禁用该 transport) log.transports.console.level = false; log.transports.file.level = false; describe.skipIf(!dbAvailable)('AuditService 链式哈希', () => { let dir: string; // eslint-disable 是失效指令(测试文件 overrides 已关闭 no-explicit-any),已移除 let db: any; let service: any; beforeAll(async () => { const { AuditService } = await import('../../services/audit.service'); dir = mkdtempSync(join(tmpdir(), 'metona-audit-')); // 手动建库(绕过 DatabaseService 的 electron 依赖) db = new Database(join(dir, 'test.db')); db.exec(` CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, iteration INTEGER, event_type TEXT NOT NULL, actor TEXT NOT NULL DEFAULT 'system', target TEXT NOT NULL, details TEXT, outcome TEXT, duration_ms INTEGER, created_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000), prev_hash TEXT, current_hash TEXT ); CREATE TRIGGER IF NOT EXISTS audit_no_delete BEFORE DELETE ON audit_logs BEGIN SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY.'); END; `); service = new AuditService(() => db); }); afterAll(() => { try { db?.close(); } catch { /* ignore */ } rmSync(dir, { recursive: true, force: true }); }); it('写入审计日志并生成链式哈希', () => { service.log({ sessionId: 's1', eventType: 'tool_call', actor: 'agent', target: 'read_file', details: { file: 'a.ts' }, outcome: 'success', }); const rows = db.prepare('SELECT * FROM audit_logs').all(); expect(rows.length).toBe(1); expect(rows[0].current_hash).toBeTruthy(); // 首条 prev_hash 为 64 个 0 的创世哨兵(getLastHash 空表回退值) expect(rows[0].prev_hash).toBe('0'.repeat(64)); }); it('第二条记录的 prev_hash 链接前一条', () => { service.log({ sessionId: 's1', eventType: 'tool_call', actor: 'agent', target: 'write_file', details: { file: 'b.ts' }, outcome: 'success', }); const rows = db.prepare('SELECT * FROM audit_logs ORDER BY id').all(); expect(rows.length).toBe(2); expect(rows[1].prev_hash).toBe(rows[0].current_hash); }); it('verifyChain 对完整链返回 valid', () => { const result = service.verifyChain(); expect(result.valid).toBe(true); expect(result.totalRecords).toBe(2); }); it('篡改记录后 verifyChain 检测到 tampered', () => { // 需要先 drop 触发器才能 UPDATE(INSERT-ONLY 约束) db.exec('DROP TRIGGER audit_no_delete'); db.prepare('UPDATE audit_logs SET details = ? WHERE id = 1').run('{"tampered": true}'); db.exec(`CREATE TRIGGER audit_no_delete BEFORE DELETE ON audit_logs BEGIN SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY.'); END`); const result = service.verifyChain(); expect(result.valid).toBe(false); }); });