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
@@ -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');
});