jsql-neo 6.3.3 → 6.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ /*
2
+ * require.cache 全局劫持回归测试(P0-1)
3
+ *
4
+ * 背景:旧版 enableMySQLCompat() 会遍历 node_modules,把 mysql2 的
5
+ * require.cache 条目替换成内存兼容层。它靠 process.cwd() 猜路径,
6
+ * mysql2 通常尚未加载,于是被静默替换 —— 真实 MySQL 连接悄悄改查内存库
7
+ * 且不报错,表现为「本地跑通、上线全错」。
8
+ *
9
+ * 本测试锁定修复后的契约:
10
+ * 1. 默认调用 enableMySQLCompat() 绝不改动 require.cache
11
+ * 2. { global: true } 缺少环境变量确认时必须抛错
12
+ * 3. injectMySQLCompat() 只改写传入对象的引用,不污染全局
13
+ * 4. 兼容层仍可显式使用
14
+ */
15
+ const assert = require('assert');
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ let pass = 0, fail = 0;
21
+ function ok(name, cond, extra) {
22
+ if (cond) { pass++; console.log('[OK] ' + name); }
23
+ else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
24
+ }
25
+
26
+ // 造一个隔离的假 mysql2,用于验证劫持行为
27
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-hijack-'));
28
+ const nm = path.join(tmp, 'node_modules', 'mysql2');
29
+ fs.mkdirSync(nm, { recursive: true });
30
+ fs.writeFileSync(path.join(nm, 'index.js'),
31
+ 'exports.createConnection = function(){ return { __real: true }; };\n' +
32
+ 'exports.createPool = function(){ return { __real: true }; };\n');
33
+ fs.writeFileSync(path.join(nm, 'package.json'),
34
+ JSON.stringify({ name: 'mysql2', version: '3.0.0', main: 'index.js' }));
35
+
36
+ const jsql = require('../index.js');
37
+ const mysql2Path = path.join(nm, 'index.js');
38
+
39
+ try {
40
+ console.log('--- 1. 默认调用不得劫持 require.cache ---');
41
+ // 直接检查 index.js 源码不含无条件注入路径
42
+ const src = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
43
+ const fnBody = src.slice(src.indexOf('function enableMySQLCompat'));
44
+ ok('enableMySQLCompat 内部对 global 做了显式判断',
45
+ /opts\.global\s*!==\s*true/.test(fnBody));
46
+ ok('危险路径需环境变量确认',
47
+ /JSQL_NEO_ALLOW_GLOBAL_HIJACK/.test(fnBody));
48
+
49
+ console.log('--- 2. 调用后 require.cache 不含被伪造的 mysql2 ---');
50
+ jsql.enableMySQLCompat();
51
+ const cached = require.cache[mysql2Path];
52
+ ok('require.cache 中没有 mysql2 条目', cached === undefined,
53
+ cached ? '仍被注入' : undefined);
54
+
55
+ console.log('--- 3. { global: true } 无环境变量时抛错 ---');
56
+ let threw = null;
57
+ try { jsql.enableMySQLCompat({ global: true }); } catch (e) { threw = e; }
58
+ ok('抛错并说明原因', threw && /require\.cache/.test(threw.message), threw && threw.message);
59
+ ok('抛错后仍未注入', require.cache[mysql2Path] === undefined);
60
+
61
+ console.log('--- 4. 显式注入只影响目标对象 ---');
62
+ const target = {
63
+ createConnection: () => ({ __fake: true }),
64
+ Driver: { createPool: () => ({ __fake: true }) },
65
+ };
66
+ const before = target.createConnection;
67
+ const touched = jsql.injectMySQLCompat(target);
68
+ ok('injectMySQLCompat 返回 true', touched === true);
69
+ ok('顶层 createConnection 已被替换', target.createConnection !== before);
70
+ ok('嵌套 Driver.createPool 已被替换',
71
+ typeof target.Driver.createPool === 'function' &&
72
+ target.Driver.createPool !== before);
73
+ ok('未触碰 require.cache', require.cache[mysql2Path] === undefined);
74
+
75
+ console.log('--- 5. 兼容层仍可显式使用 ---');
76
+ ok('导出 mysql2.createConnection', typeof jsql.mysql2.createConnection === 'function');
77
+ ok('导出 mysql2.createPool', typeof jsql.mysql2.createPool === 'function');
78
+ ok('导出 createConnection 快捷方式', typeof jsql.createConnection === 'function');
79
+ ok('导出 injectMySQLCompat', typeof jsql.injectMySQLCompat === 'function');
80
+ ok('enableMySQLCompat 默认调用返回兼容层',
81
+ jsql.enableMySQLCompat() === jsql.mysql2);
82
+ } finally {
83
+ fs.rmSync(tmp, { recursive: true, force: true });
84
+ }
85
+
86
+ console.log(`\n${pass} passed, ${fail} failed`);
87
+ process.exit(fail === 0 ? 0 : 1);
@@ -0,0 +1,134 @@
1
+ /*
2
+ * native 多实例回归测试(P0-2)
3
+ *
4
+ * 背景:6.3.3 及以前 native 层是 `thread_local! static ENGINE`,
5
+ * 整个进程只有一个 HybridEngine。于是:
6
+ * new Database(dirA) → new Database(dirB)
7
+ * 会把 dirA 的所有表 clear() 掉(HybridEngine::open 无条件 clear),
8
+ * 同进程无法持有两个独立数据库 —— 对自称「嵌入式数据库」是硬伤。
9
+ *
10
+ * 6.3.4 改为句柄注册表:jsqlInstanceNew() 分配句柄,
11
+ * 所有操作走 jsqlXxxH(handle, ...);旧的 jsqlXxx() 走保留的默认实例 0。
12
+ *
13
+ * 若加载的是旧版预编译二进制(无 jsqlInstanceNew),整套多实例断言跳过,
14
+ * 但向后兼容路径仍需验证。
15
+ */
16
+ const fs = require('fs');
17
+ const os = require('os');
18
+ const path = require('path');
19
+
20
+ const native = require('../native/jsql-neo-native.node');
21
+ const { JSQL } = require('../lib/native_client.js');
22
+
23
+ let pass = 0, fail = 0, skip = 0;
24
+ function ok(name, cond, extra) {
25
+ if (cond) { pass++; console.log('[OK] ' + name); }
26
+ else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
27
+ }
28
+ function skipTest(name, why) { skip++; console.log('[SKIP] ' + name + ' — ' + why); }
29
+
30
+ const tmp = (tag) => fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-native-' + tag + '-'));
31
+ const SCHEMA = { id: { type: 'integer', primaryKey: true }, tag: { type: 'string' } };
32
+ const hasHandles = typeof native.jsqlInstanceNew === 'function';
33
+
34
+ (async () => {
35
+ console.log('--- 1. native 二进制能力探测 ---');
36
+ console.log(' 句柄版 API: ' + (hasHandles ? '可用' : '不可用(旧版二进制)'));
37
+ ok('二进制可加载', !!native);
38
+ ok('旧版无句柄 API 仍存在(向后兼容)', typeof native.jsqlOpen === 'function');
39
+
40
+ if (hasHandles) {
41
+ console.log('--- 2. 两个独立引擎实例互不干扰(核心回归)---');
42
+ const dA = tmp('A'), dB = tmp('B');
43
+ const SC = JSON.stringify(SCHEMA);
44
+ const hA = native.jsqlInstanceNew();
45
+ native.jsqlOpenH(hA, dA, 'hybrid');
46
+ native.jsqlCreateTableH(hA, 't', SC);
47
+ native.jsqlInsertH(hA, 't', JSON.stringify([{ tag: 'A1' }]));
48
+ native.jsqlInsertH(hA, 't', JSON.stringify([{ tag: 'A2' }]));
49
+
50
+ // 关键:此时再开第二个库,旧版会把 A 的表全部清空
51
+ const hB = native.jsqlInstanceNew();
52
+ native.jsqlOpenH(hB, dB, 'hybrid');
53
+ native.jsqlCreateTableH(hB, 't', SC);
54
+ native.jsqlInsertH(hB, 't', JSON.stringify([{ tag: 'B1' }]));
55
+
56
+ ok('两个句柄不同', hA !== hB, { hA, hB });
57
+ ok('A 的数据未被 B 的 open() 清空', parseInt(native.jsqlCountH(hA, 't'), 10) === 2,
58
+ native.jsqlCountH(hA, 't'));
59
+ ok('B 的数据独立', parseInt(native.jsqlCountH(hB, 't'), 10) === 1,
60
+ native.jsqlCountH(hB, 't'));
61
+ const aRows = JSON.parse(native.jsqlFindH(hA, 't', '', 10, 0)).map(r => r.fields.tag);
62
+ const bRows = JSON.parse(native.jsqlFindH(hB, 't', '', 10, 0)).map(r => r.fields.tag);
63
+ ok('A 数据内容正确', JSON.stringify(aRows) === '["A1","A2"]', aRows);
64
+ ok('B 数据内容正确', JSON.stringify(bRows) === '["B1"]', bRows);
65
+
66
+ console.log('--- 3. 实例计数与销毁 ---');
67
+ ok('显式实例数为 2', native.jsqlInstanceCount() === 2, native.jsqlInstanceCount());
68
+ native.jsqlCloseH(hA); native.jsqlCloseH(hB);
69
+ JSON.parse(native.jsqlInstanceFree(hA));
70
+ JSON.parse(native.jsqlInstanceFree(hB));
71
+ ok('free 后实例归零', native.jsqlInstanceCount() === 0, native.jsqlInstanceCount());
72
+ ok('访问已销毁实例返回错误而非崩溃',
73
+ /no such database instance/.test(native.jsqlCountH(hA, 't')),
74
+ native.jsqlCountH(hA, 't'));
75
+ fs.rmSync(dA, { recursive: true, force: true });
76
+ fs.rmSync(dB, { recursive: true, force: true });
77
+ } else {
78
+ skipTest('多实例句柄测试', '旧版预编译二进制无 jsqlInstanceNew');
79
+ }
80
+
81
+ console.log('--- 4. 默认实例向后兼容(无句柄 API)---');
82
+ {
83
+ const dD = tmp('D');
84
+ const r = JSON.parse(native.jsqlOpen(dD, 'hybrid'));
85
+ ok('jsqlOpen 走默认实例可用', r.ok === true, r);
86
+ native.jsqlCreateTable('t', JSON.stringify(SCHEMA));
87
+ native.jsqlInsert('t', JSON.stringify([{ tag: 'legacy' }]));
88
+ ok('jsqlCount 默认实例可读', native.jsqlCount('t') === '1', native.jsqlCount('t'));
89
+ const rows = JSON.parse(native.jsqlFind('t', '', 10, 0)).map(r => r.fields.tag);
90
+ ok('默认实例数据正确', JSON.stringify(rows) === '["legacy"]', rows);
91
+ native.jsqlClose();
92
+ fs.rmSync(dD, { recursive: true, force: true });
93
+ }
94
+
95
+ console.log('--- 5. JS 层:两个 NativeJSQL 并存 ---');
96
+ {
97
+ const dA = tmp('jsA'), dB = tmp('jsB');
98
+ const A = new JSQL({ path: dA, mode: 'hybrid' });
99
+ const B = new JSQL({ path: dB, mode: 'hybrid' });
100
+ await A.start();
101
+ await B.start();
102
+ if (hasHandles) {
103
+ ok('JS 层已启用多实例', A._multiInstance === true);
104
+ ok('两实例句柄不同', A._handle !== B._handle, { a: A._handle, b: B._handle });
105
+ } else {
106
+ ok('旧版二进制下回退单实例(_handle 为 null)', A._handle === null);
107
+ }
108
+ await A.createTable('t', SCHEMA);
109
+ await A.insert('t', { tag: 'A1' });
110
+ await A.insert('t', { tag: 'A2' });
111
+ await B.createTable('t', SCHEMA);
112
+ await B.insert('t', { tag: 'B1' });
113
+
114
+ const ca = await A.count('t'), cb = await B.count('t');
115
+ ok('A 保留自己的 2 行', ca === 2, ca);
116
+ ok('B 有自己的 1 行', cb === 1, cb);
117
+ const ra = (await A.find('t', {})).map(r => r.tag);
118
+ ok('A 数据内容正确', JSON.stringify(ra) === '["A1","A2"]', ra);
119
+
120
+ await B.createTable('only_b', { id: { type: 'integer', primaryKey: true } });
121
+ let aSeesB = true;
122
+ try { await A.find('only_b', {}); aSeesB = false; } catch (e) { aSeesB = true; }
123
+ ok('A 看不到 B 独有的表', aSeesB);
124
+
125
+ await A.stop();
126
+ await B.stop();
127
+ if (hasHandles) ok('stop 释放句柄', A._handle === null && B._handle === null);
128
+ fs.rmSync(dA, { recursive: true, force: true });
129
+ fs.rmSync(dB, { recursive: true, force: true });
130
+ }
131
+
132
+ console.log(`\n${pass} passed, ${fail} failed${skip ? ', ' + skip + ' skipped' : ''}`);
133
+ process.exit(fail === 0 ? 0 : 1);
134
+ })().catch(e => { console.error('FATAL', e); process.exit(1); });
@@ -528,6 +528,31 @@ async function main() {
528
528
  ok('null bulk $-1', JSON.stringify((p('*2\r\n$3\r\nGET\r\n$-1\r\n') || {}).args) === '[null]');
529
529
  }
