import {readFileSync} from 'node:fs'; import {test} from 'node:test'; import assert from 'node:assert/strict'; import {PlatformStore} from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; import {parseLoginResponse,parseOtherOfflinePayload} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; function roomData(count=4):Record{ const data=JSON.parse(readFileSync(new URL('../fixtures/contracts/player-login-room.json',import.meta.url),'utf8')).data; const self=data.players[1]; data.players=Array.from({length:count},(_,seat)=>seat===1?self:{...self,playerid:500000+seat,nickname:`玩家${seat}`,onstate:0}); return data; } function setup(count=4){const store=new PlatformStore();store.applyLoginSuccess(parseLoginResponse(roomData(count)));return store;} test('apply snapshots dynamic participants and deadline; all agree remains active pending free_room',()=>{ const store=setup(12);store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,...Array(11).fill(-1)],countdown:60}},1000); const vote=store.getState().dissolution;assert.ok(vote?.kind==='active');assert.equal(vote.participants.length,12);assert.equal(vote?.initiator?.nickname,'玩家0');assert.equal(vote?.deadline,61000); for(let seat=1;seat<12;seat++)store.applyDissolution('other_agree_free_room',{seat},5000); assert.equal(store.getState().dissolution?.kind,'active'); store.playerOffline(parseOtherOfflinePayload({seat:0}));assert.equal(store.getState().dissolution?.kind,'active'); }); test('refusal result keeps participant snapshots and acknowledgement only clears vote',()=>{ const store=setup();store.applyDissolution('self_apply_free_room',{agreefree:{state:[-1,0,-1,-1],countdown:20}},0); const room=store.getState().room; store.applyDissolution('other_refuse_free_room',{seat:2},1); const vote=store.getState().dissolution;assert.ok(vote?.kind==='result');assert.equal(vote.passed,false);assert.equal(vote.refusedSeat,2); store.acknowledgeDissolution();assert.equal(store.getState().dissolution,null);assert.equal(store.getState().room,room); }); test('server result interprets seats as states and writes zero roomcard, preserving deskfree',()=>{ const store=setup();store.applyDissolution('free_room',{seats:[0,1,-1,1],roomcard:0,deskfree:{rounds:[1]}},0); const vote=store.getState().dissolution;assert.ok(vote?.kind==='result');assert.equal(vote.passed,true);assert.equal(vote?.initiator?.seat,0);assert.deepEqual(vote?.participants.map(p=>p.state),[0,1,-1,1]);assert.deepEqual(vote?.deskfree,{rounds:[1]});assert.equal(store.getState().players.entities[430511].roomcard,0); }); test('reconnect restores vote atomically from agreefree; malformed source never commits',()=>{ const store=setup();const data=roomData();data.agreefree={state:[-1,1,0,-1],countdown:23}; let commits=0;store.subscribe(()=>commits++);const before=Date.now();store.applyLoginSuccess(parseLoginResponse(data)); const vote=store.getState().dissolution;assert.ok(vote?.kind==='active');assert.equal(commits,1);assert.equal(vote.initiator.seat,2);assert.ok(vote.deadline>=before+23000); const state=store.getState();assert.throws(()=>store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,0,-1,-1],countdown:3}},0),/initiator/);assert.equal(store.getState(),state); }); test('freeNow allows no seats and retains server tips without inventing countdown',()=>{ const store=setup();store.applyDissolution('free_room',{freeNow:1,tips:'强制解散',deskfree:{round:1}},0); const vote=store.getState().dissolution;assert.ok(vote?.kind==='result');assert.equal(vote.freeNow,true);assert.equal(vote.initiator,null);assert.equal(vote.tips,'强制解散'); }); test('sparse room vote state preserves absolute seats and rejects malformed source atomically',()=>{ const data=roomData();(data.players as unknown[])[2]=null;const store=new PlatformStore();store.applyLoginSuccess(parseLoginResponse(data)); store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,-1,null,1],countdown:0}},99); const vote=store.getState().dissolution;assert.deepEqual(vote?.participants.map(p=>p.seat),[0,1,3]); const before=store.getState();for(const state of [[0,-1,-1,1],[0,-1],[0,-1,null,9]])assert.throws(()=>store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state,countdown:10}},0)); assert.throws(()=>store.applyDissolution('free_room',{seats:[0,1,null,1],roomcard:'bad'},0));assert.equal(store.getState(),before); }); test('self vote is acknowledged by the server and a fresh login clears stale vote',()=>{ const store=setup();store.applyDissolution('other_apply_free_room',{seat:0,agreefree:{state:[0,-1,-1,-1],countdown:5}},0); store.applyDissolution('self_agree_free_room',{},1);assert.equal(store.getState().dissolution?.participants[1].state,1); store.applyLoginSuccess(parseLoginResponse(roomData()));assert.equal(store.getState().dissolution,undefined); });