Files
MetonaSqlark/tests/v025-fixes.test.ts
thzxx 0dba1abf2a test(P0): v0.8.0 验证基座与工程门禁根治
工作流 C-1 / C-3 前半 + 测试代码类型检查。

【故障注入基座】新增 tests/helpers/storage-harness.ts + faulty-backend.ts
- TransactionalFileStore:忠实 OPFS 提交语义(close 才可见)+ 字节级故障注入
  (failNextWrite/Append/Delete、truncateAppendTo 撕裂写、crashPending 真崩溃)
- 删除旧 opfs-mock:读返回内部引用、keepExistingData:false 不截断、close 空实现
  导致"提交前可见"等真实缺陷无法被测出(31 个测试文件迁移至新 harness)
- 删除 aria-opfs-backend 内的第三份重复 mock(含从未被断言使用的 writeCalls 死代码
  与 entry.content.subarray 恒等分支)
- FaultyBackend:包装任意 IStorageBackend 注入故障;crash() 明确区别于 close()
  (后者是优雅停机,会刷完写队列 —— 这正是此前所有"崩溃恢复"测试的真相)
- 16 条基座自测证明注入真的生效(含 close 不能当崩溃的对照组)

【覆盖率口径】jest.config.cjs
- 移除 '!src/**/index.ts'(该 glob 把 AriaEngine 主实现等 15 个实现文件整体
  排除出统计,与 v0.2.6 曾承认过的问题同源),改为只排除纯类型声明文件并附理由
- 新增 coverageThreshold 门禁(此前完全不存在)
- 真实基线:语句 90.66% / 分支 82.94% / 函数 94.36% / 行 93.43%
- 修正 testMatch 使 tests/helpers 下的测试可被发现

【测试代码类型检查】tsconfig.test.json + npm run typecheck:tests
- 修复 103 个测试代码类型错误(此前 babel 剥离类型 + tsconfig 排除 tests,全部隐藏)
- 新增 tests/helpers/assertions.ts:nonNull/decode/rows/object/engineMethod/expectCode
  以断言收窄替代 as any
- 消除 21 个 lint warning(含 v043-hardening 中定义后从未调用的 mockOPFS 死代码)
- parser.test.ts 12 处 toBeDefined() 空断言升级为结构断言(并新增 AND/OR 优先级用例,
  当前红灯,对应总账第 11 项,将在工作流 A 修复)

【版本契约】新增 tests/version-contract.test.ts
- 校验 src VERSION / package.json / dist 三者一致,替代两处硬编码版本字面量

【CI 门禁】.gitea/workflows/ci.yml
- lint 去掉 continue-on-error(此前永远不让 CI 变红)
- 新增 tests 类型检查、--coverage 覆盖率门禁、dist 与源码同步校验
- 版本 0.7.4 升至 0.8.0
2026-09-14 21:03:06 +08:00

