261 lines
13 KiB
Markdown
261 lines
13 KiB
Markdown
# 阶段3C组合与限定联调方案
|
||
|
||
日期:2026-09-23。状态:**独立审查PASS,F1–F6已全部采纳,已批准实施。** 审查与裁决见《阶段3C独立方案审查-2026-09-23.md》和《阶段3C方案审查裁决-2026-09-23.md》;本文定义阶段3C开发边界。
|
||
|
||
## 1. 目标和边界
|
||
|
||
阶段3C负责把已经验收的阶段2协议客户端、3A缓存状态和3B固定会话Owner组合验证,并在严格fixture边界下连接`s101`。目标是证明这些模块能共同完成安全的正常路径,不把测试扩大为游戏存档迁移。
|
||
|
||
正式API固定为`http://s101.local:18080/`,账号只允许既有测试账号`101`和`102`。
|
||
|
||
本阶段完成:
|
||
|
||
- 离线跨实例/跨进程组合验证。
|
||
- 101/102限定正式服正常路径联调。
|
||
- manifest、碰撞预检、精确清理和双账号隔离证据。
|
||
- 测试宿主生成脱敏诊断JSON。
|
||
- Unity兼容编译、源码hash、实施记录和最终审查。
|
||
|
||
本阶段不完成:
|
||
|
||
- 不接`UserService`、`PlayFabMgr`、`CustomServerMgr`、玩法或游戏存档入口。
|
||
- 不修改PlayFab读写,不删除SDK。
|
||
- 不提供自动解锁、人工解锁或强制恢复按钮。
|
||
- 不做真机文件系统、平台断电耐久或最终打包。
|
||
- 不在正式服制造超时、断网、401/403/400/500、迟到响应或UnknownOutcome。
|
||
|
||
## 2. 已选方案和备选比较
|
||
|
||
### 2.1 采用:独立`KVStage3CTests`宿主
|
||
|
||
新增独立.NET测试宿主,显式模式运行:
|
||
|
||
- `--offline`:合成传输、跨进程重启和诊断导出。
|
||
- `--live-s101`:限定正式服正常路径。
|
||
- `--cleanup <manifest>`:只按已有manifest清理遗留fixture。
|
||
|
||
不带模式或参数不合法时直接以非0退出码退出,不允许默认连接正式服或产生任何网络访问。
|
||
|
||
采用理由:阶段2、3A、3B证据保持独立;3C可以同时固定API、账号、缓存根目录和写入key范围;不需要新增生产入口。
|
||
|
||
### 2.2 未采用:拆分离线和正式服两个项目
|
||
|
||
隔离更强,但会重复manifest、身份校验、脱敏报告和结果模型,增加审查面。
|
||
|
||
### 2.3 未采用:扩展既有`KVLiveTests`
|
||
|
||
会混淆阶段2协议联调与阶段3缓存/Owner组合验收,并且现有项目使用PlayerData通配符,不适合作为新的阶段边界。
|
||
|
||
## 3. 文件边界
|
||
|
||
方案审查通过后,实施只允许新增:
|
||
|
||
- `CodexOutput/upserver/KVStage3CTests/KVStage3CTests.csproj`
|
||
- `CodexOutput/upserver/KVStage3CTests/Program.cs`
|
||
- 该目录下运行生成的manifest、结果、脱敏诊断、日志和hash文件。
|
||
- `CodexOutput/upserver/阶段3C实施记录-2026-09-23.md`
|
||
|
||
测试项目必须显式链接以下已验收生产源码,禁止`PlayerData/*.cs`通配符:
|
||
|
||
- `PlayerKVModels.cs`
|
||
- `PlayerKVClient.cs`
|
||
- `PlayerKVSyncModels.cs`
|
||
- `PlayerKVCacheStore.cs`
|
||
- `PlayerKVSyncState.cs`
|
||
- `PlayerKVSyncCoordinator.cs`
|
||
- `PlayerKVSessionOwner.cs`
|
||
- `S101AccountSession.cs`
|
||
|
||
默认**不允许修改任何生产文件、现有测试Program、现有csproj、包、asmdef或服务器代码**。如果现有internal/public接口不足以实现本方案,实施必须停止,在实施记录中给出缺口、最小接口和风险,回到方案差异评审;不得先改生产代码。
|
||
|
||
共享README、TODO、开发状态和接手入口仍由本会话维护,实施过程不得修改。
|
||
|
||
## 4. 安全架构
|
||
|
||
### 4.1 Manifest先行
|
||
|
||
每次运行在任何登录或变更请求前创建manifest。建议前缀:
|
||
|
||
`codex_kv_s3c_YYYYMMDD_<32位guid>`
|
||
|
||
所有后缀加入后仍须满足服务端128字符key上限;key使用Ordinal精确匹配,不测试大小写变体。
|
||
|
||
manifest至少包含:
|
||
|
||
- schemaVersion、runId、创建时间。
|
||
- 精确API、账号列表`[101,102]`。
|
||
- 完整测试前缀和预登记key列表。
|
||
- 模式、缓存根目录的任务内相对路径。
|
||
- collisionPreflight、cleanupAllowed、hadUnknownWrite、completed。
|
||
- 每个账号的清理状态和最后错误类别。
|
||
|
||
manifest可以保存完整测试key以便精确清理,但禁止保存Cookie、Token、测试值、业务key、响应正文或真实存档。
|
||
|
||
manifest必须原子更新;每个账号开始变更前、出现不确定结果时、每次清理读回后立即落盘。
|
||
|
||
### 4.2 受限转发Handler
|
||
|
||
3B Owner初始化固定调用完整Get。如果直接连接正式服,会把真实业务KV写入测试缓存,违反原阶段3方案。因此正式服模式必须使用仅存在于测试宿主中的受限转发Handler:
|
||
|
||
1. 捕获本次已认证CookieContainer,但不输出或序列化。
|
||
2. 只允许目标scheme/host/port/path属于固定API的`playerdata/Get|Set|Delete`。
|
||
3. Owner发起初始化Get时,转发为只读取manifest登记key的过滤Get;返回给Owner的数据中不得包含其它key。
|
||
4. 无`keys`的Get必须改写为manifest登记key;Set/Delete只要包含任一不属于manifest的key,就在进入网络前拒绝整个请求;Set空`data`按no-op放行。
|
||
5. 不修改值、不重试、不跟随重定向,不把失败转成成功。
|
||
6. 任何无法确认请求边界的情况按本地验证失败处理,不发送请求。
|
||
|
||
越界写的Handler单元检查直接断言内部转发计数为0。若通过完整Owner链检查,由于3B保守语义会把变更异常映射为`MayHaveCommitted=true`,必须使用一次性run目录,并预期“转发计数0且Owner冻结为Unknown”;不得为改变该表象而放宽生产语义。
|
||
|
||
该Handler只是测试fixture,不进入Unity生产代码。它验证真实`PlayerKVSessionOwner`、`PlayerKVClient`、Cookie认证和服务端正常路径,同时保证测试缓存只含fixture数据。
|
||
|
||
### 4.3 缓存根目录
|
||
|
||
所有3C缓存只能位于:
|
||
|
||
`C:/projects/ft/CodexOutput/upserver/KVStage3CTests/runs/<runId>/cache`
|
||
|
||
启动时用规范绝对路径验证目标位于上述run目录内。不得使用Unity真实`persistentDataPath`,不得读取、移动或删除玩家现有缓存目录。
|
||
|
||
## 5. 离线组合验证
|
||
|
||
离线模式使用实际生产源码和合成Handler,不访问网络。父进程负责生成run目录并启动有界子进程,以证明文件锁和磁盘证据跨进程有效。
|
||
|
||
### O1 基础组合
|
||
|
||
- 创建101 Owner,完成Get/Reconcile。
|
||
- Set、Delete、localOnly和水位Flush走实际3A/3B组合。
|
||
- 关闭后重新打开,确认已确认数据、pending和localOnly语义一致。
|
||
|
||
### O2 跨进程pending恢复
|
||
|
||
1. 子进程A创建Owner并接受远端编辑,但不Flush;Close不隐式Flush。
|
||
2. 子进程B使用同一缓存和合成远端重新创建Owner,确认pending仍存在并可Flush。
|
||
3. 子进程C再次打开,确认远端观察与本地状态Ready、pending清空。
|
||
|
||
子进程A必须正常`Close`并退出后才启动B。所有子进程必须有确定超时;超时强杀只作为失败收尾并保留run目录证据,不得作为正常模拟崩溃步骤。
|
||
|
||
### O3 账号和环境隔离
|
||
|
||
- 101与102使用同一缓存根但不同身份命名空间,状态和文件互不借用。
|
||
- API指纹变化创建不同命名空间;不得把IP与`s101.local`视为同环境。
|
||
- 同命名空间Owner未Close时第二Owner返回Busy,实际收尾后才可打开。
|
||
|
||
### O4 保守恢复策略组合检查
|
||
|
||
- 合成Unknown后关闭重开,Get观察等于基线或目标都不得自动解除Unknown,不得重发。
|
||
- 主文件损坏或主文件缺失但存在bak/tmp时返回RecoveryRequired;不得用旧备份自动恢复可写。
|
||
- 主文件有效时,旧/损坏bak只记诊断,不冻结有效主档。
|
||
|
||
这些是3A/3B联合路径检查,不复制全部底层故障矩阵。
|
||
|
||
### O5 诊断导出
|
||
|
||
关闭Owner并释放文件锁后,从测试缓存打开状态、读取`Inspect()`不可变快照,生成脱敏JSON。诊断失败不能改变缓存状态,也不能删除证据。
|
||
|
||
## 6. 限定正式服联调
|
||
|
||
### L0 预检
|
||
|
||
- 命令必须显式为`--live-s101`。
|
||
- API逐字符规范化后必须等于`http://s101.local:18080/`。
|
||
- 账号只允许101和102;登录结果必须匹配输入ID且`NewlyCreated=false`。
|
||
- manifest在任何可能的变更前落盘。
|
||
- 使用过滤Get确认两个账号全部manifest key不存在;任一碰撞立即退出,`cleanupAllowed=false`,不得删除碰撞key。
|
||
- 碰撞预检全部通过后才设置`cleanupAllowed=true`。
|
||
|
||
### L1 101正常路径
|
||
|
||
- 使用受限Handler创建真实Owner。
|
||
- 接受两个测试key的Set和一个Delete目标;Flush并逐票据确认Confirmed。
|
||
- Close释放;使用新登录、新Cookie、新Owner和相同测试缓存重新打开。
|
||
- 过滤Get/Reconcile确认Set结果存在、Delete目标不存在、缓存Ready且无pending/unknown。
|
||
|
||
### L2 102隔离
|
||
|
||
- 对相同fixture key名写入不同测试值并确认。
|
||
- 重新读取101,证明102操作不改变101;不得读取或比较业务KV。
|
||
- 关闭102后新登录重开,确认Cookie/Owner隔离和正常持久化。
|
||
|
||
### L3 精确清理
|
||
|
||
- 只Delete manifest登记key;禁止空keys清理、前缀扫描或全量字典改写。
|
||
- `--cleanup`只接受位于`KVStage3CTests/runs`内、schema/API/账号/前缀/key集合全部校验通过的manifest;使用新登录和受限直接`PlayerKVClient`清理,不打开或改写生产缓存状态。
|
||
- 每个账号用新登录和过滤Get读回为空。
|
||
- 再使用第二个新会话复核为空,完成后才把该账号加入`cleanedAccounts`。
|
||
- 两账号都清理、所有正常断言通过、`hadUnknownWrite=false`时才能`completed=true`。
|
||
|
||
### L4 正式服失败退出规则
|
||
|
||
- 不主动故障注入,不自动重试变更请求。
|
||
- 任何变更返回`MayHaveCommitted=true`、Owner进入Unknown、超时或网络中断:立即将`hadUnknownWrite=true`落盘,停止新增写入,本次运行永久判失败。
|
||
- 出现Unknown时不自动执行清理并宣称成功;保留manifest,由操作者确认无测试进程/传输存活后显式运行`--cleanup <manifest>`。
|
||
- `--cleanup`即使成功也只关闭遗留fixture,不把原运行改成通过,也不解除生产缓存Unknown。
|
||
- `--cleanup`自身若出现MayHaveCommitted或网络不确定,同样保留manifest、退出非0,不循环重试Delete。
|
||
- 普通断言失败但所有变更均有确定成功/未发送证据时,可进入正常finally精确清理;清理失败必须保留manifest并退出非0。
|
||
|
||
## 7. 脱敏诊断JSON合同
|
||
|
||
诊断仅由测试宿主生成,不新增运行时或Editor入口。
|
||
|
||
允许字段:
|
||
|
||
- schemaVersion、runId、mode、generatedAtUtc。
|
||
- playerId、epoch、API的SHA256指纹。
|
||
- PlayerKVSyncStatus、revision、nextSequence、hasBaseline、authenticationInvalid。
|
||
- baselineCount、localOnlyCount、pendingCount、conflictCount、activeItemCount、unknownItemCount。
|
||
- 票据状态计数、最后错误类别、manifest文件名、清理状态。
|
||
- manifest测试key的SHA256摘要;不得输出原始key。
|
||
|
||
禁止字段:
|
||
|
||
- Cookie、Token、认证头、完整URL查询、请求/响应正文。
|
||
- baseline/localOnly/pending/conflict中的值或完整JSON。
|
||
- 业务key、测试值、完整本地用户名路径、异常堆栈中可能含敏感请求内容的文本。
|
||
|
||
诊断序列化前构建专用DTO,不得直接序列化`PlayerKVDiagnosticSnapshot`。票据状态计数由测试宿主在Owner关闭前,从票据对象、`WaitForTicketAsync`结果和`FlushAsync`汇总自行累计;`Inspect()`不提供票据历史。
|
||
|
||
## 8. 证据产物
|
||
|
||
每次运行至少生成:
|
||
|
||
- `<runId>.manifest.json`
|
||
- `<runId>.result.json`
|
||
- `<runId>.diagnostic.json`
|
||
- `<runId>.log`(只含测试名称、状态、数量和错误类别)
|
||
|
||
最终目录另生成:
|
||
|
||
- `Latest-offline.log`
|
||
- `Latest-live.log`
|
||
- `Regression-summary.log`
|
||
- `final-source-hashes.json`
|
||
- `unity-compile-final.json`
|
||
|
||
结果文件必须明确:synthetic/live、是否访问s101、是否发生任何Unknown、清理是否双会话确认、未验收边界。
|
||
|
||
除manifest外,`result.json`、`diagnostic.json`和日志均不得回显完整fixture key或测试值,只允许runId、key摘要、数量、状态和错误类别。
|
||
|
||
## 9. 验收门
|
||
|
||
阶段3C通过需要同时满足:
|
||
|
||
1. 离线组合和跨进程用例全部通过。
|
||
2. 正式服101/102正常路径通过,无Unknown、无越界请求。
|
||
3. manifest登记key在两个账号均经双新会话确认不存在。
|
||
4. 脱敏诊断结构检查通过,禁止字段扫描为0。
|
||
5. 阶段2 42、3A 86、3B 29回归保持通过。
|
||
6. Unity CLI Loop编译0错误,PlayerData新增警告0。
|
||
7. 生产源码hash与3B终审基线一致;3C默认不修改生产文件。
|
||
8. 实施记录按任务书格式交付,本会话完成代码审查和文档终审。
|
||
|
||
任一清理未确认、发生Unknown、正式目标不匹配或生产源码意外变化,均不得标记3C通过。
|
||
|
||
## 10. 自审
|
||
|
||
- 无TBD、TODO或待定字段;API、账号、文件边界和模式均已固定。
|
||
- 解决了Owner完整Get与“测试缓存不得包含业务KV”的冲突:正式模式只通过测试宿主受限Handler转发过滤Get。
|
||
- 正式服只验证正常路径;Unknown、取消和故障仍由离线替身验证。
|
||
- 诊断导出是测试产物,不形成生产恢复工具。
|
||
- `tickets`生命周期计数纳入诊断,但不在3C重构票据存储;游戏接线前另评估有界历史。
|
||
- 独立审查已PASS;F1–F6已全部纳入本文和任务书,主会话已批准实施。
|