guanghulab/grid-db/tests/smoke/grid-db.test.js

421 lines
15 KiB
JavaScript
Raw Normal View History

// grid-db/tests/smoke/grid-db.test.js
// Grid-DB · 冒烟测试
// PRJ-GDB-001 · Phase 0 · ZY-GDB-P0-005
// 版权:国作登字-2026-A-00037559
'use strict';
const path = require('path');
const fs = require('fs');
const os = require('os');
const {
open,
GridAPI,
GridCell,
WAL,
PageManager,
EventLog
} = require('../../src/index');
let passed = 0;
let failed = 0;
function assert(condition, message) {
if (condition) {
passed++;
// eslint-disable-next-line no-console
console.log(`${message}`);
} else {
failed++;
// eslint-disable-next-line no-console
console.error(`${message}`);
}
}
/**
* 创建临时测试目录
* @param {string} suffix
* @returns {string}
*/
function makeTempDir(suffix) {
const { randomUUID } = require('crypto');
const dir = path.join(os.tmpdir(), `griddb-test-${suffix}-${randomUUID().slice(0, 8)}`);
fs.mkdirSync(dir, { recursive: true });
return dir;
}
/**
* 递归删除目录
* @param {string} dir
*/
function cleanDir(dir) {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// 忽略清理错误
}
}
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB 冒烟测试 · Phase 0\n');
// ── 测试 1: 模块导出 ──
// eslint-disable-next-line no-console
console.log('── 测试 1: 模块导出完整性 ──');
assert(typeof open === 'function', 'open 是函数');
assert(typeof GridAPI === 'function', 'GridAPI 是构造函数');
assert(typeof GridCell === 'function', 'GridCell 是构造函数');
assert(typeof WAL === 'function', 'WAL 是构造函数');
assert(typeof PageManager === 'function', 'PageManager 是构造函数');
assert(typeof EventLog === 'function', 'EventLog 是构造函数');
// ── 测试 2: GridCell 四元组寻址 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 2: GridCell 四元组寻址 ──');
const cell1 = new GridCell('agent-zy', 10, 20, 'raw');
assert(cell1.namespace === 'agent-zy', 'namespace 正确');
assert(cell1.gridX === 10, 'gridX 正确');
assert(cell1.gridY === 20, 'gridY 正确');
assert(cell1.layer === 'raw', 'layer 正确');
assert(cell1.toKey() === 'agent-zy:10:20:raw', 'toKey() 正确');
// fromKey 还原
const cell2 = GridCell.fromKey('test-ns:5:15:semantic');
assert(cell2.namespace === 'test-ns', 'fromKey namespace 正确');
assert(cell2.gridX === 5, 'fromKey gridX 正确');
assert(cell2.gridY === 15, 'fromKey gridY 正确');
assert(cell2.layer === 'semantic', 'fromKey layer 正确');
// 序列化 / 反序列化
const serialized = cell1.serialize();
assert(serialized.namespace === 'agent-zy', 'serialize namespace');
assert(serialized.grid_x === 10, 'serialize grid_x');
assert(serialized.grid_y === 20, 'serialize grid_y');
assert(serialized.layer === 'raw', 'serialize layer');
const cell3 = GridCell.deserialize(serialized);
assert(cell3.equals(cell1), 'deserialize → equals 原始格点');
// 二进制序列化
const buf = cell1.toBuffer();
assert(buf instanceof Buffer, 'toBuffer 返回 Buffer');
const { cell: cell4, bytesRead } = GridCell.fromBuffer(buf);
assert(cell4.equals(cell1), 'fromBuffer → equals 原始格点');
assert(bytesRead === buf.length, 'bytesRead 等于 buffer 长度');
// 距离计算
assert(cell1.distanceTo(cell2) === 10, 'distanceTo 曼哈顿距离正确');
// 无效参数
let errThrown = false;
try { new GridCell('', 0, 0); } catch { errThrown = true; }
assert(errThrown, '空 namespace 抛出错误');
errThrown = false;
try { new GridCell('ns', 0, 0, 'invalid'); } catch { errThrown = true; }
assert(errThrown, '无效 layer 抛出错误');
// ── 测试 3: WAL 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 3: WAL 基础功能 ──');
const walDir = makeTempDir('wal');
const wal = new WAL(path.join(walDir, 'test.wal'));
const seq1 = wal.appendPut('ns:1:2:raw', Buffer.from('{"hello":"world"}'));
assert(seq1 === 1, 'appendPut 返回序列号 1');
const seq2 = wal.appendPut('ns:3:4:raw', Buffer.from('{"foo":"bar"}'));
assert(seq2 === 2, 'appendPut 返回序列号 2');
const seq3 = wal.appendDelete('ns:1:2:raw');
assert(seq3 === 3, 'appendDelete 返回序列号 3');
assert(wal.getSeqNo() === 3, 'getSeqNo 正确');
// 恢复
const entries = wal.recover();
assert(entries.length === 3, '恢复 3 条记录');
assert(entries[0].op === WAL.OP_PUT, '记录 1 是 PUT');
assert(entries[0].key === 'ns:1:2:raw', '记录 1 键正确');
assert(entries[1].op === WAL.OP_PUT, '记录 2 是 PUT');
assert(entries[2].op === WAL.OP_DELETE, '记录 3 是 DELETE');
// 截断
wal.truncate();
assert(wal.getSeqNo() === 0, '截断后序列号归零');
const afterTrunc = wal.recover();
assert(afterTrunc.length === 0, '截断后无记录');
wal.close();
// 崩溃恢复测试:写入 → 关闭 → 重新打开 → 验证
const wal2 = new WAL(path.join(walDir, 'crash.wal'));
wal2.appendPut('crash:1:1:raw', Buffer.from('{"data":"survive"}'));
wal2.appendPut('crash:2:2:raw', Buffer.from('{"data":"also"}'));
wal2.close();
const wal3 = new WAL(path.join(walDir, 'crash.wal'));
const recovered = wal3.recover();
assert(recovered.length === 2, '崩溃恢复2 条记录');
assert(recovered[0].key === 'crash:1:1:raw', '崩溃恢复:键正确');
assert(wal3.getSeqNo() === 2, '崩溃恢复:序列号正确');
wal3.close();
cleanDir(walDir);
// ── 测试 4: PageManager 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 4: PageManager 基础功能 ──');
const pmDir = makeTempDir('pm');
const pm = new PageManager(path.join(pmDir, 'test.gdb'));
// 分配页
const p1 = pm.allocPage();
assert(p1 === 1, '首页号 = 1');
const p2 = pm.allocPage();
assert(p2 === 2, '第二页号 = 2');
// 写入数据
const testData = Buffer.from('Hello Grid-DB!');
pm.writePage(p1, testData);
// 读取数据
const readData = pm.readPage(p1);
assert(readData !== null, '读取非 null');
assert(readData.toString() === 'Hello Grid-DB!', '读取数据正确');
// 写入更长数据
const longData = Buffer.from(JSON.stringify({ msg: '格点数据库测试', num: 42, arr: [1, 2, 3] }));
pm.writePage(p2, longData);
const readLong = pm.readPage(p2);
assert(readLong !== null, '读取长数据非 null');
assert(JSON.parse(readLong.toString()).num === 42, '长数据内容正确');
// 释放页
pm.freePage(p1);
const freedData = pm.readPage(p1);
assert(freedData === null, '释放后读取返回 null');
// 释放的页被复用
const p3 = pm.allocPage();
assert(p3 === 1, '释放的页号被复用');
// 状态
const pmStatus = pm.getStatus();
assert(pmStatus.pageSize === 4096, '页大小 = 4096');
assert(pmStatus.pageCount === 2, '页数 = 2');
pm.close();
// 重新打开验证持久化
const pm2 = new PageManager(path.join(pmDir, 'test.gdb'));
const rereadLong = pm2.readPage(2);
assert(rereadLong !== null, '重新打开后读取非 null');
assert(JSON.parse(rereadLong.toString()).num === 42, '持久化数据正确');
pm2.close();
cleanDir(pmDir);
// ── 测试 5: EventLog 基础功能 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 5: EventLog 基础功能 ──');
const eventLog = new EventLog({ maxEvents: 5 });
const evt1 = eventLog.append('ns1', 'put', 'ns1:1:1:raw', { size: 100 });
assert(evt1.eventId.startsWith('evt-'), '事件 ID 前缀正确');
assert(evt1.seqNo === 1, '事件序号 = 1');
assert(evt1.namespace === 'ns1', '事件命名空间正确');
eventLog.append('ns1', 'put', 'ns1:2:2:raw');
eventLog.append('ns2', 'delete', 'ns2:3:3:raw');
const recent = eventLog.getRecent(10);
assert(recent.length === 3, '最近事件 = 3');
const ns1Events = eventLog.getByNamespace('ns1');
assert(ns1Events.length === 2, 'ns1 事件 = 2');
// 订阅测试
let received = null;
const unsub = eventLog.subscribe('test-sub', (evt) => { received = evt; });
eventLog.append('ns1', 'put', 'ns1:4:4:raw');
assert(received !== null, '订阅回调收到事件');
assert(received.operation === 'put', '订阅事件操作正确');
unsub();
received = null;
eventLog.append('ns1', 'put', 'ns1:5:5:raw');
assert(received === null, '取消订阅后不再收到事件');
// 容量控制
eventLog.append('ns1', 'put', 'ns1:6:6:raw');
eventLog.append('ns1', 'put', 'ns1:7:7:raw');
const status = eventLog.getStatus();
assert(status.totalEvents === 5, '容量控制:最多保留 5 条');
// ── 测试 6: GridAPI · put → get → delete 全链路 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 6: GridAPI · put → get → delete 全链路 ──');
const apiDir = makeTempDir('api');
const db = open({ dataDir: apiDir });
// put
const putResult = db.put('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' }, {
agentId: 'AG-ZY-01',
context: { repo: 'guanghulab', branch: 'main' },
timestamp: '2026-03-26T13:00:00Z'
});
assert(putResult.key === 'exe-engine:1:2:raw', 'put 返回正确 key');
assert(putResult.seqNo === 1, 'put 返回序列号 1');
assert(putResult.event.operation === 'put', 'put 生成事件');
// get
const getData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(getData !== null, 'get 返回数据');
assert(getData.agentId === 'AG-ZY-01', 'get 数据内容正确');
assert(getData.context.repo === 'guanghulab', 'get 嵌套数据正确');
// get 不存在的格点
const noData = db.get('exe-engine', { gridX: 99, gridY: 99, layer: 'raw' });
assert(noData === null, 'get 不存在的格点返回 null');
// 更新
db.put('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' }, {
agentId: 'AG-ZY-01',
context: { repo: 'guanghulab', branch: 'feature' },
timestamp: '2026-03-26T14:00:00Z'
});
const updatedData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(updatedData.context.branch === 'feature', '更新后数据正确');
// 多命名空间隔离
db.put('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' }, { source: 'notion' });
const dcData = db.get('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' });
assert(dcData.source === 'notion', '不同命名空间数据隔离');
const exeData = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(exeData.agentId === 'AG-ZY-01', '原命名空间数据不受影响');
// delete
const deleted = db.delete('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(deleted === true, 'delete 返回 true');
const afterDelete = db.get('exe-engine', { gridX: 1, gridY: 2, layer: 'raw' });
assert(afterDelete === null, '删除后 get 返回 null');
// delete 不存在的
const deleteMissing = db.delete('exe-engine', { gridX: 99, gridY: 99, layer: 'raw' });
assert(deleteMissing === false, 'delete 不存在的格点返回 false');
// ── 测试 7: 范围扫描 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 7: 范围扫描 ──');
db.put('scan-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { id: 'a' });
db.put('scan-ns', { gridX: 2, gridY: 2, layer: 'raw' }, { id: 'b' });
db.put('scan-ns', { gridX: 3, gridY: 3, layer: 'raw' }, { id: 'c' });
db.put('scan-ns', { gridX: 5, gridY: 5, layer: 'indexed' }, { id: 'd' });
const scanAll = db.scan('scan-ns');
assert(scanAll.length === 4, '扫描全部 = 4');
const scanRange = db.scan('scan-ns', { xRange: [1, 3], yRange: [1, 3] });
assert(scanRange.length === 3, '范围扫描 [1,3] = 3 条');
const scanLayer = db.scan('scan-ns', { layer: 'indexed' });
assert(scanLayer.length === 1, '按 layer 过滤 = 1 条');
assert(scanLayer[0].data.id === 'd', '过滤结果正确');
// ── 测试 8: stats ──
// eslint-disable-next-line no-console
console.log('\n── 测试 8: stats 统计 ──');
const dbStats = db.stats('scan-ns');
assert(dbStats.namespace === 'scan-ns', 'stats 命名空间正确');
assert(dbStats.cellCount === 4, 'scan-ns 格点数 = 4');
const allStats = db.stats();
assert(allStats.namespace === '*', 'stats 全局');
assert(allStats.cellCount >= 5, '全局格点数 >= 5');
assert(allStats.wal !== undefined, 'stats 含 wal');
assert(allStats.pageManager !== undefined, 'stats 含 pageManager');
assert(allStats.eventLog !== undefined, 'stats 含 eventLog');
// ── 测试 9: checkpoint ──
// eslint-disable-next-line no-console
console.log('\n── 测试 9: checkpoint ──');
db.checkpoint();
const afterCheckpoint = db.stats();
assert(afterCheckpoint.wal.seqNo === 0, 'checkpoint 后 WAL seqNo = 0');
// 数据仍然可读
const postCheckpoint = db.get('dc-v1', { gridX: 1, gridY: 2, layer: 'raw' });
assert(postCheckpoint !== null, 'checkpoint 后数据仍可读');
assert(postCheckpoint.source === 'notion', 'checkpoint 后数据正确');
// ── 测试 10: 事件订阅 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 10: 事件订阅 ──');
let subscribedEvent = null;
const unsubscribe = db.subscribe('test-audit', (evt) => {
subscribedEvent = evt;
});
db.put('audit-ns', { gridX: 0, gridY: 0, layer: 'raw' }, { audit: true });
assert(subscribedEvent !== null, '事件订阅收到事件');
assert(subscribedEvent.namespace === 'audit-ns', '订阅事件命名空间正确');
unsubscribe();
subscribedEvent = null;
db.put('audit-ns', { gridX: 1, gridY: 1, layer: 'raw' }, { audit: false });
assert(subscribedEvent === null, '取消订阅后不再收到');
db.close();
cleanDir(apiDir);
// ── 测试 11: open() 便捷函数 ──
// eslint-disable-next-line no-console
console.log('\n── 测试 11: open() 便捷函数 ──');
const openDir = makeTempDir('open');
const db2 = open({ dataDir: openDir });
assert(db2 instanceof GridAPI, 'open 返回 GridAPI 实例');
db2.put('test', { gridX: 0, gridY: 0, layer: 'raw' }, 'hello');
const result = db2.get('test', { gridX: 0, gridY: 0, layer: 'raw' });
assert(result === 'hello', 'open → put → get 正确');
db2.close();
cleanDir(openDir);
// ── 测试 12: GridCell 使用 GridCell 对象调用 API ──
// eslint-disable-next-line no-console
console.log('\n── 测试 12: GridCell 对象直接调用 API ──');
const gcDir = makeTempDir('gc');
const db3 = open({ dataDir: gcDir });
const myCell = new GridCell('my-ns', 42, 84, 'cleaned');
db3.put('my-ns', myCell, { value: 'direct cell' });
const directResult = db3.get('my-ns', myCell);
assert(directResult !== null, 'GridCell 对象直接调用 get');
assert(directResult.value === 'direct cell', '数据正确');
db3.close();
cleanDir(gcDir);
// ── 测试结果汇总 ──
// eslint-disable-next-line no-console
console.log('\n══════════════════════════════════════');
// eslint-disable-next-line no-console
console.log('🗄️ Grid-DB 冒烟测试完成');
// eslint-disable-next-line no-console
console.log(` ✅ 通过: ${passed}`);
// eslint-disable-next-line no-console
console.log(` ❌ 失败: ${failed}`);
// eslint-disable-next-line no-console
console.log(` 📊 总计: ${passed + failed}`);
// eslint-disable-next-line no-console
console.log('══════════════════════════════════════\n');
if (failed > 0) {
process.exit(1);
}