367 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* v0.2.5 修复验证测试
* 验证所有 P0/P1/P2 修复点
*/
import { VERSION } from '../src/constants';
import { MetonaSqlark } from '../src/core';
import { MemoryEngine } from '../src/engine/memory';
import { parse } from '../src/sql/parser';
import { QueryExecutor } from '../src/query/executor';
import { WAL } from '../src/engine/aria/wal/log';
import { SSTableReader } from '../src/engine/aria/index/sstable';
import { CryptoManager } from '../src/engine/aria/crypto';
import { PluginManager } from '../src/plugin/index';
import type { SSTableMeta } from '../src/engine/aria/types';
import type { MetonaPlugin } from '../src/constants';
import { createSchema } from '../src/table/schema';
import { resetOPFSMock } from './helpers/storage-harness';
beforeEach(() => { resetOPFSMock(); });
// ---------------------------------------------------------------------------
// P0-1: 版本号统一
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-1: 版本号统一', () => {
// v0.8.0: 版本号一致性收敛到单一契约测试(tests/version-contract.test.ts),
// 此处不再硬编码版本字面量 —— 此前每次发版必须手改本文件,且只校验源码常量、
// 与 package.json / dist 之间没有任何一致性检查。
test('VERSION 常量是合法的语义化版本', () => {
expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/);
});
});
// ---------------------------------------------------------------------------
// P0-2: AriaEngine OPFS 后端映射修复
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-2: AriaEngine OPFS 后端映射', () => {
test('mode=aria + diskEngine=opfs 时应使用 opfs 后端', () => {
const db = new MetonaSqlark({ name: `test-opfs-map-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'opfs' });
// 不实际 open(需要浏览器环境),只验证 createEngine 逻辑
// 通过 getEngine 在 init 后检查
expect(db).toBeDefined();
});
test('mode=aria + diskEngine=indexeddb 时应使用 indexeddb 后端', () => {
const db = new MetonaSqlark({ name: `test-idb-map-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, mode: 'aria', diskEngine: 'opfs' });
expect(db).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// P0-3: _onError 接入执行路径
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-3: _onError 接入执行路径', () => {
test('query 失败时调用 onError 回调', async () => {
const errors: Error[] = [];
const db = new MetonaSqlark({
name: 'test-onerror',
mode: 'memory',
onError: (e) => errors.push(e),
});
await db.init();
await db.defineTable('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
// 故意执行不存在的表查询
await expect(db.query('SELECT * FROM nonexistent')).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0);
await db.close();
});
test('defineTable 失败时调用 onError', async () => {
const errors: Error[] = [];
const db = new MetonaSqlark({
name: 'test-onerror2',
mode: 'memory',
onError: (e) => errors.push(e),
});
await db.init();
await db.defineTable('dup', {
id: { type: 'string', primaryKey: true },
});
// 重复创建
await expect(db.defineTable('dup', {
id: { type: 'string', primaryKey: true },
})).rejects.toThrow();
expect(errors.length).toBeGreaterThan(0);
await db.close();
});
});
// ---------------------------------------------------------------------------
// P0-4: maxRowsPerQuery 生效
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-4: maxRowsPerQuery 生效', () => {
test('结果集被截断为 maxRowsPerQuery', async () => {
const engine = new MemoryEngine();
await engine.open('test-maxrows', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
val: { type: 'number' },
}));
// 插入 100 行
for (let i = 0; i < 100; i++) {
await engine.insert('items', [{ id: `item${i}`, val: i }]);
}
const executor = new QueryExecutor(engine, 10); // maxRowsPerQuery=10
const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(10); // 截断为 10 行
await engine.close();
});
test('maxRowsPerQuery=0 表示不限制', async () => {
const engine = new MemoryEngine();
await engine.open('test-nolimit', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
}));
for (let i = 0; i < 50; i++) {
await engine.insert('items', [{ id: `i${i}` }]);
}
const executor = new QueryExecutor(engine, 0);
const stmt = parse('SELECT * FROM items');
const result = await executor.execute(stmt) as Record<string, unknown>[];
expect(result.length).toBe(50);
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P0-5: WAL full 模式真正同步
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-5: WAL full 模式同步', () => {
test('append 在 full 模式下是 async 且可 await', async () => {
let appendCount = 0;
const wal = new WAL({
append: async (_data: Uint8Array) => { appendCount++; },
readAll: async () => new Uint8Array(0),
truncate: async () => {},
exists: async () => false,
}, true, 'full');
// append 现在返回 Promise
await wal.append({
type: 1, // INSERT
txnId: 0,
tableName: 'test',
key: 'k1',
data: { v: 1 },
} as any);
expect(appendCount).toBe(1);
});
});
// ---------------------------------------------------------------------------
// P0-6: PluginManager.install 传 db 实例
// ---------------------------------------------------------------------------
describe('[v0.2.5] P0-6: PluginManager.install 传 db 实例', () => {
test('register 传 db 给 install', () => {
let receivedDb: unknown = null;
const plugin: MetonaPlugin = {
name: 'test-plugin',
version: '1.0.0',
install: (db) => { receivedDb = db; },
destroy: () => {},
};
const pm = new PluginManager();
const fakeDb = { name: 'fake' };
pm.register(plugin, fakeDb);
expect(receivedDb).toBe(fakeDb);
});
});
// ---------------------------------------------------------------------------
// P1-7: SSTableReader 二分查找统一
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-7: SSTableReader 二分查找', () => {
test('rangeScan 使用二分查找正确定位', () => {
// 构建一个 SSTable 手工
const { SSTableBuilder } = require('../src/engine/aria/index/sstable_builder');
const builder = new SSTableBuilder(4096);
// 添加足够多的条目以形成多个 block
for (let i = 0; i < 100; i++) {
const key = `key${String(i).padStart(5, '0')}`;
builder.add(key, { data: `value${i}` });
}
const { sstableData } = builder.build();
const meta: SSTableMeta = {
id: 1,
level: 0,
minKey: 'key00000',
maxKey: 'key00099',
blockCount: 1,
totalSize: sstableData.byteLength,
bloomData: null,
};
const reader = new SSTableReader(sstableData, meta);
// 精确查找
const result = reader.get('key00050');
expect(result).not.toBeNull();
expect((result as any).data).toBe('value50');
// 范围扫描
const collected: string[] = [];
reader.rangeScan('key00010', 'key00020', (k) => collected.push(k));
expect(collected.length).toBeGreaterThan(0);
expect(collected[0]).toBe('key00010');
});
});
// ---------------------------------------------------------------------------
// P1-8: SQL 注入防护 — 表名校验
// 真实覆盖位置:tests/integrations/react.test.tsuseTable
// tests/integrations/vue.test.tsuseSqlarkTable
// 通过真实 hooks 调用触发 validateTableName,验证非法表名抛错。
// 注:v0.2.6 移除此处"复制正则自测"的伪测试(未触达真实代码)。
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// P1-9: crypto 实例化
// ---------------------------------------------------------------------------
describe('[v0.2.5] P1-9: CryptoManager 实例化', () => {
test('CryptoManager 可以独立实例化', () => {
const cm1 = new CryptoManager();
const cm2 = new CryptoManager();
expect(cm1.enabled).toBe(false);
expect(cm2.enabled).toBe(false);
// 两个实例互不影响
expect(cm1).not.toBe(cm2);
});
});
// ---------------------------------------------------------------------------
// P2-14: ALTER TABLE 语法
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-14: ALTER TABLE', () => {
test('解析 ALTER TABLE ADD COLUMN', () => {
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE');
expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).name).toBe('users');
expect((stmt as any).action).toBe('ADD');
expect((stmt as any).column.name).toBe('email');
});
test('解析 ALTER TABLE DROP COLUMN', () => {
const stmt = parse('ALTER TABLE users DROP COLUMN email');
expect(stmt.type).toBe('ALTER_TABLE');
expect((stmt as any).action).toBe('DROP');
expect((stmt as any).column.name).toBe('email');
});
test('执行 ALTER TABLE ADD COLUMN', async () => {
const engine = new MemoryEngine();
await engine.open('test-alter', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
}));
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users ADD COLUMN email VARCHAR(255)');
await executor.execute(stmt);
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeDefined();
await engine.close();
});
test('执行 ALTER TABLE DROP COLUMN', async () => {
const engine = new MemoryEngine();
await engine.open('test-alter-drop', 1);
await engine.createTable(createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
email: { type: 'string' },
}));
const executor = new QueryExecutor(engine);
const stmt = parse('ALTER TABLE users DROP COLUMN email');
await executor.execute(stmt);
const schema = await engine.getTableSchema('users');
expect(schema!.columns.email).toBeUndefined();
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P2-15: TRUNCATE TABLE 语法
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-15: TRUNCATE TABLE', () => {
test('解析 TRUNCATE TABLE', () => {
const stmt = parse('TRUNCATE TABLE users');
expect(stmt.type).toBe('TRUNCATE_TABLE');
expect((stmt as any).name).toBe('users');
});
test('执行 TRUNCATE TABLE 清空数据', async () => {
const engine = new MemoryEngine();
await engine.open('test-truncate', 1);
await engine.createTable(createSchema('items', {
id: { type: 'string', primaryKey: true },
}));
await engine.insert('items', [
{ id: 'a' }, { id: 'b' }, { id: 'c' },
]);
const executor = new QueryExecutor(engine);
const stmt = parse('TRUNCATE TABLE items');
await executor.execute(stmt);
const rows = await engine.find('items', { table: 'items' });
expect(rows.length).toBe(0);
await engine.close();
});
});
// ---------------------------------------------------------------------------
// P2-12: WAL 大小阈值接入 checkpoint
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-12: WAL 大小阈值', () => {
test('CheckpointManager 接收 walSizeThreshold 参数', () => {
const { CheckpointManager } = require('../src/engine/aria/wal/checkpoint');
const fakeLsm = { flush: async () => {} };
const fakeWal = { flush: async () => {}, checkpoint: async () => {}, getBufferedCount: () => 0 };
const cm = new CheckpointManager(fakeLsm, fakeWal, null, 1000, 1024);
expect(cm).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// P2-13: compactLevelSync 接口公开化
// ---------------------------------------------------------------------------
describe('[v0.2.5] P2-13: compactLevel public', () => {
test('LSM.compactLevel 是 public 方法', () => {
const { LSM } = require('../src/engine/aria/index/lsm');
const lsm = new LSM({
sstableStore: {
save: async () => {}, load: async () => null, delete: async () => {},
allocateId: async () => 1, listMeta: async () => [], saveMeta: async () => {}, deleteMeta: async () => {},
},
});
expect(typeof lsm.compactLevel).toBe('function');
});
});