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
This commit is contained in:
+122
-38
@@ -1,69 +1,152 @@
|
||||
/**
|
||||
* Parser 边缘场景测试
|
||||
*
|
||||
* v0.8.0 强化说明:
|
||||
* 1. 此前 12 处断言写着 `expect(ast.where).toBeDefined()` —— 既没有验证结构也没有验证
|
||||
* 值,属于"空断言"。审计(AUDIT-query-layer / SQL 层)指出:`parser.test.ts` 无一处
|
||||
* 断言混合 AND/OR 的**结构**,这正是 AND/OR 无优先级缺陷(总账第 11 项)能长期存活的原因。
|
||||
* 2. 本文件此前 `parse()` 的返回类型是联合类型 `Statement`,直接访问 `.where` / `.columns`
|
||||
* 在类型上不成立(tests 从不做类型检查,所以没人发现)。现在用类型收窄辅助函数显式
|
||||
* 区分语句种类 —— 收窄失败即测试失败,而不是静默读到一个 undefined。
|
||||
*/
|
||||
|
||||
import { parse } from '../../src/sql/parser';
|
||||
import type {
|
||||
SelectStatement,
|
||||
InsertStatement,
|
||||
UpdateStatement,
|
||||
DeleteStatement,
|
||||
CreateTableStatement,
|
||||
} from '../../src/query/ast';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 类型收窄辅助:断言语句种类并返回收窄后的类型
|
||||
// (替代 `as any` —— 收窄失败会立刻让测试失败,而不是让断言落空)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function asSelect(stmt: ReturnType<typeof parse>): SelectStatement {
|
||||
expect(stmt.type).toBe('SELECT');
|
||||
if (stmt.type !== 'SELECT') throw new Error(`expected SELECT, got ${stmt.type}`);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
function asInsert(stmt: ReturnType<typeof parse>): InsertStatement & { values: unknown[][] } {
|
||||
expect(stmt.type).toBe('INSERT');
|
||||
if (stmt.type !== 'INSERT') throw new Error(`expected INSERT, got ${stmt.type}`);
|
||||
if (!stmt.values) throw new Error('INSERT statement is missing values');
|
||||
return stmt as InsertStatement & { values: unknown[][] };
|
||||
}
|
||||
|
||||
function asUpdate(stmt: ReturnType<typeof parse>): UpdateStatement {
|
||||
expect(stmt.type).toBe('UPDATE');
|
||||
if (stmt.type !== 'UPDATE') throw new Error(`expected UPDATE, got ${stmt.type}`);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
function asDelete(stmt: ReturnType<typeof parse>): DeleteStatement {
|
||||
expect(stmt.type).toBe('DELETE');
|
||||
if (stmt.type !== 'DELETE') throw new Error(`expected DELETE, got ${stmt.type}`);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
function asCreateTable(stmt: ReturnType<typeof parse>): CreateTableStatement {
|
||||
expect(stmt.type).toBe('CREATE_TABLE');
|
||||
if (stmt.type !== 'CREATE_TABLE') throw new Error(`expected CREATE_TABLE, got ${stmt.type}`);
|
||||
return stmt;
|
||||
}
|
||||
|
||||
describe('Parser 边缘场景', () => {
|
||||
// ---- SELECT 扩展 ----
|
||||
|
||||
describe('SELECT 扩展', () => {
|
||||
it('WHERE 多条件 AND', () => {
|
||||
const ast = parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'");
|
||||
expect(ast.type).toBe('SELECT');
|
||||
expect(ast.where).toBeDefined();
|
||||
const ast = asSelect(parse("SELECT * FROM users WHERE age > 18 AND name LIKE 'A%'"));
|
||||
// v0.8.0: 断言真实结构而非 toBeDefined()
|
||||
expect(ast.where).toEqual({
|
||||
$and: [{ age: { $gt: 18 } }, { name: { $like: 'A%' } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('WHERE IN 列表', () => {
|
||||
const ast = parse('SELECT * FROM users WHERE id IN (1, 2, 3)');
|
||||
expect(ast.type).toBe('SELECT');
|
||||
expect(ast.where).toBeDefined();
|
||||
const cond = (ast.where as any).id;
|
||||
expect(cond.$in).toEqual([1, 2, 3]);
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE id IN (1, 2, 3)'));
|
||||
expect(ast.where).toEqual({ id: { $in: [1, 2, 3] } });
|
||||
});
|
||||
|
||||
it('WHERE IS NULL', () => {
|
||||
const ast = parse('SELECT * FROM users WHERE bio IS NULL');
|
||||
expect(ast.type).toBe('SELECT');
|
||||
expect(ast.where).toBeDefined();
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL'));
|
||||
expect(ast.where).toEqual({ bio: { $eq: null } });
|
||||
});
|
||||
|
||||
it('WHERE IS NOT NULL', () => {
|
||||
const ast = parse('SELECT * FROM users WHERE bio IS NOT NULL');
|
||||
expect(ast.type).toBe('SELECT');
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NOT NULL'));
|
||||
expect(ast.where).toEqual({ bio: { $ne: null } });
|
||||
});
|
||||
|
||||
it('WHERE NOT', () => {
|
||||
const ast = parse("SELECT * FROM users WHERE NOT age = 18");
|
||||
expect(ast.type).toBe('SELECT');
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE NOT age = 18'));
|
||||
expect(ast.where).toEqual({ $not: { age: { $eq: 18 } } });
|
||||
});
|
||||
|
||||
it('WHERE 括号分组', () => {
|
||||
const ast = parse("SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE");
|
||||
expect(ast.type).toBe('SELECT');
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE (age > 18 OR age < 10) AND active = TRUE'));
|
||||
expect(ast.where).toEqual({
|
||||
$and: [{ $or: [{ age: { $gt: 18 } }, { age: { $lt: 10 } }] }, { active: { $eq: true } }],
|
||||
});
|
||||
});
|
||||
|
||||
// v0.8.0 新增:AND 优先级必须高于 OR(SQL 标准),此前是纯左折叠
|
||||
it('AND 优先级高于 OR(SQL 标准)', () => {
|
||||
const ast = asSelect(parse('SELECT * FROM t WHERE a = 1 OR a = 2 AND b = 3'));
|
||||
expect(ast.where).toEqual({
|
||||
$or: [{ a: { $eq: 1 } }, { $and: [{ a: { $eq: 2 } }, { b: { $eq: 3 } }] }],
|
||||
});
|
||||
});
|
||||
|
||||
it('显式括号可覆盖默认优先级', () => {
|
||||
const ast = asSelect(parse('SELECT * FROM t WHERE (a = 1 OR a = 2) AND b = 3'));
|
||||
expect(ast.where).toEqual({
|
||||
$and: [{ $or: [{ a: { $eq: 1 } }, { a: { $eq: 2 } }] }, { b: { $eq: 3 } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('NOT 优先级高于 AND/OR', () => {
|
||||
const ast = asSelect(parse('SELECT * FROM t WHERE NOT a = 1 OR b = 2'));
|
||||
expect(ast.where).toEqual({
|
||||
$or: [{ $not: { a: { $eq: 1 } } }, { b: { $eq: 2 } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('多层 AND/OR 交替的结合性', () => {
|
||||
const ast = asSelect(parse('SELECT * FROM t WHERE a = 1 AND b = 2 OR c = 3 AND d = 4'));
|
||||
expect(ast.where).toEqual({
|
||||
$or: [
|
||||
{ $and: [{ a: { $eq: 1 } }, { b: { $eq: 2 } }] },
|
||||
{ $and: [{ c: { $eq: 3 } }, { d: { $eq: 4 } }] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('ORDER BY 多列', () => {
|
||||
const ast = parse('SELECT * FROM users ORDER BY age DESC, name ASC');
|
||||
expect(ast.type).toBe('SELECT');
|
||||
const ast = asSelect(parse('SELECT * FROM users ORDER BY age DESC, name ASC'));
|
||||
expect(ast.orderBy).toHaveLength(2);
|
||||
expect(ast.orderBy![0]).toEqual({ column: 'age', direction: 'desc' });
|
||||
expect(ast.orderBy![1]).toEqual({ column: 'name', direction: 'asc' });
|
||||
});
|
||||
|
||||
it('只有 LIMIT 没有 OFFSET', () => {
|
||||
const ast = parse('SELECT * FROM users LIMIT 5');
|
||||
const ast = asSelect(parse('SELECT * FROM users LIMIT 5'));
|
||||
expect(ast.limit).toBe(5);
|
||||
expect(ast.offset).toBeUndefined();
|
||||
});
|
||||
|
||||
it('只有 OFFSET 没有 LIMIT', () => {
|
||||
const ast = parse('SELECT * FROM users OFFSET 10');
|
||||
expect(ast).toMatchObject({ offset: 10 });
|
||||
const ast = asSelect(parse('SELECT * FROM users OFFSET 10'));
|
||||
expect(ast.offset).toBe(10);
|
||||
expect(ast.limit).toBeUndefined();
|
||||
});
|
||||
|
||||
it('不带 WHERE 的 SELECT', () => {
|
||||
const ast = parse('SELECT id, name FROM users ORDER BY id LIMIT 5');
|
||||
const ast = asSelect(parse('SELECT id, name FROM users ORDER BY id LIMIT 5'));
|
||||
expect(ast.where).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -72,18 +155,18 @@ describe('Parser 边缘场景', () => {
|
||||
|
||||
describe('INSERT 扩展', () => {
|
||||
it('不带列名的 INSERT', () => {
|
||||
const ast = parse("INSERT INTO users VALUES ('1', 'Alice', 30)");
|
||||
const ast = asInsert(parse("INSERT INTO users VALUES ('1', 'Alice', 30)"));
|
||||
expect(ast.columns).toBeUndefined();
|
||||
expect(ast.values).toEqual([['1', 'Alice', 30]]);
|
||||
});
|
||||
|
||||
it('INSERT 布尔值和 NULL', () => {
|
||||
const ast = parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)');
|
||||
const ast = asInsert(parse('INSERT INTO users VALUES (TRUE, FALSE, NULL)'));
|
||||
expect(ast.values[0]).toEqual([true, false, null]);
|
||||
});
|
||||
|
||||
it('INSERT 负数和浮点数', () => {
|
||||
const ast = parse('INSERT INTO scores VALUES (-1, 3.14)');
|
||||
const ast = asInsert(parse('INSERT INTO scores VALUES (-1, 3.14)'));
|
||||
expect(ast.values[0]).toEqual([-1, 3.14]);
|
||||
});
|
||||
});
|
||||
@@ -92,13 +175,13 @@ describe('Parser 边缘场景', () => {
|
||||
|
||||
describe('UPDATE 扩展', () => {
|
||||
it('UPDATE 多列', () => {
|
||||
const ast = parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'");
|
||||
expect(ast.type).toBe('UPDATE');
|
||||
const ast = asUpdate(parse("UPDATE users SET name = 'Bob', age = 26 WHERE id = '1'"));
|
||||
expect(ast.sets).toEqual({ name: 'Bob', age: 26 });
|
||||
expect(ast.where).toEqual({ id: { $eq: '1' } });
|
||||
});
|
||||
|
||||
it('UPDATE 不带 WHERE', () => {
|
||||
const ast = parse('UPDATE users SET active = FALSE');
|
||||
const ast = asUpdate(parse('UPDATE users SET active = FALSE'));
|
||||
expect(ast.where).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -107,13 +190,15 @@ describe('Parser 边缘场景', () => {
|
||||
|
||||
describe('DELETE 扩展', () => {
|
||||
it('DELETE 不带 WHERE', () => {
|
||||
const ast = parse('DELETE FROM users');
|
||||
const ast = asDelete(parse('DELETE FROM users'));
|
||||
expect(ast.where).toEqual({});
|
||||
});
|
||||
|
||||
it('DELETE 带复杂 WHERE', () => {
|
||||
const ast = parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'");
|
||||
expect(ast.type).toBe('DELETE');
|
||||
const ast = asDelete(parse("DELETE FROM users WHERE age < 18 OR status = 'inactive'"));
|
||||
expect(ast.where).toEqual({
|
||||
$or: [{ age: { $lt: 18 } }, { status: { $eq: 'inactive' } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,8 +206,7 @@ describe('Parser 边缘场景', () => {
|
||||
|
||||
describe('DDL 扩展', () => {
|
||||
it('CREATE TABLE 完整修饰符', () => {
|
||||
const ast = parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)");
|
||||
expect(ast.type).toBe('CREATE_TABLE');
|
||||
const ast = asCreateTable(parse("CREATE TABLE products (id STRING PRIMARY KEY, name STRING NOT NULL UNIQUE, price NUMBER DEFAULT 0, active BOOLEAN DEFAULT TRUE)"));
|
||||
expect(ast.columns).toHaveLength(4);
|
||||
expect(ast.columns[0]).toMatchObject({ name: 'id', primaryKey: true });
|
||||
expect(ast.columns[1]).toMatchObject({ name: 'name', required: true, unique: true });
|
||||
@@ -145,13 +229,13 @@ describe('Parser 边缘场景', () => {
|
||||
|
||||
describe('常量值解析', () => {
|
||||
it('布尔值 TRUE/FALSE', () => {
|
||||
const ast = parse("SELECT * FROM users WHERE active = TRUE");
|
||||
expect(ast.where).toBeDefined();
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE active = TRUE'));
|
||||
expect(ast.where).toEqual({ active: { $eq: true } });
|
||||
});
|
||||
|
||||
it('NULL 值', () => {
|
||||
const ast = parse('SELECT * FROM users WHERE bio IS NULL');
|
||||
expect(ast.where).toBeDefined();
|
||||
const ast = asSelect(parse('SELECT * FROM users WHERE bio IS NULL'));
|
||||
expect(ast.where).toEqual({ bio: { $eq: null } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user