fix(platform): preserve recoverable outcomes and terminal sessions

This commit is contained in:
2026-09-05 10:50:55 +08:00
parent 1e3a21b476
commit 8eae3258ca
16 changed files with 500 additions and 48 deletions
@@ -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');
});