feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
||||
|
||||
const GAME_ROOMTYPE = JSON.parse(readFileSync(
|
||||
new URL('../fixtures/contracts/player-login-room.json', import.meta.url), 'utf8',
|
||||
)).data.roomtype as readonly unknown[];
|
||||
)).data.roomtype as import('../../YouleNexus/assets/framework/sdk/contracts/roomtype.ts').Roomtype;
|
||||
|
||||
test('browser-like loader evaluates the emitted harness without Node builtin modules', () => {
|
||||
const source = readFileSync(new URL(
|
||||
@@ -74,7 +74,7 @@ test('assertGameContract accepts a fixed-four game with its real nested roomtype
|
||||
));
|
||||
assert.equal(invalidations, 2);
|
||||
assert.equal(restoredDeskinfo.length, 0);
|
||||
assert.ok(nextModuleId >= 2);
|
||||
assert.equal(nextModuleId, 4, 'two explicit isolation fixtures and two conformance sessions; no constructor probes');
|
||||
});
|
||||
|
||||
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {test} from 'node:test';
|
||||
import {createGameRoomProjection} from '../../YouleNexus/assets/framework/sdk/game-room-projection.ts';
|
||||
import {gameAllowsRoomAction} from '../../YouleNexus/assets/framework/presentation/game-room-presentation.ts';
|
||||
test('game owns atomic complete projections and exposes no consumer setter',()=>{
|
||||
const initial={roundPhase:'waiting',selfParticipation:'waiting' as const,scoresBySeat:{0:10},restrictions:{}};
|
||||
const owner=createGameRoomProjection(initial);const source=owner.source;const versions:any[]=[];
|
||||
source.subscribe(value=>versions.push(value));owner.commit({roundPhase:'playing',selfParticipation:'playing',scoresBySeat:{0:20},restrictions:{prepare:false}});
|
||||
assert.deepEqual(versions.map(v=>[v.revision,v.roundPhase,v.scoresBySeat[0]]),[[0,'waiting',10],[1,'playing',20]]);
|
||||
initial.scoresBySeat[0]=999;assert.equal(versions[0].scoresBySeat[0],10);
|
||||
assert.equal('commit' in source,false);assert.ok(Object.isFrozen(source.getSnapshot().scoresBySeat));
|
||||
assert.equal(gameAllowsRoomAction(false,source.getSnapshot(),'exit'),false);assert.equal(gameAllowsRoomAction(true,source.getSnapshot(),'prepare'),false);
|
||||
assert.throws(()=>owner.commit({...initial,restrictions:{exit:true} as any}),/disable/);assert.equal(source.getSnapshot().revision,1);
|
||||
owner.dispose();assert.throws(()=>owner.commit(initial),/disposed/);assert.throws(()=>source.subscribe(()=>{}),/disposed/);
|
||||
});
|
||||
test('reentrant projection commits notify complete versions in order and isolate listener failure',()=>{
|
||||
const initial={roundPhase:'waiting',selfParticipation:'waiting' as const,scoresBySeat:{},restrictions:{}};const owner=createGameRoomProjection(initial);const seen:number[]=[];
|
||||
owner.source.subscribe(p=>{if(p.revision===1){owner.commit({...initial,roundPhase:'finished'});throw Error('listener failed');}});
|
||||
owner.source.subscribe(p=>seen.push(p.revision));assert.throws(()=>owner.commit({...initial,roundPhase:'playing'}),/notification/);assert.deepEqual(seen,[0,1,2]);
|
||||
});
|
||||
@@ -82,14 +82,15 @@ test('assertGameEntry rejects invalid identity, routes, resolver presence, and m
|
||||
assert.throws(() => assertGameEntry({ ...entry, route }), /reserved.*route/);
|
||||
}
|
||||
assert.throws(() => assertGameEntry({ ...entry, resolveSeatCount: undefined as never }), /resolveSeatCount/);
|
||||
assert.throws(() => assertGameEntry({ ...entry, createModule: () => undefined as never }), /module/);
|
||||
assert.throws(() => assertGameEntry({ ...entry, createModule: undefined as never }), /createModule/);
|
||||
});
|
||||
|
||||
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[]) {
|
||||
resolveSeatCount(roomtype: import('../../YouleNexus/assets/framework/sdk/contracts/roomtype.ts').Roomtype) {
|
||||
assert.ok(Array.isArray(roomtype));
|
||||
calls.push(roomtype);
|
||||
if (roomtype[1] !== 4) throw new Error('Game roomtype requires four seats at index 1');
|
||||
return 4;
|
||||
@@ -99,16 +100,26 @@ test('GameEntry preflight does not call a resolver before its game-owned roomtyp
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('assertGameEntry disposes its validation module exactly once', () => {
|
||||
let disposals = 0;
|
||||
const entry = makeEntry(() => ({
|
||||
...makeModule([]),
|
||||
dispose: () => { disposals++; },
|
||||
}));
|
||||
test('declaration checks and constructor have zero factory or disposer side effects', () => {
|
||||
let created = 0, disposals = 0;
|
||||
const entry = makeEntry(() => {
|
||||
created++;
|
||||
return {...makeModule([]),dispose: () => { disposals++; }};
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() => assertGameEntry(entry));
|
||||
|
||||
const session = new GameSessionHost(entry);
|
||||
assert.equal(created, 0);
|
||||
assert.equal(disposals, 0);
|
||||
session.open(makeHost());
|
||||
assert.equal(created, 1);
|
||||
session.close();
|
||||
assert.equal(disposals, 1);
|
||||
session.open(makeHost());
|
||||
session.dispose();
|
||||
assert.equal(created, 2);
|
||||
assert.equal(disposals, 2);
|
||||
});
|
||||
|
||||
test('GameSessionHost delivers room events and deskinfo to one active module in order', () => {
|
||||
@@ -186,18 +197,17 @@ test('GameSessionHost preserves create and contract failures when open cleanup s
|
||||
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');
|
||||
assert.equal(createCalls, 1);
|
||||
|
||||
let contractCalls = 0;
|
||||
let contractDisposals = 0;
|
||||
const contractSessions = new GameSessionHost(makeEntry(() => {
|
||||
contractCalls += 1;
|
||||
if (contractCalls === 1) return makeModule([]);
|
||||
return {
|
||||
handlePlatformEvent() {},
|
||||
handleGameMessage() {},
|
||||
@@ -208,6 +218,7 @@ test('GameSessionHost preserves create and contract failures when open cleanup s
|
||||
|
||||
assert.match(caughtError(() => contractSessions.open(makeHost())).message, /implement attach/);
|
||||
assert.equal(contractDisposals, 1);
|
||||
assert.equal(contractCalls, 1);
|
||||
assert.equal(contractSessions.state, 'idle');
|
||||
});
|
||||
|
||||
@@ -218,7 +229,6 @@ test('GameSessionHost preserves attach then dispose failures by identity and occ
|
||||
let disposals = 0;
|
||||
const sessions = new GameSessionHost(makeEntry(() => {
|
||||
createCalls += 1;
|
||||
if (createCalls === 1) return makeModule([]);
|
||||
return {
|
||||
...makeModule([]),
|
||||
attach() { throw attachError; },
|
||||
@@ -514,3 +524,19 @@ test('terminal disposal during module creation cleans the returned module withou
|
||||
assert.equal(attaches, 0);
|
||||
assert.equal(sessions.state, 'disposed');
|
||||
});
|
||||
|
||||
test('active game projection is exposed read-only and disappears with its owning module',()=>{
|
||||
const projection={getSnapshot(){return {revision:0,roundPhase:'waiting',selfParticipation:'waiting' as const,scoresBySeat:{},restrictions:{}};},subscribe(){return ()=>{};}};
|
||||
const session=new GameSessionHost(makeEntry(()=>({...makeModule([]),roomProjection:projection})));
|
||||
assert.equal(session.roomProjection,null);session.open(makeHost());assert.equal(session.roomProjection,projection);session.close();assert.equal(session.roomProjection,null);
|
||||
const unsupported=new GameSessionHost(makeEntry(()=>makeModule([])));unsupported.open(makeHost());assert.equal(unsupported.roomProjection,null);unsupported.dispose();
|
||||
});
|
||||
|
||||
test('an undefined module is rejected only when the actual room opens',()=>{
|
||||
let creations=0;
|
||||
const entry=makeEntry(()=>{creations++;return undefined as never;});
|
||||
const sessions=new GameSessionHost(entry);
|
||||
assert.equal(creations,0);assert.equal(sessions.state,'idle');
|
||||
assert.throws(()=>sessions.open(makeHost()),/module/);
|
||||
assert.equal(creations,1);assert.equal(sessions.state,'idle');sessions.close();sessions.dispose();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user