d
This commit is contained in:
parent
c3a6f5ff37
commit
4c4e767ec6
73 changed files with 3502 additions and 741 deletions
64
test/v4800-shared-clear-economy-smoke-test.js
Normal file
64
test/v4800-shared-clear-economy-smoke-test.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
'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})});
|
||||
Loading…
Add table
Add a link
Reference in a new issue