Files
patbond-doc/docs/development/iterations/iteration-3.5/08-release-e2e-regression.md
T
lixi 5cc6361534
CI / docs-build (push) Successful in 1m22s
docs: v0.4.0 发布记录 + 功能清单 M3.5 节 + 发布 E2E 回归报告
- releases.md 新增 v0.4.0(M3.5 体验补齐):三仓 tag、门禁 8 项证据、
  首次经 PR 流程发布的操作记录与流程验证结论、期间安全事件索引
- checklist 变更登记:**回归清单由两份改为四份**(M1/M2/M3/M3.5),
  原则「每个引入对外端点的迭代都应有对应 E2E 脚本并在此后每次发布回归」
- feature-checklist 新增第 13 节(M3.5 共 13 条)
- 08 号报告:E2E 四份 42/42 场景 234 断言零失败、契约偏差 0;
  另实证零迁移在存量库升级路径(Flyway: No migration necessary)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-14 11:19:13 +08:00

19 KiB
Raw Blame History

08 v0.4.0 发布前 E2E 回归门禁

作者:QAFrontend Developer 角色执行) 日期:2026-09-14 任务:v0.4.0 发布 checklist 第 2 步「compose 全栈起,跑 E2E 烟囱脚本,全场景 PASS」 输入基线:patbond-api dev@3cd8005379 测试)、patbond-flutter dev@6945436597 测试)、 契约 openapi.yaml v1.4.032 路径 / 45 操作 / 75 schema 提交:test_e2e_m35_manual.dart 新增,已推 origin/dev(见 §7 结论先行:门禁 PASS。四份脚本 42/42 场景全绿(M1 7/7、M2 11/11、M3 14/14、M3.5 新写 10/10), 共 234 条断言零失败;契约偏差 0;M3.5 新增对外面 8 项逐项验证通过;三门禁命令全绿 (analyze 0 issue、format 0 changed、flutter test 597 passed)。patbond-api 与 patbond-doc 代码/契约零改动。


1. 结论表:四份脚本场景通过数

脚本 迭代 场景 通过 断言数 退出码 判定
test_e2e_manual.dart M1 7 7/7 7 0 PASS
test_e2e_m2_manual.dart M2 11 11/11 44 0 PASS
test_e2e_m3_manual.dart M3 14 14/14 88 0 PASS
test_e2e_m35_manual.dart M3.5(本次新写) 10 10/10 95 0 PASS
合计 42 42/42 234 PASS
  • 契约偏差数:0。四份脚本对 v1.4.0 冻结契约的每一处断言(状态码、业务码、字段键集合、 可空性、枚举、错误谱)均命中,无一处需要"以实现为准"地放宽期望。
  • 失败项:无。M3.5 脚本首次运行即 10/10 通过(未出现需要调整断言或重试的场景)。
  • 三份既有回归脚本逐字未改——回归价值恰在于脚本本身不动。

执行顺序与理由

依次串行 M1 → M2 → M3 → M3.5,不并行。M3 场景 10/11 对 Feed 做全量翻页前后计数比对 idsAfter.length == idsBefore.length - 1),任何并发发帖都会污染该断言;M3.5 场景 10 也要发帖。 串行是这两个断言成立的前提。


2. M3.5 新增对外面逐项验证结果

任务列出的 8 项必覆盖点,逐项对照:

