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({ mode: 'release', 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({ mode: 'release', 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_,settings 不参与身份或 gameserver', async () => { const fetcher = returning(REMOTE); const result = await resolveRuntimeConfig({ mode: 'release', 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({ mode: 'release', 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({ mode: 'release', hostKind: 'uAgent_3', win, search: '', fetcher: returning(REMOTE), }), new RegExp(`identity.*${sample.field}.*finite`, 'i'), ); } await assert.rejects( resolveRuntimeConfig({ mode: 'release', 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({ mode: 'release', 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({ mode: 'debug', 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({ mode: 'release', 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({ mode: 'release', 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({ mode: 'debug', 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 }); 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({ mode, hostKind: 'h5', win: {}, search: '', fetcher }), /game_server_tcp\/player_server_tcp/); assert.equal(fetcher.calls.length, 1); } });