docs(tests): clarify browser storage evidence and lifecycle checks
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* 大多数文档/上传服务为内存替身;网络流场景只连接本地可控 fixture,不代表远程 Foundry 数据库验证。
|
||||
* 持久化场景使用独立 scope/数据库,显示借用等 helper 也可复用传入引擎;run 串行编排,通过断言后才追加结果。
|
||||
* 版本号需区分:数据库 schema、checkpoint/journal 编码、recovery 镜像、loading 描述和服务协议分别演进。
|
||||
* engine 的“可见”指工作模型已发布,显示借用检查 Blob URL;本文件未挂载 renderer,不凭模型状态证明屏幕已绘制。
|
||||
*/
|
||||
import {blobCheckpointCase,blockedUpgradeCase} from './blob-checkpoint-browser-cases.mjs';
|
||||
import {depotFilesCase,depotFilesUpgradeCase,depotAdoptionRaceCase} from './depot-files-browser-cases.mjs';
|
||||
@@ -88,7 +89,7 @@ async function defaultImageSizeCase(name, assert) {
|
||||
const scope = { accountId: 'sizing-account', workspaceId: 'sizing-workspace', projectId: 'sizing-project' }, registry = createRegistry(builtinComponents());
|
||||
const original = createDocument(scope.projectId, [{ id: 'root', parent: null }], []), receipts = new Map(), writes = [], queries = [], uploads = [];
|
||||
let server = original, version = 1, next = 0, engine;
|
||||
// 文档服务是替身:compile 固定版本条件,submit 只对 historyEffect 的尺寸变更丢 ACK,query 查询已保存成功结果。
|
||||
// 文档服务是替身:compile 读取当前 ACK 版本作为条件,submit 只对 historyEffect 的尺寸变更丢 ACK,query 查询已保存成功结果。
|
||||
const persistence = {
|
||||
compile(intent, id, snapshot) { return { payload: { intent, id }, canvasIds: intent.canvasIds, expectedVersions: { root: snapshot.runtime.versions.get('root') } }; },
|
||||
async submit(request) { writes.push(request); const { intent } = JSON.parse(request.body); server = applyChanges(server, intent.changes); receipts.set(request.id, { document: server, versions: new Map([['root', ++version]]) }); return intent.historyEffect ? { outcome: 'unresolved', blocked: true, error: 'lost_size_ack' } : { outcome: 'applied' }; },
|
||||
@@ -119,7 +120,8 @@ async function defaultImageSizeCase(name, assert) {
|
||||
} finally { await engine?.close(); images.close(); storage.close(); indexedDB.deleteDatabase(`${name}-default-size`); }
|
||||
}
|
||||
|
||||
// 包装真实 Worker 记录并发和释放:三个 PNG 解码峰值为二,坏字节或中止都不能留下活跃 Worker。
|
||||
// 包装真实 Worker 记录并发和释放:三个 PNG 解码峰值为二,坏字节或中止都不能留下未终止的句柄。
|
||||
// active 在首次 terminate 调用时减一;它验证客户端调用了清理 API,不是浏览器内部线程已经退出的直接测量。
|
||||
async function imageDecodeWorkerCase(assert){
|
||||
// 子类保留原生行为,只在构造成功和首次 terminate 时增减计数;finally 恢复全局构造器。
|
||||
const NativeWorker=globalThis.Worker;let active=0,peak=0,created=0;globalThis.Worker=class extends NativeWorker{constructor(...args){super(...args);active++;created++;peak=Math.max(peak,active);this.live=true;}terminate(){if(this.live){this.live=false;active--;}super.terminate();}};
|
||||
@@ -646,6 +648,7 @@ async function sourceIntegrityCase(name,assert){
|
||||
const node=id=>({kind:'element',key:id,record:{id,incarnation:1,structure:'node',componentId:'text',dataVersion:1,ownerCanvasId:'$target',data:{title:id,text:''},geometry:{x:0,y:0,width:200,height:100,rotation:0}}}),bytes=new Blob([[node('first'),node('last')].map(row=>JSON.stringify(row)).join('\n')]),options={scope,registry,document:original,versions:new Map(versions),storage,persistence:createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport})};
|
||||
try{
|
||||
// 等一个 committedSourceKey 后 pause 并核对源摘要已存在,再关闭引擎保留恢复点。
|
||||
// 摘要断言发生在 settle 后,不证明首项发布前就算完 SHA;读取证明允许早发布,完整 SHA 封存控制后端发送。
|
||||
engine=await openProjectEngine(options);const id=engine.imports.startNdjson(bytes,root);await new Promise(resolve=>{const stop=engine.editor.subscribe((_,snapshot)=>{if(snapshot.runtime.imports.get(id).committedSourceKeys.length===1){stop();resolve();}});});await engine.imports.pause(id);await engine.settle();const before=engine.imports.getJobs().get(id),first=before.stableIds['element:first'];assert(/^[a-f0-9]{64}$/.test(before.source.sha256),'source hash was not bound before publication');await engine.close();const sent=calls;
|
||||
// 原始 IDB 连接定位作用域内 source.fileId,绕过正常存储不可变规则以制造损坏。
|
||||
raw=await new Promise((resolve,reject)=>{const request=indexedDB.open(databaseName);request.onsuccess=()=>resolve(request.result);request.onerror=()=>reject(request.error);});const key=[JSON.stringify([scope.accountId,scope.workspaceId,scope.projectId]),before.source.fileId];
|
||||
@@ -695,7 +698,8 @@ async function deltaJournalCase(name,assert){
|
||||
await storage.commit(leader,{image:image(6)});const extended={marker:'retained header',...image(7)};await storage.commit(leader,{image:extended});assert(JSON.stringify(await storage.recover(leader))===JSON.stringify(extended),'noncanonical header lost fields or property order');const fullRow=await read(raw.transaction('journal').objectStore('journal').get([key,7]));assert(fullRow.checkpointVersion===1&&fullRow.bytes instanceof Blob,'noncanonical header was encoded as a lossy delta');
|
||||
// 手工删掉 durable head 后不能凭内存缓存继续写;失败不推进基线,还原 metadata 后仍恢复 revision 7。
|
||||
const metadata=await read(raw.transaction('metadata').objectStore('metadata').get(key));let metadataTx=raw.transaction('metadata','readwrite'),metadataDone=done(metadataTx);const missingHead={...metadata};delete missingHead.throughRevision;metadataTx.objectStore('metadata').put(missingHead,key);await metadataDone;code=undefined;try{await storage.commit(leader,{image:image(8)});}catch(error){code=error.code;}assert(code==='recovery_delta_base_mismatch','cached baseline overwrote missing durable head');metadataTx=raw.transaction('metadata','readwrite');metadataDone=done(metadataTx);metadataTx.objectStore('metadata').put(metadata,key);await metadataDone;assert((await storage.recover(leader)).rootRevision===7,'head validation failure advanced cached state');
|
||||
// 旧 DB schema 1 客户端必须得到 VersionError,不能用旧解释器读取新差量编码。
|
||||
// 当前数据库版本高于 1,显式用 schema 1 打开必须得到浏览器 VersionError。
|
||||
// 这验证 IDB 版本门禁;journalVersion=99 的解释器拒绝由上面的 future 日志损坏用例单独验证。
|
||||
const oldError=await new Promise(resolve=>{const request=indexedDB.open(databaseName,1);request.onerror=event=>{event.preventDefault();resolve(request.error.name);};request.onsuccess=()=>{request.result.close();resolve('accepted');};});assert(oldError==='VersionError','old database reader accepted the delta format');
|
||||
}finally{raw?.close();await leader?.release();storage.close();indexedDB.deleteDatabase(databaseName);}
|
||||
}
|
||||
@@ -730,6 +734,7 @@ async function referenceCycleCase(name,assert){
|
||||
// 先导入 F/S/A 并核对端口指向新 A,再复制 F 的深闭包,要求 F′ 端口指向 A′。
|
||||
engine=await openProjectEngine(options);const imported=engine.imports.startNdjson(new Blob([[node('F','$target','frame'),{kind:'canvas',key:'S',record:{id:'S',parent:{canvasId:'$target',entryElementId:'F'}}},node('A','S')].map(row=>JSON.stringify(row)).join('\n')]),root);await engine.settle();const first=engine.imports.getJobs().get(imported),F=first.stableIds['element:F'],A=first.stableIds['element:A'];assert(first.status==='completed'&&first.committedSourceKeys.length===3,'native cycle did not complete atomically');assert(document.elements.get(F).data.ports[0].nodeId===A,'native port kept the source ID');
|
||||
const copied=await engine.imports.duplicate([{id:F,incarnation:1}]);await engine.settle();const job=engine.imports.getJobs().get(copied),F2=job.stableIds[`element:${F}`],A2=job.stableIds[`element:${A}`];assert(job.status==='completed'&&job.committedSourceKeys.length===3,'copy cycle lost a member');assert(document.elements.get(F2).data.ports[0].nodeId===A2&&A2!==A,'copy port attached to the original child');
|
||||
// 此 helper 核对提交后、undo/redo 和重开后的闭包,不订阅每个中间 Root;不能仅凭最终结果证明发布期间从无半组。
|
||||
// 重开保持重映射;undo 不留下半组,redo 后子元素是第二代且端口仍连接同一新 ID。
|
||||
await engine.close();engine=await openProjectEngine(options);assert(engine.getSnapshot().document.elements.get(F2).data.ports[0].nodeId===A2,'IndexedDB lost the copied reference');assert(engine.editor.history.undo().status==='applied','cycle undo failed');await engine.settle();assert(!document.elements.has(F2)&&!document.elements.has(A2)&&document.elements.has(F),'cycle undo exposed half a group');assert(engine.editor.history.redo().status==='applied','cycle redo failed');await engine.settle();await engine.close();engine=await openProjectEngine(options);assert(engine.getSnapshot().document.elements.get(F2).data.ports[0].nodeId===A2&&engine.getSnapshot().document.elements.get(A2).incarnation===2,'cycle redo/reopen lost the restored identity');
|
||||
}finally{await engine?.close();storage.close();indexedDB.deleteDatabase(database);}
|
||||
|
||||
Reference in New Issue
Block a user