Files
MetonaSqlark/tests/v080-plugin-priority.test.ts
thzxx 799560ea05 docs(G6): 宣称与实现一致性收口 —— priority 真正生效、MVCC/backup/引擎数/打包 全部对齐
独立核验(12 条宣称逐条对源码验证)发现 5 处**硬伤**与 2 处**数字过期**,
本提交按"能改代码就让宣称成立、改不动就如实描述"的原则全部收口。

让实现符合文档(2 处):
1. **插件 priority 此前不生效** — `register()` 虽按 priority 插入数组,但
   `install()` 在 register 内**立即**执行,因此 install 与钩子顺序 = config 数组
   顺序(实测 priority low=1/high=100/mid=50 时钩子按 low→high→mid 触发,
   只有 `getPlugins()` 是 high,mid,low)。而 README/CONTRIBUTING/constants
   一直宣称"越大越先执行"。
   现在 Core 注册前按 priority **稳定降序**排序(同优先级保持数组顺序),
   install 与钩子都按优先级执行 → 宣称成立。新增
   `tests/v080-plugin-priority.test.ts` 锁定 install 顺序、钩子顺序、稳定性、缺省值。
2. **连接池静态方法不在类型系统里** — `MetonaSqlark.connect/disconnect/
   disconnectAll/getActiveConnections` 由 connection-manager 用
   `as unknown as Record<string, unknown>` 注入,README 的连接池表格在
   TypeScript 下全部 TS2339。现在在类上声明为可选静态成员,注入处去掉断言。

如实描述(3 处):
3. **MVCC 快照隔离**(README 三处 + 实现对照)— `snapshotLsn` / `prevVersion`
   只写不读,事务读走 `txnSnapshot`+LSM,commit 即清理版本链,并发
   `beginTransaction` 抛 `TX_ACTIVE`。改为"快照回滚(事务串行,非 MVCC 隔离)",
   并在 README 架构图与维护语句表里同步措辞。
4. **"存储引擎(5 种)"** — 实际是 4 种模式 + 3 种后端,引擎类只有 4 个
   (Memory / KVStore / Hybrid / Aria),OPFS 是后端而非引擎。标题与条目已改写,
   并写明"`disk`/`hybrid` 恒用 KVStore"。
5. **`diskEngine` 生效范围** — 仅 `mode:'aria'` 生效;`constants.ts` 的注释
   此前写成"仅 mode='disk'|'hybrid' 时生效"(正好写反),已改正;README 配置表、
   快速开始示例与 Aria 示例同步标注。

数字口径统一(可复现):
- 测试 1872(90 套件)+ 14 e2e,另 4 个重型套件在独立 CI job 串行运行;
- 覆盖率 语句 90.43% / 分支 82.21% / 函数 94.27% / 行 93.44%;
- README 明确写出**产出这些数字的完整命令**(与 CI 常规 job 一致),
  并要求改动覆盖范围/阈值时同步更新表格(G5)。
- CHANGELOG 0.8.0 条目与 site 首页/文档页同步。

另修 **CONTRIBUTING 的钩子契约**:明确写出"返回值被忽略(不能取消/改写)、
就地改参数在 Table API 生效、抛异常可取消、SQL 路径的 beforeInsert 收到副本"
—— 此前只写 "allow intercepting",容易被理解为返回值可改变行为。

验证:全量 90 套件 / 1872 测试通过(+4 重型套件);覆盖率四项均高于阈值;
typecheck(src+tests)、lint、build 零错误零告警;e2e 14 项通过;dist 已重建。
2026-09-15 08:06:23 +08:00

92 lines
4.0 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.8.0 回归套件 —— 插件 priority 语义(G6 宣称与实现一致)
* ============================================================================
* 修复前:`PluginManager.register()` 会把插件**按 priority 插入数组的正确位置**
* 但 `install()` 是在 `register()` 内部**立即**调用的 —— 于是 install 与钩子的
* 实际执行顺序仍等于 `config.plugins` 的数组顺序。
* 实测(priority 为 low=1 / high=100 / mid=50,数组顺序 low,high,mid):
* 钩子顺序 low→high→mid,只有 `getPlugins()` 是 high,mid,low。
* 而 README / CONTRIBUTING / `constants.ts` 一直宣称"priority 越大越先执行"
* —— 文档与实现不符。本套件锁定**让实现符合文档**后的行为(priority 是用户
* 可见的配置项,静默无效比没有这个字段更糟)。
*
* 实现要点:Core 在注册前按 priority **稳定**排序(同优先级保持数组顺序),
* 因此 `install()` 与由它注册的钩子都按优先级降序执行。
*/
import { describe, it, expect } from '@jest/globals';
import { MetonaSqlark } from '../src/core';
import { PluginManager } from '../src/plugin/index';
import type { MetonaPlugin } from '../src/constants';
function makePlugin(name: string, priority: number, log: string[]): MetonaPlugin {
return {
name,
version: '1.0.0',
priority,
install: (db?: unknown) => {
log.push(`install:${name}`);
// register(plugin) 不带 db 时(纯 PluginManager 用法)没有 db 实例可挂钩子
if (db) (db as MetonaSqlark).on('afterQuery', () => { log.push(`hook:${name}`); });
},
destroy: () => log.push(`destroy:${name}`),
} as MetonaPlugin;
}
describe('[v0.8.0] 插件 priority', () => {
it('install 与钩子都按 priority 降序执行(同优先级保持数组顺序)', async () => {
const log: string[] = [];
const db = await MetonaSqlark.create({
name: 'prio-order',
mode: 'memory',
plugins: [makePlugin('low', 1, log), makePlugin('high', 100, log), makePlugin('mid', 50, log)],
});
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
expect(log.filter((l) => l.startsWith('install:'))).toEqual([
'install:high', 'install:mid', 'install:low',
]);
expect(log.filter((l) => l.startsWith('hook:'))).toEqual([
'hook:high', 'hook:mid', 'hook:low',
]);
await db.close();
});
it('同优先级保持 config 数组顺序(稳定排序)', async () => {
const log: string[] = [];
const db = await MetonaSqlark.create({
name: 'prio-stable',
mode: 'memory',
plugins: [makePlugin('a', 5, log), makePlugin('b', 5, log), makePlugin('c', 5, log)],
});
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
expect(log.filter((l) => l.startsWith('install:'))).toEqual(['install:a', 'install:b', 'install:c']);
await db.close();
});
it('缺省 priority 视为 0(排在显式正优先级之后)', async () => {
const log: string[] = [];
const noPriority = { ...makePlugin('none', 0, log) } as MetonaPlugin & { priority?: number };
delete noPriority.priority;
const db = await MetonaSqlark.create({
name: 'prio-default',
mode: 'memory',
plugins: [noPriority, makePlugin('explicit', 10, log)],
});
await db.defineTable('t', { id: { type: 'string', primaryKey: true } });
await db.query("INSERT INTO t VALUES ('1')");
expect(log.filter((l) => l.startsWith('install:'))).toEqual(['install:explicit', 'install:none']);
await db.close();
});
it('PluginManager.getPlugins() 按 priority 降序(回归护栏)', () => {
const log: string[] = [];
const pm = new PluginManager();
pm.register(makePlugin('low', 1, log));
pm.register(makePlugin('high', 100, log));
pm.register(makePlugin('mid', 50, log));
expect(pm.getPlugins().map((p) => p.name)).toEqual(['high', 'mid', 'low']);
});
});