feat(config): unify startup mode and server selection
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import type { DebugAccount, DebugProfile } from './profiles.ts';
|
import type { DebugAccount } from './profiles.ts';
|
||||||
import { PROFILES, resolveActiveProfile } from './profiles.ts';
|
|
||||||
import {
|
import {
|
||||||
resolveRuntimeConfig,
|
resolveRuntimeConfig,
|
||||||
type HostKind,
|
type HostKind,
|
||||||
@@ -24,7 +23,7 @@ export type BootstrapResult = Omit<RuntimeConfig, 'servers' | 'source' | 'gamese
|
|||||||
readonly source?: RuntimeConfig['source'];
|
readonly source?: RuntimeConfig['source'];
|
||||||
readonly gameserver?: RuntimeConfig['gameserver'];
|
readonly gameserver?: RuntimeConfig['gameserver'];
|
||||||
readonly account?: DebugAccount;
|
readonly account?: DebugAccount;
|
||||||
readonly profile: Pick<DebugProfile, 'name'>;
|
readonly profile: Readonly<{ name: 'local' | 'prod' }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function bootstrapHostKind(options: BootstrapOptions): HostKind {
|
function bootstrapHostKind(options: BootstrapOptions): HostKind {
|
||||||
@@ -34,9 +33,8 @@ function bootstrapHostKind(options: BootstrapOptions): HostKind {
|
|||||||
|
|
||||||
/** 兼容启动编排外观;所有运行契约均由唯一的 RuntimeConfig 边界产出。 */
|
/** 兼容启动编排外观;所有运行契约均由唯一的 RuntimeConfig 边界产出。 */
|
||||||
export async function resolveBootstrap(options: BootstrapOptions): Promise<BootstrapResult> {
|
export async function resolveBootstrap(options: BootstrapOptions): Promise<BootstrapResult> {
|
||||||
const profile = options.mode === 'release' ? PROFILES.prod! : resolveActiveProfile(options.search);
|
|
||||||
const runtime = await resolveRuntimeConfig({
|
const runtime = await resolveRuntimeConfig({
|
||||||
mode: options.mode,
|
startupConfig: options.startupConfig,
|
||||||
hostKind: bootstrapHostKind(options),
|
hostKind: bootstrapHostKind(options),
|
||||||
win: options.win,
|
win: options.win,
|
||||||
search: options.search,
|
search: options.search,
|
||||||
@@ -46,7 +44,6 @@ export async function resolveBootstrap(options: BootstrapOptions): Promise<Boots
|
|||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...runtime,
|
...runtime,
|
||||||
servers: [...runtime.servers],
|
servers: [...runtime.servers],
|
||||||
account: profile.account,
|
profile: Object.freeze({ name: runtime.startupConfig.useLocalServer ? 'local' as const : 'prod' as const }),
|
||||||
profile,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { resolveIdentity } from './identity.ts';
|
import { resolveIdentity } from './identity.ts';
|
||||||
import { PROFILES } from './profiles.ts';
|
import { STARTUP_CONFIG, validateStartupConfig, validateLocalServer } from './profiles.ts';
|
||||||
import {
|
import {
|
||||||
resolveRuntimeConfig,
|
resolveRuntimeConfig,
|
||||||
type ResolveRuntimeConfigOptions,
|
type ResolveRuntimeConfigOptions,
|
||||||
@@ -13,8 +13,6 @@ export interface LocalStartupContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QUERY_CONTROLS = Object.freeze([
|
const QUERY_CONTROLS = Object.freeze([
|
||||||
'profile',
|
|
||||||
'mode',
|
|
||||||
'gameconfig',
|
'gameconfig',
|
||||||
'agentid',
|
'agentid',
|
||||||
'channelid',
|
'channelid',
|
||||||
@@ -27,41 +25,12 @@ function fail(field: string, reason: string): never {
|
|||||||
throw new Error(`Invalid local startup ${field}: ${reason}`);
|
throw new Error(`Invalid local startup ${field}: ${reason}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateLocalUrl(url: string, field: string): void {
|
|
||||||
let parsed: URL;
|
|
||||||
try {
|
|
||||||
parsed = new URL(url);
|
|
||||||
} catch {
|
|
||||||
fail(field, 'url must be absolute');
|
|
||||||
}
|
|
||||||
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
|
|
||||||
fail(field, 'url protocol must be ws: or wss:');
|
|
||||||
}
|
|
||||||
if (parsed.username !== '' || parsed.password !== '') {
|
|
||||||
fail(field, 'url credentials are forbidden');
|
|
||||||
}
|
|
||||||
const hostname = parsed.hostname.toLowerCase();
|
|
||||||
if (
|
|
||||||
hostname !== 'localhost'
|
|
||||||
&& hostname !== '[::1]'
|
|
||||||
&& !/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)
|
|
||||||
) {
|
|
||||||
fail(field, 'url hostname must be loopback');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertLocalQuery(search: string): void {
|
function assertLocalQuery(search: string): void {
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
for (const field of QUERY_CONTROLS) {
|
for (const field of QUERY_CONTROLS) {
|
||||||
if (query.getAll(field).length > 1) fail(field, 'duplicate query control');
|
if (query.getAll(field).length > 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');
|
const gameconfigs = query.getAll('gameconfig');
|
||||||
if (gameconfigs.length === 1 && gameconfigs[0] !== '') {
|
if (gameconfigs.length === 1 && gameconfigs[0] !== '') {
|
||||||
fail('gameconfig', 'must be empty');
|
fail('gameconfig', 'must be empty');
|
||||||
@@ -72,11 +41,13 @@ function assertLocalQuery(search: string): void {
|
|||||||
export async function resolveLocalStartup(
|
export async function resolveLocalStartup(
|
||||||
options: ResolveRuntimeConfigOptions,
|
options: ResolveRuntimeConfigOptions,
|
||||||
): Promise<LocalStartupContext> {
|
): Promise<LocalStartupContext> {
|
||||||
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');
|
if (options.hostKind !== 'h5') fail('hostKind', 'must be h5');
|
||||||
assertLocalQuery(options.search);
|
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 });
|
const context = Object.freeze({ hostKind: 'h5' as const, profile: 'local' as const, config });
|
||||||
assertLocalStartupContext(context);
|
assertLocalStartupContext(context);
|
||||||
return context;
|
return context;
|
||||||
@@ -87,10 +58,13 @@ export function assertLocalStartupContext(context: LocalStartupContext): void {
|
|||||||
if (context.profile !== 'local') fail('profile', 'must be local');
|
if (context.profile !== 'local') fail('profile', 'must be local');
|
||||||
|
|
||||||
const { config } = context;
|
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.mode !== 'debug') fail('mode', 'must be debug');
|
||||||
if (config.isDebugger !== true) fail('isDebugger', 'must be true');
|
if (config.isDebugger !== true) fail('isDebugger', 'must be true');
|
||||||
if (config.source !== 'remote') fail('source', 'must be remote');
|
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.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');
|
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;
|
throw wrapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authoritative = PROFILES.local?.servers;
|
const authoritative = startupConfig.localServers;
|
||||||
if (!authoritative || authoritative.length === 0) {
|
|
||||||
fail('servers', 'PROFILES.local must define servers');
|
|
||||||
}
|
|
||||||
if (config.servers.length !== authoritative.length) {
|
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) {
|
for (let index = 0; index < authoritative.length; index += 1) {
|
||||||
const expected = authoritative[index];
|
const expected = authoritative[index];
|
||||||
validateLocalUrl(expected, `servers[${index}]`);
|
validateLocalServer(expected, `servers[${index}]`);
|
||||||
if (config.servers[index] !== expected) {
|
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 {
|
export function assertLocalConnectionTarget(context: LocalStartupContext, url: string): void {
|
||||||
assertLocalStartupContext(context);
|
assertLocalStartupContext(context);
|
||||||
validateLocalUrl(url, 'url');
|
validateLocalServer(url, 'url');
|
||||||
if (!PROFILES.local?.servers?.includes(url)) {
|
if (!context.config.startupConfig.localServers.includes(url)) {
|
||||||
fail('url', 'target must occur in PROFILES.local.servers');
|
fail('url', 'target must occur in startupConfig.localServers');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { resolveRuntimeMode, type RuntimeMode } from './runtime-mode.ts';
|
||||||
|
|
||||||
/** 调试用登录玩家档案;值只能由测试或本地启动配置显式注入。 */
|
/** 调试用登录玩家档案;值只能由测试或本地启动配置显式注入。 */
|
||||||
export interface DebugAccount {
|
export interface DebugAccount {
|
||||||
openid: string;
|
openid: string;
|
||||||
@@ -9,38 +11,49 @@ export interface DebugAccount {
|
|||||||
unionid?: string | number;
|
unionid?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProfileSource = 'remote';
|
export interface StartupConfig {
|
||||||
|
readonly mode: RuntimeMode;
|
||||||
export interface DebugProfile {
|
|
||||||
readonly name: string;
|
|
||||||
readonly source: ProfileSource;
|
|
||||||
readonly gameserver: string;
|
readonly gameserver: string;
|
||||||
readonly servers?: readonly string[];
|
readonly useLocalServer: boolean;
|
||||||
readonly account?: DebugAccount;
|
readonly localServers: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 正式 gameserver:原 00_SubGame_Config.js:11。 */
|
/** 唯一发布配置入口;mode 与服务器选择互相独立。 */
|
||||||
export const DEFAULT_GAMESERVER = 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt';
|
export const STARTUP_CONFIG: Readonly<StartupConfig> = Object.freeze({
|
||||||
|
mode: 'debug',
|
||||||
export const PROFILES: Readonly<Record<string, DebugProfile>> = Object.freeze({
|
gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt',
|
||||||
prod: Object.freeze({ name: 'prod', source: 'remote', gameserver: DEFAULT_GAMESERVER }),
|
useLocalServer: true,
|
||||||
staging: Object.freeze({ name: 'staging', source: 'remote', gameserver: DEFAULT_GAMESERVER }),
|
localServers: Object.freeze(['ws://127.0.0.1:3088']),
|
||||||
local: Object.freeze({
|
|
||||||
name: 'local',
|
|
||||||
source: 'remote',
|
|
||||||
gameserver: DEFAULT_GAMESERVER,
|
|
||||||
servers: 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 {
|
export function validateLocalServer(value: string, field = 'localServers'): void {
|
||||||
const requested = new URLSearchParams(search).get('profile');
|
if (typeof value !== 'string' || value.trim() !== value) invalid(field, 'must be an absolute URL');
|
||||||
const name = requested || ACTIVE_PROFILE;
|
let url: URL;
|
||||||
const profile = PROFILES[name];
|
try { url = new URL(value); } catch { invalid(field, 'must be an absolute URL'); }
|
||||||
if (!profile) throw new Error(`未知 profile: ${name}`);
|
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') invalid(field, 'protocol must be ws: or wss:');
|
||||||
return profile;
|
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<StartupConfig>): Readonly<StartupConfig> {
|
||||||
|
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。 */
|
/** 忠实旧 min_replaceAll 链:- → /,# → :,再包成 http://...txt。 */
|
||||||
@@ -55,11 +68,11 @@ export interface ResolveGameServerOptions {
|
|||||||
readonly injectedGameConfig: unknown;
|
readonly injectedGameConfig: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 profile 配置源。 */
|
/** 注入的 gameconfig 非空时是唯一覆盖源;否则只使用 启动配置源。 */
|
||||||
export function resolveGameServer(options: ResolveGameServerOptions): string {
|
export function resolveGameServer(options: ResolveGameServerOptions): string {
|
||||||
const injected = options.injectedGameConfig;
|
const injected = options.injectedGameConfig;
|
||||||
if (injected === '' || injected === undefined || injected === null) {
|
if (injected === '' || injected === undefined || injected === null) {
|
||||||
if (!options.configured) throw new Error('profile 未配置 gameserver');
|
if (!options.configured) throw new Error('启动配置未配置 gameserver');
|
||||||
return options.configured;
|
return options.configured;
|
||||||
}
|
}
|
||||||
if (typeof injected !== 'string') throw new Error('gameconfig 必须是字符串');
|
if (typeof injected !== 'string') throw new Error('gameconfig 必须是字符串');
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { ChannelIdentity, IdentitySource } from './identity.ts';
|
import type { ChannelIdentity, IdentitySource } from './identity.ts';
|
||||||
import { resolveIdentity } from './identity.ts';
|
import { resolveIdentity } from './identity.ts';
|
||||||
import {
|
import {
|
||||||
PROFILES,
|
STARTUP_CONFIG,
|
||||||
resolveActiveProfile,
|
validateStartupConfig,
|
||||||
resolveGameServer,
|
resolveGameServer,
|
||||||
type DebugProfile,
|
type StartupConfig,
|
||||||
} from './profiles.ts';
|
} from './profiles.ts';
|
||||||
import {
|
import {
|
||||||
ConfigFetchError,
|
ConfigFetchError,
|
||||||
@@ -27,6 +27,7 @@ import { queryStringSource } from './sources/query-string.ts';
|
|||||||
export type HostKind = 'h5' | NativeHostKind;
|
export type HostKind = 'h5' | NativeHostKind;
|
||||||
|
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
|
readonly startupConfig: Readonly<StartupConfig>;
|
||||||
readonly mode: RuntimeMode;
|
readonly mode: RuntimeMode;
|
||||||
readonly isDebugger: boolean;
|
readonly isDebugger: boolean;
|
||||||
readonly identity: Readonly<ChannelIdentity>;
|
readonly identity: Readonly<ChannelIdentity>;
|
||||||
@@ -38,7 +39,7 @@ export interface RuntimeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolveRuntimeConfigOptions {
|
export interface ResolveRuntimeConfigOptions {
|
||||||
readonly mode: RuntimeMode;
|
readonly startupConfig?: Readonly<StartupConfig>;
|
||||||
readonly hostKind: HostKind;
|
readonly hostKind: HostKind;
|
||||||
readonly win: NativeHost;
|
readonly win: NativeHost;
|
||||||
readonly search: string;
|
readonly search: string;
|
||||||
@@ -67,10 +68,6 @@ function resolveInjectedGameConfig(options: ResolveRuntimeConfigOptions): unknow
|
|||||||
return getothername(options.win, options.hostKind, 'gameconfig');
|
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 {
|
function freezeResult(result: RuntimeConfig): RuntimeConfig {
|
||||||
return Object.freeze(result);
|
return Object.freeze(result);
|
||||||
}
|
}
|
||||||
@@ -78,14 +75,11 @@ function freezeResult(result: RuntimeConfig): RuntimeConfig {
|
|||||||
export async function resolveRuntimeConfig(
|
export async function resolveRuntimeConfig(
|
||||||
options: ResolveRuntimeConfigOptions,
|
options: ResolveRuntimeConfigOptions,
|
||||||
): Promise<RuntimeConfig> {
|
): Promise<RuntimeConfig> {
|
||||||
const profile = selectedProfile(options.mode, options.search);
|
const startupConfig = validateStartupConfig(options.startupConfig === undefined ? STARTUP_CONFIG : options.startupConfig);
|
||||||
const identity = resolveHostIdentity(options);
|
const identity = resolveHostIdentity(options);
|
||||||
|
|
||||||
if (!profile.gameserver) {
|
|
||||||
throw new ConfigFetchError(`profile '${profile.name}' 未配置 gameserver`);
|
|
||||||
}
|
|
||||||
const gameserver = resolveGameServer({
|
const gameserver = resolveGameServer({
|
||||||
configured: profile.gameserver,
|
configured: startupConfig.gameserver,
|
||||||
injectedGameConfig: resolveInjectedGameConfig(options),
|
injectedGameConfig: resolveInjectedGameConfig(options),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -97,19 +91,12 @@ export async function resolveRuntimeConfig(
|
|||||||
throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`);
|
throw new ConfigFetchError(`远程配置抓取失败: ${gameserver}`);
|
||||||
}
|
}
|
||||||
const remoteConfig = createRemoteConfig(raw, identity);
|
const remoteConfig = createRemoteConfig(raw, identity);
|
||||||
let servers: readonly string[];
|
const servers = startupConfig.useLocalServer ? startupConfig.localServers : parseUrlServers(remoteConfig);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return freezeResult({
|
return freezeResult({
|
||||||
mode: options.mode,
|
startupConfig,
|
||||||
isDebugger: options.mode === 'debug',
|
mode: startupConfig.mode,
|
||||||
|
isDebugger: startupConfig.mode === 'debug',
|
||||||
identity,
|
identity,
|
||||||
servers,
|
servers,
|
||||||
source: 'remote',
|
source: 'remote',
|
||||||
|
|||||||
@@ -1,32 +1,7 @@
|
|||||||
// 1. 先 export type
|
|
||||||
export type RuntimeMode = 'debug' | 'release';
|
export type RuntimeMode = 'debug' | 'release';
|
||||||
|
|
||||||
declare const DEBUG: boolean | undefined;
|
/** 应用模式仅来自显式配置,不读取 URL 或 Creator 编译标志。 */
|
||||||
|
export function resolveRuntimeMode(mode: RuntimeMode): RuntimeMode {
|
||||||
// 2. 函数引用 RuntimeMode(此时 type 已 export)
|
if (mode !== 'debug' && mode !== 'release') throw new Error('Invalid startup config mode: must be debug or release');
|
||||||
function readModeFromUrl(): RuntimeMode | null {
|
return mode;
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,20 +9,7 @@ import type { Transport } from '../framework/net/transport.ts';
|
|||||||
|
|
||||||
const { ccclass } = _decorator;
|
const { ccclass } = _decorator;
|
||||||
|
|
||||||
/**
|
/** 旧登录编排兼容入口;启动配置由 profiles.ts 统一提供。 */
|
||||||
* 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')
|
|
||||||
*/
|
|
||||||
|
|
||||||
class WSTransport implements Transport {
|
class WSTransport implements Transport {
|
||||||
private onOpenCb?: () => void;
|
private onOpenCb?: () => void;
|
||||||
@@ -73,11 +60,9 @@ export class LoginFlow extends Component {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
// 调试模式 + ?profile=local → 跳过远程,直连本地 ws://127.0.0.1:3088
|
|
||||||
const result = await resolveBootstrap({
|
const result = await resolveBootstrap({
|
||||||
mode: 'debug',
|
|
||||||
win: window as any,
|
win: window as any,
|
||||||
search: '?profile=local',
|
search: window.location.search,
|
||||||
fetcher: new HttpConfigFetcher(),
|
fetcher: new HttpConfigFetcher(),
|
||||||
isNative: false,
|
isNative: false,
|
||||||
});
|
});
|
||||||
@@ -86,11 +71,7 @@ export class LoginFlow extends Component {
|
|||||||
console.log('[LoginFlow] bootstrap OK, servers=' + result.servers[0] + ' profile=' + result.profile.name);
|
console.log('[LoginFlow] bootstrap OK, servers=' + result.servers[0] + ' profile=' + result.profile.name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[LoginFlow] bootstrap failed: ' + (e as Error).message);
|
console.warn('[LoginFlow] bootstrap failed: ' + (e as Error).message);
|
||||||
session.app.setIdentity({
|
return;
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构造 NetClient + 真实 WS transport
|
// 构造 NetClient + 真实 WS transport
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ export class LocalPlatformLogin extends Component {
|
|||||||
const view = this.view;
|
const view = this.view;
|
||||||
const browser = window as unknown as NativeHost & { returnCitySN?: unknown };
|
const browser = window as unknown as NativeHost & { returnCitySN?: unknown };
|
||||||
this.host = createLocalLoginHost({
|
this.host = createLocalLoginHost({
|
||||||
configOptions: { mode: 'debug', hostKind: 'h5', search: window.location.search,
|
configOptions: { hostKind: 'h5', search: window.location.search,
|
||||||
win: browser, fetcher: new HttpConfigFetcher() },
|
win: browser, fetcher: new HttpConfigFetcher() },
|
||||||
storage: window.localStorage, random: Math.random, now: Date.now,
|
storage: window.localStorage, random: Math.random, now: Date.now,
|
||||||
sampleHost: () => ({ location: null,
|
sampleHost: () => ({ location: null,
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { remoteFixture } from '../../fixtures/remote-config.ts';
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts';
|
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 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 { 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';
|
import { createLocalLoginSources, type LocalHostSnapshot } from '../../../YouleNexus/assets/framework/adapters/local/login-sources.ts';
|
||||||
|
|
||||||
const context: LocalStartupContext = { hostKind: 'h5', profile: 'local', config: {
|
const context: LocalStartupContext = { hostKind: 'h5', profile: 'local', config: {
|
||||||
mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(),
|
startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, source: 'remote', gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(),
|
||||||
servers: PROFILES.local!.servers!, identity: BUILD_IDENTITY,
|
servers: STARTUP_CONFIG.localServers, identity: BUILD_IDENTITY,
|
||||||
} };
|
} };
|
||||||
class MapStorage implements LocalStoragePort {
|
class MapStorage implements LocalStoragePort {
|
||||||
readonly values = new Map<string, string>();
|
readonly values = new Map<string, string>();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { remoteFixture } from '../../fixtures/remote-config.ts';
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { BUILD_IDENTITY } from '../../../YouleNexus/assets/framework/config/sources/defaults.ts';
|
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 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 { 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';
|
import { createLocalVisitorProvider } from '../../../YouleNexus/assets/framework/adapters/local/visitor-account.ts';
|
||||||
@@ -16,8 +16,8 @@ class MapStorage implements LocalStoragePort {
|
|||||||
|
|
||||||
function context(overrides: Partial<typeof BUILD_IDENTITY> = {}): LocalStartupContext {
|
function context(overrides: Partial<typeof BUILD_IDENTITY> = {}): LocalStartupContext {
|
||||||
return { hostKind: 'h5', profile: 'local', config: {
|
return { hostKind: 'h5', profile: 'local', config: {
|
||||||
mode: 'debug', isDebugger: true, source: 'remote', gameserver: PROFILES.local!.gameserver, ...remoteFixture(),
|
startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true, source: 'remote', gameserver: STARTUP_CONFIG.gameserver, ...remoteFixture(),
|
||||||
servers: PROFILES.local!.servers!, identity: { ...BUILD_IDENTITY, ...overrides },
|
servers: STARTUP_CONFIG.localServers, identity: { ...BUILD_IDENTITY, ...overrides },
|
||||||
} };
|
} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { resolveBootstrap } from '../../YouleNexus/assets/framework/config/bootstrap.ts';
|
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' }; },
|
async fetch() { return { player_server_tcp: '5.5.5.5:5000' }; },
|
||||||
};
|
};
|
||||||
const result = await resolveBootstrap({
|
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.source, 'remote');
|
||||||
assert.equal(result.profile.name, 'local');
|
assert.equal(result.profile.name, 'local');
|
||||||
assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']);
|
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']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
resolveLocalStartup,
|
resolveLocalStartup,
|
||||||
type LocalStartupContext,
|
type LocalStartupContext,
|
||||||
} from '../../YouleNexus/assets/framework/config/local-startup.ts';
|
} 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 { ResolveRuntimeConfigOptions, RuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||||||
import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||||||
import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.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 };
|
fetcher: ConfigFetcher & { calls: number };
|
||||||
} {
|
} {
|
||||||
const fetcher = {
|
const fetcher = {
|
||||||
@@ -22,7 +22,7 @@ function options(search = '?profile=local'): ResolveRuntimeConfigOptions & {
|
|||||||
return { player_server_tcp: 'remote.example:3088' };
|
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 () => {
|
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.hostKind, 'h5');
|
||||||
assert.equal(context.profile, 'local');
|
assert.equal(context.profile, 'local');
|
||||||
assert.equal(context.config.identity.gameid, BUILD_IDENTITY.gameid);
|
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(input.fetcher.calls, 1);
|
||||||
assert.equal(context.config.source, 'remote');
|
assert.equal(context.config.source, 'remote');
|
||||||
assert.equal(Object.isFrozen(context), true);
|
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 () => {
|
test('rejects non-local startup modes and hosts before fetching', async () => {
|
||||||
const cases: Array<[Partial<ResolveRuntimeConfigOptions>, RegExp]> = [
|
const cases: Array<[Partial<ResolveRuntimeConfigOptions>, 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: 'native-settings' }, /hostKind.*h5/i],
|
||||||
[{ hostKind: 'uAgent_3' }, /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 () => {
|
test('ignores conflicting and duplicate mode/profile queries', async () => {
|
||||||
const cases: Array<[string, RegExp]> = [
|
for (const search of ['', '?profile=prod', '?profile=local&profile=unknown&mode=release&mode=other']) {
|
||||||
['', /profile/i],
|
const input=options(search); const result=await resolveLocalStartup(input);
|
||||||
['?profile=prod', /profile/i],
|
assert.deepEqual(result.config.servers,STARTUP_CONFIG.localServers); assert.equal(input.fetcher.calls,1);
|
||||||
['?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);
|
|
||||||
}
|
}
|
||||||
await resolveLocalStartup(options('?profile=local&mode=debug'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects gameconfig and ambiguous identity controls before fetching', async () => {
|
test('rejects gameconfig and ambiguous identity controls before fetching', async () => {
|
||||||
@@ -105,12 +96,13 @@ function validContext(): LocalStartupContext {
|
|||||||
hostKind: 'h5',
|
hostKind: 'h5',
|
||||||
profile: 'local',
|
profile: 'local',
|
||||||
config: {
|
config: {
|
||||||
|
startupConfig: STARTUP_CONFIG,
|
||||||
mode: 'debug',
|
mode: 'debug',
|
||||||
isDebugger: true,
|
isDebugger: true,
|
||||||
identity: BUILD_IDENTITY,
|
identity: BUILD_IDENTITY,
|
||||||
servers: PROFILES.local!.servers!,
|
servers: STARTUP_CONFIG.localServers,
|
||||||
source: 'remote',
|
source: 'remote',
|
||||||
gameserver: PROFILES.local!.gameserver,
|
gameserver: STARTUP_CONFIG.gameserver,
|
||||||
...remoteFixture(BUILD_IDENTITY),
|
...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);
|
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/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
ACTIVE_PROFILE,
|
STARTUP_CONFIG,
|
||||||
PROFILES,
|
validateStartupConfig,
|
||||||
decodeGameConfig,
|
decodeGameConfig,
|
||||||
resolveActiveProfile,
|
|
||||||
resolveGameServer,
|
resolveGameServer,
|
||||||
} from '../../YouleNexus/assets/framework/config/profiles.ts';
|
} from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
|
|
||||||
@@ -23,19 +22,13 @@ test('resolveGameServer:空 gameconfig 使用配置源,非空值使用解码
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('默认 profile 是 ACTIVE_PROFILE,local 保持 remote 源与独立连接目标', () => {
|
test('one frozen source defines the complete default startup configuration', () => {
|
||||||
assert.equal(resolveActiveProfile('').name, ACTIVE_PROFILE);
|
assert.deepEqual(STARTUP_CONFIG, { mode: 'debug', gameserver: 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt', useLocalServer: true, localServers: ['ws://127.0.0.1:3088'] });
|
||||||
const local = resolveActiveProfile('?profile=local');
|
assert.ok(Object.isFrozen(STARTUP_CONFIG)); assert.ok(Object.isFrozen(STARTUP_CONFIG.localServers));
|
||||||
assert.equal(local.source, 'remote');
|
|
||||||
assert.ok(local.gameserver);
|
|
||||||
assert.deepEqual(local.servers, ['ws://127.0.0.1:3088']);
|
|
||||||
});
|
});
|
||||||
|
test('source validation rejects malformed configuration without defaults', () => {
|
||||||
test('未知 profile 显式报错,不回退到 ACTIVE_PROFILE', () => {
|
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(() => resolveActiveProfile('?profile=nope'), /未知 profile/);
|
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]);
|
||||||
test('正常 build 可达 profile 不含占位调试账号', () => {
|
|
||||||
assert.equal(JSON.stringify(PROFILES).includes('REPLACE_ME'), false);
|
|
||||||
assert.equal(PROFILES.staging.account, undefined);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { HttpConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config-fetcher.ts';
|
import { HttpConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config-fetcher.ts';
|
||||||
@@ -26,7 +27,7 @@ test('resolveRuntimeConfig:把 cacheBust 注入唯一的 HTTP 请求构造点'
|
|||||||
});
|
});
|
||||||
|
|
||||||
await resolveRuntimeConfig({
|
await resolveRuntimeConfig({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG,mode:'release',useLocalServer:false},
|
||||||
hostKind: 'h5',
|
hostKind: 'h5',
|
||||||
win: {},
|
win: {},
|
||||||
search: '?agentid=A&channelid=C',
|
search: '?agentid=A&channelid=C',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts';
|
import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts';
|
||||||
@@ -18,7 +19,7 @@ test('h5:只读 query 身份,不访问原生宿主,并生成严格 remote
|
|||||||
const fetcher = returning(REMOTE);
|
const fetcher = returning(REMOTE);
|
||||||
const throwingSettings = new Proxy({}, { get() { throw new Error('native access forbidden'); } });
|
const throwingSettings = new Proxy({}, { get() { throw new Error('native access forbidden'); } });
|
||||||
const result = await resolveRuntimeConfig({
|
const result = await resolveRuntimeConfig({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||||||
hostKind: 'h5',
|
hostKind: 'h5',
|
||||||
win: { settings: throwingSettings },
|
win: { settings: throwingSettings },
|
||||||
search: '?agentid=HA&channelid=HC',
|
search: '?agentid=HA&channelid=HC',
|
||||||
@@ -41,7 +42,7 @@ test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserv
|
|||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const fetcher = returning(REMOTE);
|
const fetcher = returning(REMOTE);
|
||||||
const result = await resolveRuntimeConfig({
|
const result = await resolveRuntimeConfig({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||||||
hostKind: 'native-settings',
|
hostKind: 'native-settings',
|
||||||
win: {
|
win: {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -65,7 +66,7 @@ test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserv
|
|||||||
test('uAgent_3:只读 app_<name>,settings 不参与身份或 gameserver', async () => {
|
test('uAgent_3:只读 app_<name>,settings 不参与身份或 gameserver', async () => {
|
||||||
const fetcher = returning(REMOTE);
|
const fetcher = returning(REMOTE);
|
||||||
const result = await resolveRuntimeConfig({
|
const result = await resolveRuntimeConfig({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||||||
hostKind: 'uAgent_3',
|
hostKind: 'uAgent_3',
|
||||||
win: {
|
win: {
|
||||||
settings: new Proxy({}, { get() { throw new Error('settings forbidden'); } }),
|
settings: new Proxy({}, { get() { throw new Error('settings forbidden'); } }),
|
||||||
@@ -84,7 +85,7 @@ test('uAgent_3:只读 app_<name>,settings 不参与身份或 gameserver', as
|
|||||||
test('native-settings:不完整最终身份显式拒绝,不拿 build identity 兜底', async () => {
|
test('native-settings:不完整最终身份显式拒绝,不拿 build identity 兜底', async () => {
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
resolveRuntimeConfig({
|
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,
|
/identity.*agentid.*channelid/i,
|
||||||
);
|
);
|
||||||
@@ -103,14 +104,14 @@ test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值',
|
|||||||
};
|
};
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
resolveRuntimeConfig({
|
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'),
|
new RegExp(`identity.*${sample.field}.*finite`, 'i'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
resolveRuntimeConfig({
|
resolveRuntimeConfig({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||||||
hostKind: 'h5',
|
hostKind: 'h5',
|
||||||
win: {},
|
win: {},
|
||||||
search: '?agentid=A&channelid=C&version=NaN',
|
search: '?agentid=A&channelid=C&version=NaN',
|
||||||
@@ -123,7 +124,7 @@ test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值',
|
|||||||
test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => {
|
test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => {
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
resolveRuntimeConfig({
|
resolveRuntimeConfig({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||||||
hostKind: 'uAgent_3',
|
hostKind: 'uAgent_3',
|
||||||
win: { app_agent: true, app_channel: 'C', app_market: 4, app_gameconfig: '' },
|
win: { app_agent: true, app_channel: 'C', app_market: 4, app_gameconfig: '' },
|
||||||
search: '',
|
search: '',
|
||||||
@@ -136,7 +137,7 @@ test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => {
|
|||||||
test('显式 local debug:远程配置保留内存,连接服务器来自 profile', async () => {
|
test('显式 local debug:远程配置保留内存,连接服务器来自 profile', async () => {
|
||||||
const fetcher = returning(REMOTE);
|
const fetcher = returning(REMOTE);
|
||||||
const result = await resolveRuntimeConfig({
|
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.equal(result.source, 'remote');
|
||||||
assert.ok(result.gameserver);
|
assert.ok(result.gameserver);
|
||||||
@@ -149,7 +150,7 @@ test('显式 local debug:远程配置保留内存,连接服务器来自 prof
|
|||||||
test('远程抓取失败显式报错,不降级 direct', async () => {
|
test('远程抓取失败显式报错,不降级 direct', async () => {
|
||||||
const fetcher: ConfigFetcher = { async fetch() { throw new Error('net down'); } };
|
const fetcher: ConfigFetcher = { async fetch() { throw new Error('net down'); } };
|
||||||
await assert.rejects(
|
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 () => {
|
test('release ignores local profile but still fetches and selects remote servers', async () => {
|
||||||
const fetcher = returning(REMOTE);
|
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.equal(fetcher.calls.length, 1);
|
||||||
assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']);
|
assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']);
|
||||||
assert.equal(result.isDebugger, false);
|
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 { ConfigParseError } = await import('../../YouleNexus/assets/framework/config/remote-config.ts');
|
||||||
const error = new ConfigParseError('invalid JSON');
|
const error = new ConfigParseError('invalid JSON');
|
||||||
const fetcher = { async fetch() { throw error; } };
|
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);
|
actual => actual === error);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('local consumes explicit loopback targets after valid remote JSON even when remote candidates are absent', async () => {
|
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 raw = { visitor_server_tcp: 'unused.example:1234', notice: 'kept' };
|
||||||
const fetcher = returning(raw);
|
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.equal(fetcher.calls.length, 1);
|
||||||
assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']);
|
assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']);
|
||||||
assert.strictEqual(result.rawConfig, raw);
|
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 () => {
|
test('remote profiles reject missing game/player addresses without selecting visitor', async () => {
|
||||||
for (const mode of ['debug', 'release'] as const) {
|
for (const mode of ['debug', 'release'] as const) {
|
||||||
const fetcher = returning({ visitor_server_tcp: 'unused.example:1234' });
|
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);
|
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}),/远程配置抓取失败/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { resolveRuntimeMode } from '../../YouleNexus/assets/framework/config/runtime-mode.ts';
|
import { resolveRuntimeMode } from '../../YouleNexus/assets/framework/config/runtime-mode.ts';
|
||||||
|
test('runtime mode is an explicit pure reader without environment defaults', () => {
|
||||||
test('override 优先于构建类型', () => {
|
assert.equal(resolveRuntimeMode('debug'),'debug'); assert.equal(resolveRuntimeMode('release'),'release');
|
||||||
assert.equal(resolveRuntimeMode(true, 'release'), 'release');
|
for (const value of [undefined,null,true,'other']) assert.throws(() => resolveRuntimeMode(value as any), /mode/i);
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class MapStorage implements LocalStoragePort {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolve(search = '?profile=local') {
|
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' }; } } });
|
fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
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';
|
import { FakeTransport } from '../helpers/fake-transport.ts';
|
||||||
|
|
||||||
const CONFIG: RuntimeConfig = {
|
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 },
|
identity: { agentid: 'A', channelid: 'C', gameid: '41', version: 1, marketid: 4 },
|
||||||
source: 'remote', servers: ['ws://agent'], gameserver: 'https://fixture/config', ...remoteFixture(),
|
source: 'remote', servers: ['ws://agent'], gameserver: 'https://fixture/config', ...remoteFixture(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { test } from 'node:test';
|
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({
|
const runtime = new PlatformRuntime({
|
||||||
gameEntry: entry,
|
gameEntry: entry,
|
||||||
resolveRuntimeConfig: () => resolveRuntimeConfig({
|
resolveRuntimeConfig: () => resolveRuntimeConfig({
|
||||||
mode: 'release', hostKind: 'native-settings', search: '',
|
startupConfig: {...STARTUP_CONFIG,mode:'release',useLocalServer:false}, hostKind: 'native-settings', search: '',
|
||||||
win: { settings: {
|
win: { settings: {
|
||||||
getothername(name: string) {
|
getothername(name: string) {
|
||||||
nativeCalls.push(`getothername:${name}`);
|
nativeCalls.push(`getothername:${name}`);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
@@ -39,7 +40,7 @@ async function setup(overrides: { config?: Partial<ResolveRuntimeConfigOptions>;
|
|||||||
const values = new Map<string, string>(), frames = new Set<() => void>();
|
const values = new Map<string, string>(), frames = new Set<() => void>();
|
||||||
let fetches = 0;
|
let fetches = 0;
|
||||||
const host = api.createLocalLoginHost({
|
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 },
|
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); } },
|
storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } },
|
||||||
random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }),
|
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, []);
|
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 });
|
const h = await setup({ config });
|
||||||
await assert.rejects(h.host.start(), /local startup/);
|
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);
|
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();
|
t.after(() => h.host.stop()); await h.ready();
|
||||||
assert.equal(JSON.stringify(h.evidence).includes('DO_NOT_RECORD_REMOTE_SECRET'), false);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async function setup(overrides: {
|
|||||||
const module = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts')
|
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)}`));
|
.catch((error: unknown) => assert.fail(`LocalLoginTransport implementation must exist: ${String(error)}`));
|
||||||
const context = await resolveLocalStartup({
|
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' }; } },
|
fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } },
|
||||||
});
|
});
|
||||||
const sockets: FakeSocket[] = [];
|
const sockets: FakeSocket[] = [];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function fixture(name: string) {
|
|||||||
async function setup(recordHook?: (event: { kind: string; data: unknown }) => void) {
|
async function setup(recordHook?: (event: { kind: string; data: unknown }) => void) {
|
||||||
const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-wire.ts')
|
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)}`));
|
.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' }; } } });
|
fetcher: { async fetch() { return { player_server_tcp: 'remote.example:3088' }; } } });
|
||||||
const transports: FakeTransport[] = [];
|
const transports: FakeTransport[] = [];
|
||||||
const events: WireEvent[] = [];
|
const events: WireEvent[] = [];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
|
} from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
|
||||||
|
|
||||||
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release',
|
||||||
isDebugger: false,
|
isDebugger: false,
|
||||||
identity: Object.freeze({
|
identity: Object.freeze({
|
||||||
agentid: 'agent-from-config',
|
agentid: 'agent-from-config',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -30,7 +31,7 @@ import {
|
|||||||
} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
|
} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
|
||||||
|
|
||||||
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release',
|
||||||
isDebugger: false,
|
isDebugger: false,
|
||||||
identity: Object.freeze({
|
identity: Object.freeze({
|
||||||
agentid: 17,
|
agentid: 17,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -48,7 +49,7 @@ const DEVICE = Object.freeze({
|
|||||||
|
|
||||||
function runtimeConfig(gameid: string | number = '41'): RuntimeConfig {
|
function runtimeConfig(gameid: string | number = '41'): RuntimeConfig {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
mode: 'debug',
|
startupConfig: STARTUP_CONFIG, mode: 'debug',
|
||||||
isDebugger: true,
|
isDebugger: true,
|
||||||
identity: Object.freeze({
|
identity: Object.freeze({
|
||||||
agentid: 'veRa0qrBf',
|
agentid: 'veRa0qrBf',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -11,11 +12,11 @@ const IDENTITY = { agentid: 'A', channelid: 'C', gameid: 'G', marketid: 4, versi
|
|||||||
|
|
||||||
function bootstrapOf(): BootstrapResult {
|
function bootstrapOf(): BootstrapResult {
|
||||||
return {
|
return {
|
||||||
mode: 'debug', isDebugger: true,
|
startupConfig: STARTUP_CONFIG, mode: 'debug', isDebugger: true,
|
||||||
identity: IDENTITY,
|
identity: IDENTITY,
|
||||||
servers: ['ws://test'],
|
servers: ['ws://test'],
|
||||||
...remoteFixture(),
|
...remoteFixture(),
|
||||||
profile: { name: 'debug' },
|
profile: { name: 'local' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { STARTUP_CONFIG } from '../../YouleNexus/assets/framework/config/profiles.ts';
|
||||||
import { remoteFixture } from '../fixtures/remote-config.ts';
|
import { remoteFixture } from '../fixtures/remote-config.ts';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
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({
|
const RUNTIME_CONFIG: RuntimeConfig = Object.freeze({
|
||||||
mode: 'release',
|
startupConfig: {...STARTUP_CONFIG,mode:'release' as const,useLocalServer:false}, mode: 'release',
|
||||||
isDebugger: false,
|
isDebugger: false,
|
||||||
identity: Object.freeze({
|
identity: Object.freeze({
|
||||||
agentid: 'fixture-agent', channelid: 'fixture-channel', gameid: '41',
|
agentid: 'fixture-agent', channelid: 'fixture-channel', gameid: '41',
|
||||||
|
|||||||
Reference in New Issue
Block a user