Files
MetonaSqlark/tests/table/schema.test.ts
T
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

190 lines
6.4 KiB
TypeScript
Raw 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.
/**
* Schema 边缘场景测试
*/
import { createSchema, validateRow, getPrimaryKey, checkFieldType, astColumnToColumnDef } from '../../src/table/schema';
describe('Schema 边缘场景', () => {
// ---- createSchema ----
describe('createSchema', () => {
it('空列表抛出错误', () => {
expect(() => createSchema('empty', {})).toThrow('at least one column');
});
it('多主键定义(取第一个标记的)', () => {
const schema = createSchema('multi', {
id1: { type: 'string', primaryKey: true },
id2: { type: 'string' },
});
expect(getPrimaryKey(schema)).toBe('id1');
});
it('大表定义', () => {
const cols: Record<string, any> = {};
for (let i = 0; i < 50; i++) {
cols[`col${i}`] = { type: 'string' };
}
cols.id = { type: 'string', primaryKey: true };
const schema = createSchema('big', cols);
expect(Object.keys(schema.columns)).toHaveLength(51);
});
});
// ---- getPrimaryKey ----
describe('getPrimaryKey', () => {
it('没有标记主键时返回第一列', () => {
const _schema = createSchema('test', {
uuid: { type: 'string', primaryKey: true },
name: { type: 'string' },
});
// 重新构造不带主键标记的 schema
const noPkSchema = { name: 'test', columns: {
first: { type: 'string' as const },
second: { type: 'string' as const },
}};
expect(getPrimaryKey(noPkSchema)).toBe('first');
});
});
// ---- validateRow ----
describe('validateRow', () => {
const schema = createSchema('users', {
id: { type: 'string', primaryKey: true },
name: { type: 'string', required: true },
bio: { type: 'string', maxLength: 200 },
score: { type: 'number', min: 0, max: 100, default: 50 },
active: { type: 'boolean', default: true },
birthday: { type: 'date' },
tags: { type: 'json', default: [] },
});
it('跳过 undefined 字段(使用默认值)', () => {
const row = validateRow(schema, { id: '1', name: 'Alice' });
expect(row.id).toBe('1');
expect(row.name).toBe('Alice');
expect(row.score).toBe(50);
expect(row.active).toBe(true);
expect(row.tags).toEqual([]);
});
it('null 值被视为不满足必填', () => {
expect(() => validateRow(schema, { id: '1', name: null })).toThrow('required');
});
it('json 类型接受对象', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', tags: { key: 'value' } });
expect(row.tags).toEqual({ key: 'value' });
});
it('json 类型接受数组', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', tags: [1, 2, 3] });
expect(row.tags).toEqual([1, 2, 3]);
});
it('date 类型接受有效日期字符串', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', birthday: '2024-01-15' });
expect(row.birthday).toBe('2024-01-15');
});
it('date 类型拒绝无效格式', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', birthday: 'not-a-date' }))
.toThrow('expects valid date');
expect(() => validateRow(schema, { id: '1', name: 'Alice', birthday: 123 }))
.toThrow('expects valid date');
});
it('boolean 类型', () => {
const row = validateRow(schema, { id: '1', name: 'Alice', active: false });
expect(row.active).toBe(false);
});
it('boolean 拒绝非布尔值', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', active: 'yes' }))
.toThrow('expects boolean');
});
it('number 拒绝字符串', () => {
expect(() => validateRow(schema, { id: '1', name: 'Alice', score: 'high' }))
.toThrow('expects number');
});
it('string 拒绝数字', () => {
expect(() => validateRow(schema, { id: 123, name: 'Alice' }))
.toThrow('expects string');
});
});
// ---- checkFieldType ----
describe('checkFieldType', () => {
it('string 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'string', 'hello')).not.toThrow();
});
it('string 类型拒绝', () => {
expect(() => checkFieldType('t', 'c', 'string', 123)).toThrow('expects string');
});
it('number 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'number', 42)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'number', 3.14)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'number', 0)).not.toThrow();
});
it('boolean 类型通过', () => {
expect(() => checkFieldType('t', 'c', 'boolean', true)).not.toThrow();
expect(() => checkFieldType('t', 'c', 'boolean', false)).not.toThrow();
});
it('json 拒绝非对象', () => {
expect(() => checkFieldType('t', 'c', 'json', 'not-object')).toThrow('expects object');
expect(() => checkFieldType('t', 'c', 'json', 123)).toThrow('expects object');
});
});
// ---- 约束校验(v0.2.3 ----
describe('约束校验', () => {
it('string 超出 maxLength 抛出错误', () => {
expect(() => checkFieldType('t', 'c', 'string', 'hello', { type: 'string', maxLength: 3 }))
.toThrow('exceeds max length');
});
it('string 未超 maxLength 不报错', () => {
expect(() => checkFieldType('t', 'c', 'string', 'hi', { type: 'string', maxLength: 5 }))
.not.toThrow();
});
it('number 低于 min 抛出错误', () => {
expect(() => checkFieldType('t', 'c', 'number', -5, { type: 'number', min: 0 }))
.toThrow('below minimum');
});
it('number 高于 max 抛出错误', () => {
expect(() => checkFieldType('t', 'c', 'number', 200, { type: 'number', max: 100 }))
.toThrow('above maximum');
});
it('number 在范围内不报错', () => {
expect(() => checkFieldType('t', 'c', 'number', 50, { type: 'number', min: 0, max: 100 }))
.not.toThrow();
});
});
// ---- astColumnToColumnDef ----
describe('astColumnToColumnDef', () => {
it('转换基础列', () => {
const result = astColumnToColumnDef({ name: 'id', type: 'string' });
expect(result.type).toBe('string');
});
it('转换带修饰符的列', () => {
const result = astColumnToColumnDef({
name: 'id', type: 'string', primaryKey: true, unique: true, required: true,
});
expect(result.primaryKey).toBe(true);
expect(result.unique).toBe(true);
expect(result.required).toBe(true);
});
});
});