53 lines
5.2 KiB
JavaScript
53 lines
5.2 KiB
JavaScript
/**
|
||
* 浏览器迁移控制器与旧画布编辑租约测试。旧编辑视图持 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/);
|
||
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);
|
||
});
|