# 要求 覆盖场景 结果 关键证据
1 注册 → GET /me 断言 nickname 为 null(未设)、avatarUrl 为 null [1/10] 全新账号 nickname=null avatarUrl=null,两键恒在;同时断言 Me 形态在冻结的 6 字段内
2 PATCH /me 设昵称 → 回读一致 [3/10] PATCH 回显与 GET /me 回读逐字段一致userId/username/phone/createdAt/avatarUrl 五字段逐一比对),证明两动词同一 Me 形态
3 三态:只带 nickname 不影响头像 / 显式 null 清空生效 / 空 patch 400 / 纯空白 400 [5/10] 见 §2.1 展开
4 昵称边界:1 可、32 可、33 拒、32 emoji 按码点计应通过 [6/10] 见 §2.2 展开
5 用户头像两步上传(user_avatar) → 挂载 → avatarUrl 可下载且字节一致 → 响应不含 avatarAssetId [4/10] 见 §2.3 展开
6 宠物头像(pet_avatar) → 挂载 → 详情/列表 avatarUrl 可用;错 purpose 被拒 [8/10] + [9/10] 见 §2.4 展开。错 purpose 实测为 404/40405(与契约一致,非 42203
7 GET /me/community-stats:空 0/0 → 发帖 → 自赞 → 1/1 → 软删归零 [10/10] 见 §2.5 展开
8 purpose 白名单外(id_card)创建上传被拒 [2/10] 400/40000message 回显白名单三值

/internal 侧按任务要求未测(内部端点不入公网契约)。

2.1 PATCH /api/v1/me 三态与错误谱(场景 5,13 条断言)

请求 期望 实测 说明
{nickname: "豆豆"}(头像已在) 200,头像不变 200avatarUrl 仍非 null 键缺省 = 不改:只带昵称的 patch 不碰头像
{nickname: null} 200,昵称清空、头像留存 200nickname=nullavatarUrl 非 null 显式 null = 清空,且只作用于携带的键;回读复核已持久化
{avatarAssetId: null} 200avatarUrl 回落 null 200avatarUrl=null 头像清除生效
{} 400/40000 400/40000 message请至少提交一个可更新字段:nickname 或 avatarAssetId——不静默 200
{nickname: " "} 400/40000 400/40000 纯空白不隐式清空,清空只留显式 null 一条路
{nickname: ""} 400/40000 400/40000 空串与纯空白同判
{bio: "…"} 400/40000 400/40000 只带未声明字段 = 空 patchbio 列存在但 M3.5 未开放读写)
{avatarAssetId: "not-a-uuid"} 400/40000 400/40000 参数错先于引用校验
坏 JSON{"nickname": 400/40000 400/40000 非法 body
无 token 401/40101 401/40101 写入口同样强制鉴权
同 body 连发两次 两次 200 且状态一致 两次 200nickname 与"有头像"布尔一致 Idempotency-Key,天然幂等

每一处 PATCH 成功响应都跑了 assertMeShape键集合不超出冻结 6 字段,且不含 avatarAssetId

2.2 昵称边界——码点计而非 UTF-16 长度(场景 6,7 条断言)

昵称 码点 UTF-16 长度 期望 实测
1 1 200 200
×32 32 32 200 200
🐕×32 32 64 200 200
×33 33 33 400/40000 400/40000
🐕×33 33 66 400/40000 400/40000
" 豆豆 " 200 且存为 豆豆 btrim 对齐

32 emoji 昵称通过是这一格的核心:若应用层按 String.length() 校验,UTF-16 长度 64 会被误拒, 而 PostgreSQL char_length 数的是码点、数据库本可存下。实测通过 + 回读逐字一致 (runes.length == 32,无截断),证明校验确实按码点。上边界对 CJK 与 emoji 两种字符集行为一致。

2.3 用户头像端到端(场景 4,9 条断言)

链路:POST /media/uploads(purpose=user_avatar) → 预签名 PUT 直传 MinIO → POST /complete(→ready) → PATCH /me {avatarAssetId}GET <avatarUrl>

  • avatarUrl 由 null 转为现签预签名 GET,携带 X-Amz-Signature SigV4 query 族。
  • objectKey 前缀随 purposepatbond-media/user_avatar/2026/09/{assetId}——证明 §4 的前缀规则生效。
  • 预签名 GET 取回 344 字节,与上传逐字节一致ListEquality 全等比对)。
  • GET /me 再取一次 avatarUrl 仍可下载 → 每次响应现签(客户端不得持久化)。
  • 同一对象去掉签名直访 → 403:桶保持私有,头像不靠公开读。
  • 挂头像时 nickname 未被改动(三态交叉验证)。
  • 挂载/清空/初始三态的响应均不含 avatarAssetId

2.4 宠物头像(场景 8 + 9,20 条断言)

