补齐游戏分发分片续传与失败恢复用例并收口阶段 A 第 5 条
Project CI / AI game creator shell Rust crates (push) Successful in 1m41s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m12s
Project CI / Backend tests (push) Successful in 5m14s
Project CI / Native shell tests (push) Successful in 6m19s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m41s
Project CI / Frontend tests (push) Successful in 2m13s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m41s
Project CI / AI game creator shell web tests (push) Successful in 1m38s
Project CI / Repository checks (push) Successful in 2m12s

- 新增 scripts/check-game-distribution-upload-resume.mjs:prepare 段造 9,437,658 字节真实 ZIP 只传第一片 8,388,608 字节,杀掉 api-server(PID 53844 -> 7728)后 resume 段从对象存储读回权威偏移继续传完、确认 uploaded、送审 202 pending_review
- 上传安全检查脚本改名并扩展为 check-game-distribution-upload-safety:新增「结构合法但摘要与声明不符 -> 409 PACKAGE_MISMATCH -> upload_failed + recoveryAction=reupload -> reset 归零 -> 重传确认成功」失败恢复用例,共 24 项 PASS
- package.json 注册 check:game-distribution-upload-resume 并改名 check:game-distribution-upload-safety
- 游戏分发里程碑阶段 A 第 5 条改为已勾选,写入三段式取证
- decision-log 记录越权一律 404、并发落败给权威偏移两条错误码口径
This commit is contained in:
kdletters
2026-09-28 18:44:16 +08:00
parent 665f88b85f
commit 23de78b476
5 changed files with 600 additions and 10 deletions
@@ -52,7 +52,7 @@
- [x] 服务端接收真实 ZIP 字节,重算摘要/字节数并建立展开清单;只有 metadata 的请求不能获得已上传或已发布状态。
- [x] 缺入口、越界/重复/大小写冲突路径、符号链接、压缩炸弹、敏感内容和额度超限均失败关闭,原私有对象和公开状态保持一致。
- [x] 一份版本只接受一份已确认内容;同 key 同请求重放无重复游戏/版本,不同请求冲突;同版本并发上传不混写。
- [ ] 校验可异步恢复,响应丢失、服务进程退出和客户端重试均回到原版本;确定失败和未知结果在响应中可区分。
- [x] 校验可异步恢复,响应丢失、服务进程退出和客户端重试均回到原版本;确定失败和未知结果在响应中可区分。
- [ ] 正常及失败状态、私有查询和错误 envelope 在 Rust 与 TypeScript DTO 中一致;新增 schema、迁移、表目录与绑定一致。
### 证据要求
@@ -164,8 +164,11 @@
- **本轮顺带修掉一处真实缺陷**:`submit_version` 之前直接调 `get_owner_game_distribution_version`,owner 不匹配被映射成 403,而读版本/上传状态/分片/确认/撤回都按 404 处理——同一条「不属于当前主体」的语义出现两种错误码,送审入口会泄露版本是否存在。改成复用 `load_owner_version_or_404` 后,他人送审作者版本与送审不存在的版本都返回 404;脚本里这两条断言分别覆盖,修复前前者是 403 `FORBIDDEN` 让用例变红。
- 边界:脚本会为「建游戏必须提供封面」写一个 67 字节 PNG 到 dev bucket(走现役直传 + 确认链路);发行包分片与确认全部停在越权拒绝之前,不产生任何对象。
- 已勾选(第 4 条:一份版本只接受一份已确认内容、同 key 重放不产生第二份、换请求体冲突、同版本并发上传不混写)——本轮补齐
- 领域层原有 `idempotency_replays_same_snapshot_and_rejects_digest_conflict`、`validation_failure_can_retry_same_confirmed_package`、`a_version_accepts_only_one_confirmed_package`;**本轮新增** `scripts/check-game-distribution-upload-concurrency.mjs`(`npm run check:game-distribution-upload-concurrency`)在本地真实栈补接口层:同 key 同请求重放建游戏/建版本返回同一条记录、同 key 换请求体建游戏 409 `CONFLICT`、同版本同偏移并发写两片相同字节只成功一片、服务端权威已收字节等于单份包长(没有重复追加)、随后确认校验通过并落到 `uploaded`(重复追加或混写都会让校验失败)、换 key 第二次送审 409 且版本数仍为 1。**17 项全部 PASS**。
- 领域层原有 `idempotency_replays_same_snapshot_and_rejects_digest_conflict`、`validation_failure_can_retry_same_confirmed_package`、`a_version_accepts_only_one_confirmed_package`;**本轮新增** `scripts/check-game-distribution-upload-safety.mjs`(`npm run check:game-distribution-upload-safety`)在本地真实栈补接口层:同 key 同请求重放建游戏/建版本返回同一条记录、同 key 换请求体建游戏 409 `CONFLICT`、同版本同偏移并发写两片相同字节只成功一片、服务端权威已收字节等于单份包长(没有重复追加)、随后确认校验通过并落到 `uploaded`(重复追加或混写都会让校验失败)、换 key 第二次送审 409 且版本数仍为 1。**24 项全部 PASS**(本轮再补「结构合法但摘要与声明不符 → 409 `PACKAGE_MISMATCH` → `upload_failed` → reset → 重传成功」这条失败恢复用例)。
- **本轮顺带修掉并发落败请求的错误码**:分片追加失败时之前直接把 OSS 的 position 冲突映射成 `502 UPSTREAM_ERROR`;现在追加失败后回读权威已收字节,只要长度已前进就返回 409 `PACKAGE_UPLOAD_OFFSET_MISMATCH` 并带上 `receivedBytes`,客户端可直接按权威偏移续传。修复前脚本观察到 `502/UPSTREAM_ERROR`,修复后同一条断言看到 `409/PACKAGE_UPLOAD_OFFSET_MISMATCH receivedBytes=751`。
- 已勾选(第 5 条:校验失败可恢复、进程退出与重试回到原版本、确定失败与未知结果可区分)——本轮补齐
- 「客户端重试回到原版本」与「确定失败可区分」:`scripts/check-game-distribution-upload-safety.mjs`(`npm run check:game-distribution-upload-safety`,**24 项 PASS**)里同 key 重放送审得到 `202 replayed=true` 且仍是同一版本、换 key 重复送审 409 且版本数仍为 1;结构合法但摘要与声明不符的包确认时返回 409 `PACKAGE_MISMATCH`,版本落到 `upload_failed` 且 `recoveryAction=reupload`,与「未知结果」停在同一版本的 `awaiting_upload` + 权威偏移(并发落败那片返回 409 `PACKAGE_UPLOAD_OFFSET_MISMATCH`)区分开。
- 「校验可异步恢复」:同一版本失败后 `POST /api/game-distribution/versions/{id}/package/reset` 把权威已收字节归零,重传正确字节后确认成功回到 `uploaded`;域侧另有 `validation_failure_can_retry_same_confirmed_package`、`recovery_action_covers_every_version_status`。
- 「服务进程退出后回到原版本」:`scripts/check-game-distribution-upload-resume.mjs`(`npm run check:game-distribution-upload-resume`,`prepare` / `resume` 两段)。prepare 造 **9,437,658** 字节真实 ZIP、按分片协议只传第一片 **8,388,608** 字节(8 项 PASS);随后杀掉 api-server(PID `53844`,重启后为 `7728`);resume 重新登录同一作者、`/api/runtime/frontend-config` 仍显示灰度开启、`upload-state` 的权威已收字节仍是 `8,388,608`(来自对象存储而不是进程内存)、从该偏移续传剩余 `1,049,050` 字节、确认成功落到 `uploaded` 并送审 `202 pending_review`(9 项 PASS)。
- 仍未勾选(缺口写具体,避免"看起来做了")
- 第 5 条(校验可异步恢复;响应丢失、服务进程退出和客户端重试回到原版本;确定失败和未知结果可区分):`recovery_action_covers_every_version_status` 覆盖状态到恢复动作的映射;**本轮补齐「客户端重试」**——`check:game-distribution-upload-concurrency` 里同 key 重放送审得到 `202 replayed=true` 且仍是同一个版本、换 key 重复送审 409 且不产生第二份提交。**仍缺**「响应丢失与 api-server 进程退出后回到原版本」:需要在 8 MiB 分片上传中途重启 api-server 再续传的编排用例,本轮未做。
- 第 6 条(状态/私有查询/错误 envelope 的 Rust 与 TS DTO 一致;新增 schema、迁移、表目录与绑定一致):后半句有门禁(`npm run lint` 内的 SpacetimeDB schema guard 覆盖 85 张表、生成绑定校验通过)。**本轮新增** `check:game-distribution-dto-parity`(已接进 `npm run lint`):按显式映射表逐字段/逐变体比对 14 组 Rust `shared-contracts` DTO 与手写 `packages/shared/src/contracts/gameDistribution.ts`,两个方向都做过变异验证——TS 侧把 `name` 改成 `displayName`、Rust 侧给 `GameDistributionAuthor` 加 `extra_field`,各自都让门禁失败并指出缺哪个字段;脚本同时登记了 7 个「服务端逐字段手拼 JSON、没有 Rust 结构体」的 TS 类型。**本轮补齐** `coverObjectKey` / `screenshots` / `publicationRevision` 四个字段进 Rust 结构体(依据是 `game_payload` 与 `private_version_payload` 实际发出的键),并删掉脚本里用来豁免它们的 `TS_ONLY_FIELDS` 白名单:现在任一方向多出字段都会让门禁失败,作者侧响应省略 `currentVersion` 这一条差异改用 TS 可选字段描述。**本轮再补构建器一层**:门禁新增 `RESPONSE_BUILDERS`,解析 `server-rs/crates/api-server/src/modules/game_distribution.rs` 里 `game_payload` / `public_game_payload` / `private_version_payload` / `version_summary_payload` 的 `json!` 顶层键与顶层 `object.insert(…)`,逐键比对 TS 类型:发出的键必须都在类型里、类型的必需字段必须都发出、`public_game_payload` 还必须发出被 TS 标成可选的 `currentVersion`,`game_payload` 的 `author` / `deviceSupport` 两个嵌套字面量同样逐键比对。变异验证五种改法各自让门禁失败并指出具体键:删掉 `game_payload.publicationRevision`、删掉 `deviceSupport.touch`、把 `author.avatarUrl` 改名、把插入的 `currentVersion` 改名、给 `version_summary_payload` 加一个 TS 没有的键;恢复后通过。**剩余缺口**:门禁比对的是键而不是值的类型,也覆盖不到未登记的嵌套对象与 envelope——成功/失败 envelope 的字段由 TS 侧运行时守卫(`packages/shared/src/http.ts`、`src/services/apiClient.ts`)消费,要彻底类型化得先把这两个响应改成结构化构建。
@@ -9687,4 +9687,13 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 决策(跟随 master 退役诊断详情入口):master `5398a53e6` 明确把诊断引用移出用户可见文案并删除 `read_agent_runtime_error_detail` 与渲染层读取逻辑,本分支《AGC统一错误诊断与验收反馈》里程碑第 6 条因此被上游取代,实现与专属用例一并退役;失败说明继续由 `turn.completed.failure` 事件投影。
- 影响范围:AGC 客户端(`direct_thread_manager`、`direct_runtime`、`platform_maintenance`、`llm_catalog`、`account_api`、`auth_session`、`platform_asset_upload`、`assets`、`game_distribution_publish`、`agent/generation/canvas_generation`、渲染层 chat 控制器与视图、`tests/`)、nginx SPA 白名单、`.gitignore`、mobile 检查脚本、capabilities 描述、文档与共享记忆。
- 验证:`cargo check`(0 error)、`cargo fmt -- --check`、Rust 定向 `direct_thread_manager` 17 passed 与 `platform_maintenance` 1 passed、`npx vitest run tests/appSurface.test.ts`(201 passed / 9 skipped)与受影响套件、`npm run ai-game-creator-shell:check:web`、根 `npm test`、`npm run lint`、`npm run check:encoding`、`npm run check:doc-index`、`git diff --check`。
- 边界(未验证):真实维护窗口下的弹窗与并发失败表现、真实 Provider 下长回合的活动回合事件时序,都需要真机运行时证据;本轮只到源码级与定向用例级。
- 边界(未验证):真实维护窗口下的弹窗与并发失败表现、真实 Provider 下长回合的活动回合事件时序,都需要真机运行时证据;本轮只到源码级与定向用例级。
## 2026-09-28 游戏分发作者写入口的两条错误码口径:越权一律 404、并发落败给权威偏移
- 背景:给游戏分发阶段 A 第 1、4、5 条补接口级用例时,在本地真实栈(SpacetimeDB `xushi-p4wfr` + api-server + dev bucket)发现两处错误码口径不一致。
- 决策(越权一律 404):`submit_version` 之前直接把 owner 不匹配映射成 403,而读私有版本、读上传状态、写分片、确认分包、撤回都返回 404。现在送审也走 `load_owner_version_or_404`,六个作者入口统一按「资源不存在」回应,不用错误码区分「别人的版本」是否存在。
- 决策(并发落败给权威偏移):同版本同偏移并发写分片时,OSS 的 position 冲突之前被映射成 `502 UPSTREAM_ERROR`。现在追加失败后回读权威已收字节,只要长度已经前进就返回 `409 PACKAGE_UPLOAD_OFFSET_MISMATCH` 并带 `receivedBytes`,与「偏移不符」的既有语义一致;客户端按权威偏移续传,不必猜测自己有没有写进去。
- 决策(确定失败与未知结果的分界):结构合法但摘要/体积/文件数/入口与声明不符 → `409 PACKAGE_MISMATCH`,版本落 `upload_failed` 且 `recoveryAction=reupload`;中断、进程退出与并发落败都停在原版本 `awaiting_upload`,靠 `upload-state` 的权威偏移继续,不落成失败态。
- 影响范围:`server-rs/crates/api-server/src/modules/game_distribution.rs`、`scripts/check-game-distribution-owner-isolation.mjs`、`scripts/check-game-distribution-upload-safety.mjs`、`scripts/check-game-distribution-upload-resume.mjs`、`package.json`、游戏分发里程碑取证。
- 验证:本地真实栈三个脚本全部 PASS(越权隔离 21 项、上传安全 24 项、分片续传 prepare 8 项 + resume 9 项);分片续传中途杀掉 api-server 进程(PID `53844` → 重启 `7728`)后仍从 `8,388,608` 偏移续传成功;`cargo test -p api-server -- package_` 6 passed 与 `game_distribution` 27 passed、`npm run lint`、`check:encoding`、`check:doc-index`、`git diff --check`。
- 边界:证据来自本机 dev 栈与 dev bucket;生产域名、CDN 缓存窗口与真实客户端安装版的自动上传仍未验证。