From facceeae9be963e08a250b2f4c65f192d3abaa64 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 22:52:14 +0800 Subject: [PATCH] feat(config): unify startup mode and server selection --- .../assets/framework/config/bootstrap.ts | 11 ++- .../assets/framework/config/local-startup.ts | 61 +++++----------- .../assets/framework/config/profiles.ts | 69 +++++++++++-------- .../assets/framework/config/runtime-config.ts | 35 +++------- .../assets/framework/config/runtime-mode.ts | 33 ++------- .../YouleNexus/assets/scripts/LoginFlow.ts | 25 +------ .../LocalPlatformLogin.ts | 2 +- .../adapters/local/login-sources.test.ts | 6 +- .../adapters/local/visitor-account.test.ts | 6 +- .../framework-tests/config/bootstrap.test.ts | 13 +++- .../config/local-startup.test.ts | 44 ++++++------ .../framework-tests/config/profiles.test.ts | 27 +++----- .../config/remote-config-fetcher.test.ts | 3 +- .../config/runtime-config.test.ts | 53 ++++++++++---- .../config/runtime-mode.test.ts | 15 +--- .../integration/local-login-sources.test.ts | 2 +- .../integration/login-flow.test.ts | 3 +- .../platform-vertical-slice.test.ts | 3 +- .../local-platform-login/host.test.ts | 12 +++- .../local-platform-login/transport.test.ts | 2 +- .../local-platform-login/wire.test.ts | 2 +- .../framework-tests/platform/commands.test.ts | 3 +- .../platform/game-host-adapter.test.ts | 3 +- .../framework-tests/platform/runtime.test.ts | 3 +- .../framework-tests/platform/startup.test.ts | 5 +- .../sdk/game-contract-harness.test.ts | 3 +- 26 files changed, 204 insertions(+), 240 deletions(-) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts index 6623c00..ae9314f 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/bootstrap.ts @@ -1,5 +1,4 @@ -import type { DebugAccount, DebugProfile } from './profiles.ts'; -import { PROFILES, resolveActiveProfile } from './profiles.ts'; +import type { DebugAccount } from './profiles.ts'; import { resolveRuntimeConfig, type HostKind, @@ -24,7 +23,7 @@ export type BootstrapResult = Omit; + readonly profile: Readonly<{ name: 'local' | 'prod' }>; }; function bootstrapHostKind(options: BootstrapOptions): HostKind { @@ -34,9 +33,8 @@ function bootstrapHostKind(options: BootstrapOptions): HostKind { /** 兼容启动编排外观;所有运行契约均由唯一的 RuntimeConfig 边界产出。 */ export async function resolveBootstrap(options: BootstrapOptions): Promise { - const profile = options.mode === 'release' ? PROFILES.prod! : resolveActiveProfile(options.search); const runtime = await resolveRuntimeConfig({ - mode: options.mode, + startupConfig: options.startupConfig, hostKind: bootstrapHostKind(options), win: options.win, search: options.search, @@ -46,7 +44,6 @@ export async function resolveBootstrap(options: BootstrapOptions): Promise 1) fail(field, 'duplicate query control'); } - const profiles = query.getAll('profile'); - if (profiles.length !== 1 || profiles[0] !== 'local') { - fail('profile', 'must occur exactly once with value local'); - } - const modes = query.getAll('mode'); - if (modes.length === 1 && modes[0] !== 'debug') fail('mode', 'must be debug'); const gameconfigs = query.getAll('gameconfig'); if (gameconfigs.length === 1 && gameconfigs[0] !== '') { fail('gameconfig', 'must be empty'); @@ -72,11 +41,13 @@ function assertLocalQuery(search: string): void { export async function resolveLocalStartup( options: ResolveRuntimeConfigOptions, ): Promise { - if (options.mode !== 'debug') fail('mode', 'must be debug'); + const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig); + if (startupConfig.mode !== 'debug') fail('mode', 'must be debug'); + if (!startupConfig.useLocalServer) fail('useLocalServer', 'must be true'); if (options.hostKind !== 'h5') fail('hostKind', 'must be h5'); assertLocalQuery(options.search); - const config = await resolveRuntimeConfig(options); + const config = await resolveRuntimeConfig({...options, startupConfig}); const context = Object.freeze({ hostKind: 'h5' as const, profile: 'local' as const, config }); assertLocalStartupContext(context); return context; @@ -87,10 +58,13 @@ export function assertLocalStartupContext(context: LocalStartupContext): void { if (context.profile !== 'local') fail('profile', 'must be local'); const { config } = context; + const startupConfig = validateStartupConfig(config.startupConfig); + if (startupConfig.mode !== 'debug') fail('mode', 'source must be debug'); + if (!startupConfig.useLocalServer) fail('useLocalServer', 'source must be true'); if (config.mode !== 'debug') fail('mode', 'must be debug'); if (config.isDebugger !== true) fail('isDebugger', 'must be true'); if (config.source !== 'remote') fail('source', 'must be remote'); - if (config.gameserver !== PROFILES.local!.gameserver) fail('gameserver', 'must equal local profile remote entry'); + if (config.gameserver !== startupConfig.gameserver) fail('gameserver', 'must equal startupConfig remote entry'); if (!config.remoteConfig || typeof config.remoteConfig.getValue !== 'function') fail('remoteConfig', 'must provide memory view'); if (!config.rawConfig || config.rawConfig !== config.remoteConfig.raw) fail('rawConfig', 'must share remoteConfig.raw'); @@ -105,26 +79,23 @@ export function assertLocalStartupContext(context: LocalStartupContext): void { throw wrapped; } - const authoritative = PROFILES.local?.servers; - if (!authoritative || authoritative.length === 0) { - fail('servers', 'PROFILES.local must define servers'); - } + const authoritative = startupConfig.localServers; if (config.servers.length !== authoritative.length) { - fail('servers', 'must equal PROFILES.local.servers'); + fail('servers', 'must equal startupConfig.localServers'); } for (let index = 0; index < authoritative.length; index += 1) { const expected = authoritative[index]; - validateLocalUrl(expected, `servers[${index}]`); + validateLocalServer(expected, `servers[${index}]`); if (config.servers[index] !== expected) { - fail('servers', 'must equal PROFILES.local.servers'); + fail('servers', 'must equal startupConfig.localServers'); } } } export function assertLocalConnectionTarget(context: LocalStartupContext, url: string): void { assertLocalStartupContext(context); - validateLocalUrl(url, 'url'); - if (!PROFILES.local?.servers?.includes(url)) { - fail('url', 'target must occur in PROFILES.local.servers'); + validateLocalServer(url, 'url'); + if (!context.config.startupConfig.localServers.includes(url)) { + fail('url', 'target must occur in startupConfig.localServers'); } } diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts index 0be0355..b19c9a1 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/profiles.ts @@ -1,3 +1,5 @@ +import { resolveRuntimeMode, type RuntimeMode } from './runtime-mode.ts'; + /** 调试用登录玩家档案;值只能由测试或本地启动配置显式注入。 */ export interface DebugAccount { openid: string; @@ -9,38 +11,49 @@ export interface DebugAccount { unionid?: string | number; } -export type ProfileSource = 'remote'; - -export interface DebugProfile { - readonly name: string; - readonly source: ProfileSource; +export interface StartupConfig { + readonly mode: RuntimeMode; readonly gameserver: string; - readonly servers?: readonly string[]; - readonly account?: DebugAccount; + readonly useLocalServer: boolean; + readonly localServers: readonly string[]; } -/** 正式 gameserver:原 00_SubGame_Config.js:11。 */ -export const DEFAULT_GAMESERVER = 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt'; - -export const PROFILES: Readonly> = Object.freeze({ - prod: Object.freeze({ name: 'prod', source: 'remote', gameserver: DEFAULT_GAMESERVER }), - staging: Object.freeze({ name: 'staging', source: 'remote', gameserver: DEFAULT_GAMESERVER }), - local: Object.freeze({ - name: 'local', - source: 'remote', - gameserver: DEFAULT_GAMESERVER, - servers: Object.freeze(['ws://127.0.0.1:3088']), - }), +/** 唯一发布配置入口;mode 与服务器选择互相独立。 */ +export const STARTUP_CONFIG: Readonly = Object.freeze({ + mode: 'debug', + gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', + useLocalServer: true, + localServers: Object.freeze(['ws://127.0.0.1:3088']), }); -export const ACTIVE_PROFILE = 'prod'; +function invalid(field: string, reason: string): never { + throw new Error('Invalid startup config ' + field + ': ' + reason); +} -export function resolveActiveProfile(search: string): DebugProfile { - const requested = new URLSearchParams(search).get('profile'); - const name = requested || ACTIVE_PROFILE; - const profile = PROFILES[name]; - if (!profile) throw new Error(`未知 profile: ${name}`); - return profile; +export function validateLocalServer(value: string, field = 'localServers'): void { + if (typeof value !== 'string' || value.trim() !== value) invalid(field, 'must be an absolute URL'); + let url: URL; + try { url = new URL(value); } catch { invalid(field, 'must be an absolute URL'); } + if (url.protocol !== 'ws:' && url.protocol !== 'wss:') invalid(field, 'protocol must be ws: or wss:'); + if (url.username || url.password || value.includes('#')) invalid(field, 'credentials and fragments are forbidden'); + const host = url.hostname.toLowerCase(); + if (host !== 'localhost' && host !== '[::1]' && !/^127(?:\.[0-9]{1,3}){3}$/.test(host)) { + invalid(field, 'hostname must be loopback'); + } +} + +/** 验证后复制为不可变快照,异步抓取期间调用方不能改变配置。 */ +export function validateStartupConfig(source: Readonly): Readonly { + if (!source || typeof source !== 'object') invalid('source', 'must be an object'); + const mode = resolveRuntimeMode(source.mode); + if (typeof source.useLocalServer !== 'boolean') invalid('useLocalServer', 'must be boolean'); + if (typeof source.gameserver !== 'string' || source.gameserver.trim() !== source.gameserver) invalid('gameserver', 'must be http(s) URL'); + let url: URL; + try { url = new URL(source.gameserver); } catch { invalid('gameserver', 'must be http(s) URL'); } + if (url.protocol !== 'http:' && url.protocol !== 'https:') invalid('gameserver', 'must be http(s) URL'); + if (!Array.isArray(source.localServers) || source.localServers.length === 0) invalid('localServers', 'must be a nonempty array'); + for (let index = 0; index < source.localServers.length; index++) validateLocalServer(source.localServers[index], 'localServers[' + index + ']'); + return Object.freeze({mode, gameserver:source.gameserver, useLocalServer:source.useLocalServer, localServers:Object.freeze([...source.localServers])}); } /** 忠实旧 min_replaceAll 链:- → /,# → :,再包成 http://...txt。 */ @@ -55,11 +68,11 @@ export interface ResolveGameServerOptions { readonly injectedGameConfig: unknown; } -/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 profile 配置源。 */ +/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 启动配置源。 */ export function resolveGameServer(options: ResolveGameServerOptions): string { const injected = options.injectedGameConfig; if (injected === '' || injected === undefined || injected === null) { - if (!options.configured) throw new Error('profile 未配置 gameserver'); + if (!options.configured) throw new Error('启动配置未配置 gameserver'); return options.configured; } if (typeof injected !== 'string') throw new Error('gameconfig 必须是字符串'); diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts index f2e5c8e..6a8c41b 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-config.ts @@ -1,10 +1,10 @@ import type { ChannelIdentity, IdentitySource } from './identity.ts'; import { resolveIdentity } from './identity.ts'; import { - PROFILES, - resolveActiveProfile, + STARTUP_CONFIG, + validateStartupConfig, resolveGameServer, - type DebugProfile, + type StartupConfig, } from './profiles.ts'; import { ConfigFetchError, @@ -27,6 +27,7 @@ import { queryStringSource } from './sources/query-string.ts'; export type HostKind = 'h5' | NativeHostKind; export interface RuntimeConfig { + readonly startupConfig: Readonly; readonly mode: RuntimeMode; readonly isDebugger: boolean; readonly identity: Readonly; @@ -38,7 +39,7 @@ export interface RuntimeConfig { } export interface ResolveRuntimeConfigOptions { - readonly mode: RuntimeMode; + readonly startupConfig?: Readonly; readonly hostKind: HostKind; readonly win: NativeHost; readonly search: string; @@ -67,10 +68,6 @@ function resolveInjectedGameConfig(options: ResolveRuntimeConfigOptions): unknow return getothername(options.win, options.hostKind, 'gameconfig'); } -function selectedProfile(mode: RuntimeMode, search: string): DebugProfile { - return mode === 'release' ? PROFILES.prod! : resolveActiveProfile(search); -} - function freezeResult(result: RuntimeConfig): RuntimeConfig { return Object.freeze(result); } @@ -78,14 +75,11 @@ function freezeResult(result: RuntimeConfig): RuntimeConfig { export async function resolveRuntimeConfig( options: ResolveRuntimeConfigOptions, ): Promise { - const profile = selectedProfile(options.mode, options.search); + const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig); const identity = resolveHostIdentity(options); - if (!profile.gameserver) { - throw new ConfigFetchError(`profile '${profile.name}' 未配置 gameserver`); - } const gameserver = resolveGameServer({ - configured: profile.gameserver, + configured: startupConfig.gameserver, injectedGameConfig: resolveInjectedGameConfig(options), }); @@ -97,19 +91,12 @@ export async function resolveRuntimeConfig( throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`); } const remoteConfig = createRemoteConfig(raw, identity); - let servers: readonly string[]; - if (profile.name === 'local') { - if (!profile.servers || profile.servers.length === 0) { - throw new ConfigFetchError('Local profile must define servers'); - } - servers = Object.freeze([...profile.servers]); - } else { - servers = parseUrlServers(remoteConfig); - } + const servers = startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig); return freezeResult({ - mode: options.mode, - isDebugger: options.mode === 'debug', + startupConfig, + mode: startupConfig.mode, + isDebugger: startupConfig.mode === 'debug', identity, servers, source: 'remote', diff --git a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts index 220eb37..4e03b9e 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/config/runtime-mode.ts @@ -1,32 +1,7 @@ -// 1. 先 export type export type RuntimeMode = 'debug' | 'release'; -declare const DEBUG: boolean | undefined; - -// 2. 函数引用 RuntimeMode(此时 type 已 export) -function readModeFromUrl(): RuntimeMode | null { - if (typeof window === 'undefined' || !window.location) return null; - const m = new URLSearchParams(window.location.search).get('mode'); - if (m === 'debug' || m === 'release') return m; - return null; -} - -function detectDebugBuild(): boolean { - if (typeof DEBUG !== 'undefined') return DEBUG; - return true; // Cocos 编辑器内 dev workflow 默认;生产 build 改 false -} - -// 3. 导出常量 -export const MODE_OVERRIDE: RuntimeMode | null = - readModeFromUrl() ?? (detectDebugBuild() ? 'debug' : 'release'); - -/** - * 解析运行模式:override 优先,否则由是否 debug 构建决定。 - */ -export function resolveRuntimeMode( - isDebugBuild: boolean, - override: RuntimeMode | null = MODE_OVERRIDE, -): RuntimeMode { - if (override) return override; - return isDebugBuild ? 'debug' : 'release'; +/** 应用模式仅来自显式配置,不读取 URL 或 Creator 编译标志。 */ +export function resolveRuntimeMode(mode: RuntimeMode): RuntimeMode { + if (mode !== 'debug' && mode !== 'release') throw new Error('Invalid startup config mode: must be debug or release'); + return mode; } diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts b/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts index 7b9cc6b..952ee93 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/LoginFlow.ts @@ -9,20 +9,7 @@ import type { Transport } from '../framework/net/transport.ts'; const { ccclass } = _decorator; -/** - * LoginFlow: 登录场景编排器(真机版,接入 PlatformSession 真实流)。 - * - * 调试期间: - * - mode='debug' + ?profile=local → 跳过远程配置,直连本地服务器 - * - 真实 WebSocket 传输(用浏览器原生 WebSocket API) - * - * 流程: - * 1. 构造 EventBus + PlatformSession + NetClient(WebSocket transport) - * 2. bootstrap() 跳过远程,直接拿 local server='ws://127.0.0.1:3088' - * 3. NetClient.start() → 真 WS connect → server 回 player_login - * 4. PlatformSession 落 3 Store → app.phase=loggedIn - * 5. director.loadScene('MainMenu') - */ +/** 旧登录编排兼容入口;启动配置由 profiles.ts 统一提供。 */ class WSTransport implements Transport { private onOpenCb?: () => void; @@ -73,11 +60,9 @@ export class LoginFlow extends Component { (async () => { try { - // 调试模式 + ?profile=local → 跳过远程,直连本地 ws://127.0.0.1:3088 const result = await resolveBootstrap({ - mode: 'debug', win: window as any, - search: '?profile=local', + search: window.location.search, fetcher: new HttpConfigFetcher(), isNative: false, }); @@ -86,11 +71,7 @@ export class LoginFlow extends Component { console.log('[LoginFlow] bootstrap OK, servers=' + result.servers[0] + ' profile=' + result.profile.name); } catch (e) { console.warn('[LoginFlow] bootstrap failed: ' + (e as Error).message); - session.app.setIdentity({ - agentid: 'A', channelid: 'C', gameid: 'G', marketid: 4, version: 10000, - }); - session.app.setServers(['ws://127.0.0.1:3088']); - console.log('[LoginFlow] fallback to ws://127.0.0.1:3088'); + return; } // 构造 NetClient + 真实 WS transport diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts index 337f498..718f156 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/LocalPlatformLogin.ts @@ -66,7 +66,7 @@ export class LocalPlatformLogin extends Component { const view = this.view; const browser = window as unknown as NativeHost & { returnCitySN?: unknown }; this.host = createLocalLoginHost({ - configOptions: { mode: 'debug', hostKind: 'h5', search: window.location.search, + configOptions: { hostKind: 'h5', search: window.location.search, win: browser, fetcher: new HttpConfigFetcher() }, storage: window.localStorage, random: Math.random, now: Date.now, sampleHost: () => ({ location: null, diff --git a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts index effb8d7..f6ee075 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/login-sources.test.ts @@ -2,14 +2,14 @@ import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; -import { PROFILES } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalLoginSources, type LocalHostSnapshot } from '../../../YouleNexus/assets/framework/adapters/local/login-sources.ts'; const context: LocalStartupContext = { hostKind: 'h5', profile: 'local', config: { - mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(), - servers: PROFILES.local!.servers!, identity: BUILD_IDENTITY, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, source: 'remote', gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(), + servers: STARTUP_CONFIG.localServers, identity: BUILD_IDENTITY, } }; class MapStorage implements LocalStoragePort { readonly values = new Map(); diff --git a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts index f8a1ccd..cda6f2f 100644 --- a/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts +++ b/cocoscreator_projects/framework-tests/adapters/local/visitor-account.test.ts @@ -2,7 +2,7 @@ import { remoteFixture } from '../../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts'; -import { PROFILES } from '../../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG } from '../../../YouleNexus/assets/framework/config/profiles.ts'; import type { LocalStartupContext } from '../../../YouleNexus/assets/framework/config/local-startup.ts'; import { LOCAL_MACHINE_KEY, localAccountKey, type LocalStoragePort } from '../../../YouleNexus/assets/framework/adapters/local/local-storage.ts'; import { createLocalVisitorProvider } from '../../../YouleNexus/assets/framework/adapters/local/visitor-account.ts'; @@ -16,8 +16,8 @@ class MapStorage implements LocalStoragePort { function context(overrides: Partial = {}): LocalStartupContext { return { hostKind: 'h5', profile: 'local', config: { - mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(), - servers: PROFILES.local!.servers!, identity: { ...BUILD_IDENTITY, ...overrides }, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, source: 'remote', gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(), + servers: STARTUP_CONFIG.localServers, identity: { ...BUILD_IDENTITY, ...overrides }, } }; } diff --git a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts index 358a2e5..378eeda 100644 --- a/cocoscreator_projects/framework-tests/config/bootstrap.test.ts +++ b/cocoscreator_projects/framework-tests/config/bootstrap.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveBootstrap } from '../../YouleNexus/assets/framework/config/bootstrap.ts'; @@ -8,9 +9,19 @@ test('resolveBootstrap:显式 hostKind 透传严格 RuntimeConfig,并保留 async fetch() { return { player_server_tcp: '5.5.5.5:5000' }; }, }; const result = await resolveBootstrap({ - mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher, + hostKind: 'h5', win: {}, search: '', fetcher, }); assert.equal(result.source, 'remote'); assert.equal(result.profile.name, 'local'); assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); }); + +test('facade forwards the entire injected source and derives its compatibility label', async () => { + for (const useLocalServer of [true,false]) { + const startupConfig={...STARTUP_CONFIG,mode:'release' as const,useLocalServer,gameserver:'https://fixture.example/config.txt'}; + const result=await resolveBootstrap({startupConfig,isNative:false,win:{},search:'?mode=debug&profile=unknown&profile=local',fetcher:{async fetch(url){assert.equal(url,startupConfig.gameserver);return {player_server_tcp:'remote.example:3088'};}}}); + assert.deepEqual(result.startupConfig,startupConfig); assert.equal(result.mode,'release'); + assert.equal(result.profile.name,useLocalServer?'local':'prod'); + assert.deepEqual(result.servers,useLocalServer?STARTUP_CONFIG.localServers:['ws://remote.example:3088']); + } +}); diff --git a/cocoscreator_projects/framework-tests/config/local-startup.test.ts b/cocoscreator_projects/framework-tests/config/local-startup.test.ts index 542089d..3270bf3 100644 --- a/cocoscreator_projects/framework-tests/config/local-startup.test.ts +++ b/cocoscreator_projects/framework-tests/config/local-startup.test.ts @@ -7,12 +7,12 @@ import { resolveLocalStartup, type LocalStartupContext, } from '../../YouleNexus/assets/framework/config/local-startup.ts'; -import { PROFILES } from '../../YouleNexus/assets/framework/config/profiles.ts'; +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import type { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts'; import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; -function options(search = '?profile=local'): ResolveRuntimeConfigOptions & { +function options(search = ''): ResolveRuntimeConfigOptions & { fetcher: ConfigFetcher & { calls: number }; } { const fetcher = { @@ -22,7 +22,7 @@ function options(search = '?profile=local'): ResolveRuntimeConfigOptions & { return { player_server_tcp: 'remote.example:3088' }; }, }; - return { mode: 'debug', hostKind: 'h5', win: {}, search, fetcher }; + return { hostKind: 'h5', win: {}, search, fetcher }; } test('local debug fetches remote config before choosing authoritative local servers', async () => { @@ -32,7 +32,7 @@ test('local debug fetches remote config before choosing authoritative local serv assert.equal(context.hostKind, 'h5'); assert.equal(context.profile, 'local'); assert.equal(context.config.identity.gameid, BUILD_IDENTITY.gameid); - assert.deepEqual(context.config.servers, PROFILES.local!.servers); + assert.deepEqual(context.config.servers, STARTUP_CONFIG.localServers); assert.equal(input.fetcher.calls, 1); assert.equal(context.config.source, 'remote'); assert.equal(Object.isFrozen(context), true); @@ -40,7 +40,8 @@ test('local debug fetches remote config before choosing authoritative local serv test('rejects non-local startup modes and hosts before fetching', async () => { const cases: Array<[Partial, RegExp]> = [ - [{ mode: 'release' }, /mode.*debug/i], + [{ startupConfig: {...STARTUP_CONFIG, mode:'release'} }, /mode.*debug/i], + [{ startupConfig: {...STARTUP_CONFIG, useLocalServer:false} }, /useLocalServer/i], [{ hostKind: 'native-settings' }, /hostKind.*h5/i], [{ hostKind: 'uAgent_3' }, /hostKind.*h5/i], ]; @@ -52,21 +53,11 @@ test('rejects non-local startup modes and hosts before fetching', async () => { } }); -test('requires exactly one local profile and only an optional single debug mode', async () => { - const cases: Array<[string, RegExp]> = [ - ['', /profile/i], - ['?profile=prod', /profile/i], - ['?profile=staging', /profile/i], - ['?profile=local&profile=local', /profile/i], - ['?profile=local&mode=release', /mode.*debug/i], - ['?profile=local&mode=debug&mode=debug', /mode/i], - ]; - for (const [search, expected] of cases) { - const input = options(search); - await assert.rejects(resolveLocalStartup(input), expected); - assert.equal(input.fetcher.calls, 0); +test('ignores conflicting and duplicate mode/profile queries', async () => { + for (const search of ['', '?profile=prod', '?profile=local&profile=unknown&mode=release&mode=other']) { + const input=options(search); const result=await resolveLocalStartup(input); + assert.deepEqual(result.config.servers,STARTUP_CONFIG.localServers); assert.equal(input.fetcher.calls,1); } - await resolveLocalStartup(options('?profile=local&mode=debug')); }); test('rejects gameconfig and ambiguous identity controls before fetching', async () => { @@ -105,12 +96,13 @@ function validContext(): LocalStartupContext { hostKind: 'h5', profile: 'local', config: { + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: BUILD_IDENTITY, - servers: PROFILES.local!.servers!, + servers: STARTUP_CONFIG.localServers, source: 'remote', - gameserver: PROFILES.local!.gameserver, + gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(BUILD_IDENTITY), }, }; @@ -172,3 +164,13 @@ test('connection target must be a valid URL from the authoritative local profile assert.throws(() => assertLocalConnectionTarget(context, url), expected); } }); + +test('local consumers validate the injected source snapshot and its exact targets', async () => { + const startupConfig={...STARTUP_CONFIG,gameserver:'https://fixture.example/config.txt',localServers:['ws://[::1]:4300']}; + const context=await resolveLocalStartup({...options(),startupConfig}); + assertLocalConnectionTarget(context,'ws://[::1]:4300'); + assert.throws(()=>assertLocalConnectionTarget(context,STARTUP_CONFIG.localServers[0]),/target/); + for (const patch of [{mode:'release'}, {useLocalServer:false}, {localServers:['ws://elsewhere:3088']}, {gameserver:'https://other.example/config.txt'}]) { + assert.throws(()=>assertLocalStartupContext({...context,config:{...context.config,startupConfig:{...context.config.startupConfig,...patch}}} as any),/mode|useLocalServer|localServers|gameserver/); + } +}); diff --git a/cocoscreator_projects/framework-tests/config/profiles.test.ts b/cocoscreator_projects/framework-tests/config/profiles.test.ts index fbd5869..1afbea5 100644 --- a/cocoscreator_projects/framework-tests/config/profiles.test.ts +++ b/cocoscreator_projects/framework-tests/config/profiles.test.ts @@ -1,10 +1,9 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { - ACTIVE_PROFILE, - PROFILES, + STARTUP_CONFIG, + validateStartupConfig, decodeGameConfig, - resolveActiveProfile, resolveGameServer, } from '../../YouleNexus/assets/framework/config/profiles.ts'; @@ -23,19 +22,13 @@ test('resolveGameServer:空 gameconfig 使用配置源,非空值使用解码 ); }); -test('默认 profile 是 ACTIVE_PROFILE,local 保持 remote 源与独立连接目标', () => { - assert.equal(resolveActiveProfile('').name, ACTIVE_PROFILE); - const local = resolveActiveProfile('?profile=local'); - assert.equal(local.source, 'remote'); - assert.ok(local.gameserver); - assert.deepEqual(local.servers, ['ws://127.0.0.1:3088']); +test('one frozen source defines the complete default startup configuration', () => { + assert.deepEqual(STARTUP_CONFIG, { mode: 'debug', gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', useLocalServer: true, localServers: ['ws://127.0.0.1:3088'] }); + assert.ok(Object.isFrozen(STARTUP_CONFIG)); assert.ok(Object.isFrozen(STARTUP_CONFIG.localServers)); }); - -test('未知 profile 显式报错,不回退到 ACTIVE_PROFILE', () => { - assert.throws(() => resolveActiveProfile('?profile=nope'), /未知 profile/); -}); - -test('正常 build 可达 profile 不含占位调试账号', () => { - assert.equal(JSON.stringify(PROFILES).includes('REPLACE_ME'), false); - assert.equal(PROFILES.staging.account, undefined); +test('source validation rejects malformed configuration without defaults', () => { + for (const patch of [{mode:'other'}, {useLocalServer:undefined}, {gameserver:'ws://host'}, {gameserver:''}, {localServers:[]}, {localServers:['ws://example.com:3088']}, {localServers:['ws://user:pass@localhost']}, {localServers:['ws://localhost/#fragment']}, {localServers:['ws://127.999.0.1']}, {localServers:['not-a-url']}]) { + assert.throws(() => validateStartupConfig({...STARTUP_CONFIG,...patch} as any), /startup config/i); + } + for (const url of ['ws://127.4.3.2:3088','wss://localhost:3088','ws://[::1]:3088']) assert.deepEqual(validateStartupConfig({...STARTUP_CONFIG,localServers:[url]}).localServers,[url]); }); diff --git a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts index 1a892e6..01f2382 100644 --- a/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts +++ b/cocoscreator_projects/framework-tests/config/remote-config-fetcher.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { HttpConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config-fetcher.ts'; @@ -26,7 +27,7 @@ test('resolveRuntimeConfig:把 cacheBust 注入唯一的 HTTP 请求构造点' }); await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release',useLocalServer:false}, hostKind: 'h5', win: {}, search: '?agentid=A&channelid=C', diff --git a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts index 58e03fe..32393d3 100644 --- a/cocoscreator_projects/framework-tests/config/runtime-config.test.ts +++ b/cocoscreator_projects/framework-tests/config/runtime-config.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts'; @@ -18,7 +19,7 @@ test('h5:只读 query 身份,不访问原生宿主,并生成严格 remote const fetcher = returning(REMOTE); const throwingSettings = new Proxy({}, { get() { throw new Error('native access forbidden'); } }); const result = await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: { settings: throwingSettings }, search: '?agentid=HA&channelid=HC', @@ -41,7 +42,7 @@ test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserv const calls: string[] = []; const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'native-settings', win: { settings: { @@ -65,7 +66,7 @@ test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserv test('uAgent_3:只读 app_,settings 不参与身份或 gameserver', async () => { const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'uAgent_3', win: { settings: new Proxy({}, { get() { throw new Error('settings forbidden'); } }), @@ -84,7 +85,7 @@ test('uAgent_3:只读 app_,settings 不参与身份或 gameserver', as test('native-settings:不完整最终身份显式拒绝,不拿 build identity 兜底', async () => { await assert.rejects( resolveRuntimeConfig({ - mode: 'release', hostKind: 'native-settings', win: {}, search: '', fetcher: returning(REMOTE), + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'native-settings', win: {}, search: '', fetcher: returning(REMOTE), }), /identity.*agentid.*channelid/i, ); @@ -103,14 +104,14 @@ test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值', }; await assert.rejects( resolveRuntimeConfig({ - mode: 'release', hostKind: 'uAgent_3', win, search: '', fetcher: returning(REMOTE), + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'uAgent_3', win, search: '', fetcher: returning(REMOTE), }), new RegExp(`identity.*${sample.field}.*finite`, 'i'), ); } await assert.rejects( resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '?agentid=A&channelid=C&version=NaN', @@ -123,7 +124,7 @@ test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值', test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => { await assert.rejects( resolveRuntimeConfig({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'uAgent_3', win: { app_agent: true, app_channel: 'C', app_market: 4, app_gameconfig: '' }, search: '', @@ -136,7 +137,7 @@ test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => { test('显式 local debug:远程配置保留内存,连接服务器来自 profile', async () => { const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ - mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher, + startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher, }); assert.equal(result.source, 'remote'); assert.ok(result.gameserver); @@ -149,7 +150,7 @@ test('显式 local debug:远程配置保留内存,连接服务器来自 prof test('远程抓取失败显式报错,不降级 direct', async () => { const fetcher: ConfigFetcher = { async fetch() { throw new Error('net down'); } }; await assert.rejects( - resolveRuntimeConfig({ mode: 'release', hostKind: 'h5', win: {}, search: '', fetcher }), + resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '', fetcher }), /远程配置抓取失败/, ); }); @@ -157,7 +158,7 @@ test('远程抓取失败显式报错,不降级 direct', async () => { test('release ignores local profile but still fetches and selects remote servers', async () => { const fetcher = returning(REMOTE); - const result = await resolveRuntimeConfig({ mode: 'release', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); + const result = await resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); assert.equal(fetcher.calls.length, 1); assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']); assert.equal(result.isDebugger, false); @@ -168,14 +169,14 @@ test('JSON parse errors retain their classification through the runtime boundary const { ConfigParseError } = await import('../../YouleNexus/assets/framework/config/remote-config.ts'); const error = new ConfigParseError('invalid JSON'); const fetcher = { async fetch() { throw error; } }; - await assert.rejects(resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }), + await assert.rejects(resolveRuntimeConfig({ startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher }), actual => actual === error); }); test('local consumes explicit loopback targets after valid remote JSON even when remote candidates are absent', async () => { const raw = { visitor_server_tcp: 'unused.example:1234', notice: 'kept' }; const fetcher = returning(raw); - const result = await resolveRuntimeConfig({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); + const result = await resolveRuntimeConfig({ startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher }); assert.equal(fetcher.calls.length, 1); assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']); assert.strictEqual(result.rawConfig, raw); @@ -185,7 +186,33 @@ test('local consumes explicit loopback targets after valid remote JSON even when test('remote profiles reject missing game/player addresses without selecting visitor', async () => { for (const mode of ['debug', 'release'] as const) { const fetcher = returning({ visitor_server_tcp: 'unused.example:1234' }); - await assert.rejects(resolveRuntimeConfig({ mode, hostKind: 'h5', win: {}, search: '', fetcher }), /game_server_tcp\/player_server_tcp/); + await assert.rejects(resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode, useLocalServer: false}, hostKind: 'h5', win: {}, search: '', fetcher }), /game_server_tcp\/player_server_tcp/); assert.equal(fetcher.calls.length, 1); } }); + +for (const mode of ['debug','release'] as const) for (const useLocalServer of [true,false]) test('independent source '+mode+'/'+useLocalServer+' ignores URL selectors', async () => { + const fetcher = returning(useLocalServer ? {visitor_server_tcp:'visitor:1'} : REMOTE); + const source = {...STARTUP_CONFIG,mode,useLocalServer}; + const result = await resolveRuntimeConfig({startupConfig:source,hostKind:'h5',win:{},search:'?mode=debug&mode=release&profile=prod&profile=unknown',fetcher}); + assert.equal(fetcher.calls.length,1); assert.equal(result.mode,mode); assert.equal(result.isDebugger,mode==='debug'); + assert.deepEqual(result.servers,useLocalServer ? STARTUP_CONFIG.localServers : ['ws://5.5.5.5:5000','ws://5.5.5.5:5000']); + assert.deepEqual(result.startupConfig,source); assert.ok(Object.isFrozen(result.startupConfig)); + source.localServers=['ws://localhost:9999']; assert.deepEqual(result.startupConfig.localServers,STARTUP_CONFIG.localServers); +}); +test('default runtime starts without query configuration', async () => { + const result=await resolveRuntimeConfig({hostKind:'h5',win:{},search:'',fetcher:returning({notice:'kept'})}); + assert.deepEqual(result.startupConfig,STARTUP_CONFIG); assert.deepEqual(result.servers,STARTUP_CONFIG.localServers); +}); + +test('invalid source fails before remote fetch, including null and missing fields', async () => { + for (const patch of [null, {useLocalServer: undefined}, {gameserver: ''}, {localServers: ['ws://elsewhere:3088']}]) { + const fetcher=returning(REMOTE); + await assert.rejects(resolveRuntimeConfig({startupConfig: patch === null ? null as any : {...STARTUP_CONFIG,...patch} as any,hostKind:'h5',win:{},search:'',fetcher}),/startup config/i); + assert.equal(fetcher.calls.length,0); + } +}); +for (const mode of ['debug','release'] as const) for (const useLocalServer of [true,false]) test('remote failure never falls back '+mode+'/'+useLocalServer, async () => { + const fetcher={async fetch(){throw new Error('remote unavailable');}}; + await assert.rejects(resolveRuntimeConfig({startupConfig:{...STARTUP_CONFIG,mode,useLocalServer},hostKind:'h5',win:{},search:'',fetcher}),/远程配置抓取失败/); +}); diff --git a/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts b/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts index b3138cf..fd90df1 100644 --- a/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts +++ b/cocoscreator_projects/framework-tests/config/runtime-mode.test.ts @@ -1,16 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { resolveRuntimeMode } from '../../YouleNexus/assets/framework/config/runtime-mode.ts'; - -test('override 优先于构建类型', () => { - assert.equal(resolveRuntimeMode(true, 'release'), 'release'); - assert.equal(resolveRuntimeMode(false, 'debug'), 'debug'); -}); - -test('无 override:debug 构建 → debug', () => { - assert.equal(resolveRuntimeMode(true, null), 'debug'); -}); - -test('无 override:release 构建 → release', () => { - assert.equal(resolveRuntimeMode(false, null), 'release'); +test('runtime mode is an explicit pure reader without environment defaults', () => { + assert.equal(resolveRuntimeMode('debug'),'debug'); assert.equal(resolveRuntimeMode('release'),'release'); + for (const value of [undefined,null,true,'other']) assert.throws(() => resolveRuntimeMode(value as any), /mode/i); }); diff --git a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts index 40203b3..12d3543 100644 --- a/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts +++ b/cocoscreator_projects/framework-tests/integration/local-login-sources.test.ts @@ -13,7 +13,7 @@ class MapStorage implements LocalStoragePort { } function resolve(search = '?profile=local') { - return resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search, + return resolveLocalStartup({ hostKind: 'h5', win: {}, search, fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); } diff --git a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts index 43f0b2b..174014a 100644 --- a/cocoscreator_projects/framework-tests/integration/login-flow.test.ts +++ b/cocoscreator_projects/framework-tests/integration/login-flow.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -12,7 +13,7 @@ import type { GameHost } from '../../YouleNexus/assets/framework/sdk/index.ts'; import { FakeTransport } from '../helpers/fake-transport.ts'; const CONFIG: RuntimeConfig = { - mode: 'debug', isDebugger: true, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: { agentid: 'A', channelid: 'C', gameid: '41', version: 1, marketid: 4 }, source: 'remote', servers: ['ws://agent'], gameserver: 'https://fixture/config', ...remoteFixture(), }; diff --git a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts index 38d283f..db91afe 100644 --- a/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts +++ b/cocoscreator_projects/framework-tests/integration/platform-vertical-slice.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -121,7 +122,7 @@ test('real modern vertical replay preserves wire contracts, room ordering and te const runtime = new PlatformRuntime({ gameEntry: entry, resolveRuntimeConfig: () => resolveRuntimeConfig({ - mode: 'release', hostKind: 'native-settings', search: '', + startupConfig: {...STARTUP_CONFIG,mode:'release',useLocalServer:false}, hostKind: 'native-settings', search: '', win: { settings: { getothername(name: string) { nativeCalls.push(`getothername:${name}`); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts index 754a066..5e43abf 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/host.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; @@ -39,7 +40,7 @@ async function setup(overrides: { config?: Partial; const values = new Map(), frames = new Set<() => void>(); let fetches = 0; const host = api.createLocalLoginHost({ - configOptions: { mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local&version=731', + configOptions: { hostKind: 'h5', win: {}, search: '?profile=local&version=731', fetcher: { async fetch() { fetches++; return { player_server_tcp: 'remote.example:3088' }; } }, ...overrides.config }, storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } }, random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }), @@ -138,7 +139,7 @@ for (const phase of ['config', 'resources', 'prepare'] as const) test(`stop duri assert.deepEqual(h.failures, []); }); -for (const config of [{ search: '?profile=prod' }, { hostKind: 'native-settings' as const }]) test('invalid local input cannot create an account or connection', async () => { +for (const config of [{ startupConfig: {...STARTUP_CONFIG,mode:'release' as const} }, { startupConfig: {...STARTUP_CONFIG,useLocalServer:false} }, { hostKind: 'native-settings' as const }]) test('invalid local input cannot create an account or connection', async () => { const h = await setup({ config }); await assert.rejects(h.host.start(), /local startup/); assert.equal(h.values.size, 0); assert.equal(h.sockets.length, 0); assert.equal(h.failures.length, 1); @@ -242,3 +243,10 @@ test('startup diagnostics exclude raw remote fields', async t => { t.after(() => h.host.stop()); await h.ready(); assert.equal(JSON.stringify(h.evidence).includes('DO_NOT_RECORD_REMOTE_SECRET'), false); }); + +test('default no-query host reaches login ready and ignores conflicting selectors', async t => { + for (const search of ['', '?mode=release&mode=other&profile=prod&profile=unknown']) { + const h=await setup({config:{search}}); t.after(()=>h.host.stop()); await h.ready(); + assert.equal(h.fetches(),1); assert.equal(h.host.getSnapshot().ready,true); assert.ok(h.activations.includes('login')); assert.equal(h.failures.length,0); + } +}); diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts index 0630ce5..752fbab 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -36,7 +36,7 @@ async function setup(overrides: { const module = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts') .catch((error: unknown) => assert.fail(`LocalLoginTransport implementation must exist: ${String(error)}`)); const context = await resolveLocalStartup({ - mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', + hostKind: 'h5', win: {}, search: '?profile=local', fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } }, }); const sockets: FakeSocket[] = []; diff --git a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts index bd47479..339488a 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/wire.test.ts @@ -14,7 +14,7 @@ function fixture(name: string) { async function setup(recordHook?: (event: { kind: string; data: unknown }) => void) { const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts') .catch(error => assert.fail(`Local login Wire implementation must exist: ${String(error)}`)); - const context = await resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search: '?profile=local', + const context = await resolveLocalStartup({ hostKind: 'h5', win: {}, search: '?profile=local', fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } }); const transports: FakeTransport[] = []; const events: WireEvent[] = []; diff --git a/cocoscreator_projects/framework-tests/platform/commands.test.ts b/cocoscreator_projects/framework-tests/platform/commands.test.ts index f9a07c8..1748b2f 100644 --- a/cocoscreator_projects/framework-tests/platform/commands.test.ts +++ b/cocoscreator_projects/framework-tests/platform/commands.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -11,7 +12,7 @@ import { } from '../../YouleNexus/assets/framework/protocol/contracts/index.ts'; const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', isDebugger: false, identity: Object.freeze({ agentid: 'agent-from-config', diff --git a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts index 961ebfe..e3e83c8 100644 --- a/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts +++ b/cocoscreator_projects/framework-tests/platform/game-host-adapter.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -30,7 +31,7 @@ import { } from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts'; const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', isDebugger: false, identity: Object.freeze({ agentid: 17, diff --git a/cocoscreator_projects/framework-tests/platform/runtime.test.ts b/cocoscreator_projects/framework-tests/platform/runtime.test.ts index b29b45b..a826318 100644 --- a/cocoscreator_projects/framework-tests/platform/runtime.test.ts +++ b/cocoscreator_projects/framework-tests/platform/runtime.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import { readFileSync } from 'node:fs'; import assert from 'node:assert/strict'; @@ -48,7 +49,7 @@ const DEVICE = Object.freeze({ function runtimeConfig(gameid: string | number = '41'): RuntimeConfig { return Object.freeze({ - mode: 'debug', + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: Object.freeze({ agentid: 'veRa0qrBf', diff --git a/cocoscreator_projects/framework-tests/platform/startup.test.ts b/cocoscreator_projects/framework-tests/platform/startup.test.ts index 93525e1..8630652 100644 --- a/cocoscreator_projects/framework-tests/platform/startup.test.ts +++ b/cocoscreator_projects/framework-tests/platform/startup.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -11,11 +12,11 @@ const IDENTITY = { agentid: 'A', channelid: 'C', gameid: 'G', marketid: 4, versi function bootstrapOf(): BootstrapResult { return { - mode: 'debug', isDebugger: true, + startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, identity: IDENTITY, servers: ['ws://test'], ...remoteFixture(), - profile: { name: 'debug' }, + profile: { name: 'local' }, }; } diff --git a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts index 61d9076..11f20be 100644 --- a/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts +++ b/cocoscreator_projects/framework-tests/sdk/game-contract-harness.test.ts @@ -1,3 +1,4 @@ +import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts'; import { remoteFixture } from '../fixtures/remote-config.ts'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -75,7 +76,7 @@ test('assertGameContract accepts a fixed-four game with its real nested roomtype }); const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({ - mode: 'release', + startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release', isDebugger: false, identity: Object.freeze({ agentid: 'fixture-agent', channelid: 'fixture-channel', gameid: '41',