fix(platform): preserve recoverable outcomes and terminal sessions
This commit is contained in:
@@ -17,7 +17,11 @@ import {
|
||||
type GameContractHost,
|
||||
} from '../../YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts';
|
||||
|
||||
test('assertGameContract runs the reusable isolated session conformance suite', () => {
|
||||
const GAME_ROOMTYPE = JSON.parse(readFileSync(
|
||||
new URL('../fixtures/contracts/player-login-room.json', import.meta.url), 'utf8',
|
||||
)).data.roomtype as readonly unknown[];
|
||||
|
||||
test('assertGameContract accepts a fixed-four game with its real nested roomtype and host fixture', () => {
|
||||
let invalidations = 0;
|
||||
let nextModuleId = 0;
|
||||
const restoredDeskinfo: unknown[] = [];
|
||||
@@ -25,17 +29,17 @@ test('assertGameContract runs the reusable isolated session conformance suite',
|
||||
key: 'fixture',
|
||||
gameId: 41,
|
||||
route: 'fixture-route',
|
||||
resolveSeatCount: (roomtype) => {
|
||||
const seats = roomtype[0];
|
||||
if (seats === 2 || seats === 4 || seats === 10) return seats;
|
||||
return 4;
|
||||
},
|
||||
resolveSeatCount: () => 4,
|
||||
createModule: () => makeModule(++nextModuleId, (deskinfo) => { restoredDeskinfo.push(deskinfo); }),
|
||||
};
|
||||
|
||||
assert.doesNotThrow(() => assertGameContract(
|
||||
entry,
|
||||
makeConformanceHost(entry, () => { invalidations++; }, restoredDeskinfo),
|
||||
[{
|
||||
roomtype: GAME_ROOMTYPE,
|
||||
expectedSeatCount: 4,
|
||||
makeHost: makeConformanceHost(entry, () => { invalidations++; }, restoredDeskinfo),
|
||||
}],
|
||||
));
|
||||
assert.equal(invalidations, 2);
|
||||
assert.equal(restoredDeskinfo.length, 0);
|
||||
@@ -65,7 +69,8 @@ function makeModule(id: number, onRestore: (deskinfo: unknown) => void): GameMod
|
||||
handlePlatformEvent(event) {
|
||||
eventCount++;
|
||||
if (event.type === 'room.entered') {
|
||||
assert.equal(event.roomtype[0], 4);
|
||||
assert.deepEqual(event.roomtype, GAME_ROOMTYPE);
|
||||
assert.deepEqual(host!.getSnapshot().room.roomtype, event.roomtype);
|
||||
}
|
||||
},
|
||||
handleGameMessage(message) {
|
||||
@@ -105,7 +110,6 @@ function makeConformanceHost(
|
||||
new URL('../fixtures/contracts/player-login-room.json', import.meta.url),
|
||||
'utf8',
|
||||
)) as { data: Record<string, unknown> };
|
||||
login.data.roomtype = [4];
|
||||
login.data.isbattle = 0;
|
||||
const store = new PlatformStore();
|
||||
store.applyLoginSuccess(parseLoginResponse(login.data));
|
||||
|
||||
@@ -73,7 +73,7 @@ function makeModule(log: string[], onRestore?: (deskinfo: unknown) => void): Gam
|
||||
};
|
||||
}
|
||||
|
||||
test('assertGameEntry rejects invalid identity, routes, seat counts, and module factories', () => {
|
||||
test('assertGameEntry rejects invalid identity, routes, resolver presence, and module factories', () => {
|
||||
const entry = makeEntry(() => makeModule([]));
|
||||
|
||||
assert.throws(() => assertGameEntry({ ...entry, key: '' }), /key/);
|
||||
@@ -81,11 +81,24 @@ test('assertGameEntry rejects invalid identity, routes, seat counts, and module
|
||||
for (const route of ['platform', 'agent', 'room']) {
|
||||
assert.throws(() => assertGameEntry({ ...entry, route }), /reserved.*route/);
|
||||
}
|
||||
assert.throws(() => assertGameEntry({ ...entry, resolveSeatCount: () => 0 }), /seat count/);
|
||||
assert.throws(() => assertGameEntry({ ...entry, resolveSeatCount: () => 2.5 }), /seat count/);
|
||||
assert.throws(() => assertGameEntry({ ...entry, resolveSeatCount: undefined as never }), /resolveSeatCount/);
|
||||
assert.throws(() => assertGameEntry({ ...entry, createModule: () => undefined as never }), /module/);
|
||||
});
|
||||
|
||||
test('GameEntry preflight does not call a resolver before its game-owned roomtype exists', () => {
|
||||
const calls: Array<readonly unknown[]> = [];
|
||||
const entry = {
|
||||
...makeEntry(() => makeModule([])),
|
||||
resolveSeatCount(roomtype: readonly unknown[]) {
|
||||
calls.push(roomtype);
|
||||
if (roomtype[1] !== 4) throw new Error('Game roomtype requires four seats at index 1');
|
||||
return 4;
|
||||
},
|
||||
};
|
||||
assert.doesNotThrow(() => new GameSessionHost(entry));
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('assertGameEntry disposes its validation module exactly once', () => {
|
||||
let disposals = 0;
|
||||
const entry = makeEntry(() => ({
|
||||
@@ -428,3 +441,76 @@ test('GameSessionHost returns to active after a restore handler throws', () => {
|
||||
assert.throws(() => sessions.restore({}), /restore failed/);
|
||||
assert.equal(sessions.state, 'active');
|
||||
});
|
||||
|
||||
for (const callback of ['attach', 'restore'] as const) {
|
||||
for (const throwsAfterStop of [false, true]) {
|
||||
test(`terminal dispose during ${callback} survives callback completion (throws=${throwsAfterStop})`, () => {
|
||||
let sessions: GameSessionHost;
|
||||
let disposals = 0;
|
||||
const failure = new Error('callback failed after disposal');
|
||||
sessions = new GameSessionHost(makeEntry(() => ({
|
||||
...makeModule([]),
|
||||
[callback]() {
|
||||
sessions.dispose();
|
||||
sessions.dispose();
|
||||
assert.equal(sessions.state, 'disposed');
|
||||
if (throwsAfterStop) throw failure;
|
||||
},
|
||||
dispose() { disposals += 1; },
|
||||
})));
|
||||
disposals = 0;
|
||||
const invoke = () => {
|
||||
sessions.open(makeHost());
|
||||
if (callback === 'restore') sessions.restore({ round: 1 });
|
||||
};
|
||||
if (throwsAfterStop) assert.equal(caughtError(invoke), failure);
|
||||
else assert.doesNotThrow(invoke);
|
||||
assert.equal(disposals, 1);
|
||||
assert.equal(sessions.state, 'disposed');
|
||||
sessions.dispose();
|
||||
assert.equal(disposals, 1);
|
||||
assert.throws(() => sessions.open(makeHost()), /disposed/);
|
||||
assert.throws(() => sessions.restore({}), /disposed/);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const release of ['close', 'dispose'] as const) {
|
||||
test(`${release} tolerates module.dispose reentering terminal disposal`, () => {
|
||||
let sessions: GameSessionHost | undefined;
|
||||
let disposals = 0;
|
||||
sessions = new GameSessionHost(makeEntry(() => ({
|
||||
...makeModule([]),
|
||||
dispose() {
|
||||
if (sessions === undefined) return;
|
||||
disposals += 1;
|
||||
sessions.dispose();
|
||||
},
|
||||
})));
|
||||
sessions.open(makeHost());
|
||||
assert.doesNotThrow(() => sessions![release]());
|
||||
assert.equal(sessions.state, 'disposed');
|
||||
assert.equal(disposals, 1);
|
||||
sessions.dispose();
|
||||
assert.equal(disposals, 1);
|
||||
});
|
||||
}
|
||||
|
||||
test('terminal disposal during module creation cleans the returned module without attaching it', () => {
|
||||
let sessions: GameSessionHost | undefined;
|
||||
let attaches = 0;
|
||||
let disposals = 0;
|
||||
sessions = new GameSessionHost(makeEntry(() => {
|
||||
if (sessions !== undefined) sessions.dispose();
|
||||
return {
|
||||
...makeModule([]),
|
||||
attach() { attaches += 1; },
|
||||
dispose() { disposals += 1; },
|
||||
};
|
||||
}));
|
||||
disposals = 0;
|
||||
assert.doesNotThrow(() => sessions!.open(makeHost()));
|
||||
assert.equal(disposals, 1);
|
||||
assert.equal(attaches, 0);
|
||||
assert.equal(sessions.state, 'disposed');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user