64 lines
8.2 KiB
JavaScript
64 lines
8.2 KiB
JavaScript
|
|
'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,read,functionSource,loadBendPuzzle}=require('./helpers/app-source');
|
||
|
|
|
||
|
|
const app=read('app.js'),catalog=require('../store-catalog.json');
|
||
|
|
const bindBoard=functionSource('bindBoard'),claimRequest=functionSource('requestBoardClaim'),removeClaim=functionSource('removeClaim'),claimPresentation=functionSource('applyClaimPresentationToBoard');
|
||
|
|
assert(!bindBoard.includes('pointerover'),'Hover still starts board ownership');
|
||
|
|
assert(bindBoard.includes("const endpointTarget=e.target.closest?.('.endpoint-hit'),gateTarget=e.target.closest?.('.gate-hit')")&&bindBoard.indexOf('!endpointTarget&&!gateTarget')<bindBoard.indexOf('ensureBoardClaimForInput(b)'),'Ownership is requested before a knob/endpoint operation starts');
|
||
|
|
assert(claimPresentation.includes("own?'プレイ中'"),'Own board badge is not labelled プレイ中');
|
||
|
|
assert(!claimRequest.includes('toast(')&&!removeClaim.includes('toast('),'Board ownership still emits bottom notifications');
|
||
|
|
assert(functionSource('applyCloudEnvelope').includes('applyPlayerEconomyEnvelope(result)'),'Clear push response does not update the local gem wallet');
|
||
|
|
assert(functionSource('checkSolvedAndExpand').includes('data.states[b.id]=previous.state')&&functionSource('checkSolvedAndExpand').includes('for(let attempt=0;attempt<3&&!published;attempt++)'),'Unconfirmed clears can remain locally solved');
|
||
|
|
assert(functionSource('canExpandSharedBoard').includes('state.solvedById===currentPlayerId()'),'A non-solving player can generate transient expansion boards');
|
||
|
|
const normalStarterIds=new Set(catalog.filter(item=>item.lineColor&&!item.aurora&&item.cost===5000).map(item=>item.id));
|
||
|
|
assert(normalStarterIds.size>=2,'Normal starter color pool is missing');
|
||
|
|
assert(app.includes("LINE_COLOR_ITEMS.filter(item=>item.effectLabel==='ラインカラー')"),'Client starter color pool includes premium colors');
|
||
|
|
|
||
|
|
const BendPuzzle=loadBendPuzzle();
|
||
|
|
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'linkfield-v4800-'));
|
||
|
|
const port=33000+Math.floor(Math.random()*2000),base=`http://127.0.0.1:${port}`,sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||
|
|
let child=null,stderr='';
|
||
|
|
function start(){
|
||
|
|
stderr='';child=spawn(process.execPath,[path.join(root,'server.js')],{env:{...process.env,PORT:String(port),HOST:'127.0.0.1',LINK_FIELD_TEST_DATA_ROOT:dataDir},stdio:['ignore','pipe','pipe']});
|
||
|
|
child.stderr.on('data',chunk=>stderr+=chunk);
|
||
|
|
}
|
||
|
|
async function stop(){if(!child)return;const target=child;child=null;target.kill('SIGTERM');await Promise.race([new Promise(resolve=>target.once('exit',resolve)),sleep(3000)]);if(target.exitCode==null)target.kill('SIGKILL')}
|
||
|
|
async function request(url,options={}){const response=await fetch(base+url,options),text=await response.text();let body;try{body=JSON.parse(text)}catch{body={raw:text}}return{response,body}}
|
||
|
|
async function ready(){for(let i=0;i<120;i++){try{const result=await request('/api/cloud/status');if(result.response.ok)return result.body}catch{}await sleep(30)}throw new Error(`Server did not start: ${stderr}`)}
|
||
|
|
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:BendPuzzle.solverDifficulty(puzzle,1),targetLevel:1,seed,axis:puzzle.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])})),specialProgress:{crossings:[]},solved:true,expanded:false,rev:2,revAuthor:'client'}}
|
||
|
|
|
||
|
|
(async()=>{
|
||
|
|
start();await ready();
|
||
|
|
const sessions=[];for(let index=0;index<12;index++)sessions.push((await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:`Player ${index}`})})).body);
|
||
|
|
for(const session of sessions)assert(normalStarterIds.has(session.starterLineColor),`Premium or unknown starter color returned: ${session.starterLineColor}`);
|
||
|
|
assert(new Set(sessions.map(session=>session.starterLineColor)).size>1,'Initial line color is not distributed across the normal color pool');
|
||
|
|
const [alice,bob]=sessions;
|
||
|
|
const puzzle=starterPuzzle(),b0=boardMeta('B0',0,15,puzzle);
|
||
|
|
let result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:0,global:{nextId:1},metas:[b0],states:[{id:'B0',value:{paths:[],specialProgress:{crossings:[]},solved:false}}]})});
|
||
|
|
assert.equal(result.response.status,200,JSON.stringify(result.body));
|
||
|
|
const connected=await request('/api/realtime/connect',{method:'POST',headers:auth(alice),body:'{}'});assert.equal(connected.response.status,200);const presenceId=connected.body.presenceId;
|
||
|
|
const claim=await request('/api/realtime/claim',{method:'POST',headers:auth(alice),body:JSON.stringify({presenceId,boardId:'B0'})});assert.equal(claim.response.status,200);assert.equal(claim.body.ok,true);
|
||
|
|
result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:1,global:{nextId:1},metas:[],states:[{id:'B0',value:solvedState(puzzle)}]})});
|
||
|
|
assert.equal(result.response.status,200,JSON.stringify(result.body));assert.equal(result.body.clearEvents.length,1,'Clear was not authoritatively accepted');assert(result.body.player.earnedScore>0,'Clear response did not include earned gems');assert.equal(result.body.player.availableScore,result.body.player.earnedScore);
|
||
|
|
const reward=result.body.player.earnedScore,clearRevision=result.body.revision;
|
||
|
|
const polling=await request(`/api/realtime/poll?presenceId=${encodeURIComponent(presenceId)}&afterSequence=${connected.body.sequence||0}`,{headers:auth(alice)});assert.equal(polling.response.status,200);assert(polling.body.messages.some(message=>message.type==='claim-release'&&message.boardId==='B0'),'Clear did not release the プレイ中 claim');
|
||
|
|
let pull=await request('/api/cloud/pull?since=0',{headers:auth(bob)});assert.equal(pull.body.page.states.B0.solved,true,'Other player did not receive the clear');
|
||
|
|
const playerState=await request('/api/player/state',{headers:auth(alice)});assert.equal(playerState.body.player.earnedScore,reward,'Gem wallet did not persist the clear reward');
|
||
|
|
const stale=await request('/api/cloud/push',{method:'POST',headers:auth(bob),body:JSON.stringify({baseRevision:clearRevision,global:{nextId:1},metas:[],states:[{id:'B0',value:{paths:[],specialProgress:{crossings:[]},solved:false}}]})});assert.equal(stale.response.status,200,JSON.stringify(stale.body));
|
||
|
|
pull=await request('/api/cloud/pull?since=0',{headers:auth(bob)});assert.equal(pull.body.page.states.B0.solved,true,'A stale unfinished update reverted a cleared board');
|
||
|
|
const b1Puzzle=BendPuzzle.generatePuzzle([[0,0]],0x48000001,1,1,0),b1=boardMeta('B1',1,0x48000001,b1Puzzle);
|
||
|
|
result=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:stale.body.revision,global:{nextId:2},metas:[b1],states:[{id:'B1',value:{paths:[],specialProgress:{crossings:[]},solved:false}}]})});
|
||
|
|
assert.equal(result.response.status,200,JSON.stringify(result.body));const expansionRevision=result.body.revision;
|
||
|
|
pull=await request('/api/cloud/pull?since=0',{headers:auth(bob)});assert(pull.body.page.metas.B1&&pull.body.page.states.B1,'New shared board disappeared before the next pull');
|
||
|
|
await stop();start();const restarted=await ready();assert.equal(restarted.revision,expansionRevision);
|
||
|
|
pull=await request('/api/cloud/pull?since=0',{headers:auth(bob)});assert.equal(pull.body.page.states.B0.solved,true,'Cleared board reverted after server restart');assert(pull.body.page.metas.B1&&pull.body.page.states.B1,'New board disappeared after server restart');
|
||
|
|
const economyAfterRestart=await request('/api/player/state',{headers:auth(alice)});assert.equal(economyAfterRestart.body.player.earnedScore,reward,'Gem wallet disappeared after server restart');
|
||
|
|
console.log('LinkField v48.00 shared clear, claim release, expansion, and gem persistence smoke test passed');
|
||
|
|
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(async()=>{await stop();fs.rmSync(dataDir,{recursive:true,force:true})});
|