219 lines
10 KiB
TypeScript
219 lines
10 KiB
TypeScript
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';
|
||
import { resolveRuntimeConfig } from '../../YouleNexus/assets/framework/config/runtime-config.ts';
|
||
import type { ConfigFetcher } from '../../YouleNexus/assets/framework/config/remote-config.ts';
|
||
|
||
function returning(value: unknown): ConfigFetcher & { calls: string[] } {
|
||
const calls: string[] = [];
|
||
return {
|
||
calls,
|
||
async fetch(gameserver: string) { calls.push(gameserver); return value; },
|
||
};
|
||
}
|
||
|
||
const REMOTE = { player_server_tcp: ['5.5.5.5:5000', '5.5.5.5:5000'] };
|
||
|
||
test('h5:只读 query 身份,不访问原生宿主,并生成严格 remote 配置', async () => {
|
||
const fetcher = returning(REMOTE);
|
||
const throwingSettings = new Proxy({}, { get() { throw new Error('native access forbidden'); } });
|
||
const result = await resolveRuntimeConfig({
|
||
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||
hostKind: 'h5',
|
||
win: { settings: throwingSettings },
|
||
search: '?agentid=HA&channelid=HC',
|
||
fetcher,
|
||
cacheBust: () => '123',
|
||
});
|
||
|
||
assert.deepEqual(result.identity, { ...BUILD_IDENTITY, agentid: 'HA', channelid: 'HC' });
|
||
assert.deepEqual(result.servers, ['ws://5.5.5.5:5000', 'ws://5.5.5.5:5000']);
|
||
assert.equal(result.source, 'remote');
|
||
assert.equal(result.gameserver, 'https://tsgames.daoqi88.cn/config/update_jsonv2.txt');
|
||
assert.equal(result.rawConfig, REMOTE);
|
||
assert.deepEqual(fetcher.calls, ['https://tsgames.daoqi88.cn/config/update_jsonv2.txt']);
|
||
assert.equal(Object.isFrozen(result), true);
|
||
assert.equal(Object.isFrozen(result.identity), true);
|
||
assert.equal(Object.isFrozen(result.servers), true);
|
||
});
|
||
|
||
test('native-settings:使用四个原生方法,gameconfig 是唯一 gameserver 注入名', async () => {
|
||
const calls: string[] = [];
|
||
const fetcher = returning(REMOTE);
|
||
const result = await resolveRuntimeConfig({
|
||
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||
hostKind: 'native-settings',
|
||
win: {
|
||
settings: {
|
||
getothername(name: string) { calls.push(`getothername:${name}`); return name === 'agent' ? 'NA' : 'host-path#80-a'; },
|
||
getchannelName() { calls.push('getchannelName'); return 'NC'; },
|
||
getmarketname() { calls.push('getmarketname'); return 7; },
|
||
},
|
||
},
|
||
search: '',
|
||
fetcher,
|
||
});
|
||
|
||
assert.deepEqual(calls, ['getothername:agent', 'getchannelName', 'getmarketname', 'getothername:gameconfig']);
|
||
assert.deepEqual(result.identity, {
|
||
agentid: 'NA', channelid: 'NC', gameid: BUILD_IDENTITY.gameid, marketid: 7, version: BUILD_IDENTITY.version,
|
||
});
|
||
assert.equal(result.gameserver, 'http://host/path:80/a.txt');
|
||
assert.deepEqual(fetcher.calls, ['http://host/path:80/a.txt']);
|
||
});
|
||
|
||
test('uAgent_3:只读 app_<name>,settings 不参与身份或 gameserver', async () => {
|
||
const fetcher = returning(REMOTE);
|
||
const result = await resolveRuntimeConfig({
|
||
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||
hostKind: 'uAgent_3',
|
||
win: {
|
||
settings: new Proxy({}, { get() { throw new Error('settings forbidden'); } }),
|
||
app_agent: 'UA', app_channel: 'UC', app_market: 9, app_gameconfig: 'u-host#80-cfg',
|
||
},
|
||
search: '',
|
||
fetcher,
|
||
});
|
||
|
||
assert.deepEqual(result.identity, {
|
||
agentid: 'UA', channelid: 'UC', gameid: BUILD_IDENTITY.gameid, marketid: 9, version: BUILD_IDENTITY.version,
|
||
});
|
||
assert.equal(result.gameserver, 'http://u/host:80/cfg.txt');
|
||
});
|
||
|
||
test('native-settings:不完整最终身份显式拒绝,不拿 build identity 兜底', async () => {
|
||
await assert.rejects(
|
||
resolveRuntimeConfig({
|
||
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'native-settings', win: {}, search: '', fetcher: returning(REMOTE),
|
||
}),
|
||
/identity.*agentid.*channelid/i,
|
||
);
|
||
});
|
||
|
||
test('uAgent_3:所有允许 number 的身份字段都拒绝 NaN 和无穷值', async () => {
|
||
const cases = [
|
||
{ key: 'app_agent', value: Number.NaN, field: 'agentid' },
|
||
{ key: 'app_channel', value: Number.POSITIVE_INFINITY, field: 'channelid' },
|
||
{ key: 'app_market', value: Number.NEGATIVE_INFINITY, field: 'marketid' },
|
||
] as const;
|
||
for (const sample of cases) {
|
||
const win = {
|
||
app_agent: 'A', app_channel: 'C', app_market: 4, app_gameconfig: '',
|
||
[sample.key]: sample.value,
|
||
};
|
||
await assert.rejects(
|
||
resolveRuntimeConfig({
|
||
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({
|
||
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||
hostKind: 'h5',
|
||
win: {},
|
||
search: '?agentid=A&channelid=C&version=NaN',
|
||
fetcher: returning(REMOTE),
|
||
}),
|
||
/identity.*version.*finite/i,
|
||
);
|
||
});
|
||
|
||
test('uAgent_3:拒绝宿主提供的非法身份字段类型', async () => {
|
||
await assert.rejects(
|
||
resolveRuntimeConfig({
|
||
startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false},
|
||
hostKind: 'uAgent_3',
|
||
win: { app_agent: true, app_channel: 'C', app_market: 4, app_gameconfig: '' },
|
||
search: '',
|
||
fetcher: returning(REMOTE),
|
||
}),
|
||
/identity.*agentid.*type/i,
|
||
);
|
||
});
|
||
|
||
test('显式 local debug:远程配置保留内存,连接服务器来自 profile', async () => {
|
||
const fetcher = returning(REMOTE);
|
||
const result = await resolveRuntimeConfig({
|
||
startupConfig: STARTUP_CONFIG, hostKind: 'h5', win: {}, search: '?profile=local', fetcher,
|
||
});
|
||
assert.equal(result.source, 'remote');
|
||
assert.ok(result.gameserver);
|
||
assert.equal(result.rawConfig, REMOTE);
|
||
assert.strictEqual(result.remoteConfig.raw, result.rawConfig);
|
||
assert.deepEqual(result.servers, ['ws://127.0.0.1:3088']);
|
||
assert.equal(fetcher.calls.length, 1);
|
||
});
|
||
|
||
test('远程抓取失败显式报错,不降级 direct', async () => {
|
||
const fetcher: ConfigFetcher = { async fetch() { throw new Error('net down'); } };
|
||
await assert.rejects(
|
||
resolveRuntimeConfig({ startupConfig: {...STARTUP_CONFIG, mode: 'release', useLocalServer: false}, hostKind: 'h5', win: {}, search: '', fetcher }),
|
||
/远程配置抓取失败/,
|
||
);
|
||
});
|
||
|
||
|
||
test('release ignores local profile but still fetches and selects remote servers', async () => {
|
||
const fetcher = returning(REMOTE);
|
||
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);
|
||
});
|
||
|
||
|
||
test('JSON parse errors retain their classification through the runtime boundary', async () => {
|
||
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({ 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({ 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);
|
||
assert.equal(result.remoteConfig.getValue('notice'), 'kept');
|
||
});
|
||
|
||
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({ 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}),/远程配置抓取失败/);
|
||
});
|