Files
Endoleap/tests/headless/browser-migration.test.mjs

55 lines
5.5 KiB
JavaScript
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.
/**
* 浏览器迁移控制器与旧画布编辑租约测试。旧编辑视图持 shared lease,迁移要求独占且不等待的 lease,避免新旧写入并行。
* 使用内存 localStorage/Web Locks 替身验证协调逻辑,不代表真实多标签 Web Locks 已在本文件执行。
*/
import {test} from 'node:test';
import assert from 'node:assert/strict';
import {claimLegacyCanvasEditing,createBrowserCanvasMigration} from '../../src/lib/browser-canvas-migration.ts';
import {canvasMigrationStorageKey} from '../../src/lib/canvas-migration.ts';
import {createRegistry,builtinComponents} from '../../src/lib/canvas-core/index.ts';
// 确定性 UUID 及账户/workspace 作用域;editing 锁名与迁移持久化键都从此作用域派生。
const id=n=>`10000000-0000-4000-8000-${String(n).padStart(12,'0')}`,scope={accountId:id(1),workspaceId:id(2)},root=id(3),registry=createRegistry(builtinComponents());
// 搭建 records、held 锁表及调用记录,controller 控制当前视图生命周期。
function fixture(){
const records=new Map(),held=new Map(),calls=[];
// 最小 localStorage 接口,保存原字符串以检查未来/损坏材料不会被覆盖。
const storage={getItem:key=>records.get(key)??null,setItem:(key,value)=>records.set(key,value),removeItem:key=>records.delete(key)};
// 异步模拟共享/独占互斥:不可用时调用 work(null),成功期间增加持有计数,finally 总是归还锁。
const locks={request(name,options,work){calls.push({name,options});return Promise.resolve().then(async()=>{
// shared 只排斥 exclusive,exclusive 排斥任意持有者;这是 fixture 的即时 ifAvailable 模拟,不实现公平等待队列。
const state=held.get(name)??{shared:0,exclusive:false},granted=options.mode==='shared'?!state.exclusive:!state.exclusive&&!state.shared;
if(!granted)return work(null);
if(options.mode==='shared')state.shared++;else state.exclusive=true;held.set(name,state);
try{return await work({name,mode:options.mode});}finally{if(options.mode==='shared')state.shared--;else state.exclusive=false;if(!state.shared&&!state.exclusive)held.delete(name);}
});}};
// 所有租约和迁移实例共享当前 AbortSignal;isCurrentIdentity 可在用例中覆写为过期身份。
const controller=new AbortController(),options={scope,registry,locks,storage,signal:controller.signal,isCurrentIdentity:()=>true};
return{records,held,calls,controller,options};
}
// 两个旧编辑器可共享锁,释放一个仍不能迁移;全部释放后 exclusive 迁移检查可进入 idle,所有请求均 ifAvailable。
test('editable legacy views share a lease and exclude migration until every editor releases',async()=>{
const f=fixture(),one=await claimLegacyCanvasEditing({...f.options,canvasId:root}),two=await claimLegacyCanvasEditing({...f.options,canvasId:root});
const migration=createBrowserCanvasMigration(f.options);
await assert.rejects(()=>migration.resume(),/migration_busy/);await one.release();await assert.rejects(()=>migration.resume(),/migration_busy/);
// 所有 shared 租约释放后 resume 只取得独占检查并返回 idle;夹具里没有待迁移任务,不会发生迁移提交。
await two.release();assert.equal((await migration.resume()).status,'idle');assert.equal(f.held.size,0);
assert.ok(f.calls.every(call=>call.name===canvasMigrationStorageKey(scope)+':editing'&&call.options.ifAvailable===true));
assert.deepEqual(f.calls.slice(0,2).map(call=>call.options.mode),['shared','shared']);assert.ok(f.calls.slice(2).every(call=>call.options.mode==='exclusive'));
});
// pending 迁移只阻止受影响旧 canvas,其他 canvas 可编辑;未来记录版本无法解释时拒绝但原字符串保持。
test('pending migration blocks affected old views and corrupt records cannot enable editing',async()=>{
const f=fixture(),key=canvasMigrationStorageKey(scope),job={version:1,...scope,rootId:root,id:id(4),phase:'pending',body:JSON.stringify({protocol_version:2,request_id:id(4),expected_versions:{[root]:1}})};
f.storage=f.options.storage;f.storage.setItem(key,JSON.stringify(job));
await assert.rejects(()=>claimLegacyCanvasEditing({...f.options,canvasId:root}),/migration_response_unknown/);assert.equal(f.held.size,0);
const unrelated=await claimLegacyCanvasEditing({...f.options,canvasId:id(99)});await unrelated.release();
// 用未来版本记录覆盖测试存储,拒绝后原字符串必须保留,不能把“无法解释”当作“没有任务”。
const saved=JSON.stringify({version:2,data:'retain'});f.storage.setItem(key,saved);
await assert.rejects(()=>claimLegacyCanvasEditing({...f.options,canvasId:root}),/migration_interpreter_unavailable/);assert.equal(f.storage.getItem(key),saved);assert.equal(f.held.size,0);
});
// 视图 abort 或身份失效不能取得迟到编辑授权;已领取和未领取路径都归还锁,重复 release 安全。
test('aborted or stale view lifetimes release leases and cannot acquire a late editing grant',async()=>{
const f=fixture(),lease=await claimLegacyCanvasEditing({...f.options,canvasId:root});f.controller.abort();await lease.release();assert.equal(f.held.size,0);await lease.release();
const g=fixture(),waiting=claimLegacyCanvasEditing({...g.options,canvasId:root});g.controller.abort();await assert.rejects(waiting,{name:'AbortError'});assert.equal(g.held.size,0);
const h=fixture();let current=true;const stale=claimLegacyCanvasEditing({...h.options,canvasId:root,isCurrentIdentity:()=>current});current=false;await assert.rejects(stale,/identity_changed/);assert.equal(h.held.size,0);
});