fix(platform): snapshot session fault errors safely

This commit is contained in:
2026-09-05 07:05:06 +08:00
parent 77b2bff40a
commit a383c0330e
4 changed files with 209 additions and 26 deletions
@@ -416,6 +416,41 @@ test('RuntimeSession flattens a foreign-realm branded open fault before lease in
);
});
test('RuntimeSession snapshots branded open errors without invoking their hostile iterator', () => {
const setup = makeLeaseSession();
const iteratorReplacement = new Error('iterator replacement');
let iteratorReads = 0;
const sourceErrors = [setup.errors.attach, setup.errors.dispose];
Object.defineProperty(sourceErrors, Symbol.iterator, {
configurable: true,
get() {
iteratorReads += 1;
sourceErrors[0] = iteratorReplacement;
throw new Error('source iterator must not run');
},
});
const brandedFault = new Error('hostile branded open fault');
Object.defineProperties(brandedFault, {
[Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true },
errors: { value: sourceErrors },
});
setup.setFailures('invalidate');
setup.gameSession.open = () => { throw brandedFault; };
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(iteratorReads, 0);
assert.equal(sourceErrors[0], setup.errors.attach);
assert.deepEqual(
(error as Error & { errors?: readonly unknown[] }).errors,
[setup.errors.attach, setup.errors.dispose, 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');
@@ -445,12 +480,26 @@ test('RuntimeSession keeps ordinary aggregates and unbranded or accessor spoofs
},
errors: { value: [primary, cleanup] },
});
const descriptorTarget = new Error('descriptor failure');
Object.defineProperties(descriptorTarget, {
[Symbol.for('youle.framework.sdk.GameSessionOpenFault')]: { value: true },
errors: { value: [primary, cleanup] },
});
let descriptorReads = 0;
const descriptorFailure = new Proxy(descriptorTarget, {
getOwnPropertyDescriptor(target, key) {
descriptorReads += 1;
if (key === 'errors') throw new Error('descriptor trap failed');
return Reflect.getOwnPropertyDescriptor(target, key);
},
});
for (const candidate of [
ordinaryAggregate,
unbranded,
accessorSpoof,
brandAccessorSpoof,
descriptorFailure,
]) {
const setup = makeLeaseSession();
setup.setFailures('invalidate');
@@ -467,4 +516,5 @@ test('RuntimeSession keeps ordinary aggregates and unbranded or accessor spoofs
assert.equal(setup.leases[0]?.invalidations, 1);
}
assert.equal(accessorReads, 0);
assert.equal(descriptorReads, 2);
});