bend_puzzle/test/realtime-phase2-smoke-test.js

70 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-07-30 15:13:53 +09:00
'use strict';
const {spawn}=require('child_process');
const fs=require('fs');
const os=require('os');
const path=require('path');
const assert=require('assert/strict');
const {root,starterPuzzle}=require('./helpers/app-source');
const {connectRealtime}=require('./helpers/realtime-client');
const port=20000+Math.floor(Math.random()*10000);
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-realtime-phase2-'));
const child=spawn(process.execPath,[path.join(root,'server.js')],{
env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']
});
let stderr='',aliceRealtime=null,bobRealtime=null;child.stderr.on('data',chunk=>stderr+=chunk);
const base=`http://127.0.0.1:${port}`;
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function request(url,options={}){const response=await fetch(base+url,options),body=await response.json();return{response,body}}
function auth(session){return{authorization:`Bearer ${session.playerId}.${session.token}`,'content-type':'application/json'}}
function boardMeta(id,x,seed,puzzle){return{id,x,y:0,chunks:[[0,0]],level:1,targetLevel:1,seed,axis:'MIX',sealedSides:[],puzzle,rev:1,revAuthor:'client'}}
function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate:route.startGate,endGate:route.endGate,cells:route.cells.map(cell=>[...cell])})),solved:true,expanded:false,rev:2,revAuthor:'client'}}
(async()=>{
for(let index=0;index<80;index++){try{const status=await request('/api/cloud/status');if(status.response.ok)break}catch(_){}await sleep(50)}
const status=await request('/api/cloud/status');assert.equal(status.body.realtime,true);assert.equal(status.body.claimTtlMs,300000);
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
const bob=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Bob'})})).body;
const puzzle=starterPuzzle(),b0=boardMeta('B0',0,111,puzzle),b1=boardMeta('B1',2,222,puzzle);
const bootstrap=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'v47-field-reset-20260728-interaction-fix',nextId:2},metas:[b0,b1],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}},{id:'B1',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]})});
assert.equal(bootstrap.response.status,200);assert.equal(bootstrap.body.revision,1);
aliceRealtime=await connectRealtime(base,alice);bobRealtime=await connectRealtime(base,bob);
aliceRealtime.send({type:'viewport',minX:-8,minY:-8,maxX:8,maxY:8});bobRealtime.send({type:'viewport',minX:-8,minY:-8,maxX:8,maxY:8});
await aliceRealtime.waitFor('snapshot');await bobRealtime.waitFor('snapshot');
aliceRealtime.send({type:'cursor',x:.25,y:.5,cursorStyle:'default'});
const cursor=await bobRealtime.waitFor(message=>message.type==='cursor'&&message.playerId===alice.playerId);
assert.equal(cursor.name,'Alice');assert.equal(cursor.x,.25);assert.equal(cursor.y,.5);assert.equal(cursor.cursorStyle,'default');
aliceRealtime.send({type:'cursor-hide'});await bobRealtime.waitFor(message=>message.type==='player-left'&&message.presenceId===aliceRealtime.ready.presenceId);
await sleep(55);aliceRealtime.send({type:'cursor',x:.25,y:.5,cursorStyle:'default'});await bobRealtime.waitFor(message=>message.type==='cursor'&&message.playerId===alice.playerId);
aliceRealtime.send({type:'claim',requestId:'alice-b0',boardId:'B0'});
const aliceClaim=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='alice-b0');assert.equal(aliceClaim.ok,true);assert.equal(aliceClaim.claim.boardId,'B0');
const observedClaim=await bobRealtime.waitFor(message=>message.type==='claim'&&message.claim?.boardId==='B0');assert.equal(observedClaim.claim.playerName,'Alice');
bobRealtime.send({type:'claim',requestId:'bob-b0',boardId:'B0'});
const denied=await bobRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='bob-b0');assert.equal(denied.ok,false);assert.equal(denied.reason,'occupied');
const rejectedClear=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({baseRevision:1,global:{nextId:2},metas:[],states:[{id:'B0',value:solvedState(puzzle)}],deleted:[]})});
assert.equal(rejectedClear.response.status,423);assert.match(rejectedClear.body.error,/claim/i);
const clearPromise=bobRealtime.waitFor(message=>message.type==='board-cleared'&&message.event?.id==='B0');
const releasePromise=bobRealtime.waitFor(message=>message.type==='claim-release'&&message.boardId==='B0'&&message.reason==='cleared');
const cleared=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:2,lastSolveAt:Date.now()},metas:[],states:[{id:'B0',value:solvedState(puzzle)}],deleted:[]})});
assert.equal(cleared.response.status,200);assert.equal(cleared.body.clearEvents[0].playerName,'Alice');
const clearEvent=await clearPromise;assert.equal(clearEvent.event.level,1);await releasePromise;
const profilePromise=bobRealtime.waitFor(message=>message.type==='player-profile'&&message.playerId===alice.playerId);
const renamed=await request('/api/cloud/profile',{method:'POST',headers:auth(alice),body:JSON.stringify({name:'Alice 2'})});assert.equal(renamed.response.status,200);assert.equal((await profilePromise).name,'Alice 2');
aliceRealtime.send({type:'claim',requestId:'alice-b1',boardId:'B1'});
const b1Claim=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='alice-b1');assert.equal(b1Claim.ok,true);
await bobRealtime.waitFor(message=>message.type==='claim'&&message.claim?.boardId==='B1');
aliceRealtime.close();aliceRealtime=null;
await bobRealtime.waitFor(message=>message.type==='player-left'&&message.playerId===alice.playerId);
bobRealtime.send({type:'claim',requestId:'bob-b1-after-disconnect',boardId:'B1'});
const retained=await bobRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='bob-b1-after-disconnect');assert.equal(retained.ok,false);assert.equal(retained.reason,'occupied');
console.log('BEND FIELD realtime phase 2 smoke test passed');
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{aliceRealtime?.close();bobRealtime?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});