530
530
 
531
+ console.log('--- 版本号一致性(package.json 是唯一真相)---');
532
+ {
533
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
534
+ const raw = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
535
+ const v = pkg.version;
536
+ ok('package.json 版本号格式合法', /^\d+\.\d+\.\d+$/.test(v), v);
537
+
538
+ // README 头部横幅必须与 package.json 一致
539
+ const banner = raw.match(/^> \*\*v(\d+\.\d+\.\d+)\*\*/m);
540
+ ok('README 顶部版本横幅与 package.json 一致', banner && banner[1] === v,
541
+ banner ? `README=${banner[1]} package=${v}` : '未找到顶部横幅');
542
+
543
+ // 所有 "jsql-neo vX.Y.Z" / "文档版本:vX.Y.Z" 形式的版本声明都必须等于当前版本
544
+ const claims = [...raw.matchAll(/(?:jsql-neo v|文档版本:v|ver=)(\d+\.\d+\.\d+)/g)]
545
+ .map(m => m[1]);
546
+ const stale = [...new Set(claims.filter(c => c !== v))];
547
+ ok('README 无陈旧版本号声明', stale.length === 0, stale);
548
+
549
+ // index.js 头部注释也必须同步
550
+ const idx = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
551
+ const hdr = idx.match(/JSQL-NEO v(\d+\.\d+\.\d+)/);
552
+ ok('index.js 头部版本注释与 package.json 一致', hdr && hdr[1] === v,
553
+ hdr ? `index.js=${hdr[1]} package=${v}` : '未找到版本注释');
554
+ }
555
+
531
556
  console.log(`\n${pass} passed, ${fail} failed`);