断言 结果
建档响应 avatarUrl 键恒在且为 null不含 avatarAssetId
PATCH /pets/{id} {version, avatarAssetId} 仅头像字段 → 200(WRITE 档)
version 0→1(头像与资料共用同一把乐观锁)
只带 avatarAssetId 未动 name/species
详情 GET /pets/{id} 的 avatarUrl 可下载且字节一致
列表 GET /pets 的 avatarUrl 可下载且字节一致(两处各自现签、均有效)
列表项同样不外露 avatarAssetId
{avatarAssetId: null} 显式清除 → avatarUrl=null,回读复核

asset 校验矩阵(场景 9):

引用的 asset 期望 实测 message
错用途 user_avatar(跨域头像不通用) 404/40405 该媒体资源的用途不是 pet_avatar,不能作为宠物头像
错用途 post_image 404/40405 同上
幽灵 assetId 404/40405 媒体资源不存在
他人(B)的 asset 404/40405 媒体资源不存在(防枚举合并)
本人 pet_avatar 但仍 uploading 422/42203 媒体尚未就绪
version(只带 avatarAssetId 400/40000 version 不能为空——即便只改头像仍必填

副作用复核:以上 6 次失败的 PATCH 之后,宠物 avatarUrl 仍为 null 且 version 未 +1 ——失败路径零副作用。

任务提出的"错 purpose 以实现为准(40405 或 42203"已定论:实测 404/40405,与 openapi.yaml v1.4.0 的 paths./api/v1/pets/{petId}.patch 404 描述及 03 号报告 §2.6 完全一致。42203 只留给"用途相符但状态未就绪"。契约与实现无偏差。

用户侧同构矩阵(场景 7,6 条断言):幽灵 / 他人(B) / 错用途 post_image / 错用途 pet_avatar 四路均 404/40405,且幽灵 id 与他人 asset 的响应体逐字节一致(不能据差异探出 id 是真的); 本人 user_avataruploading → 422/42203(用真未直传的 asset,非 SQL 造数据)。 5 次失败后头像未被改动。

2.5 GET /api/v1/me/community-stats(场景 1014 条断言)

步骤 期望 实测
无 token 401/40101(唯一错误谱) 401/40101
A 全新账号(无帖) 200{0, 0}永不 404 {"receivedLikeCount":0,"publishedPostCount":0}
形态 恰两字段,非 null 无多余键
发布 1 帖 {0, 1}
A 自赞该帖 {1, 1}(自赞计入 {"receivedLikeCount":1,"publishedPostCount":1}
对账 帖详情 likeCount == receivedLikeCount 两处均为 1口径对得上
再建 1 草稿 {1, 1}草稿不计 作品数仍 1
再发布 1 帖 {1, 2}(多帖求和)
B 发布并自赞一帖 A 视角仍 {1, 2}他人帖不串号 主体恒为 token 里的调用者
软删被赞的那帖 {0, 1}(获赞归零 {"receivedLikeCount":0,"publishedPostCount":1}

超出任务要求补测的两格:草稿不计他人帖不串号——前者是 §2.5 口径表最容易实现错的一格, 后者证明"路径上没有 userId"确实等价于主体隔离。


3. 契约偏差数:0

逐面核对 patbond-doc/docs/api/openapi.yaml v1.4.0,脚本断言与契约声明零分歧:

契约条目 声明 实测
info.version 1.4.0
路径 / 操作 / schema 计数 32 / 45 / 75 与基线一致(机械计数复核)
Me.required [userId, username, nickname, avatarUrl, createdAt] 五键恒在;phone 可选可空
Me 不含 avatarAssetId 响应一律不外露 6 处响应(GET 初始/PATCH 回显/挂载后/清空后 等)逐一断言
PetavatarUrl 进 required 键恒在、值可空 创建/详情/列表三处
Pet 不含 avatarAssetId 只写不读 三处断言
UpdateMeRequest 三态 缺省/null/给值 三态各一格
PATCH /me 400 谱 空 patch、长度越界、纯空白/空串、非法 UUID、坏 JSON 6 格全中
PATCH /me 401/404/422 40101 / 40405 / 42203 全中
PATCH /pets/{petId} 新增 404/40405、422/42203 asset 四态 + 未就绪 6 格全中
CommunityStats 两 int64、required 非 nullable、空数据 0、永不 404 全中
CreateMediaUploadRequest.purpose 枚举 [post_image, user_avatar, pet_avatar],外值 400/40000 三值放行 + id_card 被拒

4. 环境记录

日期 2026-09-14
patbond-api dev@3cd8005(工作树干净,零改动)
patbond-doc dev@38d9e97(契约零改动;本报告为新增文件)
patbond-flutter dev@6945436 + 新增 test_e2e_m35_manual.dart
契约 openapi.yaml v1.4.0 — 32 路径 / 45 操作 / 75 schema(机械计数复核一致)
Docker 29.7.2Docker Compose 5.5.1
Dart SDK 3.12.2 (stable)
Flutter 3.44.6 (stable)framework ee80f08bbf
JDK(打包) JAVA_HOME=/usr/lib/jvm/java-17-openjdk17.0.20.1

六容器实况

容器 镜像 状态 端口
patbond-postgres-1 postgres:1818.6 Up (healthy) 不发布(仅容器网)
patbond-minio-1 minio/minio:RELEASE.2025-04-22T22-12-26Z Up (healthy) 9000
patbond-auth-1 patbond-auth(本地构建) Up 8081
patbond-user-1 patbond-user Up 8082
patbond-pet-1 patbond-pet Up 8083
patbond-community-1 patbond-community Up 8084

MinIO 镜像 tag 与集成测试的 Testcontainer 钉同一版本(三环境零分叉,ADR-016)。

起停命令

cd <你的工作区>/patbond-api
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package   # exit 0
docker compose up -d --build                                        # 等 postgres/minio healthy
# …跑四份脚本…
docker compose down

附带取证:M3.5 的"零 Flyway 迁移"在真实升级路径上成立

本次 compose 起在已存在的 pgdata volume 上(非全新库),user 服务(迁移唯一所有者)日志:

o.f.core.internal.command.DbValidate : Successfully validated 5 migrations
o.f.core.internal.command.DbMigrate  : Current version of schema "public": 5
o.f.core.internal.command.DbMigrate  : Schema "public" is up to date. No migration necessary.

即 M3.5 相对 M3 未新增任何 Flyway 版本(仍停在 V5,V6 留给后续真正建表的迭代), ADR-022 的"零迁移"前提在存量库升级场景下得到实证,而不只是全新安装。 auth / pet / community 三服务不跑 Flyway(无迁移输出),与设计一致。

配置侧复核(03 号报告 §4 的部署警告)

03 号报告提醒:若环境的 application.yml 显式写了 allowed-purposes: post_image,头像上传会 400。 compose 挂载的是 patbond-user/src/main/resources/application.yml.sample,实测该文件第 59 行为 allowed-purposes: post_image,user_avatar,pet_avatar pet 服务已补齐 PATBOND_MINIO_PUBLIC_ENDPOINT/ACCESS_KEY/SECRET_KEYcompose 第 107 行), 故宠物侧 avatarUrl 才可能非 null——场景 8 的下载成功即其端到端证明。


5. 门禁三命令输出

cd <你的工作区>/patbond-flutter

5.1 flutter analyze

Analyzing patbond-flutter...
No issues found! (ran in 2.1s)

退出码 0。(新增的根目录脚本也在分析范围内——flutter analyze 覆盖整仓 .dart。)

5.2 dart format --set-exit-if-changed lib test

Formatted 161 files (0 changed) in 0.66 seconds.

退出码 0

补充:该命令的范围是 lib test不覆盖仓库根目录的四份 E2E 脚本。为免留下格式债, 另跑了一次显式检查,四份并列脚本均已 format-clean

$ dart format --output=none --set-exit-if-changed \
    test_e2e_manual.dart test_e2e_m2_manual.dart \
    test_e2e_m3_manual.dart test_e2e_m35_manual.dart
Formatted 4 files (0 changed) in 0.06 seconds.

退出码 0。(新脚本首版曾 1 changed,已 dart format 归一;formatter 拆出的一处 无花括号 if 已补花括号,避免 curly_braces_in_flow_control_structures 风格债。 归一后重跑脚本复核仍 10/10,即上表证据出自最终提交的文件。)

5.3 flutter test

00:30 +597 ~2: All tests passed!

退出码 0597 passed、2 skipped、0 failed,与基线 597 逐格一致(本次未新增单测: E2E 脚本是独立可执行的手动脚本,不进 flutter test 反应堆)。


6. 失败项与判定

无失败项。

类别 数量 说明
真实回归 0 M1/M2/M3 三份既有脚本 32/32 场景全绿,M3.5 新增字段未破坏任何既有断言
契约偏差 0 见 §3
需放宽的断言 0 含任务预留的"错 purpose 以实现为准"一格——实测与契约一致(40405)
环境/配置问题 0 §4 两处部署风险点均实测已闭环

判定:v0.4.0 发布 checklist 第 2 步 PASS。

本次门禁未覆盖的范围(不构成 FAIL,按既定方案挂起)

  • 真机四项development/device-verification.md):弱网上传头像、预签名 URL 过期后重取、 caregiver 账号改宠物头像、资料页获赞数与帖子详情点赞数对账。本脚本纯 dart:io HttpClient 跑后端契约面,不驱动 Flutter UI,故这四项仍按 M3 起的方案 A 挂起。 其中"获赞数对账"的后端口径已在场景 10 证明(详情 likeCount == receivedLikeCount), 真机侧待验的只剩 UI 呈现。
  • caregiver 分档viewer 改头像 403 / caregiver 夹带混合 403):需要第二账号建立 pet_owners 照护关系,M2/M3 脚本均未铺该前置,本次亦未铺;后端已有 PetAvatarIntegrationTest 三段断言覆盖(03 号报告 §2.4)。
  • 并发 PATCH /me 列级 UPDATE:由后端 MeProfileIntegrationTest.concurrentDisjointPatchesBothSurviveCyclicBarrier 覆盖,单线程脚本不复现,本次以"重放幂等"作为可观测替代。
  • /internal/**:按任务要求不测(不入公网契约)。

7. 脚本落位与提交

新增文件 <你的工作区>/patbond-flutter/test_e2e_m35_manual.dart(仓库根目录,与前三份并列,不在 test/
行数 1166 行,10 场景 / 95 断言
依赖 无(纯 dart:io HttpClient + dart:convert,不引 collection 包——ListEquality 内联,与 M3 脚本同先例)
运行 cd <你的工作区>/patbond-flutter && dart run test_e2e_m35_manual.dart
提交 dev 分支(main 受保护禁直推)

脱敏纪律(与 M3 脚本一致)

  • token:仅打印前 20 字符 + ...<REDACTED>
  • 预签名 URLredactSignedUrl() 保留 scheme/host/port/path签名 query 整体替换为 <SIGNATURE_REDACTED>——objectKey 前缀(user_avatar/2026/09/…)仍可读,便于核对 purpose 前缀规则,而签名族一字不落盘。
  • Idempotency-Key:日志中以 <KEY-1> 占位,不打印真实 UUID。
  • 超长昵称按"前 4 码点 + 码点数/UTF-16 长度"概述,不整条刷屏。

硬约束遵守情况

  • patbond-apigit status --porcelain HEAD 仍 3cd8005 — 代码/契约零改动,仅起 compose。
  • patbond-doc:契约 openapi.yaml 零改动,HEAD 仍 38d9e97;本报告为新增文件, 只写不提交(随后统一入档);mkdocs.yml 未动
  • patbond-flutter:仅新增 1 个文件,无既有文件改动。

8. 交接给发布 checklist 后续步骤

  1. 第 2 步可勾:42/42 场景、234 断言、契约偏差 0、门禁三命令全绿。
  2. 本报告入档时需挂 mkdocs.yml 导航(本次按纪律未动)。
  3. 建议把 test_e2e_m35_manual.dart 与前三份一同写进发布 checklist 的常设回归清单—— 下个迭代的发布门禁应跑四份而非三份。
  4. 真机四项与 caregiver 分档仍挂起,见 §6;若 v0.4.0 定位为可发布版本, 建议在 device-verification.md 明确登记"头像相关四项待真机补验",避免遗忘。
  5. 收尾已执行 docker compose downvolume 保留,未 -v)。