工作流 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
190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|
||
});
|