532
557
  process.exit(fail === 0 ? 0 : 1);
533
558
  }
@@ -0,0 +1,161 @@
1
+ /*
2
+ * WAL 崩溃恢复回归测试(P1-1)
3
+ *
4
+ * 背景:6.3.3 及以前的 _recoverFromWAL() 只做 fs.unlinkSync 删除 WAL 文件,
5
+ * 从不回放任何操作;且构造函数用 fs.existsSync(dataFile) 短路,
6
+ * 新建库崩溃后连 WAL 都不会被读取。合起来的效果是:
7
+ * 「WAL / 崩溃恢复」是虚假宣传 —— 实测开启 wal:true 后 kill -9 必然丢数据。
8
+ *
9
+ * 6.3.4 修复:
10
+ * 1. WAL 改为 append-only JSONL + fsync
11
+ * 2. insert/update/removeById 现在都写 WAL
12
+ * 3. _recoverFromWAL 真正按 sequence 顺序回放
13
+ * 4. 构造函数在「数据文件不存在但 WAL 存在」时也走恢复流程
14
+ * 5. .jsql 二进制格式分支不再提前 return 跳过恢复
15
+ *
16
+ * 本测试用「不调用 save() 直接丢弃实例」模拟 kill -9。
17
+ */
18
+ const fs = require('fs');
19
+ const os = require('os');
20
+ const path = require('path');
21
+ const Database = require('../lib/database.js');
22
+
23
+ let pass = 0, fail = 0;
24
+ function ok(name, cond, extra) {
25
+ if (cond) { pass++; console.log('[OK] ' + name); }
26
+ else { fail++; console.log('[FAIL] ' + name + (extra !== undefined ? ' → ' + JSON.stringify(extra) : '')); }
27
+ }
28
+
29
+ function tmpFile(tag) {
30
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsql-wal-' + tag + '-'));
31
+ return { dir, file: path.join(dir, 'db.jsql') };
32
+ }
33
+ const SCHEMA = { id: { type: 'integer', primaryKey: true }, v: { type: 'string' }, n: { type: 'integer' } };
34
+
35
+ (async () => {
36
+ console.log('--- 1. 全新库崩溃:WAL 必须恢复出全部数据 ---');
37
+ {
38
+ const { dir, file } = tmpFile('new');
39
+ const db = new Database(file, { wal: true, autoSave: false });
40
+ await db.createTable('t', SCHEMA);
41
+ await db.insert('t', { id: 1, v: 'important' });
42
+ await db.insert('t', { id: 2, v: 'second' });
43
+ await db.updateById('t', 1, { v: 'updated' });
44
+ await db.insert('t', { id: 3, v: 'third' });
45
+ await db.removeById('t', 3);
46
+ // 模拟 kill -9:直接丢弃,不 save 不 stop
47
+ const db2 = new Database(file, { wal: true });
48
+ const rows = await db2.find('t', {});
49
+ ok('崩溃后表仍存在', Object.keys(db2._tables).includes('t'));
50
+ ok('崩溃后行数正确 (2)', rows.length === 2, rows.length);
51
+ ok('崩溃后 update 已回放', rows.find(r => r.id === 1)?.v === 'updated', rows);
52
+ ok('崩溃后第二行存在', rows.find(r => r.id === 2)?.v === 'second');
53
+ ok('崩溃后 delete 已回放', !rows.find(r => r.id === 3));
54
+ ok('回放后 WAL 已检查点', !fs.existsSync(file + '.wal'));
55
+ fs.rmSync(dir, { recursive: true, force: true });
56
+ }
57
+
58
+ console.log('--- 2. 快照 + WAL 叠加:save 后的新写入也要恢复 ---');
59
+ {
60
+ const { dir, file } = tmpFile('mix');
61
+ const db = new Database(file, { wal: true, autoSave: false });
62
+ await db.createTable('t', SCHEMA);
63
+ await db.insert('t', { id: 1, v: 'a' });
64
+ await db.insert('t', { id: 2, v: 'b' });
65
+ db.save(); // 快照:2 行,WAL 应清空
66
+ ok('save 后 WAL 被清空', !fs.existsSync(file + '.wal'));
67
+ await db.insert('t', { id: 3, v: 'c-after-save' }); // 只进 WAL
68
+ const db2 = new Database(file, { wal: true });
69
+ const rows = await db2.find('t', {});
70
+ ok('快照 2 行 + WAL 1 行 = 3 行', rows.length === 3, rows.length);
71
+ ok('WAL 中的新行已恢复', rows.find(r => r.id === 3)?.v === 'c-after-save');
72
+ fs.rmSync(dir, { recursive: true, force: true });
73
+ }
74
+
75
+ console.log('--- 3. 幂等:二次重开不得重复回放 ---');
76
+ {
77
+ const { dir, file } = tmpFile('idem');
78
+ const db = new Database(file, { wal: true, autoSave: false });
79
+ await db.createTable('t', SCHEMA);
80
+ await db.insert('t', { id: 1, v: 'x' });
81
+ await db.insert('t', { id: 2, v: 'y' });
82
+ const db2 = new Database(file, { wal: true });
83
+ const r2 = await db2.find('t', {});
84
+ const db3 = new Database(file, { wal: true });
85
+ const r3 = await db3.find('t', {});
86
+ ok('首次恢复 2 行', r2.length === 2, r2.length);
87
+ ok('二次重开仍 2 行(不重复回放)', r3.length === 2, r3.length);
88
+ fs.rmSync(dir, { recursive: true, force: true });
89
+ }
90
+
91
+ console.log('--- 4. WAL 为 append-only JSONL,崩溃写半的尾行可容错 ---');
92
+ {
93
+ const { dir, file } = tmpFile('torn');
94
+ const db = new Database(file, { wal: true, autoSave: false });
95
+ await db.createTable('t', SCHEMA);
96
+ await db.insert('t', { id: 1, v: 'ok' });
97
+ // 模拟崩溃时最后一行只写了一半
98
+ fs.appendFileSync(file + '.wal', '{"op":"insert","table":"t","ro');
99
+ const db2 = new Database(file, { wal: true });
100
+ const rows = await db2.find('t', {});
101
+ ok('半截尾行被丢弃,完整记录仍恢复', rows.length === 1, rows.length);
102
+ ok('数据内容正确', rows[0] && rows[0].v === 'ok', rows);
103
+ fs.rmSync(dir, { recursive: true, force: true });
104
+ }
105
+
106
+ console.log('--- 5. deleteById / dropTable 也进 WAL ---');
107
+ {
108
+ const { dir, file } = tmpFile('del');
109
+ const db = new Database(file, { wal: true, autoSave: false });
110
+ await db.createTable('t', SCHEMA);
111
+ await db.createTable('u', SCHEMA);
112
+ await db.insert('t', { id: 1, v: 'a' });
113
+ await db.insert('u', { id: 1, v: 'b' });
114
+ await db.dropTable('u');
115
+ const db2 = new Database(file, { wal: true });
116
+ ok('被 drop 的表未复活', !db2._tables.u, Object.keys(db2._tables));
117
+ ok('保留的表数据完整', (await db2.find('t', {})).length === 1);
118
+ fs.rmSync(dir, { recursive: true, force: true });
119
+ }
120
+
121
+ console.log('--- 6. 未开启 wal 时不应产生 .wal 文件 ---');
122
+ {
123
+ const { dir, file } = tmpFile('off');
124
+ const db = new Database(file, { wal: false, autoSave: false });
125
+ await db.createTable('t', SCHEMA);
126
+ await db.insert('t', { id: 1, v: 'a' });
127
+ ok('未开启 wal 时无 .wal 文件', !fs.existsSync(file + '.wal'));
128
+ fs.rmSync(dir, { recursive: true, force: true });
129
+ }
130
+
131
+ console.log('--- 6b. 兼容 6.3.3 的旧格式 WAL(单行 JSON 数组)---');
132
+ {
133
+ // 升级场景:用户本地残留旧版写出的数组格式 WAL,不得被静默丢弃
134
+ const { dir, file } = tmpFile('oldfmt');
135
+ fs.writeFileSync(file + '.wal', JSON.stringify([
136
+ { op: 'createTable', table: 'legacy', schema: SCHEMA },
137
+ ]));
138
+ const db = new Database(file, { wal: true });
139
+ ok('旧格式数组 WAL 能被回放', !!db._tables.legacy, Object.keys(db._tables));
140
+ fs.rmSync(dir, { recursive: true, force: true });
141
+ }
142
+
143
+ console.log('--- 7. 回放后索引仍可用(B-Tree 重建)---');
144
+ {
145
+ const { dir, file } = tmpFile('idx');
146
+ const db = new Database(file, { wal: true, autoSave: false });
147
+ await db.createTable('t', SCHEMA);
148
+ db.createTable && 0;
149
+ for (let i = 1; i <= 30; i++) await db.insert('t', { id: i, v: 'v' + i, n: i });
150
+ db._tables.t.createIndex('n');
151
+ const db2 = new Database(file, { wal: true });
152
+ const found = await db2.find('t', { n: 17 });
153
+ ok('回放后按索引字段查询正确', found.length === 1 && found[0].id === 17, found);
154
+ const cnt = await db2.find('t', { n: { $gte: 25 } });
155
+ ok('回放后范围查询正确', cnt.length === 6, cnt.length);
156
+ fs.rmSync(dir, { recursive: true, force: true });
157
+ }
158
+
159
+ console.log(`\n${pass} passed, ${fail} failed`);
160
+ process.exit(fail === 0 ? 0 : 1);
161
+ })().catch(e => { console.error('FATAL', e); process.exit(1); });