fix(platform): recognize session faults across realms

This commit is contained in:
2026-09-05 06:49:59 +08:00
parent 224df1c21c
commit 77b2bff40a
4 changed files with 181 additions and 3 deletions
@@ -1,6 +1,7 @@
import { readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { runInNewContext } from 'node:vm';
import type {
GameEntry,
GameHost,
@@ -8,7 +9,10 @@ import type {
PlatformGameSnapshot,
PlatformToGameEvent,
} from '../../YouleNexus/assets/framework/sdk/contracts/index.ts';
import { GameSessionHost } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
import {
GameSessionHost,
GameSessionOpenFault,
} 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';
@@ -383,3 +387,84 @@ test('RuntimeSession flattens attach cleanup failures before appending lease inv
[setup.errors.attach, setup.errors.dispose, setup.errors.invalidate],
);
});
test('RuntimeSession flattens a foreign-realm branded open fault before lease invalidation', () => {
const setup = makeLeaseSession();
const primary = new Error('foreign primary');
const cleanup = new Error('foreign cleanup');
const foreignFault = runInNewContext(`(() => {
const fault = new Error('foreign open fault');
Object.defineProperties(fault, {
[Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true },
errors: { value: [primary, cleanup] },
});
return fault;
})()`, { primary, cleanup });
assert.equal(foreignFault instanceof GameSessionOpenFault, false);
setup.setFailures('invalidate');
setup.gameSession.open = () => { throw foreignFault; };
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.deepEqual(
(error as Error & { errors?: readonly unknown[] }).errors,
[primary, cleanup, setup.errors.invalidate],
);
});
test('RuntimeSession keeps ordinary aggregates and unbranded or accessor spoofs as single errors', () => {
const primary = new Error('user primary');
const cleanup = new Error('user cleanup');
const ordinaryAggregate = runInNewContext(
'new AggregateError([primary, cleanup], "user aggregate")',
{ primary, cleanup },
);
const unbranded = { errors: [primary, cleanup] };
let accessorReads = 0;
const accessorSpoof = new Error('accessor spoof');
Object.defineProperties(accessorSpoof, {
[Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true },
errors: {
get() {
accessorReads += 1;
throw new Error('errors getter must not run');
},
},
});
const brandAccessorSpoof = new Error('brand accessor spoof');
Object.defineProperties(brandAccessorSpoof, {
[Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: {
get() {
accessorReads += 1;
throw new Error('brand getter must not run');
},
},
errors: { value: [primary, cleanup] },
});
for (const candidate of [
ordinaryAggregate,
unbranded,
accessorSpoof,
brandAccessorSpoof,
]) {
const setup = makeLeaseSession();
setup.setFailures('invalidate');
setup.gameSession.open = () => { throw candidate; };
const error = caughtError(() => setup.session.handleLogin(fixture('player-login-room.json')));
const errors = (error as Error & { errors?: readonly unknown[] }).errors;
assert.equal(errors?.length, 2);
assert.equal(errors?.[0], candidate);
assert.equal(errors?.[1], setup.errors.invalidate);
assert.equal(setup.session.state, 'faulted');
assert.equal(setup.gameSession.state, 'idle');
assert.equal(setup.leases[0]?.invalidations, 1);
}
assert.equal(accessorReads, 0);
});
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { runInNewContext } from 'node:vm';
import type {
GameEntry,
GameHost,
@@ -11,7 +12,11 @@ import type {
import {
assertGameEntry,
GameSessionHost,
GameSessionOpenFault,
} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
import * as gameSessionRuntime from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
const OPEN_FAULT_BRAND_KEY = 'youle.framework.sdk.GameSessionOpenFault';
function makeHost(): GameHost {
const snapshot: PlatformGameSnapshot = {
@@ -221,6 +226,61 @@ test('GameSessionHost preserves attach then dispose failures by identity and occ
assert.equal(sessions.state, 'idle');
});
test('open fault guard recognizes the global brand across realms without reading accessors', () => {
type OpenFaultShape = { readonly errors: readonly unknown[] };
type OpenFaultGuard = (value: unknown) => value is OpenFaultShape;
const exported = (gameSessionRuntime as unknown as Record<string, unknown>)
.isGameSessionOpenFault;
assert.equal(typeof exported, 'function');
const isGameSessionOpenFault = exported as OpenFaultGuard;
const primary = new Error('primary');
const cleanup = new Error('cleanup');
const sameRealm = new GameSessionOpenFault(primary, cleanup);
const foreignRealm = runInNewContext(`(() => {
const fault = new Error('foreign open fault');
Object.defineProperties(fault, {
[Symbol.for(${JSON.stringify(OPEN_FAULT_BRAND_KEY)})]: { value: true },
errors: { value: [primary, cleanup] },
});
return fault;
})()`, { primary, cleanup });
const ordinaryAggregate = runInNewContext(
'new AggregateError([primary, cleanup], "user aggregate")',
{ primary, cleanup },
);
const unbranded = { errors: [primary, cleanup] };
let accessorReads = 0;
const accessorSpoof = new Error('accessor spoof');
Object.defineProperties(accessorSpoof, {
[Symbol.for(OPEN_FAULT_BRAND_KEY)]: { value: true },
errors: {
get() {
accessorReads += 1;
throw new Error('errors getter must not run');
},
},
});
const brandAccessorSpoof = new Error('brand accessor spoof');
Object.defineProperties(brandAccessorSpoof, {
[Symbol.for(OPEN_FAULT_BRAND_KEY)]: {
get() {
accessorReads += 1;
throw new Error('brand getter must not run');
},
},
errors: { value: [primary, cleanup] },
});
assert.equal(foreignRealm instanceof GameSessionOpenFault, false);
assert.equal(isGameSessionOpenFault(sameRealm), true);
assert.equal(isGameSessionOpenFault(foreignRealm), true);
assert.equal(isGameSessionOpenFault(ordinaryAggregate), false);
assert.equal(isGameSessionOpenFault(unbranded), false);
assert.equal(isGameSessionOpenFault(accessorSpoof), false);
assert.equal(isGameSessionOpenFault(brandAccessorSpoof), false);
assert.equal(accessorReads, 0);
});
test('GameSessionHost disposes a module exactly once and becomes terminal after dispose', () => {
const log: string[] = [];
const sessions = new GameSessionHost(makeEntry(() => makeModule(log)));