2026-07-30 15:13:53 +09:00
'use strict' ;
const assert = require ( 'assert/strict' ) ;
const vm = require ( 'vm' ) ;
const { functionSource , loadAppLogic } = require ( './helpers/app-source' ) ;
2026-07-31 12:54:46 +09:00
const { normalizeSpecialMechanics } = require ( '../shared-contracts' ) ;
2026-07-30 15:13:53 +09:00
const outboxContext = {
data : { states : { B0 : { solved : false } , B1 : { solved : true } } } , cloudApiEnabled : true ,
cloudJournalMetaIds : new Set ( ) , cloudJournalStateIds : new Set ( ) , cloudJournalDeletedIds : new Set ( ) , cloudOutboxDeleteKeys : new Set ( ) , cloudJournalChangeSeq : 0 , cloudJournalGlobalChanged : false
} ;
vm . createContext ( outboxContext ) ;
vm . runInContext ( ` ${ functionSource ( 'currentCloudPending' ) } \n ${ functionSource ( 'noteCloudRow' ) } \n this.logic={currentCloudPending,noteCloudRow}; ` , outboxContext ) ;
outboxContext . logic . noteCloudRow ( 'state' , 'B0' ) ;
2026-08-01 16:06:14 +09:00
assert . equal ( outboxContext . cloudJournalStateIds . has ( 'B0' ) , true , 'Unfinished shared path was not added to the shared journal' ) ;
assert . equal ( outboxContext . cloudOutboxDeleteKeys . has ( 'state:B0' ) , false , 'Unfinished shared path was incorrectly deleted from the outbox' ) ;
2026-07-30 15:13:53 +09:00
outboxContext . logic . noteCloudRow ( 'state' , 'B1' ) ;
assert . equal ( outboxContext . cloudJournalStateIds . has ( 'B1' ) , true , 'Solved state was not added to the shared journal' ) ;
2026-08-01 16:06:14 +09:00
assert . deepEqual ( [ ... outboxContext . logic . currentCloudPending ( ) . stateIds ] , [ 'B0' , 'B1' ] ) ;
2026-07-30 15:13:53 +09:00
const AppLogic = loadAppLogic ( ) , now = 1_800_000_000_000 ;
const leaseContext = {
cloudAvailable : true , data : { cloudProfile : { playerId : 'bbbbbbbbbbbbbbbb' } } , currentPlayerId : ( ) => 'bbbbbbbbbbbbbbbb' , trustedNow : ( ) => now , AppLogic ,
SHARED _EXPANSION _GRACE _MS : 60_000 , SHARED _EXPANSION _JITTER _MS : 30_000
} ;
vm . createContext ( leaseContext ) ;
vm . runInContext ( ` ${ functionSource ( 'sharedExpansionRepairDelay' ) } \n this.sharedExpansionRepairDelay=sharedExpansionRepairDelay; ` , leaseContext ) ;
assert . equal ( leaseContext . sharedExpansionRepairDelay ( { solved : true , solvedById : 'bbbbbbbbbbbbbbbb' , solvedAt : now } ) , 0 , 'The solving client cannot expand its own clear' ) ;
const wait = leaseContext . sharedExpansionRepairDelay ( { solved : true , solvedById : 'aaaaaaaaaaaaaaaa' , solvedAt : now } ) ;
2026-08-01 16:06:14 +09:00
assert . equal ( wait , Infinity , 'A non-solving client can generate transient boards that the server will reject' ) ;
assert . equal ( leaseContext . sharedExpansionRepairDelay ( { solved : true , solvedById : 'aaaaaaaaaaaaaaaa' , solvedAt : now - 100_000 } ) , Infinity , 'Expansion authority silently transfers away from the solver' ) ;
2026-07-30 15:13:53 +09:00
const localMeta = { id : 'B0' , x : 99 , y : 99 , seed : 1 , chunks : [ [ 0 , 0 ] ] , sealedSides : [ ] , rev : 9_999 } ;
const remoteMeta = { id : 'B0' , x : 0 , y : 0 , seed : 2 , chunks : [ [ 0 , 0 ] ] , sealedSides : [ ] , rev : 2_000 } ;
const authoritativeContext = {
2026-07-31 12:54:46 +09:00
cloudApiEnabled : true ,
2026-07-30 15:13:53 +09:00
data : {
metas : { B0 : localMeta } , states : { B0 : { solved : true , solvedBy : 'Local' , paths : [ { cells : [ [ 0 , 0 ] ] } ] , rev : 9_999 } } ,
nextId : 99 , solved : 1 , lastSolveAt : 123 , specialMechanicsSeen : [ 'warp' ] , quarantine : { local : true } ,
playerName : 'Player' , score : 777 , cursorStyle : 'flag-jp'
} ,
isPlainObject : value => value && typeof value === 'object' && ! Array . isArray ( value ) , lastRevision : 0 , normalizedStateObjects : new Set ( ) ,
2026-07-31 12:54:46 +09:00
normalizeSpecialMechanics ,
cloudJournalMetaIds : new Set ( [ 'B0' ] ) , cloudJournalStateIds : new Set ( [ 'B0' ] ) , cloudJournalDeletedIds : new Set ( ) , cloudOutboxDeleteKeys : new Set ( ) , cloudJournalChangeSeq : 0 ,
2026-07-30 15:13:53 +09:00
sameMetaGeometry : ( a , b ) => a . x === b . x && a . y === b . y && a . seed === b . seed && JSON . stringify ( a . chunks ) === JSON . stringify ( b . chunks ) && JSON . stringify ( a . sealedSides || [ ] ) === JSON . stringify ( b . sealedSides || [ ] ) ,
2026-07-31 12:54:46 +09:00
destroyBoard : ( ) => { } , rendered : new Map ( ) , deepClone : value => JSON . parse ( JSON . stringify ( value ) ) , sameDataValue : ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) ,
2026-07-30 15:13:53 +09:00
compareRevisionVersions : ( a , b ) => ( a ? . rev || 0 ) - ( b ? . rev || 0 ) ,
mergeBoardStates : ( current , incoming ) => {
2026-07-31 12:54:46 +09:00
if ( current ? . solved && ! incoming ? . solved ) return JSON . parse ( JSON . stringify ( current ) ) ;
2026-07-30 15:13:53 +09:00
if ( current && ! current . solved && ! incoming . solved ) return { ... JSON . parse ( JSON . stringify ( incoming ) ) , paths : JSON . parse ( JSON . stringify ( current . paths || [ ] ) ) } ;
return JSON . parse ( JSON . stringify ( incoming ) ) ;
} ,
2026-08-01 16:06:14 +09:00
markStateDirty : id => authoritativeContext . dirtyStateIds . add ( id ) , dirtyStateIds : new Set ( ) , sanitizeStateForPuzzle : ( ) => { } , boardClaimOwnedByMe : ( ) => false ,
2026-07-30 15:13:53 +09:00
mergeGlobalFields : ( ) => { throw new Error ( 'Authoritative shared global unexpectedly used generic merge' ) } , resolveMergedOverlaps : ( ) => [ ] , statsDirty : false
} ;
vm . createContext ( authoritativeContext ) ;
2026-07-31 12:54:46 +09:00
vm . runInContext ( ` ${ functionSource ( 'clearSharedWorldJournalRow' ) } \n ${ functionSource ( 'noteCloudRow' ) } \n ${ functionSource ( 'applyAuthoritativeSharedGlobal' ) } \n ${ functionSource ( 'mergeSnapshotIntoData' ) } \n this.mergeSnapshotIntoData=mergeSnapshotIntoData; ` , authoritativeContext ) ;
2026-07-30 15:13:53 +09:00
authoritativeContext . mergeSnapshotIntoData ( {
metas : { B0 : remoteMeta } , states : { B0 : { solved : false , paths : [ ] , rev : 2_000 } } ,
nextId : 2 , solved : 0 , lastSolveAt : 0 , specialMechanicsSeen : [ 'lock' ] , quarantine : { shared : true }
} , { finalize : false , authoritativeWorld : true } ) ;
assert . equal ( authoritativeContext . data . metas . B0 . seed , 2 , 'An existing local world row overrode the authoritative shared board' ) ;
2026-07-31 12:54:46 +09:00
assert . equal ( authoritativeContext . data . states . B0 . solved , false , 'A clear from a replaced board definition leaked into the authoritative shared board' ) ;
2026-07-30 15:13:53 +09:00
assert . equal ( authoritativeContext . data . nextId , 2 , 'A private local board counter leaked into the shared world' ) ;
assert . equal ( authoritativeContext . data . playerName , 'Player' ) ; assert . equal ( authoritativeContext . data . score , 777 ) ; assert . equal ( authoritativeContext . data . cursorStyle , 'flag-jp' ) ;
assert . equal ( authoritativeContext . cloudJournalMetaIds . size , 0 ) ; assert . equal ( authoritativeContext . cloudJournalStateIds . size , 0 ) ;
2026-07-31 12:54:46 +09:00
assert ( authoritativeContext . cloudOutboxDeleteKeys . has ( 'meta:B0' ) && authoritativeContext . cloudOutboxDeleteKeys . has ( 'state:B0' ) , 'Replaced shared rows did not clear stale local outbox records' ) ;
// A durable clear for the same board definition survives a stale authoritative snapshot and is queued for upload.
authoritativeContext . cloudJournalStateIds . clear ( ) ; authoritativeContext . cloudOutboxDeleteKeys . clear ( ) ;
authoritativeContext . data . metas . B0 = JSON . parse ( JSON . stringify ( remoteMeta ) ) ;
authoritativeContext . data . states . B0 = { solved : true , solvedBy : 'Local' , paths : [ { cells : [ [ 0 , 0 ] ] } ] , rev : 9_999 } ;
authoritativeContext . mergeSnapshotIntoData ( { metas : { B0 : { ... remoteMeta , rev : 4_000 } } , states : { B0 : { solved : false , paths : [ ] , rev : 4_000 } } , nextId : 2 } , { finalize : false , authoritativeWorld : true } ) ;
assert . equal ( authoritativeContext . data . states . B0 . solved , true , 'A durable clear was downgraded by a stale authoritative snapshot of the same board' ) ;
assert . deepEqual ( [ ... authoritativeContext . cloudJournalStateIds ] , [ 'B0' ] , 'The retained clear was not queued for shared upload' ) ;
assert . equal ( authoritativeContext . cloudOutboxDeleteKeys . has ( 'state:B0' ) , false , 'The retained clear was incorrectly scheduled for deletion' ) ;
2026-07-30 15:13:53 +09:00
2026-08-01 16:06:14 +09:00
// Matching unsolved boards adopt the authoritative shared progress when this player has no pending claim.
2026-07-30 15:13:53 +09:00
authoritativeContext . data . metas . B0 = remoteMeta ;
authoritativeContext . data . states . B0 = { solved : false , paths : [ { startGate : 0 , cells : [ [ 0 , 0 ] , [ 0 , 1 ] ] } ] , rev : 3_000 } ;
2026-08-01 16:06:14 +09:00
authoritativeContext . cloudJournalStateIds . clear ( ) ;
2026-07-30 15:13:53 +09:00
authoritativeContext . mergeSnapshotIntoData ( { metas : { B0 : { ... remoteMeta , rev : 4_000 } } , states : { B0 : { solved : false , paths : [ ] , rev : 4_000 } } , nextId : 2 } , { finalize : false , authoritativeWorld : true } ) ;
2026-08-01 16:06:14 +09:00
assert . equal ( authoritativeContext . data . states . B0 . paths . length , 0 , 'Authoritative shared progress did not replace stale local unfinished progress' ) ;
// The active claimant keeps an unsent local update during a revision-conflict pull, then retries it.
authoritativeContext . data . states . B0 = { solved : false , paths : [ { startGate : 0 , cells : [ [ 0 , 0 ] , [ 0 , 1 ] ] } ] , rev : 5_000 } ;
authoritativeContext . cloudJournalStateIds . add ( 'B0' ) ; authoritativeContext . boardClaimOwnedByMe = ( ) => true ;
authoritativeContext . mergeSnapshotIntoData ( { metas : { B0 : { ... remoteMeta , rev : 6_000 } } , states : { B0 : { solved : false , paths : [ ] , rev : 6_000 } } , nextId : 2 } , { finalize : false , authoritativeWorld : true } ) ;
assert . equal ( authoritativeContext . data . states . B0 . paths . length , 1 , 'The active claimant lost an unsent path during conflict recovery' ) ;
2026-07-30 15:13:53 +09:00
console . log ( 'Shared-world phase 1 client synchronization smoke test passed' ) ;