56 KiB
YouleNexus Platform Vertical Slice Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 建立可复用、可测试、与具体子游戏解耦的平台运行时,并跑通配置、登录、大厅、进房、准备和断线重连的第一条纵向链路。
Architecture: 平台核心通过单一 PlatformRuntime 装配配置、网络、协议、状态和场景端口;所有业务消息只经过一个 Router。子游戏通过版本化 sdk/contracts 接入,平台通过 GameSessionHost 调用游戏,游戏只能通过受限 GameHost 调用平台,不得导入任何平台内部实现。
Tech Stack: TypeScript 6、Node.js test runner、tsx、Cocos Creator 3.8+、原生 WebSocket、funplay-cocos MCP。
Spec: docs/superpowers/specs/2026-09-04-platform-vertical-slice-design.md
Global Constraints
- 服务器零改动:信封固定为
{app:"youle",route,rpc,data},route/rpc/字段名/类型/时序与docs/protocol/一致。 - 配置服务和原生 App 零改动:
gameserver → data.urlserver、window.settings名称和数据格式逐字兼容。 - 配置、RPC route、玩家实体和房间座位各自只有一个权威来源;下游不补默认值。
- 平台不解析
roomtype和deskinfo;deskinfo严格按原工程if (deskinfo)的真值语义触发GameModule.restore,不进入 PlatformState。 sdk/contracts不得依赖 platform/net/protocol/ui/core 内部模块、Cocoscc或具体子游戏。- 具体子游戏不得导入 framework 的 platform/net/protocol/ui/core 内部模块。
- 所有
.scene、.prefab、.anim、.meta修改必须通过 funplay-cocos MCP,禁止文本编辑。 - 每个任务遵循测试先行;提交时只暂存本任务文件,不夹带工作区已有 UI 迁移改动。
- 测试命令均从
cocoscreator_projects/目录执行。
File Structure
本计划完成后的核心结构:
cocoscreator_projects/YouleNexus/assets/framework/
├── config/
│ ├── runtime-config.ts
│ ├── bootstrap.ts
│ ├── remote-config.ts
│ └── sources/native-settings.ts
├── net/
│ ├── net-client.ts
│ └── ...
├── protocol/
│ ├── contracts/
│ │ ├── validation.ts
│ │ ├── login-contract.ts
│ │ ├── join-room-contract.ts
│ │ └── room-event-contracts.ts
│ ├── rpc-route.ts
│ ├── platform-handlers.ts
│ └── router.ts
├── platform/
│ ├── runtime.ts
│ ├── ready-gate.ts
│ ├── session.ts
│ ├── commands.ts
│ ├── game-host-adapter.ts
│ ├── scene-port.ts
│ └── stores/
│ ├── platform-store.ts
│ ├── platform-types.ts
│ ├── selectors.ts
│ └── types.ts # legacy types, removed after migration
├── sdk/
│ ├── contracts/
│ │ ├── api-version.ts
│ │ ├── game-descriptor.ts
│ │ ├── game-host.ts
│ │ ├── game-module.ts
│ │ ├── game-seat-mapper.ts
│ │ ├── platform-events.ts
│ │ ├── snapshots.ts
│ │ └── index.ts
│ ├── runtime/
│ │ ├── game-registry.ts
│ │ └── game-session-host.ts
│ └── testing/game-contract-harness.ts
└── ui/controllers/
├── connection-overlay-controller.ts
├── join-room-controller.ts
└── room-shell-controller.ts
具体子游戏后续统一放在 YouleNexus/assets/games/<game-key>/,本计划只创建测试游戏,不创建真实玩法。
Task 1: 建立纯净、版本化的子游戏 SDK 契约
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/api-version.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-descriptor.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-host.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-module.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/game-seat-mapper.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/platform-events.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/snapshots.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/contracts/index.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/sdk/index.ts - Create:
cocoscreator_projects/framework-tests/sdk/contracts.test.ts - Create:
cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs
Interfaces:
-
Produces:
PLATFORM_GAME_API_VERSION = 1,GameDescriptor,GameModule,GameHost,GameSeatMapper,PlatformToGameEvent,GameServerMessage,PlatformGameSnapshot,GameHostCommand。 -
Consumes: only TypeScript standard types.
-
Step 1: Write the failing public-contract test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { PLATFORM_GAME_API_VERSION } from '../../YouleNexus/assets/framework/sdk/contracts/index.ts';
test('platform game API version is exact', () => {
assert.equal(PLATFORM_GAME_API_VERSION, 1);
});
- Step 2: Write the failing import-boundary test
The test recursively reads assets/framework/sdk/contracts/**/*.ts and rejects imports containing /platform/, /net/, /protocol/, /ui/, /core/, from 'cc', or from "cc".
assert.equal(
forbidden,
undefined,
`${file} imports forbidden dependency: ${forbidden}`,
);
- Step 3: Run the tests and verify failure
Run:
node --import tsx --test framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs
Expected: FAIL because sdk/contracts/index.ts does not exist.
- Step 4: Add the exact contract types
export const PLATFORM_GAME_API_VERSION = 1 as const;
export type PlatformGameApiVersion = typeof PLATFORM_GAME_API_VERSION;
export interface GameDescriptor {
readonly apiVersion: PlatformGameApiVersion;
readonly gameId: string | number;
readonly route: string;
readonly key: string;
resolveSeatCount(roomtype: readonly unknown[]): number;
create(): GameModule;
}
export interface GameModule {
attach(host: GameHost): void;
handlePlatformEvent(event: PlatformToGameEvent): void;
handleGameMessage(message: GameServerMessage): void;
restore(deskinfo: unknown): void;
dispose(): void;
}
Define GameHost exactly as the approved spec: bound seat: GameSeatMapper, getSnapshot, immediate subscribe, route-bound sendGameMessage, and execute with only room.prepare|room.exit. GameSeatMapper exposes toView(serverSeat) and toServer(viewSeat). Define snapshot DTOs locally in sdk/contracts; do not import Store types.
- Step 5: Add the new contracts without breaking legacy callers
export * from './contracts/index.ts';
Add this export to the existing sdk/index.ts while temporarily retaining its legacy interfaces. Only sdk/contracts is the new public boundary; Task 7 migrates callers and removes the legacy definitions in the same green commit.
- Step 6: Run targeted tests and typecheck
node --import tsx --test framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: SDK tests PASS and typecheck reports zero errors.
- Step 7: Commit
git add -- YouleNexus/assets/framework/sdk framework-tests/sdk/contracts.test.ts framework-tests/architecture/import-boundaries.test.mjs
git commit -m "feat(sdk): define decoupled game contracts"
Task 2: 实现游戏注册表、会话生命周期与接入测试工具
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-registry.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/runtime/game-session-host.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/sdk/testing/game-contract-harness.ts - Create:
cocoscreator_projects/framework-tests/sdk/game-registry.test.ts - Create:
cocoscreator_projects/framework-tests/sdk/game-session-host.test.ts - Create:
cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts
Interfaces:
-
Consumes: Task 1
GameDescriptor,GameModule,GameHost, event/message contracts. -
Produces:
GameRegistry.register/getByRoute/getByGameId,GameSessionHost.attach/enter/handleGameMessage/restore/dispose,assertGameContract。 -
Step 1: Write registry failure tests
test('registry rejects duplicate route and API mismatch', () => {
const registry = new GameRegistry();
registry.register(descriptor({ key: 'a', gameId: 1, route: 'g1' }));
assert.throws(() => registry.register(descriptor({ key: 'b', gameId: 2, route: 'g1' })), /route.*g1/);
assert.throws(() => registry.register({ ...descriptor({ key: 'c', gameId: 3, route: 'g3' }), apiVersion: 2 as never }), /apiVersion/);
});
Also reject a missing resolveSeatCount contract. The conformance harness calls it with fixture roomtype values and rejects zero, negative, fractional, or non-number results.
- Step 2: Write lifecycle and generation tests
Cover detached → attached → entered → disposed, illegal double attach, event before enter, dispose exactly once, and calls through an old Host after a new generation begins.
session.attach(host);
session.enter({ type: 'room-entered', roomtype: rawRoomtype });
session.dispose();
assert.throws(() => oldHost.sendGameMessage('play', {}), /disposed/);
- Step 3: Run tests and verify failure
node --import tsx --test framework-tests/sdk/game-registry.test.ts framework-tests/sdk/game-session-host.test.ts
Expected: FAIL because runtime classes do not exist.
- Step 4: Implement strict registry
Use three maps keyed by key, normalized String(gameId), and route. Reject empty values, duplicates, API mismatch, and factories that return the same object twice during conformance testing.
register(value: GameDescriptor): void {
assertDescriptor(value);
if (this.byRoute.has(value.route)) throw new Error(`duplicate game route: ${value.route}`);
this.byRoute.set(value.route, value);
this.byGameId.set(String(value.gameId), value);
this.byKey.set(value.key, value);
}
- Step 5: Implement serialized session dispatch
GameSessionHost owns one module and one Host. Every public method checks state; fail(error) records the first failure, disposes once, and rejects later dispatch. Store commit ordering remains the platform adapter's responsibility.
- Step 6: Implement the reusable conformance harness
assertGameContract(descriptor, makeHost) runs descriptor validation, seat-count resolution, fresh factory, lifecycle, route-bound send, snapshot immutability, subscription cleanup, deskinfo identity, 2/4/10-seat mapper round trips, and two-session isolation.
- Step 7: Run tests and typecheck
node --import tsx --test framework-tests/sdk/game-registry.test.ts framework-tests/sdk/game-session-host.test.ts framework-tests/sdk/game-contract-harness.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 8: Commit
git add -- YouleNexus/assets/framework/sdk/runtime YouleNexus/assets/framework/sdk/testing framework-tests/sdk
git commit -m "feat(sdk): add game registry and isolated sessions"
Task 3: 修正运行配置、原生身份与 urlserver 单一来源
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/config/remote-config.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/config/sources/native-settings.ts - Modify:
cocoscreator_projects/framework-tests/config/bootstrap.test.ts - Modify:
cocoscreator_projects/framework-tests/config/remote-config.test.ts - Modify:
cocoscreator_projects/framework-tests/config/profiles.test.ts - Modify:
cocoscreator_projects/framework-tests/config/sources/native-settings.test.ts - Create:
cocoscreator_projects/framework-tests/config/runtime-config.test.ts
Interfaces:
-
Produces:
RuntimeConfig,resolveRuntimeConfig(options),decodeNativeGameConfig(value),parseUrlServers(config). -
Consumes: existing runtime mode and identity source abstractions.
-
Step 1: Add failing urlserver tests
Before changing configuration or identity code, read .agents/skills/native-bridge-contract/SKILL.md completely and follow its referenced source map for gameserver, gameconfig, urlserver, and window.settings. Record the exact legacy source lines beside each fixture assertion; do not infer bridge names or transforms from current YouleNexus code.
test('remote config uses data.urlserver as the only WS source', () => {
assert.deepEqual(parseUrlServers({ data: { urlserver: ['10.0.0.1:3088', 'wss://b'] } }), [
'ws://10.0.0.1:3088',
'wss://b',
]);
assert.throws(() => parseUrlServers({ data: { player_server_tcp: 'wrong:1' } }), /data\.urlserver/);
});
- Step 2: Add failing native branch tests
assert.equal(decodeNativeGameConfig('host-path#8080-config'), 'http://host/path:8080/config.txt');
assert.throws(() => nativeSettingsSource(nonUAgentHostWithoutSettings)(), /getothername/);
assert.equal(nativeSettingsSource(uAgent3Host)().agentid, 'legacy-agent');
The uAgent_3 case must be selected by an explicit environment discriminator, not by catch-all exception handling.
- Step 3: Run config tests and verify failure
node --import tsx --test framework-tests/config/*.test.ts framework-tests/config/sources/native-settings.test.ts
Expected: FAIL on urlserver, gameconfig decoding, and current fallback behavior.
- Step 4: Implement strict remote config parsing
export interface RemoteConfig {
data: { urlserver: string | readonly string[]; [key: string]: unknown };
}
export function parseUrlServers(input: unknown): string[] {
const value = requireRecord(input, '$').data;
const raw = requireRecord(value, '$.data').urlserver;
const entries = typeof raw === 'string' ? [raw] : requireStringArray(raw, '$.data.urlserver');
if (entries.length === 0) throw new ConfigParseError('$.data.urlserver must not be empty');
return entries.map(toWebSocketUrl);
}
Do not read player_server_tcp, visitor_server_tcp, or game_server_tcp for the first WebSocket path.
- Step 5: Correct profiles and bootstrap
Set the release default to the exact original source value from Game_Surface_3/js/01_SubGame/00_SubGame_Config.js:11. Keep explicit local/staging profiles, but remove REPLACE_ME data from production paths and prevent unknown profile names from silently selecting prod; unknown names throw in debug.
- Step 6: Build immutable RuntimeConfig
Validate channel identity and login identity before constructing NetClient. Return a frozen object and frozen server list. No controller may call resolveBootstrap directly after this task.
- Step 7: Run tests and typecheck
node --import tsx --test framework-tests/config/*.test.ts framework-tests/config/sources/native-settings.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 8: Commit
git add -- YouleNexus/assets/framework/config framework-tests/config
git commit -m "fix(config): align runtime configuration with legacy contract"
Task 4: 建立首批 RPC 的唯一 route 表、解析器与黄金样本
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/rpc-route.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/validation.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/login-contract.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/join-room-contract.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/room-event-contracts.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/contracts/index.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/protocol/routes.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/protocol/typed-send.ts - Replace:
cocoscreator_projects/YouleNexus/assets/framework/core/types/login.ts - Create:
cocoscreator_projects/framework-tests/fixtures/protocol/player-login-success.json - Create:
cocoscreator_projects/framework-tests/fixtures/protocol/player-login-room.json - Create:
cocoscreator_projects/framework-tests/fixtures/protocol/self-join-room.json - Create:
cocoscreator_projects/framework-tests/fixtures/protocol/room-events.json - Create:
cocoscreator_projects/framework-tests/protocol/contracts.test.ts - Modify:
cocoscreator_projects/framework-tests/protocol/routes.test.ts - Modify:
cocoscreator_projects/framework-tests/protocol/net-typed-send.test.ts - Modify:
cocoscreator_projects/framework-tests/protocol/login.test.ts
Interfaces:
-
Produces: exhaustive
RPC_ROUTE satisfies Record<RpcName, RouteName>, request/response parsers for login, join, prepare, join/exit/online/offline, switch-server, kick. -
Consumes:
docs/protocol/01-05and the contract inventory; no Store imports. -
Step 1: Add failing exhaustive route test
Before writing or changing a network contract, re-read docs/protocol/01-网络通信与生命周期.md through 05-游戏内协议与桥接.md sections covering that RPC and verify every fixture field against the cited source. The contract inventory is an index, not a substitute for the protocol chapters.
for (const rpc of Object.values(Rpc)) {
assert.ok(RPC_ROUTE[rpc], `missing route for ${rpc}`);
}
assert.equal(RPC_ROUTE.self_join_room, Route.agent);
assert.equal(RPC_ROUTE.player_prepare, Route.room);
- Step 2: Add golden fixture tests
Copy the verified player_login response from docs/protocol/04-数据结构.md; give other fixtures only fields justified by protocol tables. Tests assert exact encoded envelope and parser output. Do not add values for undocumented fields.
const deskinfo = roomFixture.data.deskinfo;
const parsed = parseLoginResponse(roomFixture.data);
assert.equal(parsed.reconnect.present, true);
assert.equal(parsed.reconnect.value, deskinfo);
- Step 3: Add failing negative tests
Cover numeric version, missing playerid, missing room seat, invalid players array, invalid urlserver, and the critical cases where isbattle is set but deskinfo is absent or falsey.
const { deskinfo: _ignored, ...withoutDeskinfo } = room;
assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1 }).reconnect.present, false);
assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1, deskinfo: null }).reconnect.present, false);
assert.equal(parseLoginResponse({ ...withoutDeskinfo, isbattle: 1, deskinfo: { round: 2 } }).reconnect.present, true);
Use Boolean(data.deskinfo) for this trigger because the original source is exactly if (_msg.data.deskinfo). Keep the truthy value opaque and preserve its identity; do not validate or interpret the snapshot structure in platform code.
- Step 4: Run tests and verify failure
node --import tsx --test framework-tests/protocol/contracts.test.ts framework-tests/protocol/routes.test.ts framework-tests/protocol/net-typed-send.test.ts framework-tests/protocol/login.test.ts
Expected: FAIL because the production route table and strict parsers do not exist.
- Step 5: Implement reusable validation helpers
Provide requireRecord, requireString, requireNumber, requireInteger, requireArray, optionalField, and hasOwn. Every thrown message includes the JSON path.
- Step 6: Implement exact parsers and request builders
Request builders return fresh objects containing only protocol-defined fields. Response parsers preserve optional protocol values and keep roomtype/deskinfo opaque; the reconnect projection uses the original client's truthiness semantics.
- Step 7: Replace caller-supplied route maps
export function typedSend<T>(rpc: RpcName, data: T): TypedEnvelope<T> {
return { route: RPC_ROUTE[rpc], rpc, data };
}
Delete the map parameter so controllers and games cannot provide a second route source.
- Step 8: Run protocol tests and typecheck
node --import tsx --test framework-tests/protocol/*.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 9: Commit
git add -- YouleNexus/assets/framework/protocol YouleNexus/assets/framework/core/types/login.ts framework-tests/protocol framework-tests/fixtures/protocol
git commit -m "feat(protocol): add strict vertical-slice contracts"
Task 5: 用单一 PlatformStore 替换玩家与房间镜像状态
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/selectors.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-types.ts - Create:
cocoscreator_projects/framework-tests/platform/platform-store.test.ts - Create:
cocoscreator_projects/framework-tests/platform/selectors.test.ts
Interfaces:
-
Produces:
PlatformStore,PlatformState,PlayerDirectoryState,RoomState, atomic actions and read-only selectors. -
Consumes: Task 4 parsed DTOs; does not consume raw unknown server payloads.
-
Step 1: Write failing atomic-state tests
const store = new PlatformStore();
let notifications = 0;
store.state.subscribe(() => notifications++);
store.applyLoginSuccess(parsedLogin);
assert.equal(notifications, 1);
assert.equal(store.state.value.players.selfPlayerId, parsedLogin.player.playerid);
Add a room fixture whose self player also appears in players; assert only one entity object exists and the room stores only player IDs.
- Step 2: Write failing push/update tests
Cover other join, exit, ready, offline, online, full reconnect replacement, and failure operations that leave the prior state reference unchanged.
const before = store.state.value;
assert.throws(() => store.playerReady({ seat: 99 }), /seat/);
assert.equal(store.state.value, before);
- Step 3: Run tests and verify failure
node --import tsx --test framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts
Expected: FAIL because PlatformStore does not exist.
- Step 4: Define internal state without sentinel identities
export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
readonly entities: Readonly<Record<number, PlayerState>>;
}
export interface RoomState {
readonly inRoom: boolean;
readonly roomcode: string | null;
readonly selfSeat: number | null;
readonly seatPlayerIds: readonly (number | null)[];
readonly roomtype: readonly unknown[] | null;
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
}
There is no fake playerid:0, seat:-1, or empty server-derived player object. Keep the existing stores/types.ts unchanged until all legacy stores are removed in Task 12, so this task remains independently green.
- Step 5: Implement one-signal atomic actions
PlatformStore owns one signal<PlatformState>. Every action validates preconditions, builds all changed subtrees, then assigns this.s.value exactly once. It never mutates this.s.value or nested arrays in place. On room entry/recovery, recursively freeze the received JSON roomtype tree without interpreting positions, then keep that one canonical reference; tests assert nested mutation throws and no normalized copy is introduced.
- Step 6: Implement read-only selectors
Provide selectSelfPlayer, selectSeat, selectRoomPlayers, and selectGameSnapshot. Snapshot creation returns a frozen DTO and maps protocol onstate/isprepare to public booleans without changing the stored protocol values.
- Step 7: Run tests and typecheck
node --import tsx --test framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS while legacy stores remain temporarily available to old callers.
- Step 8: Commit
git add -- YouleNexus/assets/framework/platform/stores framework-tests/platform/platform-store.test.ts framework-tests/platform/selectors.test.ts
git commit -m "feat(platform): add atomic platform state store"
Task 6: 收紧 NetClient 前置条件和控制包行为
Files:
- Modify:
cocoscreator_projects/YouleNexus/assets/framework/net/net-client.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/net/transport.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/net/cocos-transport.ts - Modify:
cocoscreator_projects/framework-tests/net/net-client.test.ts - Modify:
cocoscreator_projects/framework-tests/helpers/fake-transport.ts
Interfaces:
-
Produces: fail-fast
NetClient.start/send/stop, one message stream, exact login gate, switch/kick events. -
Consumes: Task 3
RuntimeConfig, Task 4 request builders/envelope codec. -
Step 1: Add failing precondition tests
Before changing NetClient, re-read the lifecycle, control-packet, heartbeat, timeout, and reconnect sections in docs/protocol/01-网络通信与生命周期.md and 02-客户端发包.md; record any discrepancy between current code and the documented original behavior in the test name.
assert.throws(() => clientWithoutIdentity.start(), /login identity/);
assert.throws(() => client.send('room', 'player_prepare', {}), /not connected/);
Also test transport send after close, duplicate onerror/onclose, and stop idempotency.
- Step 2: Add control packet negative tests
Missing roomserver/agentserver must emit a contract error or throw through the test boundary, not be ignored. kick_server must cancel login/reconnect/watchdog timers and close once.
- Step 3: Run tests and verify failure
node --import tsx --test framework-tests/net/net-client.test.ts framework-tests/net/transport.test.ts
Expected: FAIL on silent sendLogin, optional transport send, and malformed switch packets.
- Step 4: Implement explicit connection states
Use an internal union idle|connecting|open|stopped. start() requires identity and at least one validated server. send() requires open. stop() transitions once and clears all timers before closing.
- Step 5: Keep the verified legacy timing
Retain four-second login guard, ten-second reconnect delay, receive timeout, TCP generation filtering, login-period packet gate, and server rotation. Update tests rather than changing these values.
- Step 6: Run all net tests and typecheck
node --import tsx --test framework-tests/net/*.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 7: Commit
git add -- YouleNexus/assets/framework/net framework-tests/net framework-tests/helpers/fake-transport.ts
git commit -m "fix(net): fail fast on invalid client lifecycle"
Task 7: 统一平台 Handler、Router 与 Session
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/protocol/platform-handlers.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/protocol/router.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/platform/session.ts - Modify:
cocoscreator_projects/YouleNexus/assets/framework/sdk/index.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/room-rpc-bus.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/protocol/room-handlers.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/protocol/active-game.ts - Modify:
cocoscreator_projects/framework-tests/protocol/router.test.ts - Create:
cocoscreator_projects/framework-tests/protocol/platform-handlers.test.ts - Replace:
cocoscreator_projects/framework-tests/platform/session.test.ts - Delete:
cocoscreator_projects/framework-tests/platform/room-rpc-bus.test.ts - Delete:
cocoscreator_projects/framework-tests/protocol/room-handlers.test.ts - Delete:
cocoscreator_projects/framework-tests/protocol/active-game.test.ts
Interfaces:
-
Consumes: strict parsers,
PlatformStore,GameSessionHost, and an injectedGameHostFactoryport. -
Produces: one
Router.dispatch, onePlatformSession.handleLogin,GameHostFactory.create({ descriptor, selfSeat, seatCount }), and the platform handler table for the first slice. -
Step 1: Rewrite router tests around one dispatch owner
router.dispatch({ route: 'room', rpc: 'player_prepare', data: { seat: 2 } });
assert.deepEqual(platformCalls, ['player_prepare']);
router.dispatch({ route: activeDescriptor.route, rpc: 'deal', data: { card: 1 } });
assert.deepEqual(gameMessages, [{ rpc: 'deal', data: { card: 1 } }]);
Assert an unknown platform RPC throws UnsupportedPlatformRpcError; a route different from the active session descriptor throws and never reaches either side.
- Step 2: Write handler ordering tests
For each room push, subscribe to PlatformStore and log GameModule events. Assert store notification occurs before the event and that the snapshot read inside the event contains the committed state.
- Step 3: Write login/reconnect tests
session.handleLogin(roomLogin);
assert.equal(store.state.value.room.inRoom, true);
assert.equal(fakeGame.restoreCalls[0], roomLogin.deskinfo);
assert.equal('deskinfo' in store.state.value.room, false);
Also assert { isbattle:1 } with absent or null deskinfo does not call restore, while a truthy opaque object calls it exactly once.
- Step 4: Run tests and verify failure
node --import tsx --test framework-tests/protocol/router.test.ts framework-tests/protocol/platform-handlers.test.ts framework-tests/platform/session.test.ts
Expected: FAIL against the current parallel Router/RoomRPCBus implementation.
- Step 5: Implement PlatformHandlers
Handlers accept parsed data only. Implement self_join_room, other_join_room, self_exit_room, other_exit_room, player_prepare, other_offline, and other_online. Each calls one PlatformStore action, then one GameSession event where defined.
- Step 6: Replace ActiveGame with GameSessionHost in Router
if (isPlatformRoute(message.route)) {
return this.platform.dispatch(message);
}
return this.gameSessions.dispatchGameMessage(message.route, {
rpc: message.rpc,
data: message.data,
});
No other module subscribes separately to NetClient.message.
- Step 7: Make PlatformSession a parsed orchestration boundary
handleLogin(unknown) parses once, rejects failure without partial Store writes, resolves the descriptor with GameRegistry.getByGameId(RuntimeConfig.identity.gameid), obtains the seat count only through descriptor.resolveSeatCount(roomtype), asks the injected GameHostFactory for a route/seat-bound Host, opens or replaces the correct game session, atomically replaces recovery state, then calls restore if and only if Boolean(deskinfo). Task 7 tests use a strict fake factory; the production adapter arrives in Task 8, so there is no forward implementation dependency.
- Step 8: Remove obsolete buses/stores from this path
Delete the parallel bus/handler/active-game files and all imports. Replace sdk/index.ts with a pure re-export of sdk/contracts/index.ts. Do not leave forwarding shims that could become a second runtime path.
- Step 9: Run platform/protocol tests and typecheck
node --import tsx --test framework-tests/protocol/*.test.ts framework-tests/platform/*.test.ts framework-tests/sdk/*.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 10: Commit
git add -A -- YouleNexus/assets/framework/protocol YouleNexus/assets/framework/platform YouleNexus/assets/framework/sdk/index.ts framework-tests/protocol framework-tests/platform
git commit -m "refactor(platform): unify message routing and session state"
Task 8: 实现受限 GameHost 适配器与平台命令
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/game-host-adapter.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/commands.ts - Create:
cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts - Create:
cocoscreator_projects/framework-tests/platform/commands.test.ts
Interfaces:
-
Consumes: GameHost contract, Task 7
GameHostFactoryport, descriptor route, PlatformStore selectors, typed send, NetClient. -
Produces: the production
createGameHostAdapter(options)implementation ofGameHostFactoryandPlatformCommands.prepare/exitRoom. -
Step 1: Write failing route-binding and command tests
host.sendGameMessage('play-card', { card: 7 });
assert.deepEqual(sent[0], { route: 'mock-game', rpc: 'play-card', data: { card: 7 } });
host.execute({ type: 'room.prepare' });
assert.equal(sent[1].rpc, 'player_prepare');
Assert no Host method accepts a route argument. Assert room.exit rejects when room.stage !== 0.
For seat mapping, cover 2-, 4-, and 10-seat sessions. Assert host.seat.toView(selfSeat) === 0, every server seat round-trips through toView/toServer, and an out-of-range or non-integer seat throws with the offending value.
- Step 2: Write subscription/disposal tests
The first callback is immediate. Reassigning internal state with no public snapshot change does not notify. Disposing removes all listeners and every subsequent Host method throws GameSessionDisposedError.
- Step 3: Run tests and verify failure
node --import tsx --test framework-tests/platform/game-host-adapter.test.ts framework-tests/platform/commands.test.ts
Expected: FAIL because adapter and commands do not exist.
- Step 4: Implement commands through typedSend
prepare(): void {
this.net.sendTyped(typedSend(Rpc.player_prepare, {}));
}
exitRoom(): void {
if (this.store.state.value.room.stage !== 0) {
throw new UnsupportedGameHostCommandError('room.exit requires stage=0');
}
this.net.sendTyped(typedSend(Rpc.self_exit_room, {}));
}
If NetClient does not expose sendTyped, add a narrow method accepting TypedEnvelope; do not expose raw transport.
- Step 5: Implement snapshot and seat-mapper isolation
Build DTOs with selectors, freeze every newly created public object/array, and expose the Store's already-recursively-frozen canonical roomtype reference without interpreting its contents. Construct GameSeatMapper once from the current session's validated selfSeat and descriptor-derived seatCount; the adapter must not import the legacy core/seat implementation or read changing Store values inside the formulas. Do not expose PlatformStore.state.
- Step 6: Run tests, conformance harness, and typecheck
node --import tsx --test framework-tests/platform/game-host-adapter.test.ts framework-tests/platform/commands.test.ts framework-tests/sdk/game-contract-harness.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 7: Commit
git add -- YouleNexus/assets/framework/platform/game-host-adapter.ts YouleNexus/assets/framework/platform/commands.ts YouleNexus/assets/framework/net/net-client.ts framework-tests/platform framework-tests/sdk
git commit -m "feat(platform): expose restricted game host adapter"
Task 9: 建立 PlatformRuntime 和启动四门闩
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/ready-gate.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/scene-port.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/platform/runtime.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/startup.ts - Create:
cocoscreator_projects/framework-tests/platform/ready-gate.test.ts - Create:
cocoscreator_projects/framework-tests/platform/runtime.test.ts - Replace:
cocoscreator_projects/framework-tests/platform/startup.test.ts
Interfaces:
-
Consumes: RuntimeConfig resolver, NetClient factory, Router, PlatformSession, PlatformStore, GameRegistry, ScenePort.
-
Produces: single
PlatformRuntime.start/stop/joinRoom/prepare/exitRoom, read-only state, four-gate readiness. -
Step 1: Write all 16 readiness permutation tests
For each subset of the four gates, assert ready is false unless all four are true. Assert each gate can be marked once and duplicate marks are harmless.
gate.mark('resources');
gate.mark('config');
gate.mark('socket');
assert.equal(gate.ready, false);
gate.mark('minimum-display');
assert.equal(gate.ready, true);
- Step 2: Write runtime construction-order tests
Assert config resolves before NetClient creation, identity is set before start, the application composition root registers descriptors explicitly, RuntimeConfig.identity.gameid resolves exactly one descriptor before a room session can open, Router subscribes before the first post-login business message, and stop disposes game session before stores/network.
- Step 3: Write scene decision tests
No room login calls scene.showLobby(). Room login calls scene.showRoom() before game restore. Kick calls scene.showKicked(). Reconnecting/slow calls scene.showReconnect() without clearing room state.
- Step 4: Run tests and verify failure
node --import tsx --test framework-tests/platform/ready-gate.test.ts framework-tests/platform/runtime.test.ts
Expected: FAIL because runtime and gate do not exist.
- Step 5: Implement Cocos-free runtime
The runtime constructor accepts factories/ports; it never imports cc. start() launches resource/minimum-display promises plus config resolution, then creates and starts NetClient. All NetClient events are wired in one private method and unwired in stop().
- Step 6: Define narrow ScenePort
export interface ScenePort {
showLoading(): void;
showLogin(): void;
showLobby(): void;
showRoom(): void;
showReconnect(): void;
showKicked(data: unknown): void;
showFatal(error: Error): void;
}
The port describes intent, not Cocos scene or node names.
- Step 7: Delete StartupOrchestrator and adapt tests
There must be only one startup owner. Remove all RoomRPCBus factory concepts and the old “login 后构造 NetClient” flow.
- Step 8: Run tests and typecheck
node --import tsx --test framework-tests/platform/*.test.ts framework-tests/integration/login-flow.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS after the integration test is adapted to PlatformRuntime.
- Step 9: Commit
git add -A -- YouleNexus/assets/framework/platform framework-tests/platform framework-tests/integration/login-flow.test.ts
git commit -m "feat(platform): add single runtime and readiness gate"
Task 10: 编写不依赖 Cocos 的首批 Layer 控制器
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/connection-overlay-controller.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/join-room-controller.ts - Create:
cocoscreator_projects/YouleNexus/assets/framework/ui/controllers/room-shell-controller.ts - Create:
cocoscreator_projects/framework-tests/ui/connection-overlay-controller.test.ts - Create:
cocoscreator_projects/framework-tests/ui/join-room-controller.test.ts - Create:
cocoscreator_projects/framework-tests/ui/room-shell-controller.test.ts
Interfaces:
-
Consumes: narrow RuntimeCommands and read-only selector/subscription ports.
-
Produces: presentation models and user-intent methods; no Cocos imports.
-
Step 1: Write JoinRoom input-state tests
const controller = new JoinRoomController(commands, view);
for (const digit of [1, 2, 3, 4, 5, 6]) controller.inputDigit(digit);
assert.equal(view.last.roomcode, '123456');
controller.confirm();
assert.deepEqual(commands.calls, [{ type: 'join-room', roomcode: '123456' }]);
Cover 0-9 only, six-digit maximum, delete, clear, confirm before six digits, double confirm while pending, success close, and failure retaining input.
- Step 2: Write connection overlay tests
Map reconnecting/slow to Layer 615, pending commands to Layer 614, kicked to Layer 616, and normal states to all hidden. Kicked has precedence over reconnect/loading.
- Step 3: Write room shell tests
Feed snapshots and assert roomcode, seats, online/ready state, prepare visibility, and exit command. The controller stores no PlayerState objects after render.
- Step 4: Run tests and verify failure
node --import tsx --test framework-tests/ui/connection-overlay-controller.test.ts framework-tests/ui/join-room-controller.test.ts framework-tests/ui/room-shell-controller.test.ts
Expected: FAIL because controllers do not exist.
- Step 5: Implement view-port-driven controllers
Each controller receives a view interface and commands. It owns only UI transient state such as roomcode digits and pending flag. All server state comes from snapshots/selectors.
- Step 6: Run UI tests and typecheck
node --import tsx --test framework-tests/ui/connection-overlay-controller.test.ts framework-tests/ui/join-room-controller.test.ts framework-tests/ui/room-shell-controller.test.ts
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS.
- Step 7: Commit
git add -- YouleNexus/assets/framework/ui/controllers framework-tests/ui
git commit -m "feat(ui): add vertical-slice presentation controllers"
Task 11: 添加 Cocos 适配器并通过 MCP 挂接首批 prefab
Files:
- Create:
cocoscreator_projects/YouleNexus/assets/scripts/platform/PlatformRootComponent.ts - Create:
cocoscreator_projects/YouleNexus/assets/scripts/platform/CocosScenePort.ts - Create:
cocoscreator_projects/YouleNexus/assets/scripts/platform/JoinRoomLayerComponent.ts - Create:
cocoscreator_projects/YouleNexus/assets/scripts/platform/RoomShellComponent.ts - Create:
cocoscreator_projects/YouleNexus/assets/scripts/platform/ConnectionOverlayComponent.ts - Replace:
cocoscreator_projects/YouleNexus/assets/scripts/LaunchFlow.ts - Replace:
cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts - Modify via funplay-cocos MCP:
cocoscreator_projects/YouleNexus/assets/scenes/Login.scene - Modify via funplay-cocos MCP: Layer 1、4、15、50、403、411、202、416、614、615、616 prefab resources
- Create:
cocoscreator_projects/framework-tests/cocos-adapters/no-demo-fallback.test.mjs
Interfaces:
-
Consumes: Task 9 PlatformRuntime, Task 10 controllers.
-
Produces: thin Cocos Components and ScenePort adapter; no duplicated business logic.
-
Step 1: Write the static failure test first
Scan production Cocos adapters for forbidden literals/patterns:
const forbidden = [
'mock_openid',
"search: '?profile=local'",
'fallback to ws://127.0.0.1:3088',
'setIdentity({ agentid:',
];
Expected current LoginFlow.ts to fail.
- Step 2: Run the static test and verify failure
node --test framework-tests/cocos-adapters/no-demo-fallback.test.mjs
Expected: FAIL on current demo code.
- Step 3: Implement thin Cocos adapters
PlatformRootComponent creates exactly one runtime and persists it across scene changes. CocosScenePort maps semantic scene intents to root Layer activation. Other components only translate Button/Label/Node events to/from the pure controllers.
- Step 4: Remove demo networking from LaunchFlow/LoginFlow
Delete local WSTransport, mock identity, bootstrap catch fallback, direct RoomRPCBus construction, and fixed one-second scene jump. Use CocosTransport and the root runtime.
- Step 5: Use the cocos-mcp skill before editor changes
Read .agents/skills/cocos-mcp/SKILL.md, verify the editor service is healthy, and use the injected mcp__funplay_cocos__* tools. If unavailable, stop this task and ask the user to start the editor service; never edit serialized files manually.
Before the first editor mutation, record git status --short for every listed scene/prefab and its .meta. They must already be tracked or separately checkpointed as the completed UI-migration baseline. If any is still an untracked pre-existing UI artifact, stop Task 11 and ask the user to checkpoint that UI work first; an untracked serialized file cannot separate earlier UI content from this task's bindings.
- Step 6: Inspect exact prefab node paths via MCP
For each Layer, record the queried node/component path in the task log before binding. Do not infer paths from prefab text. Verify which existing Sprite nodes are intended as buttons and which Labels display room/player state.
- Step 7: Attach components and serialized references via MCP
Attach only the five adapter components, wire Buttons/Labels/Nodes through editor properties, and keep business state out of prefab components. Save each prefab/scene through the editor.
- Step 8: Preview the vertical UI states
Using injected fake runtime states, capture or visually inspect: loading, login, lobby, six-digit join input, room with two seats, ready, reconnect, and kicked. Verify inactive overlays do not block input.
- Step 9: Run static and framework checks
node --test framework-tests/cocos-adapters/no-demo-fallback.test.mjs
node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit
Expected: PASS. Also run the Cocos editor's script compilation check through MCP and require zero component/UUID errors.
- Step 10: Commit
git add -- YouleNexus/assets/scripts/platform YouleNexus/assets/scripts/LaunchFlow.ts YouleNexus/assets/scripts/LoginFlow.ts framework-tests/cocos-adapters
git add -- YouleNexus/assets/scenes/Login.scene YouleNexus/assets/scenes/Login.scene.meta
git add -- YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer1_Logo.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer4_MainMenu.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer15_JoinRoom.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer50_MainScene.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer403.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer411.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer202_PlayerHeadScore.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer416_PlayerInfo.prefab.meta
git add -- YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer614_Loading.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer615_Reconnect.prefab.meta YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab YouleNexus/assets/framework/ui/prefabs/widgets/Layer616_Kick.prefab.meta
git commit -m "feat(cocos): connect platform runtime to migrated layers"
Before committing, inspect git diff --cached --name-only and unstage any prefab unrelated to the listed first-batch Layers.
Task 12: 建立完整纵向回放测试并删除旧状态路径
Files:
- Create:
cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts - Replace:
cocoscreator_projects/framework-tests/integration/login-flow.test.ts - Modify:
cocoscreator_projects/framework-tests/architecture/import-boundaries.test.mjs - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/app-store.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/player-store.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/room-store.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/stores/types.ts - Delete:
cocoscreator_projects/YouleNexus/assets/framework/platform/readonly.ts - Modify:
cocoscreator_projects/scripts/run-framework-tests.mjs - Delete or rewrite affected legacy tests under:
cocoscreator_projects/framework-tests/platform/
Interfaces:
-
Consumes: completed runtime and fake transport/clock/scene/game adapters.
-
Produces: one deterministic end-to-end regression test and final dependency guard.
-
Step 1: Write the full replay test
Replay this exact sequence with fixtures:
runtime.start
→ config resolved
→ WS open / player_login sent
→ login success without room / lobby shown
→ joinRoom('123456') / self_join_room sent
→ self_join_room success / room shown / game session entered
→ other_join_room
→ player_prepare
→ socket close / reconnect overlay
→ reconnect open / player_login resent
→ login success with room + deskinfo / room state replaced / restore called once
At every arrow assert outbound envelope, PlatformState, ScenePort call, GameModule event, and notification count.
- Step 2: Add two-game isolation to the architecture guard
Register two descriptors, enter/leave both in sequence, and assert imports under assets/games/<key> cannot reach framework internals. Use temporary in-memory source strings in the test; do not create fake production games.
- Step 3: Run the test and verify any remaining legacy path fails
node --import tsx --test framework-tests/integration/platform-vertical-slice.test.ts framework-tests/architecture/import-boundaries.test.mjs
Expected before cleanup: FAIL if any runtime still constructs old AppStore/PlayerStore/RoomStore or RoomRPCBus.
- Step 4: Delete legacy Store facades and update remaining tests
All production state callers must use PlatformStore/selectors. Do not leave deprecated aliases, because they would recreate a second source of truth.
Also add --test-concurrency=1 to run-framework-tests.mjs so the complete suite is deterministic in the managed Windows environment:
const r = spawnSync(
process.execPath,
['--import', 'tsx', '--test', '--test-concurrency=1', ...files],
{ stdio: 'inherit' },
);
- Step 5: Run the complete framework suite
npm run typecheck:framework
npm run test:framework
Expected: every framework test PASS with zero skipped tests and zero TypeScript errors.
- Step 6: Audit forbidden patterns
rg -n "RoomRPCBus|new AppStore|new PlayerStore|new RoomStore|mock_openid|fallback to ws://|transport\?\.send|if \(!this\.identity\) return|hasBattle.*isbattle" YouleNexus/assets
Expected: no production matches. Mentions in migration documentation are allowed outside YouleNexus/assets.
- Step 7: Commit
git add -A -- YouleNexus/assets/framework/platform/stores/app-store.ts YouleNexus/assets/framework/platform/stores/player-store.ts YouleNexus/assets/framework/platform/stores/room-store.ts YouleNexus/assets/framework/platform/stores/types.ts YouleNexus/assets/framework/platform/readonly.ts
git add -- scripts/run-framework-tests.mjs framework-tests/integration/platform-vertical-slice.test.ts framework-tests/integration/login-flow.test.ts framework-tests/architecture/import-boundaries.test.mjs framework-tests/platform
git commit -m "test(platform): cover complete vertical slice replay"
Task 13: 真服与编辑器验收
Files:
- Modify only if evidence changes:
docs/superpowers/specs/2026-09-04-platform-vertical-slice-contract-inventory.md - Create:
docs/superpowers/verification/2026-09-04-platform-vertical-slice.md
Interfaces:
-
Consumes: a user-authorized test account, the original configuration service, Cocos preview/device environment.
-
Produces: sanitized verification record with packet shapes, state transitions, screenshots/observations, and remaining out-of-scope items.
-
Step 1: Run preflight verification
npm run typecheck:framework
npm run test:framework
Expected: PASS before opening a real connection.
- Step 2: Select an explicit debug profile
Put the authorized account only in the existing debug profile mechanism or injected runtime data. Do not commit openid, phone, token, machine ID, real IP, or location.
- Step 3: Run the original and Cocos clients against the same test service
Capture and sanitize these envelopes: first player_login, successful self_join_room, player_prepare, reconnect player_login, and one room push. Compare route/rpc, field presence, primitive types, and nested structure.
- Step 4: Verify UI/state behavior through Cocos MCP preview
Confirm lobby entry, join-room input, room player rendering, ready state, reconnect overlay, room recovery, and kicked overlay. Capture screenshots through MCP if the tool supports it.
- Step 5: Record evidence and discrepancies
The verification document must list each acceptance item as PASS or FAIL with the observed packet/state evidence. A discrepancy is a failed acceptance item; do not add a fallback to make the run continue.
- Step 6: Re-run complete automated verification after any correction
npm run typecheck:framework
npm run test:framework
Expected: PASS.
- Step 7: Commit the verification record
git add -- docs/superpowers/verification/2026-09-04-platform-vertical-slice.md
git commit -m "docs(verification): record platform vertical-slice results"
Final Acceptance Checklist
sdk/contractshas no framework implementation or Cocos dependencies.- Adding a test game changes only its own directory and application registration.
- Game messages are route-bound and cannot impersonate platform RPCs.
- Game sessions release subscriptions and reject stale calls after disposal.
- Remote WebSocket addresses come only from
data.urlserveror an explicit debug direct profile. - All first-batch server payloads pass strict boundary parsers.
- There is one Router and one PlatformStore.
- Login, room recovery, join, prepare, offline/online, reconnect and kick match source behavior.
deskinfouses the original client's truthiness trigger, is never stored by the platform, and is passed unchanged torestore.- Descriptor-owned seat-count resolution and the Host's 2/4/10-seat round-trip mapping tests pass without importing framework internals into a game.
- Layer adapters contain no server fallback, mock identity, network code, or writable business state.
- All serialized Cocos changes were made and verified through funplay-cocos MCP.
npm run typecheck:frameworkpasses.npm run test:frameworkpasses with zero skipped tests.- Real test-server packet comparison passes without server or native-side changes.