assert(html.includes('class="shared-indicator"')&&html.includes('<span>共有</span>'),'Compact shared indicator is missing');
assert(!html.includes('共有 · 接続中')&&!html.includes('共有成功')&&!html.includes('共有失敗'),'Variable shared-status labels remain in the visible UI');
assert(css.includes('.shared-indicator')&&css.includes('bottom:calc(16px + var(--safe-bottom))'),'Shared indicator is not fixed above FPS');
assert(!functionSource('setCloudStatus').includes('textContent'),'Shared indicator still changes visible text');
assert(functionSource('currentCloudPending').includes('stateIds:[...cloudJournalStateIds]'),'Unfinished states are omitted from cloud pending data');
assert(!functionSource('noteCloudRow').includes("solved!==true"),'Unfinished states are still removed from the shared outbox');
assert(functionSource('requestBoardClaim').indexOf("fetchJson('/api/realtime/claim'")<functionSource('requestBoardClaim').indexOf('requestBoardClaimThroughRealtime')&&functionSource('requestBoardClaimThroughRealtime').includes("type:'claim'"),'Board claim does not use the direct shared API with realtime fallback');
assert(functionSource('markPendingClaimPointerReleased').includes('pending.released=true')&&functionSource('bindBoard').includes('finishReleased'),'A drag released while claim approval is pending is discarded');
assert(serverSource.includes('const state=JSON.parse(JSON.stringify(incoming));state.solved=false'),'Server still discards unfinished board paths');
assert(serverSource.includes('unfinishedBoard')&&serverSource.includes('Board claim is required'),'Unfinished state writes are not protected by the active claim');
assert.equal(result.response.status,423,'A player without the board claim changed unfinished progress');assert.equal(result.body.boardId,'B0','Claim failure did not identify the blocked board');
constready=connected.body.messages.find(message=>message.type==='ready');assert(ready?.presenceId,'Polling realtime connection did not return a presence id');
constbobDraftEvents=awaitrequest(`/api/realtime/poll?presenceId=${encodeURIComponent(bobPresence)}&after=${bobConnected.body.sequence||0}&wait=0`,{headers:auth(bob)});assert.equal(bobDraftEvents.response.status,200);assert(!bobDraftEvents.body.messages.some(message=>message.type==='world-revision'&&message.revision===2),'Every unfinished drag was announced as shared progress');
letpulled=awaitrequest('/api/cloud/pull?since=0',{headers:auth(bob)});assert.equal(pulled.response.status,200);assert.deepEqual(pulled.body.page.states.B0.paths,[],'Another player received an open, single-ended draft line');
pulled=awaitrequest('/api/cloud/pull?since=0',{headers:auth(alice)});assert.deepEqual(pulled.body.page.states.B0.paths[0].cells,partial.paths[0].cells,'The active claimant could not recover their own draft line');
assert(puzzle.solution.length>1,'Starter puzzle needs two routes for shared-progress coverage');constsecond=puzzle.solution[1],mixed={paths:[{startGate:route.startGate,endGate:route.endGate,openGate:null,cells:route.cells.map(cell=>[...cell])},{startGate:second.startGate,endGate:null,openGate:null,cells:second.cells.slice(0,Math.max(1,Math.min(3,second.cells.length))).map(cell=>[...cell])}],specialProgress:{crossings:[]},solved:false};
constbobRealtime=awaitrequest(`/api/realtime/poll?presenceId=${encodeURIComponent(bobPresence)}&after=${bobDraftEvents.body.sequence||0}&wait=1000`,{headers:auth(bob)});assert.equal(bobRealtime.response.status,200);constprogressRevision=bobRealtime.body.messages.find(message=>message.type==='world-revision'&&message.revision===3&&message.boardIds.includes('B0'));assert(progressRevision,'A completed gate-to-gate line did not emit shared progress');assert.equal(progressRevision.page.states.B0.paths.length,1,'Realtime progress leaked an unfinished draft or omitted the completed line');assert.equal(progressRevision.page.states.B0.paths[0].endGate,route.endGate,'Realtime progress did not contain the latest completed gate-to-gate line');
pulled=awaitrequest('/api/cloud/pull?since=0',{headers:auth(bob)});assert.equal(pulled.body.page.states.B0.paths.length,1);assert.equal(pulled.body.page.states.B0.paths[0].endGate,route.endGate,'Other player did not receive the completed gate-to-gate line');
pulled=awaitrequest('/api/cloud/pull?since=0',{headers:auth(alice)});assert.equal(pulled.body.page.states.B0.paths.length,2,'The claimant lost their private open draft while sharing a completed line');