test: verify local login sources with protocol builder

This commit is contained in:
2026-09-05 14:57:59 +08:00
parent e00d051112
commit baafc4a4f4
2 changed files with 167 additions and 26 deletions
@@ -0,0 +1,121 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { resolveLocalStartup } from '../../YouleNexus/assets/framework/config/local-startup.ts';
import { BUILD_IDENTITY } from '../../YouleNexus/assets/framework/config/sources/defaults.ts';
import { localAccountKey, type LocalStoragePort } from '../../YouleNexus/assets/framework/adapters/local/local-storage.ts';
import { createLocalLoginSources } from '../../YouleNexus/assets/framework/adapters/local/login-sources.ts';
import { buildLoginRequest } from '../../YouleNexus/assets/framework/protocol/contracts/index.ts';
class MapStorage implements LocalStoragePort {
readonly values = new Map<string, string>();
getItem(key: string): string | null { return this.values.get(key) ?? null; }
setItem(key: string, value: string): void { this.values.set(key, value); }
}
function resolve(search = '?profile=local') {
return resolveLocalStartup({ mode: 'debug', hostKind: 'h5', win: {}, search,
fetcher: { async fetch(): Promise<never> { throw new Error('forbidden fetch'); } } });
}
function sourcesFor(context: Awaited<ReturnType<typeof resolve>>, storage: LocalStoragePort,
overrides: Partial<Parameters<typeof createLocalLoginSources>[0]> = {}) {
return createLocalLoginSources({ context, storage, random: () => 0.5, now: () => 123456,
sampleHost: () => ({ location: null }), isSessionActive: () => false, ...overrides });
}
test('real local resolution and visitor sources build the login envelope with query identity and omitted optional fields', async () => {
const context = await resolve('?profile=local&version=731&agentid=integration-agent&channelid=integration-channel&marketid=009');
const sources = sourcesFor(context, new MapStorage());
const account = await sources.prepareLogin();
const envelope = buildLoginRequest(context.config, account, sources.getLoginDeviceSnapshot());
assert.equal(envelope.app, 'youle');
assert.equal(envelope.route, 'agent');
assert.equal(envelope.rpc, 'player_login');
assert.equal(envelope.data.gameid, BUILD_IDENTITY.gameid);
assert.equal(envelope.data.version, 731);
assert.equal(typeof envelope.data.version, 'number');
assert.equal(envelope.data.agentid, 'integration-agent');
assert.equal(envelope.data.channelid, 'integration-channel');
assert.equal(envelope.data.marketid, '009');
assert.equal(envelope.data.openid, 'testopenid_50000');
assert.equal(envelope.data.unionid, 'ylgame50000');
assert.equal(envelope.data.province, 'jiangxi');
assert.equal(envelope.data.city, 'nanchang');
assert.equal(envelope.data.location, null);
assert.equal(envelope.data.machineid, '123456-5499');
for (const field of ['playerid', 'telphone', 'telphoneAuto', 'ip']) {
assert.equal(Object.prototype.hasOwnProperty.call(envelope.data, field), false, field);
}
});
test('one host sample overrides province and city in the real envelope without changing the visitor cache', async () => {
const context = await resolve();
const storage = new MapStorage();
const location = { longitude: 115.9, latitude: 28.7 };
const city = { ip: '10.0.0.2', province: 'P', city: 'C' };
let samples = 0;
const sources = sourcesFor(context, storage, {
sampleHost: () => { samples++; return { location, returnCitySN: city }; },
});
const account = await sources.prepareLogin();
city.ip = 'changed'; city.province = 'changed'; city.city = 'changed';
const envelope = buildLoginRequest(context.config, account, sources.getLoginDeviceSnapshot());
assert.equal(samples, 1);
assert.equal(envelope.data.province, 'P');
assert.equal(envelope.data.city, 'C');
assert.equal(envelope.data.ip, '10.0.0.2');
assert.strictEqual(envelope.data.location, location);
const cached = JSON.parse(storage.getItem(localAccountKey(context))!);
assert.equal(cached.account.province, 'jiangxi');
assert.equal(cached.account.city, 'nanchang');
});
test('a version upgrade reuses stored account and machine but sends the newly resolved numeric version', async () => {
const storage = new MapStorage();
const firstContext = await resolve('?profile=local&version=731');
const firstSources = sourcesFor(firstContext, storage);
const firstAccount = await firstSources.prepareLogin();
const first = buildLoginRequest(firstContext.config, firstAccount, firstSources.getLoginDeviceSnapshot());
const originalCache = new Map(storage.values);
const nextContext = await resolve('?profile=local&version=732');
const nextSources = sourcesFor(nextContext, storage, {
random: () => { throw new Error('cached login must not regenerate identity'); },
now: () => { throw new Error('cached login must not regenerate machine'); },
});
const nextAccount = await nextSources.prepareLogin();
const next = buildLoginRequest(nextContext.config, nextAccount, nextSources.getLoginDeviceSnapshot());
assert.equal(first.data.version, 731);
assert.equal(next.data.version, 732);
assert.deepEqual(nextAccount, firstAccount);
assert.equal(next.data.openid, first.data.openid);
assert.equal(next.data.machineid, first.data.machineid);
assert.deepEqual(storage.values, originalCache);
});
test('corrupt and unknown cached account records stop before the real builder and invalidate the prepared device', async (t) => {
const context = await resolve();
const validAccount = { openid: 'cached-user', nickname: 'Visitor', avatar: '', sex: 1,
province: 'P', city: 'C', unionid: 'cached-union' };
for (const [name, raw, error] of [
['corrupt JSON', '{broken', /invalid JSON/],
['unknown schema', JSON.stringify({ schemaVersion: 2, account: validAccount }), /schemaVersion/],
['unknown account field', JSON.stringify({ schemaVersion: 1, account: { ...validAccount, headimgurl: 'legacy' } }), /headimgurl/],
['missing identity', JSON.stringify({ schemaVersion: 1, account: { ...validAccount, openid: '' } }), /openid/],
] as const) {
await t.test(name, async () => {
const storage = new MapStorage();
const sources = sourcesFor(context, storage);
await sources.prepareLogin();
storage.setItem(localAccountKey(context), raw);
let builderReached = false;
await assert.rejects(async () => {
const account = await sources.prepareLogin();
builderReached = true;
return buildLoginRequest(context.config, account, sources.getLoginDeviceSnapshot());
}, error);
assert.equal(builderReached, false);
assert.throws(() => sources.getLoginDeviceSnapshot(), /prepare/i);
assert.equal(storage.getItem(localAccountKey(context)), raw);
});
}
});
@@ -1,6 +1,6 @@
# Startup Configuration and Visitor Sources Implementation Plan # Startup Configuration and Visitor Sources Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** 交付可供后续应用根消费的 local 启动配置、游客缓存与设备来源,并通过真实登录 builder 联合验证。 **Goal:** 交付可供后续应用根消费的 local 启动配置、游客缓存与设备来源,并通过真实登录 builder 联合验证。
@@ -47,7 +47,7 @@ git diff --check
- `assertLocalStartupContext(context: LocalStartupContext): void`。 - `assertLocalStartupContext(context: LocalStartupContext): void`。
- `assertLocalConnectionTarget(context: LocalStartupContext, url: string): void`。 - `assertLocalConnectionTarget(context: LocalStartupContext, url: string): void`。
- [ ] RED:创建测试,使用永远抛错的 fetcher 验证 local 解析不联网: - [x] RED:创建测试,使用永远抛错的 fetcher 验证 local 解析不联网:
```ts ```ts
const options = { mode: 'debug' as const, hostKind: 'h5' as const, const options = { mode: 'debug' as const, hostKind: 'h5' as const,
@@ -60,8 +60,8 @@ await assert.rejects(resolveLocalStartup({ ...options, search: '' }), /profile/)
await assert.rejects(resolveLocalStartup({ ...options, mode: 'release' }), /debug/); await assert.rejects(resolveLocalStartup({ ...options, mode: 'release' }), /debug/);
``` ```
- [ ] 运行上述 Task 1 命令,确认缺失模块为 RED。 - [x] 运行上述 Task 1 命令,确认缺失模块为 RED。
- [ ] 实现:先检查 mode/hostKind,query 必须恰有一个 profile=local;mode 若出现只能恰有一个 debug;拒绝非空 gameconfig 及歧义重复控制参数,之后才调用一次 resolveRuntimeConfig。不重建或复制 identity。 - [x] 实现:先检查 mode/hostKind,query 必须恰有一个 profile=local;mode 若出现只能恰有一个 debug;拒绝非空 gameconfig 及歧义重复控制参数,之后才调用一次 resolveRuntimeConfig。不重建或复制 identity。
```ts ```ts
const config = await resolveRuntimeConfig(options); const config = await resolveRuntimeConfig(options);
@@ -71,8 +71,8 @@ return context;
``` ```
context 校验 config.mode=debug、isDebugger=true、source=direct、gameserver/rawConfig=null;有效服务器列表须逐项严格等于 PROFILES.local.servers。解析每个 URL,协议只允许 ws:/wss:,hostname 限 localhost、127.0.0.0/8、[::1],禁止凭据。连接目标必须既在权威列表中又满足上述检查;不重复定义端口/地址。 context 校验 config.mode=debug、isDebugger=true、source=direct、gameserver/rawConfig=null;有效服务器列表须逐项严格等于 PROFILES.local.servers。解析每个 URL,协议只允许 ws:/wss:,hostname 限 localhost、127.0.0.0/8、[::1],禁止凭据。连接目标必须既在权威列表中又满足上述检查;不重复定义端口/地址。
- [ ] RED/GREEN 逐项增加:native/release/prod/staging/mode 冲突、重复 query 拒绝且 fetch 调用为零;query 身份覆盖、version 数字、gameid 不可覆盖;伪造 context/远程切服目标拒绝;原 native 缺失身份测试继续通过。对错误字段抛有字段名的 Error。 - [x] RED/GREEN 逐项增加:native/release/prod/staging/mode 冲突、重复 query 拒绝且 fetch 调用为零;query 身份覆盖、version 数字、gameid 不可覆盖;伪造 context/远程切服目标拒绝;原 native 缺失身份测试继续通过。对错误字段抛有字段名的 Error。
- [ ] 执行 Task 1、既有 config/runtime-config.test.ts、类型与导入检查;规格/质量审查通过后只提交本任务文件。 - [x] 执行 Task 1、既有 config/runtime-config.test.ts、类型与导入检查;规格/质量审查通过后只提交本任务文件。
## Task 2: 游客账号生成与独立缓存 ## Task 2: 游客账号生成与独立缓存
@@ -84,7 +84,7 @@ context 校验 config.mode=debug、isDebugger=true、source=direct、gameserver/
- `createLocalVisitorProvider({ context, storage, random, isSessionActive }): LocalVisitorProvider`,依赖均必需。 - `createLocalVisitorProvider({ context, storage, random, isSessionActive }): LocalVisitorProvider`,依赖均必需。
- `LocalVisitorProvider { getAccount(): Promise<LoginAccountIdentity>; resetAccount(): Promise<LoginAccountIdentity> }`;isSessionActive 为 `() => boolean`,只用于拒绝活动会话内 reset。 - `LocalVisitorProvider { getAccount(): Promise<LoginAccountIdentity>; resetAccount(): Promise<LoginAccountIdentity> }`;isSessionActive 为 `() => boolean`,只用于拒绝活动会话内 reset。
- [ ] RED:测试中用 Map 实现存储,注入计数随机函数,调用并发 getAccount: - [x] RED:测试中用 Map 实现存储,注入计数随机函数,调用并发 getAccount:
```ts ```ts
let randomCalls = 0; let randomCalls = 0;
@@ -98,9 +98,9 @@ assert.equal(first.sex, 2);
assert.equal(randomCalls, 3); assert.equal(randomCalls, 3);
``` ```
- [ ] Run `node --import tsx --test framework-tests/adapters/local/visitor-account.test.ts`,确认模块缺失 RED。 - [x] Run `node --import tsx --test framework-tests/adapters/local/visitor-account.test.ts`,确认模块缺失 RED。
- [ ] 实现唯一 scope key:JSON.stringify([context.profile, config.servers, identity.agentid, identity.gameid, identity.channelid, identity.marketid]);不包括 version。构造 provider 先 assertLocalStartupContext。 - [x] 实现唯一 scope key:JSON.stringify([context.profile, config.servers, identity.agentid, identity.gameid, identity.channelid, identity.marketid]);不包括 version。构造 provider 先 assertLocalStartupContext。
- [ ] 从原 05_Func.js simulatePlayerInfo 逐字迁移十项头像 URL 到 visitor 来源内唯一列表;不复制源码取值错误。三次随机分别生成编号、头像索引、性别,每次验证 finite 且 0≤r<1。 - [x] 从原 05_Func.js simulatePlayerInfo 逐字迁移十项头像 URL 到 visitor 来源内唯一列表;不复制源码取值错误。三次随机分别生成编号、头像索引、性别,每次验证 finite 且 0≤r<1。
```ts ```ts
const id = Math.floor(sample(random) * 100000); const id = Math.floor(sample(random) * 100000);
@@ -112,10 +112,10 @@ storage.setItem(key, JSON.stringify({ schemaVersion: 1, account }));
return account; return account;
``` ```
- [ ] 读取使用 schemaVersion=1 且准确记录结构;account 七字段完整、字符串/sex 类型正确,openid/unionid 不为空,nickname/avatar 不能同时为空。记录不接受未知字段或旧 headimgurl/Province 格式,不猜版本。错误带缓存键及字段路径,保留原错误原因;不泄露完整账号缓存文本。 - [x] 读取使用 schemaVersion=1 且准确记录结构;account 七字段完整、字符串/sex 类型正确,openid/unionid 不为空,nickname/avatar 不能同时为空。记录不接受未知字段或旧 headimgurl/Province 格式,不猜版本。错误带缓存键及字段路径,保留原错误原因;不泄露完整账号缓存文本。
- [ ] 对 getAccount 缓存一个进行中的 Promise;finally 清除 pending,读取成功可返回冻结账号;reset 与 pending 冲突明确拒绝。reset 前检查 isSessionActive;生成与校验成功后直接 setItem 替换,不先删旧值,失败保留存储原值。 - [x] 对 getAccount 缓存一个进行中的 Promise;finally 清除 pending,读取成功可返回冻结账号;reset 与 pending 冲突明确拒绝。reset 前检查 isSessionActive;生成与校验成功后直接 setItem 替换,不先删旧值,失败保留存储原值。
- [ ] RED/GREEN:上下随机边界、NaN/Infinity/负数/1 拒绝;损坏 JSON/缺字段/schema 错误拒绝且不写;读写异常;跨 scope 隔离、数字/字符串 marketid 区别、version 改动复用;reset 只写当前账号键,活动会话拒绝,不操作旧产品键/机器键。复用时不随机化。 - [x] RED/GREEN:上下随机边界、NaN/Infinity/负数/1 拒绝;损坏 JSON/缺字段/schema 错误拒绝且不写;读写异常;跨 scope 隔离、数字/字符串 marketid 区别、version 改动复用;reset 只写当前账号键,活动会话拒绝,不操作旧产品键/机器键。复用时不随机化。
- [ ] 测试、类型与导入检查、规格/质量审查,通过后 scoped commit。 - [x] 测试、类型与导入检查、规格/质量审查,通过后 scoped commit。
## Task 3: 设备缓存与同一次登录采样 ## Task 3: 设备缓存与同一次登录采样
@@ -127,7 +127,7 @@ return account;
- `LocalLoginSources { prepareLogin(): Promise<LoginAccountIdentity>; getLoginDeviceSnapshot(): LoginDeviceSnapshot; resetAccount(): Promise<LoginAccountIdentity> }`。 - `LocalLoginSources { prepareLogin(): Promise<LoginAccountIdentity>; getLoginDeviceSnapshot(): LoginDeviceSnapshot; resetAccount(): Promise<LoginAccountIdentity> }`。
- 内部复用 Task 2 provider;getLoginDeviceSnapshot 在成功 prepare 前、失败 prepare 后或 reset 后显式拒绝。prepare 并发合并,reset 与 prepare 冲突拒绝。 - 内部复用 Task 2 provider;getLoginDeviceSnapshot 在成功 prepare 前、失败 prepare 后或 reset 后显式拒绝。prepare 并发合并,reset 与 prepare 冲突拒绝。
- [ ] RED:注入 sampleHost 计数,检查 account 和 device 使用同次输入: - [x] RED:注入 sampleHost 计数,检查 account 和 device 使用同次输入:
```ts ```ts
let samples = 0; let samples = 0;
@@ -142,15 +142,15 @@ assert.equal(samples, 1);
assert.equal(sources.getLoginDeviceSnapshot().machineid, '123456-5499'); assert.equal(sources.getLoginDeviceSnapshot().machineid, '123456-5499');
``` ```
- [ ] Run `node --import tsx --test framework-tests/adapters/local/login-sources.test.ts`,确认 RED。 - [x] Run `node --import tsx --test framework-tests/adapters/local/login-sources.test.ts`,确认 RED。
- [ ] 实现机器缓存:使用 LOCAL_MACHINE_KEY,在存储所绑定的 origin 内复用;存储记录 schemaVersion=1、machineid 字符串。缺键才调用 now/random,now 要求非负安全整数,随机遵循 Task 2 规则(抽公共 sample helper 到 local-storage.ts,避免重复实现)。拒绝损坏记录/非法机器值,保留键名与原错误。 - [x] 实现机器缓存:使用 LOCAL_MACHINE_KEY,在存储所绑定的 origin 内复用;存储记录 schemaVersion=1、machineid 字符串。缺键才调用 now/random,now 要求非负安全整数,随机遵循 Task 2 规则(抽公共 sample helper 到 local-storage.ts,避免重复实现)。拒绝损坏记录/非法机器值,保留键名与原错误。
```ts ```ts
const machineid = `${timestamp}-${1000 + Math.floor(sample(random) * 8999)}`; const machineid = `${timestamp}-${1000 + Math.floor(sample(random) * 8999)}`;
storage.setItem(LOCAL_MACHINE_KEY, JSON.stringify({ schemaVersion: 1, machineid })); storage.setItem(LOCAL_MACHINE_KEY, JSON.stringify({ schemaVersion: 1, machineid }));
``` ```
- [ ] 在所有异步账号准备完成后调用 sampleHost 一次;location 必须显式存在且为 null/record。returnCitySN 存在时要求 ip/province/city 为 string,形成冻结的新 account;不反写 account 缓存。location 原引用透传,不复制未知协议结构。 - [x] 在所有异步账号准备完成后调用 sampleHost 一次;location 必须显式存在且为 null/record。returnCitySN 存在时要求 ip/province/city 为 string,形成冻结的新 account;不反写 account 缓存。location 原引用透传,不复制未知协议结构。
```ts ```ts
const device: LoginDeviceSnapshot = Object.freeze({ location: snapshot.location, const device: LoginDeviceSnapshot = Object.freeze({ location: snapshot.location,
@@ -159,9 +159,9 @@ const device: LoginDeviceSnapshot = Object.freeze({ location: snapshot.location,
...(city === undefined ? {} : { ip: city.ip }) }); ...(city === undefined ? {} : { ip: city.ip }) });
``` ```
- [ ] prepare 开始先使旧设备快照失效,成功时原子保存新 device 并返回 account;失败不留可发送快照。reset 也使快照失效,但不改变机器键。 - [x] prepare 开始先使旧设备快照失效,成功时原子保存新 device 并返回 account;失败不留可发送快照。reset 也使快照失效,但不改变机器键。
- [ ] RED/GREEN:缺 returnCitySN 省略 ip;非法/缺 location 与非法 city 字段拒绝;宿主只采样一次;机器持久复用/损坏/写失败;prepare 后宿主改变不污染当前 ip/省市;reset 不改机器;并发合并与 reset 冲突;第二次 prepare 失败不返回第一次旧 device。 - [x] RED/GREEN:缺 returnCitySN 省略 ip;非法/缺 location 与非法 city 字段拒绝;宿主只采样一次;机器持久复用/损坏/写失败;prepare 后宿主改变不污染当前 ip/省市;reset 不改机器;并发合并与 reset 冲突;第二次 prepare 失败不返回第一次旧 device。
- [ ] 测试、类型与导入检查、规格/质量审查,通过后 scoped commit。 - [x] 测试、类型与导入检查、规格/质量审查,通过后 scoped commit。
## Task 4: 来源到真实协议 builder 联合验收 ## Task 4: 来源到真实协议 builder 联合验收
@@ -169,7 +169,7 @@ const device: LoginDeviceSnapshot = Object.freeze({ location: snapshot.location,
**Interfaces:** 只消费 Tasks 1–3 与既有 `buildLoginRequest(context.config, account, device)`,无新增 Runtime/SDK API。 **Interfaces:** 只消费 Tasks 1–3 与既有 `buildLoginRequest(context.config, account, device)`,无新增 Runtime/SDK API。
- [ ] 增加真实 resolver → sources → builder 联合用例,测试中显式模拟 Storage,不使用引擎或真实网络: - [x] 增加真实 resolver → sources → builder 联合用例,测试中显式模拟 Storage,不使用引擎或真实网络:
```ts ```ts
const account = await sources.prepareLogin(); const account = await sources.prepareLogin();
@@ -184,10 +184,10 @@ assert.equal(Object.hasOwn(envelope.data, 'telphone'), false);
assert.equal(Object.hasOwn(envelope.data, 'ip'), false); assert.equal(Object.hasOwn(envelope.data, 'ip'), false);
``` ```
- [ ] Run `node --import tsx --test framework-tests/integration/local-login-sources.test.ts`。若首次已绿,不制造伪 RED;针对有 returnCitySN 时省市覆盖的临时变异验证测试会失败,立即恢复 TS。 - [x] Run `node --import tsx --test framework-tests/integration/local-login-sources.test.ts`。若首次已绿,不制造伪 RED;针对有 returnCitySN 时省市覆盖的临时变异验证测试会失败,立即恢复 TS。
- [ ] 覆盖真实 resolveIdentity 的 query version/渠道值传入唯一 envelope、缓存 version 升级复用、未知/损坏账号绝不进入 builder;不新增房间或测试 GameEntry。 - [x] 覆盖真实 resolveIdentity 的 query version/渠道值传入唯一 envelope、缓存 version 升级复用、未知/损坏账号绝不进入 builder;不新增房间或测试 GameEntry。
- [ ] 静态检查新增来源不依赖 cc/旧 Session/NetClient/RoomRPCBus,不导出到 SDK;代码没有重复定义 BUILD token、WS endpoint 或协议超时。不要为这项检查修改旧代码。 - [x] 静态检查新增来源不依赖 cc/旧 Session/NetClient/RoomRPCBus,不导出到 SDK;代码没有重复定义 BUILD token、WS endpoint 或协议超时。不要为这项检查修改旧代码。
- [ ] 审阅纯 TS 测试无资源生成副作用后执行: - [x] 审阅纯 TS 测试无资源生成副作用后执行:
```powershell ```powershell
$startupTests = @(rg --files framework-tests -g '*.test.ts') $startupTests = @(rg --files framework-tests -g '*.test.ts')
@@ -209,3 +209,23 @@ BASE_SHA 使用隔离 worktree 实际执行基线,不原样执行占位参数
用户已指定本会话采用 subagent-driven-development,不再询问执行方式。计划自审完成后执行 Task 1;后续 Task 按依赖串行实施,每项完成两阶段审查。 用户已指定本会话采用 subagent-driven-development,不再询问执行方式。计划自审完成后执行 Task 1;后续 Task 按依赖串行实施,每项完成两阶段审查。
真实 Runtime 宿主、GameEntry 隔离、Cocos 资源/展示时长和本地服务器接受身份的证据属于本设计已说明的后续门槛。此计划交付来源模块,不填空加载器、不发旧脚本请求来跨过门槛。 真实 Runtime 宿主、GameEntry 隔离、Cocos 资源/展示时长和本地服务器接受身份的证据属于本设计已说明的后续门槛。此计划交付来源模块,不填空加载器、不发旧脚本请求来跨过门槛。
## 2026-09-05 执行完成记录
隔离分支 `codex/startup-config-account`,worktree `.worktrees/startup-config-account`。整分支资源审计基线 `76de943`;Task 4 开始及下列代码验证 HEAD 为 `e00d051`。Tasks 1–3 已分别完成规格与质量审查;Task 4 实现及验证完成,Task 4 两阶段审查及整分支审查仍待 controller 执行。
Task 4 新增真实 `resolveLocalStartup` → `createLocalLoginSources` → contracts `buildLoginRequest` 联合测试,仅 Storage/宿主/时钟/随机来源使用显式测试输入,无引擎或真实网络。覆盖 query 身份与数字 version、可选字段省略、同次省市/IP、version 升级复用账号与机器、损坏/未知缓存拒绝且使旧设备失效。
实际验证(Node 命令工作目录为 `cocoscreator_projects/`):
- `node --import tsx --test framework-tests/integration/local-login-sources.test.ts`:首个 sandbox 调用因 `uv_os_get_passwd ENOMEM` 退出 1,属于环境失败。精确命令在批准环境首次运行即绿,退出 0,8/8 通过。
- 有界变异临时移除 `login-sources.ts` 的宿主省市覆盖,执行同一联合命令:退出 1,7 通过、1 失败,断言为 `'jiangxi' !== 'P'`。`finally` 立即按原始字节恢复 TS,`git diff --exit-code -- YouleNexus/assets/framework/adapters/local/login-sources.ts` 退出 0;同一联合命令恢复后退出 0,8/8 通过。
- `$startupTests = @(rg --files framework-tests -g '*.test.ts')` 显式枚举 56 个纯 TS 测试文件;已审阅其依赖和副作用,不调用资源生成器。`node --import tsx --test --test-concurrency=1 @startupTests` 退出 0,534 通过、0 失败、0 跳过。文件清单保存在 ignored scratch 的 `task-4-selected-tests.txt`。
- `node --test framework-tests/architecture/import-boundaries.test.mjs framework-tests/architecture/presentation-boundaries.test.mjs`:退出 0,49 通过、0 失败、0 跳过。
- `node node_modules/typescript/bin/tsc -p tsconfig.framework.json --noEmit`:退出 0。
- `node scripts/check-import-boundaries.mjs`:退出 0,`[import-boundaries] OK`。
- 静态审阅四个新增来源与全 assets 引用:无 cc/旧 Session/NetClient/RoomRPCBus 依赖,不导出 SDK;BUILD 身份、WS endpoint、协议超时未重复定义。
- `git diff --check`、`git diff --cached --check`:退出 0。`git diff --name-only 76de943 HEAD`、`git diff --name-only HEAD`、`git ls-files --others --exclude-standard` 与 `git status --short` 联合审计仅列出本次 TS 来源/测试和本计划,无资源变化。
- Controller 主 checkout 审计退出 0:43 条初始变更(41 个未跟踪 meta、2 个删除 meta)的 porcelain 路径、现存文件 SHA256 与删除项 MISSING 标记逐项不变。
完整日志和 Task 4 提交 SHA 记录于 ignored scratch `.superpowers/sdd/2026-09-05-startup-config-account/task-4-report.md` 及 `task-4-*.log`,不强制加入 Git。本批未验证真实服务器接受身份、Runtime 宿主、GameEntry 或 Creator 展示效果。