fix(platform): preserve session failures and URL syntax

This commit is contained in:
2026-09-05 06:38:00 +08:00
parent 7e5a950cc7
commit 224df1c21c
6 changed files with 123 additions and 8 deletions
@@ -366,3 +366,20 @@ test('RuntimeSession aggregates event, dispose, and invalidation failures after
[setup.errors.event, setup.errors.dispose, setup.errors.invalidate],
);
});
test('RuntimeSession flattens attach cleanup failures before appending lease invalidation', () => {
const setup = makeLeaseSession();
setup.setFailures('attach', 'dispose', 'invalidate');
const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json')));
assert.equal(setup.session.state, 'faulted');
assert.equal(setup.session.fault, error);
assert.equal(setup.gameSession.state, 'idle');
assert.equal(setup.leases[0]?.invalidations, 1);
assert.equal(setup.getModuleDisposals(), 1);
assert.deepEqual(
(error as Error & { errors?: readonly unknown[] }).errors,
[setup.errors.attach, setup.errors.dispose, setup.errors.invalidate],
);
});
@@ -512,6 +512,10 @@ test('PlatformHandlers validates every switch URL before closing the active game
'http://invalid.example',
'ws://user@example.com',
'ws://example.com/#fragment',
'ws://example.com/#',
'ws://example.com#',
'ws://@example.com',
'ws://:@example.com',
'ws:///path',
'ws://example.com:99999',
'ws://example.com:abc',
@@ -544,7 +548,10 @@ test('PlatformHandlers validates every switch URL before closing the active game
test('PlatformHandlers accepts case-insensitive WS/WSS schemes and preserves full payload identity', () => {
const setup = makeHandlers();
const roomData = { roomserver: 'WS://EXAMPLE.com:80/path?x=1', extra: { trace: 1 } };
const roomData = {
roomserver: 'WS://EXAMPLE.com:80/path/%23anchor?x=1',
extra: { trace: 1 },
};
const agentData = {
agentserver: 'WSS://secure.example:443/socket', opt: 'free_room', extra: { trace: 2 },
};
@@ -39,6 +39,16 @@ function makeEntry(createModule: () => GameModule): GameEntry {
};
}
function caughtError(operation: () => void): Error {
try {
operation();
} catch (error) {
assert.ok(error instanceof Error);
return error;
}
assert.fail('expected operation to throw');
}
function makeModule(log: string[], onRestore?: (deskinfo: unknown) => void): GameModule {
let attached = false;
return {
@@ -153,6 +163,64 @@ test('GameSessionHost disposes a module when attachment throws and returns to id
assert.equal(sessions.state, 'idle');
});
test('GameSessionHost preserves create and contract failures when open cleanup succeeds', () => {
const createError = new Error('create failed');
let createCalls = 0;
const createSessions = new GameSessionHost(makeEntry(() => {
createCalls += 1;
if (createCalls === 1) return makeModule([]);
throw createError;
}));
assert.equal(caughtError(() => createSessions.open(makeHost())), createError);
assert.equal(createSessions.state, 'idle');
let contractCalls = 0;
let contractDisposals = 0;
const contractSessions = new GameSessionHost(makeEntry(() => {
contractCalls += 1;
if (contractCalls === 1) return makeModule([]);
return {
handlePlatformEvent() {},
handleGameMessage() {},
restore() {},
dispose() { contractDisposals += 1; },
} as unknown as GameModule;
}));
assert.match(caughtError(() => contractSessions.open(makeHost())).message, /implement attach/);
assert.equal(contractDisposals, 1);
assert.equal(contractSessions.state, 'idle');
});
test('GameSessionHost preserves attach then dispose failures by identity and occurrence order', () => {
const attachError = new Error('attach primary');
const disposeError = new Error('open cleanup');
let createCalls = 0;
let disposals = 0;
const sessions = new GameSessionHost(makeEntry(() => {
createCalls += 1;
if (createCalls === 1) return makeModule([]);
return {
...makeModule([]),
attach() { throw attachError; },
dispose() {
disposals += 1;
throw disposeError;
},
};
}));
const error = caughtError(() => sessions.open(makeHost()));
assert.deepEqual(
(error as Error & { errors?: readonly unknown[] }).errors,
[attachError, disposeError],
);
assert.equal(disposals, 1);
assert.equal(sessions.state, 'idle');
});
test('GameSessionHost disposes a module exactly once and becomes terminal after dispose', () => {
const log: string[] = [];
const sessions = new GameSessionHost(makeEntry(() => makeModule(log)));