'use strict'; const {vm,app,assert,functionSource}=require('./helpers/app-source'); const persistSource=functionSource('persistDirtyToDb'); const startupSource=functionSource('loadSnapshotFromDb'); const startupV2Source=functionSource('loadV2SnapshotFromDb'); const initialSource=functionSource('readInitialDataAsync'); const replaceSource=functionSource('beginWorldReplacement'); const clearSource=functionSource('clearDatabaseWorld'); assert(startupSource.includes('loadV2SnapshotFromDb')&&startupV2Source.includes("db.transaction(['worlds','boardIndex','outboxV2','recoveryV2','tombstonesV2'],'readonly')"), 'Startup does not read the snapshot, recovery coverage, and tombstones atomically'); assert(startupV2Source.includes("objectStore('recoveryV2').getAll")&&startupV2Source.includes("objectStore('tombstonesV2').getAll"), 'Startup omits database recovery journals or tombstones'); assert(!initialSource.includes('loadRecoveryCoverage('), 'Startup still performs the recovery coverage read in a second transaction'); assert(replaceSource.includes('await preserveRecoveryDurably('), 'World replacement does not await a verified recovery backup'); assert(functionSource('runStatusRetry').includes('statusRetryAction')&&!app.includes('復元用バックアップがありません。'), 'The shared-server retry button still attempts obsolete local-backup recovery'); assert(clearSource.includes("activateReadyWorldV2({epoch:newEpoch,expected,world},{kind:'reset'})"), 'Fresh-world reset does not use atomic epoch activation'); assert(!app.includes('worldMutationLockDepth')&&functionSource('withWorldMutationLock').includes("mode:'exclusive'"), 'World mutation locking still bypasses unrelated asynchronous callers'); assert(functionSource('persistNow').includes('if(options.lockHeld===true)return run()')&&functionSource('expandMetaNow').includes('lockHeld:true'), 'Nested expansion persistence can deadlock behind a queued lock waiter'); assert(!app.includes('worldInitReady')&&!app.includes('deferredWorldSignals')&&!app.includes('BroadcastChannel'), 'Retired cross-tab board synchronization remains'); assert(functionSource('fetchCurrentSharedWorldStatus').includes("status.singleWorld===true")&&functionSource('initCloudSync').includes('resetClientToSingleSharedWorld()'), 'Startup does not require and adopt the single server-authoritative world'); const request=result=>({result}); function memoryStore(initial=[],keyOf=row=>row.id){ const normalizeKey=key=>Array.isArray(key)?key.join(':'):key,rows=new Map(initial.map(row=>[normalizeKey(keyOf(row)),structuredClone(row)])); return{ rows, get:key=>request(rows.has(normalizeKey(key))?structuredClone(rows.get(normalizeKey(key))):null), put:row=>{rows.set(normalizeKey(keyOf(row)),structuredClone(row));return request(keyOf(row))}, delete:key=>{rows.delete(normalizeKey(key));return request(undefined)}, clear:()=>{rows.clear();return request(undefined)} }; } function persistenceContext({localMeta=null,localState=null,existingMeta=null,existingState=null,deletion=null,identityEpoch='world:test'}={}){ const existingId=existingMeta?.id||'B0',existingIndex=existingMeta?{epoch:'world:test',id:existingId,metaRev:existingMeta.rev||0,stateRev:existingState?.rev||0}:null; const remoteGlobal={worldEpoch:'world:test',globalRev:20,globalRevAuthor:'remote',nextId:9,cursorStyle:'remote'}; const stores={ control:memoryStore([{key:'active',activeFormat:2,activeEpoch:identityEpoch}],row=>row.key), worlds:memoryStore([{epoch:'world:test',status:'active',createdAt:1,source:{kind:'fresh'},global:remoteGlobal}],row=>row.epoch), boardIndex:memoryStore(existingIndex?[existingIndex]:[],row=>`${row.epoch}:${row.id}`), boardPuzzles:memoryStore(existingMeta?[{epoch:'world:test',id:existingId,metaRev:existingMeta.rev||0,meta:existingMeta}]:[],row=>`${row.epoch}:${row.id}`), boardStates:memoryStore(existingState?[{epoch:'world:test',id:existingId,stateRev:existingState.rev||0,value:existingState}]:[],row=>`${row.epoch}:${row.id}`), outboxV2:memoryStore([],row=>`${row.epoch}:${row.key}`),recoveryV2:memoryStore([],row=>`${row.epoch}:${row.key}`),tombstonesV2:memoryStore([],row=>`${row.epoch}:${row.id}`) }; const id=localMeta?.id||existingMeta?.id||'B0'; const data={ worldEpoch:'world:test',globalRev:10,globalRevAuthor:'local',nextId:2,cursorStyle:'local', metas:localMeta?{[id]:structuredClone(localMeta)}:{}, states:localState?{[id]:structuredClone(localState)}:{}, updatedAt:0,clockFloor:0 }; const context={ console,structuredClone,sessionId:'local',lastRevision:100,data, dirtyMetaIds:new Set(localMeta?[id]:[]),dirtyStateIds:new Set(localState?[id]:[]), deletedBoardIds:new Set(deletion?[id]:[]), deletedBoardRevisions:new Map(deletion?[[id,deletion.rev]]:[]), deletedBoardAuthors:new Map(deletion?[[id,deletion.revAuthor]]:[]), recoveryJournalsToCover:[],recoveryJournalSeq:-1,recoveryWalPromise:Promise.resolve(), cloudOutboxDeleteKeys:new Set(),cloudJournalMetaIds:new Set(),cloudJournalStateIds:new Set(), cloudJournalDeletedIds:new Set(deletion?[id]:[]),cloudApiEnabled:false, globalDirty:true,globalChangeSeq:1,cloudJournalGlobalChanged:true,worldSignalSeq:0, idbAvailable:true,activeStorageFormat:2,FIELD_STORAGE_FORMAT:2,storageAccessError:null,normalizedStateObjects:new WeakSet(), boardIndexSummaries:new Map(), statsDirty:false,resetHistory:[], pruneAndCount:()=>{}, hasPendingPersistence:()=>context.globalDirty||context.dirtyMetaIds.size>0||context.dirtyStateIds.size>0||context.deletedBoardIds.size>0, validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'), storedWorldEpoch:()=>null,createWorldEpoch:()=> 'world:test',rememberWorldEpoch:()=>true, revisionVersion:value=>({rev:value?.rev||0,revAuthor:value?.revAuthor||value?.author||''}), compareRevisionVersions:(a,b)=>(a?.rev||0)-(b?.rev||0)||String(a?.revAuthor||a?.author||'').localeCompare(String(b?.revAuthor||b?.author||'')), newerRevisionValue:(a,b)=>context.compareRevisionVersions(a,b)>=0?a:b, trustedNow:()=>1234, globalForStorage:(source,updatedAt)=>({...source,metas:undefined,states:undefined,updatedAt,clockFloor:updatedAt}), mergeGlobalRecords:(stored,incoming)=>context.compareRevisionVersions( {rev:incoming.globalRev,revAuthor:incoming.globalRevAuthor}, {rev:stored?.globalRev,revAuthor:stored?.globalRevAuthor} )>=0?incoming:stored, applyGlobalRecordToData:record=>Object.assign(data,structuredClone(record)), staleWorldEpochError:()=>Object.assign(new Error('stale world'),{code:'STALE_WORLD_EPOCH'}), metaRowsForStorage:ids=>ids.map(rowId=>data.metas[rowId]&&structuredClone(data.metas[rowId])).filter(Boolean), stateRowsForStorage:ids=>ids.map(rowId=>data.states[rowId]&&({id:rowId,value:structuredClone(data.states[rowId])})).filter(Boolean), snapshotForStorage:()=>({}),writeCompactMirror:()=>true, openWorldDb:async()=>({transaction:()=>({objectStore:name=>stores[name],abort(){this.aborted=true}})}), transactionDone:async()=>{},requestValue:async req=>req.result, updateStorageRevision:()=>{},scheduleMirrorCheckpoint:()=>{}, normalizeMeta:(_id,value)=>structuredClone(value),normalizeState:value=>structuredClone(value||{paths:[],rev:0,revAuthor:''}), metaFromV2Records:(_index,puzzle)=>puzzle?.meta?structuredClone(puzzle.meta):null, stateFromV2Record:(_index,state)=>state?.value?structuredClone(state.value):null, puzzleRecordV2:(meta,epoch)=>({epoch,id:meta.id,metaRev:meta.rev||0,meta:structuredClone(meta)}), stateRecordV2:(rowId,state,epoch)=>({epoch,id:rowId,stateRev:state?.rev||0,value:structuredClone(state)}), summarizeBoardV2:(meta,state,epoch)=>({epoch,id:meta.id,metaRev:meta.rev||0,stateRev:state?.rev||0}), fieldBoundsFromMetas:()=>({minX:0,minY:0,maxX:1,maxY:1}),SAVE_SCHEMA:31,WORLD_GENERATION:'current-only-test', clearRecoveryJournalIfCovered:()=>{},broadcastWorldSignal:()=>{},scheduleCloudPush:()=>{}, refreshWorldView:()=>{context.refreshes++},refreshes:0, interactionActive:()=>false,waitForInteractionSettle:async()=>{}, perfStart:()=>0,perfEnd:()=>{},perfGauge:()=>{} }; vm.createContext(context); vm.runInContext(`${persistSource}\nthis.persistDirtyToDb=persistDirtyToDb;`,context); return{context,stores,id}; } async function verifyPersistenceConflicts(){ { const localMeta={id:'B0',x:1,rev:5,revAuthor:'local'},localState={paths:[],rev:5,revAuthor:'local'}; const existingMeta={id:'B0',x:7,rev:10,revAuthor:'remote'},existingState={paths:[[1]],rev:11,revAuthor:'remote'}; const {context,stores}=persistenceContext({localMeta,localState,existingMeta,existingState}); await context.persistDirtyToDb(); assert(stores.boardPuzzles.rows.get('world:test:B0').meta.x===7&&stores.boardStates.rows.get('world:test:B0').value.rev===11, 'A stale tab overwrote a newer database row'); assert(context.data.metas.B0.x===7&&context.data.states.B0.rev===11&&context.refreshes===1, 'A stale in-memory board was not reconciled to the committed winner'); assert(context.data.cursorStyle==='remote'&&context.data.nextId===9, 'A stale global record overwrote the newer global winner'); } { const existingMeta={id:'B0',x:4,rev:20,revAuthor:'remote'},existingState={paths:[],rev:21,revAuthor:'remote'}; const {context,stores}=persistenceContext({ existingMeta,existingState,deletion:{rev:15,revAuthor:'local'} }); await context.persistDirtyToDb(); assert(stores.boardPuzzles.rows.has('world:test:B0')&&stores.boardStates.rows.has('world:test:B0')&&!stores.tombstonesV2.rows.has('world:test:B0'), 'A stale deletion removed a newer database board'); assert(context.data.metas.B0?.rev===20&&context.data.states.B0?.rev===21&&context.refreshes===1, 'A rejected stale deletion was not restored in the losing tab'); } { const existingMeta={id:'B0',rev:20,revAuthor:'remote'},existingState={paths:[],rev:20,revAuthor:'remote'}; const {context,stores}=persistenceContext({ existingMeta,existingState,deletion:{rev:25,revAuthor:'local'} }); await context.persistDirtyToDb(); assert(!stores.boardPuzzles.rows.has('world:test:B0')&&!stores.boardStates.rows.has('world:test:B0'), 'A newer deletion did not remove the older board rows'); assert(stores.tombstonesV2.rows.get('world:test:B0')?.rev===25, 'A committed deletion did not leave a durable tombstone'); } { const localMeta={id:'B0',rev:5,revAuthor:'local'}; const {context}=persistenceContext({localMeta,identityEpoch:'world:replacement'}); let error=null;try{await context.persistDirtyToDb()}catch(caught){error=caught} assert(error?.code==='STALE_WORLD_EPOCH'&&context.dirtyMetaIds.has('B0'), 'A stale tab was allowed to write into a replaced world epoch'); } } function verifyGlobalMerge(){ const {normalizeSpecialMechanics}=require('../shared-contracts'); const context={ deepClone:value=>structuredClone(value),structuredClone, normalizeSpecialMechanics, validWorldEpoch:value=>typeof value==='string'&&value.startsWith('world:'), compareRevisionVersions:(a,b)=>(a?.rev||0)-(b?.rev||0)||String(a?.revAuthor||'').localeCompare(String(b?.revAuthor||'')), bonusEventTotal:events=>Object.values(events||{}).reduce((sum,value)=>sum+(value||0),0) }; vm.createContext(context); vm.runInContext(`${functionSource('mergeGlobalRecords')}\nthis.mergeGlobalRecords=mergeGlobalRecords;`,context); const merged=context.mergeGlobalRecords( {worldEpoch:'world:test',globalRev:30,globalRevAuthor:'remote',cursorStyle:'remote',nextId:8,clockFloor:11,cloudRevision:3,bonusEvents:{A:4},quarantine:{B:{failedAt:5}},lastSolveAt:10,specialMechanicsSeen:['warp'],combo:2}, {worldEpoch:'world:test',globalRev:20,globalRevAuthor:'local',cursorStyle:'local',nextId:12,clockFloor:15,cloudRevision:9,bonusEvents:{A:2,C:7},quarantine:{B:{failedAt:8}},lastSolveAt:14,specialMechanicsSeen:['lock'],combo:4} ); assert(merged.cursorStyle==='remote'&&merged.nextId===12&&merged.clockFloor===15&&merged.cloudRevision===9, 'Global LWW and monotonic fields were not merged independently'); assert(merged.bonusEvents.A===4&&merged.bonusEvents.C===7&&merged.bonusScore===11, 'Global additive bonus records were lost during a concurrent merge'); assert(merged.quarantine.B.failedAt===8&&merged.lastSolveAt===14&&!Object.prototype.hasOwnProperty.call(merged,'combo')&&merged.specialMechanicsSeen.join(',')==='lock,warp', 'Global timestamped records, encounter memory, or retired combo data were merged incorrectly'); } async function verifyDurableBackup(){ const source=functionSource('preserveRecoveryDurably'); const failing={ Date,safeLocalSet:()=>false,sessionStorage:{setItem(){throw new Error('blocked')}}, persistRecoveryEnvelope:async()=>false,volatileRecovery:null,recoveryStorageKey:'recovery' }; vm.createContext(failing);vm.runInContext(`${source}\nthis.preserveRecoveryDurably=preserveRecoveryDurably;`,failing); let rejected=false;try{await failing.preserveRecoveryDurably('save','reset')}catch(_){rejected=true} assert(rejected,'Destructive replacement proceeded without any durable backup'); const database={ Date,safeLocalSet:()=>false,sessionStorage:{setItem(){throw new Error('blocked')}}, persistRecoveryEnvelope:async()=>true,volatileRecovery:null,recoveryStorageKey:'recovery' }; vm.createContext(database);vm.runInContext(`${source}\nthis.preserveRecoveryDurably=preserveRecoveryDurably;`,database); const result=await database.preserveRecoveryDurably('save','reset'); assert(!result.browserCopy&&result.databaseCopy,'A verified database-only backup was not accepted'); } function verifyRevisionSeed(){ const context={ lastRevision:3,data:{},recoveredDeletionTombstones:new Map([['B9',{rev:81}]]) }; vm.createContext(context); vm.runInContext(`${functionSource('seedRevisionClock')}\nthis.seedRevisionClock=seedRevisionClock;`,context); const maximum=context.seedRevisionClock({ globalRev:50,clockFloor:0,metas:{B0:{rev:60}},states:{B0:{rev:70}} }); assert(maximum===81&&context.lastRevision===81,'The revision clock was not seeded from all persisted row types'); } (async()=>{ await verifyPersistenceConflicts(); verifyGlobalMerge(); await verifyDurableBackup(); verifyRevisionSeed(); console.log('Concurrency and replacement safety test passed'); })().catch(error=>{console.error(error);process.exitCode=1});