import { readFileSync } from 'node:fs'; import assert from 'node:assert/strict'; import { test } from 'node:test'; import type { GameEntry, GameHost, GameModule, PlatformGameSnapshot, PlatformToGameEvent, } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts'; import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; import { RuntimeSession } from '../../YouleNexus/assets/framework/platform/runtime-session.ts'; import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts'; import { selectGameSnapshot } from '../../YouleNexus/assets/framework/platform/stores/selectors.ts'; function fixture(name: string): Record { return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')).data; } function makeSession() { const store = new PlatformStore(); const log: string[] = []; const restored: unknown[] = []; const events: Array<{ event: PlatformToGameEvent; snapshot: PlatformGameSnapshot }> = []; let host: GameHost; const entry: GameEntry = { key: 'fixture', gameId: 41, route: 'fixture-game', resolveSeatCount: () => 4, createModule: (): GameModule => ({ attach(attachedHost) { host = attachedHost; attachedHost.getSnapshot(); log.push('attach'); }, handlePlatformEvent(event) { events.push({ event, snapshot: host.getSnapshot() }); log.push(`event:${event.type}`); }, handleGameMessage() {}, restore(deskinfo) { restored.push(deskinfo); log.push('restore'); }, dispose() { log.push('dispose'); }, }), }; const gameSession = new GameSessionHost(entry); log.length = 0; const gameHost: GameHost = { seat: { toView: (seat) => seat, toServer: (seat) => seat }, getSnapshot: () => selectGameSnapshot(store.getState()), subscribe: () => () => {}, sendGameMessage() {}, execute() {}, }; const session = new RuntimeSession({ store, gameSession, gameHost }); return { store, gameSession, session, log, restored, events }; } test('RuntimeSession commits a room login before opening, publishing, and restoring deskinfo', () => { const setup = makeSession(); const data = fixture('player-login-room.json'); setup.store.subscribe(() => { setup.log.push('store'); }); setup.session.markLoginPending(); setup.session.handleLogin(data); assert.equal(setup.session.loginPending, false); assert.equal(setup.store.getState().room.kind, 'inside'); assert.equal('deskinfo' in setup.store.getState().room, false); assert.equal(setup.restored.length, 1); assert.equal(setup.restored[0], data.deskinfo); assert.deepEqual(setup.log, ['store', 'attach', 'event:room.entered', 'restore']); assert.equal(setup.events[0]?.snapshot.room.roomcode, '100001'); assert.equal(setup.events[0]?.event.type, 'room.entered'); if (setup.events[0]?.event.type === 'room.entered') { assert.equal(setup.events[0].event.roomtype, setup.events[0].snapshot.room.roomtype); } }); test('RuntimeSession never treats isbattle as a restore signal without truthy deskinfo', () => { for (const deskinfo of [undefined, null, false, 0, '']) { const setup = makeSession(); const data = fixture('player-login-room.json'); if (deskinfo === undefined) delete data.deskinfo; else data.deskinfo = deskinfo; data.isbattle = 1; setup.session.handleLogin(data); assert.deepEqual(setup.restored, [], `deskinfo=${String(deskinfo)}`); assert.equal(setup.store.getState().room.kind, 'inside'); } }); test('RuntimeSession self join commits, opens a fresh session, and restores the same snapshot once', () => { const setup = makeSession(); setup.session.handleLogin(fixture('player-login-success.json')); setup.log.length = 0; const join = fixture('self-join-room.json'); setup.store.subscribe(() => { setup.log.push('store'); }); setup.session.handleSelfJoin(join); assert.equal(setup.store.getState().room.kind, 'inside'); assert.equal(setup.restored.length, 1); assert.equal(setup.restored[0], join.deskinfo); assert.deepEqual(setup.log, ['store', 'attach', 'event:room.entered', 'restore']); });