Files
Endoleap/tests/headless/durability.test.mjs
T

24 lines
3.9 KiB
JavaScript

/**
* 持久化元数据订阅测试。durability 的脏状态、错误和关闭进度独立于业务 Root 发布。
* 内存 commit 可注入 quota 故障,观察者异常由错误回调接收,不应破坏其他观察者或保存状态。
*/
import {test} from 'node:test';import assert from 'node:assert/strict';
import {createDocument,createRegistry,builtinComponents} from '../../src/lib/canvas-core/index.ts';
import {openProjectEngine} from '../../src/lib/canvas-runtime/engine.ts';
// 建立没有业务网络操作的空文档引擎,先保存初始状态;任何意外的编译、发送、查询都会直接失败。
async function fixture(){let broken=false;const errors=[],engine=await openProjectEngine({scope:{accountId:'a',workspaceId:'w',projectId:'p'},registry:createRegistry(builtinComponents()),document:createDocument('p',[{id:'canvas',parent:null}],[]),versions:new Map([['canvas',1]]),storage:{
// 最小存储 owner 替身;关闭会释放其 active 标记。
async claim(scope){return{scope,active:true,fence:1,async release(){this.active=false;}};},
// 无历史镜像,保证每个用例从新的持久化元数据起步。
async recover(){return null;},
// 开关启用时抛出数字 code 的 quota 对象;否则确认此次镜像实际声明的修订。
async commit(_leader,batch){if(broken)throw{code:22,message:'quota'};return{durableRootRevision:batch.image.rootRevision};},
// 本组不使用本地文件,不额外产生资源任务。
async readFile(){return;},close(){}},
// 全部网络入口都故意抛错,保证测试只观察本地元数据而不是悄悄依赖网络同步。
persistence:{compile(){throw Error('unexpected write');},async submit(){throw Error('unexpected write');},async query(){throw Error('unexpected query');},canonical(){throw Error('unexpected canonical');}},onError:error=>errors.push(error)});await engine.persist();return{engine,errors,fail:value=>broken=value};}
// runtime 修改先通知脏修订,保存后通知对应持久化修订;关闭也通知进度,但完成保存不再增加一次业务 Root。关闭后订阅为空清理函数。
test('durability subscribers observe dirty metadata, final durable cut and closure without another business Root',async()=>{const{engine}=await fixture(),states=[];const stop=engine.subscribeDurability(()=>states.push(engine.getDurability()));assert.strictEqual(engine.getDurability(),engine.getDurability());engine.editor.updateRuntime(previous=>({...previous}));const revision=engine.getSnapshot().rootRevision;assert.ok(states.some(state=>state.rootRevision>state.durableRootRevision));await engine.persist();assert.equal(states.at(-1).durableRootRevision,revision);assert.equal(engine.getSnapshot().rootRevision,revision);await engine.close();assert.ok(states.some(state=>state.closing&&!state.closed));assert.equal(states.at(-1).closed,true);stop();const noop=engine.subscribeDurability(()=>assert.fail('closed owner notified'));noop();});
// 一个监听器抛错不妨碍其他监听器看到 quota;数字 DOM 风格错误归一成字符串,保存失败保持脏状态,重试成功后清除错误。
test('storage failures remain observable and normalize numeric DOM-style codes without listener exceptions changing durability',async()=>{const f=await fixture();const states=[];f.engine.subscribeDurability(()=>{throw Error('observer');});f.engine.subscribeDurability(()=>states.push(f.engine.getDurability()));f.fail(true);f.engine.editor.updateRuntime(previous=>({...previous}));await assert.rejects(f.engine.persist());assert.equal(states.at(-1).error,'quota');assert.equal(typeof states.at(-1).error,'string');assert.ok(states.at(-1).durableRootRevision<states.at(-1).rootRevision);f.fail(false);await f.engine.persist();assert.equal(states.at(-1).error,undefined);assert.equal(states.at(-1).rootRevision,states.at(-1).durableRootRevision);assert.ok(f.errors.some(error=>error.message==='observer'));await f.engine.close();});