/** * 持久化元数据订阅测试。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。关闭后订阅为空清理函数。 // updateRuntime 即使只返回内容相同的新对象也会形成元数据根;revision 在 persist 前捕获,用于排除保存过程额外发布根。 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 风格错误归一成字符串,保存失败保持脏状态,重试成功后清除错误。 // broken 只让内存 commit 抛错,不制造真实磁盘配额不足;errors 数组单独核对观察者错误的隔离通路。 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).durableRootRevisionerror.message==='observer'));await f.engine.close();});