feat(config): add scoped local visitor accounts

This commit is contained in:
2026-09-05 14:43:11 +08:00
parent c6f1103b32
commit eb5fb44d97
4 changed files with 317 additions and 0 deletions
@@ -0,0 +1,144 @@
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 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';
class MapStorage implements LocalStoragePort {
readonly values = new Map<string, string>();
readonly writes: Array<[string, string]> = [];
getItem(key: string): string | null { return this.values.get(key) ?? null; }
setItem(key: string, value: string): void { this.writes.push([key, value]); this.values.set(key, value); }
}
function context(overrides: Partial<typeof BUILD_IDENTITY> = {}): LocalStartupContext {
return { hostKind: 'h5', profile: 'local', config: {
mode: 'debug', isDebugger: true, source: 'direct', gameserver: null, rawConfig: null,
servers: PROFILES.local!.servers!, identity: { ...BUILD_IDENTITY, ...overrides },
} };
}
test('concurrent account reads share one generated frozen identity and one write', async () => {
const storage = new MapStorage();
let randomCalls = 0;
const provider = createLocalVisitorProvider({ context: context(), storage,
random: () => { randomCalls += 1; return 0.5; }, isSessionActive: () => false });
const [first, second] = await Promise.all([provider.getAccount(), provider.getAccount()]);
assert.strictEqual(first, second);
assert.deepEqual(first, { openid: 'testopenid_50000', unionid: 'ylgame50000', nickname: '50000_游客',
avatar: 'https://img2.baidu.com/it/u=3081347414,2363887197&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
sex: 2, province: 'jiangxi', city: 'nanchang' });
assert.equal(Object.isFrozen(first), true);
assert.equal(randomCalls, 3);
assert.equal(storage.writes.length, 1);
});
test('random boundaries select first and last valid values', async () => {
for (const [samples, expected] of [
[[0, 0, 0], ['testopenid_0', 1, 'u=1823726962,2692568839']],
[[0.999999, 0.999999, 0.999999], ['testopenid_99999', 2, 'u=1801052125,1596537112']],
] as const) {
const queue = [...samples];
const account = await createLocalVisitorProvider({ context: context(), storage: new MapStorage(),
random: () => queue.shift()!, isSessionActive: () => false }).getAccount();
assert.equal(account.openid, expected[0]);
assert.equal(account.sex, expected[1]);
assert.match(account.avatar, new RegExp(expected[2]));
}
});
test('rejects every invalid random sample without writing', async () => {
for (const invalid of [Number.NaN, Number.POSITIVE_INFINITY, -0.01, 1]) {
const storage = new MapStorage();
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
random: () => invalid, isSessionActive: () => false }).getAccount(), /random.*finite|random.*range/i);
assert.equal(storage.writes.length, 0);
}
});
test('reuses only an exact schema record and never randomizes it', async () => {
const storage = new MapStorage();
const account = { openid: 'cached', nickname: '', avatar: 'avatar', sex: 1, province: '', city: '', unionid: 'u' };
storage.values.set(localAccountKey(context()), JSON.stringify({ schemaVersion: 1, account }));
const loaded = await createLocalVisitorProvider({ context: context(), storage,
random: () => { throw new Error('random forbidden'); }, isSessionActive: () => false }).getAccount();
assert.deepEqual(loaded, account);
assert.equal(Object.isFrozen(loaded), true);
assert.equal(storage.writes.length, 0);
});
test('rejects malformed cached records with key and field path without rewriting or leaking text', async () => {
const cases: Array<[string, string]> = [
['{secret-account-text', '$'],
[JSON.stringify({ schemaVersion: 2, account: {} }), 'schemaVersion'],
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x' } }), 'account.nickname'],
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x', nickname: '', avatar: '', sex: 1, province: '', city: '', unionid: 'u' } }), 'nickname'],
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x', nickname: 'n', avatar: '', sex: 1, province: '', city: '', unionid: 'u', headimgurl: 'old' } }), 'headimgurl'],
[JSON.stringify({ schemaVersion: 1, account: { openid: 'x', nickname: 'n', avatar: '', sex: 1, province: '', city: '', unionid: 'u', Province: 'old' } }), 'Province'],
];
for (const [raw, path] of cases) {
const storage = new MapStorage();
const key = localAccountKey(context());
storage.values.set(key, raw);
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
random: () => 0, isSessionActive: () => false }).getAccount(), (error: unknown) => {
assert.ok(error instanceof Error);
assert.match(error.message, new RegExp(path.replace('.', '\\.')));
assert.match(error.message, /youle\.local-test\.account\.v1:/);
assert.doesNotMatch(error.message, /secret-account-text/);
return true;
});
assert.equal(storage.writes.length, 0);
}
});
test('preserves dependency failures as causes and keeps old storage on generation failure', async () => {
const readFailure = new Error('read broke');
await assert.rejects(createLocalVisitorProvider({ context: context(),
storage: { getItem() { throw readFailure; }, setItem() {} }, random: () => 0, isSessionActive: () => false,
}).getAccount(), (error: unknown) => error instanceof Error && (error as Error & { cause?: unknown }).cause === readFailure);
const storage = new MapStorage();
const key = localAccountKey(context());
storage.values.set(key, 'old-value');
const failure = new Error('generation broke');
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
random: () => { throw failure; }, isSessionActive: () => false }).resetAccount(),
(error: unknown) => error instanceof Error && (error as Error & { cause?: unknown }).cause === failure);
assert.equal(storage.values.get(key), 'old-value');
const writeFailure = new Error('write broke');
await assert.rejects(createLocalVisitorProvider({ context: context(),
storage: { getItem() { return null; }, setItem() { throw writeFailure; } },
random: () => 0, isSessionActive: () => false,
}).getAccount(), (error: unknown) => error instanceof Error && (error as Error & { cause?: unknown }).cause === writeFailure);
});
test('scope isolates identity types, ignores version, and serializes the server configuration', () => {
const base = context({ marketid: 7, version: 1 });
assert.notEqual(localAccountKey(base), localAccountKey(context({ marketid: '7', version: 1 })));
assert.equal(localAccountKey(base), localAccountKey(context({ marketid: 7, version: 999 })));
assert.equal(localAccountKey(base),
'youle.local-test.account.v1:["local",["ws://127.0.0.1:3088"],"veRa0qrBf0df2K1G4de2tgfmVxB2jxpv","J9r4022t80pxlb01zOK76DN1WD4QewFl","FtJf073aa0d6rI1xD8J1Y42fINTm0ziK",7]');
});
test('reset is isolated, refuses active sessions, and refuses overlap with getAccount', async () => {
const storage = new MapStorage();
storage.values.set('visitorinfo', 'legacy');
storage.values.set(LOCAL_MACHINE_KEY, 'machine');
await assert.rejects(createLocalVisitorProvider({ context: context(), storage,
random: () => 0, isSessionActive: () => true }).resetAccount(), /active session/i);
assert.equal(storage.writes.length, 0);
const provider = createLocalVisitorProvider({ context: context(), storage: new MapStorage(),
random: () => 0, isSessionActive: () => false });
const pending = provider.getAccount();
await assert.rejects(provider.resetAccount(), /pending|getAccount/i);
await pending;
const reset = await createLocalVisitorProvider({ context: context(), storage,
random: () => 0, isSessionActive: () => false }).resetAccount();
assert.equal(reset.openid, 'testopenid_0');
assert.deepEqual(storage.writes.map(([key]) => key), [localAccountKey(context())]);
assert.equal(storage.values.get('visitorinfo'), 'legacy');
assert.equal(storage.values.get(LOCAL_MACHINE_KEY), 'machine');
});