Browse Source

test(测试负责人·13:25轮续): 需求变更S1 商城价格排序接口契约4例全绿 + 登记 B-45 未入库用例

- 新增 TC-S-U01~U04:锁定前端 buildSort() 三种取值被后端正确消费(price asc/desc/默认)+ 非法排序列白名单
- 合并远端 4 条需求变更说明,逐条给出用例处置(2 条已有用例但 untracked → B-45)
- 线上 E2E 合计 15/15(admin5 + frontend6 + shop-api4)
E2E Test Bot 5 days ago
parent
commit
d30c68dc0b

+ 20 - 5
docs/tdd/test/automation-run-2026-11-07-1325.md

@@ -31,6 +31,7 @@
 |---|---|---|---|---|---|---|---|
 | 浏览器 E2E · 管理端(本机,/admin) | 1 | 5 | **5** | 0 | 0 | 0 | 2.7 min |
 | 浏览器 E2E · 小程序 H5(本机,/frontend) | 1 | 6 | **6** | 0 | 0 | 0 | 1.4 min |
+| 接口契约 E2E · 商城价格排序(本机) | 1 | 4 | **4** | 0 | 0 | 0 | 22.5 s |
 | cfc-frontend Jest(单元/组件) | 16 | 313 | **313** | 0 | 0 | 0 | 71 s |
 | cfc-web Jest(单元/组件) | 12 | 90 | **90** | 0 | 0 | 0 | 303 s |
 | 根级 tests Jest(接口/集成) | 2 | 23 | 0 | 0 | 0 | **23(全 skip)** | 30 s |
@@ -38,7 +39,7 @@
 | LangGraph 线上 API(部署态 vs 251) | — | 3 | 2 | **1** | 0 | 0 | — |
 | cfc-backend surefire(主工作区 WIP 口径) | 见 §三 | 见 §三 | 见 §三 | 见 §三 | 见 §三 | 见 §三 | 进行中 |
 
-**本轮已实测合计(不含后端与 skip)**:**467 例 / 466 通过 / 1 失败**(通过率 99.79%);另 23 例根级套件全 skip(0 执行,不计入通过)。
+**本轮已实测合计(不含后端与 skip)**:**471 例 / 470 通过 / 1 失败**(通过率 99.79%);另 23 例根级套件全 skip(0 执行,不计入通过)。
 
 ---
 
@@ -46,10 +47,11 @@
 
 - 命令:`mvn -o -B clean test -DskipTests=false -Dmaven.test.failure.ignore=true`(JDK1.8 + Maven 3.6.1,全量不排除)
 - 状态:**尚未跑完**(本机同时承载其他自动化会话,单类耗时显著劣化,`ActivityControllerTest` 461.7s、`AIChatControllerTest` 407.9s)
-- 截至报告时刻进度:**50 类 / 461 例 / 456 通过 / 3F / 2E(98.92%)**,红类 2 个:
+- 截至报告时刻进度:**81 类 / 744 例 / 737 通过 / 5F / 2E(99.06%)**,红类 4 个;其中已确认的 2 个为:
   - `GuideFamilyTaskControllerTest` 20 例 **2F 1E**
   - `GuideRolePermissionTest` 21 例 **1F 1E**
   - 两者均为**主工作区 WIP 中被修改的测试文件**,且与 B-29 遗留清单(GuideFamilyTask 3 + GuideRolePermission 2)逐例吻合 → 属已知遗留,非本轮新增回归
+  - 另 2 个红类随进度滚动产生,最终归因待全量跑完后按 XML `message`/堆栈首帧重算
 - 对照口径:源码零变更 → 预期收敛于干净基线 **286 类 / 2375 例 / 95.16%**(WIP 代入理论值 96.93%,B-29 已验证过一次)
 - 节流说明:本机同时承载其他自动化会话,单类耗时为常态的 3–4 倍(`ActivityControllerTest` 461.7s、`AIChatControllerTest` 407.9s),全量预计需数小时,故本节按「进度快照」呈现
 - **本轮不以后端数字作为门禁达标依据**,仅作进度与红类观测;下一轮以跑完的全量结果替换本节
@@ -81,14 +83,26 @@
 | TC-F-U05 未登录「我的」 | 出现登录/注册入口(鉴权引导) | `frontend-05-mine.png` |
 | TC-F-U06 首屏资源 | js/css 关键资源 0 加载失败 | — |
 
-### 4.3 新增用例与 SRS/场景映射(B-30 的锚点缺口部分闭环)
+### 4.3 需求变更 → 用例更新登记(本轮 SRS 闸门)
 
-新增自动化用例 **11 条**,全部带 `TC-` 编号锚点(此前全仓 348 个测试资产中 TC 编号出现 **0 次**):
+本轮合并远端后发现 **4 条需求变更说明**(`docs/需求分析/2026-11-06~07-*.md`,远端 `6a3e6a2d2` 补齐)。按「SRS/场景变更必须更新用例」逐条处置:
+
+| 需求变更 | 实现状态 | 用例处置 | 结论 |
+|---|---|---|---|
+| 商城价格排序(`2026-11-07-商城价格排序需求变更.md`) | dev 中(纯前端) | **新增 4 例接口契约** `tests/e2e-live/shop-sort-api.spec.js`(TC-S-U01~04):锁定前端 `buildSort()` 三种取值被后端正确消费(price asc/desc/默认),并覆盖非法列名白名单(不 5xx、不注入) | ✅ **4/4 通过**(后端排序能力已完备,前端 UI 完成后可直接复用) |
+| 优惠券「已领取券」按钮修复 | WIP 已有 `cfc-frontend/__tests__/specs/coupon-claimed-actions.test.js` | 用例存在且随 313 例执行 | ⚠️ 文件**未入库**(untracked)→ 登 B-45 |
+| 我的团队收益口径 + 邀请分孚 | WIP 已有 `__tests__/specs/team-invite-share.test.js` | 用例存在且执行通过 | ⚠️ 同上,未入库 → B-45 |
+| 报告上传页关联商品卡片 | 已实现(WIP `cfc-backend` 测试 11 文件) | 无独立新增用例;由 `Article/Guide*` 等既有契约类与 5 个后端测试类覆盖 | ⚠️ 建议补端到端一键用例(G-2) |
+
+### 4.4 新增用例与 SRS/场景映射(B-30 的锚点缺口部分闭环)
+
+新增自动化用例 **15 条**,全部带 `TC-` 编号锚点(此前全仓 348 个测试资产中 TC 编号出现 **0 次**):
 
 | TC 编号 | 场景/SRS 域 | 载体 |
 |---|---|---|
 | TC-A-U01~U05 | 管理员登录 → 后台主框架 → 核心菜单(`docs/flows/admin-overview-flow.md`) | `tests/e2e-live/admin-live.spec.js` |
 | TC-F-U01~U06 | 游客浏览首页 → 五维沙盘 → TabBar 导航 → 未登录「我的」鉴权引导 | `tests/e2e-live/frontend-live.spec.js` |
+| TC-S-U01~U04 | 商城按价格排序(需求变更 S1)后端消费契约 + 排序列白名单 | `tests/e2e-live/shop-sort-api.spec.js` |
 
 配置 `tests/playwright.config.live.js` **刻意不带 globalSetup**——默认配置的 `globalSetup` 会执行 `reset-db.sh` 重置共享测试库,打已部署站点的 UI 套件绝不能触发。
 
@@ -189,7 +203,7 @@
 | G8 契约红类 | 0 | 未跑完;历史 3–4 类 | ⚠️ 待全量 |
 | G9 langgraph pytest | ≥95% | 51 pass / 0 fail(执行口径 100%) | ✅ |
 | **G10 线上 LangGraph 功能可用** | 关键接口 200 | **问卷生成 500**(P0-1) | ❌ |
-| **G11 浏览器 E2E** | 100% | 11/11(admin 5 + frontend 6) | ✅(首次达标) |
+| **G11 浏览器 E2E** | 100% | **15/15**(admin 5 + frontend 6 + 商城排序接口 4) | ✅(首次达标) |
 | **G12 接口层零执行** | 23 例必须实跑 | 23/23 skip | ❌ |
 | **G13 测试资产入库** | 新增资产全部入库 | 本轮 11 例 + 配置待提交(见 §九) | ⚠️ 提交后转 ✅ |
 
@@ -204,6 +218,7 @@
 | 本报告 | `docs/tdd/test/automation-run-2026-11-07-1325.md` | 待提交 |
 | 管理端 UI 套件(5 例) | `tests/e2e-live/admin-live.spec.js` | 待提交 |
 | 小程序 H5 UI 套件(6 例) | `tests/e2e-live/frontend-live.spec.js` | 待提交 |
+| 商城排序契约套件(4 例) | `tests/e2e-live/shop-sort-api.spec.js` | 待提交 |
 | 线上 UI 配置(无 globalSetup) | `tests/playwright.config.live.js` | 待提交 |
 | 阻断台账 | `docs/tdd/test/blockers.md`(B-34 重开、B-42 根因收敛、新增 B-43) | 待提交 |
 | 截图证据 | `_artifacts_live/*.png`(9 张) | 本地留存,不入库 |

+ 1 - 0
docs/tdd/test/blockers.md

@@ -49,3 +49,4 @@
 | B-42 | 后端 JUnit/Playwright 远程派发通道在本运行环境不可用(2914 脚本化用例零实测) | 测试/运维 | 配置(`.tdd/config.json`)要求后端 JUnit 2367 @Test 与 langgraph pytest 派发至 `opencode.iwintrue.com`(192.168.16.251)、Playwright 547 spec 派发至 `web-4091`(本机:4091)/`web-189-4090`(192.168.16.189:4090);但本运行环境无 opencode 客户端、192.168.16.189 TCP 连接拒绝不可达,仅能本地实跑 Jest/pytest。后果:后端 2367 + Playwright 547 共 2914 脚本化用例零实测,目标①「所有用例通过」无法核验。建议:在具备 opencode 会话(web-4091/web-189-4090/opencode.iwintrue.com)的主机运行本自动化,或为本环境配置派发凭证与节点可达性。闭环:派发通道可用后重跑后端/Playwright,覆盖率与通过率可计 | 测试(第11轮 巡检登记) | 2026-11-07 12:57 | **根因收敛(2026-11-07 13:25 复测)**:189 节点**网络与服务均在线**(TCP 4090 OPEN,opencode `v2.0.31` 可连),此前「TCP 连接拒绝」结论已过期。真实卡点两条:① HTTP API 需鉴权,`GET /openapi.json` → 401 `Authentication required`(`.tdd/config.json` 的 `opencode_sessions.production` 未声明鉴权方式);② `opencode run --server http://192.168.16.189:4090 ...` 报 `Location not found: D:\cfc`——本机 CLI 2.2.5 把**本地 cwd** 当远端目录下发,而远端项目目录是 `c:/code/cfc`(`opencode api` 报 `Operation not found: GET /global/health`,版本 2.0.31 与 2.2.5 协议不符)。251 侧:22/80/443 open,**9000 与 4090 均 closed** → langgraph pytest 无法派发到 251 本机。处置建议:升级/对齐 opencode 版本 + 配置 `auth` 与 `remote_dir`。本轮已按「单节点串行」完成 11 例浏览器用例(本机) |
 | B-43 | 线上 LangGraph 部署包落后于仓库(问卷生成 500,B-34 复发) | 研发/运维 | 部署态实测(`https://ai.etotem.com.cn`,`cfc-langgraph v0.3.0`):`POST /api/v1/questionnaire/generate` → **500** `'GenerateRequest' object has no attribute 'prompt_template'`;线上 `/openapi.json` 的 `GenerateRequest` 无 `prompt_template` 字段 → 部署的是修复前版本。仓库侧修复(`GenerateRequest` 补字段 + `build_prompt` 防御)已在 cfclub,但**未发布到 251**。后果:`/api/family/questionnaire` AI 生成 100% 失败并静默降级为模板。建议:重新构建并发布 LangGraph 服务,发布后按同一 curl 回归。闭环:该接口 200 且问卷 JSON 非空 + 线上 openapi 出现 `prompt_template` | 测试(13:25 轮线上实测登记) | 2026-11-07 13:25 | 待处理 |
 | B-44 | 管理端「运营驾驶舱」文案与统一用户模型冲突(孩子总数) | 研发 | 真实登录后驾驶舱统计卡显示「**孩子总数 4256**」(截图 `_artifacts_live/admin-04-logged-in.png`),而 AGENTS.md 规定「新功能禁止再引入『孩子』概念,统一用能登录用户/不能登录用户(memberType=login/local)」。属口径遗留,非功能缺陷。建议改为「未开通登录的子成员」或拆为两卡。闭环:TC-A-U04 增断言「驾驶舱不出现『孩子总数』」,改文案后转绿 | 测试(13:25 轮 UI 实测登记) | 2026-11-07 13:25 | 待处理 |
+| B-45 | 两条需求变更的前端用例只在工作区、未入库(团队邀请分享 / 优惠券已领取按钮) | 测试/流程 | 合并远端后核对当日 4 条需求变更:`2026-11-06-我的团队收益口径与邀请分孚需求变更.md` 与 `2026-11-06-优惠券已领取券按钮修复需求变更.md` 的用例已存在并**随 cfc-frontend 16 套件 313 例实际执行通过**(`__tests__/specs/team-invite-share.test.js`、`__tests__/specs/coupon-claimed-actions.test.js`),但 `git status` 显示 `cfc-frontend/__tests__/specs/` 整体为 `??`(untracked)→ 只存在于主工作区,`git clean -fd` 即静默丢失。与 B-23(SC-X-22 契约未入库)同源。建议:由研发/资产作者 `git add cfc-frontend/__tests__/specs/` 后提交;测试侧不代提交研发 WIP。闭环:`git ls-files cfc-frontend/__tests__/specs/` 有命中且 `git status` 该路径干净 | 测试(13:25 轮实测登记) | 2026-11-07 13:25 | 待处理 |

+ 74 - 0
tests/e2e-live/shop-sort-api.spec.js

@@ -0,0 +1,74 @@
+/**
+ * ================================================================
+ * 商城价格排序 · 接口级契约 E2E · TC-S-U01 ~ TC-S-U04
+ * ================================================================
+ * 需求来源:docs/需求分析/2026-11-07-商城价格排序需求变更.md(变更 S1)
+ *   - 后端能力已完备(SortInterceptor 读 body.sort 数组 + SortUtil 白名单注入)
+ *   - 本次为纯前端改动(排序栏 UI + buildSort() 传参)
+ *   → 故先锁定「后端排序契约」:前端 buildSort() 的三种取值必须被后端正确消费。
+ *
+ * 契约映射(对应 buildSort 输出):
+ *   price_asc  → [{column:'price',order:'asc'}]
+ *   price_desc → [{column:'price',order:'desc'}]
+ *   default    → [](不传,走后端默认 sort_order asc + created_at desc)
+ *
+ * 只读:仅查询商品列表,不写任何数据。
+ * ================================================================
+ */
+const { test, expect } = require('@playwright/test');
+
+const API = process.env.LIVE_API_BASE || 'http://cfc.iwintrue.com';
+
+async function listProducts(request, body) {
+  const resp = await request.post(`${API}/api/product/list`, { data: body, timeout: 30000 });
+  expect(resp.status(), '商品列表接口应 200').toBe(200);
+  const json = await resp.json();
+  expect(json.code, `业务码应为 200,实际 ${json.code} ${json.message || ''}`).toBe(200);
+  const data = json.data || {};
+  const rows = data.records || data.list || data.rows || (Array.isArray(data) ? data : []);
+  return rows;
+}
+
+function prices(rows) {
+  return rows.map((r) => Number(r.price)).filter((n) => !Number.isNaN(n));
+}
+
+test.describe('商城价格排序 · 后端排序契约(需求变更 S1)', () => {
+  test('TC-S-U01 price asc 生效(对应 buildSort → price_asc)', async ({ request }) => {
+    const rows = await listProducts(request, { page: 1, size: 8, sort: [{ column: 'price', order: 'asc' }] });
+    expect(rows.length, '应返回商品').toBeGreaterThan(1);
+    const p = prices(rows);
+    const sorted = [...p].sort((a, b) => a - b);
+    expect(p, `价格应非降序,实际 ${p.join(',')}`).toEqual(sorted);
+  });
+
+  test('TC-S-U02 price desc 生效(对应 buildSort → price_desc)', async ({ request }) => {
+    const rows = await listProducts(request, { page: 1, size: 8, sort: [{ column: 'price', order: 'desc' }] });
+    expect(rows.length).toBeGreaterThan(1);
+    const p = prices(rows);
+    const sorted = [...p].sort((a, b) => b - a);
+    expect(p, `价格应非升序,实际 ${p.join(',')}`).toEqual(sorted);
+  });
+
+  test('TC-S-U03 默认排序不变(对应 buildSort → default,不传 sort)', async ({ request }) => {
+    const rows = await listProducts(request, { page: 1, size: 8 });
+    expect(rows.length, '默认查询应返回商品').toBeGreaterThan(0);
+    // 默认口径:sort_order asc, created_at desc —— 不做值断言,只锁定「结构不回归」
+    expect(rows[0]).toHaveProperty('id');
+    expect(rows[0]).toHaveProperty('price');
+  });
+
+  test('TC-S-U04 非法排序列被白名单拦截(不 5xx、不注入)', async ({ request }) => {
+    const resp = await request.post(`${API}/api/product/list`, {
+      data: { page: 1, size: 5, sort: [{ column: 'price;drop table product', order: 'asc' }] },
+      timeout: 30000,
+    });
+    expect(resp.status(), '非法列名不应导致 5xx').toBeLessThan(500);
+    const json = await resp.json();
+    expect([200, 400], `应返回 200(忽略非法列)或 400(显式拒绝),实际 ${json.code}`).toContain(json.code);
+    if (json.code === 200) {
+      const rows = (json.data && (json.data.records || json.data.list)) || [];
+      expect(rows.length, '合法查询仍应返回数据(证明未抛异常)').toBeGreaterThan(0);
+    }
+  });
+});

+ 6 - 0
tests/playwright.config.live.js

@@ -46,5 +46,11 @@ module.exports = {
       testMatch: /frontend-live\.spec\.js$/,
       use: { browserName: 'chromium', viewport: { width: 375, height: 812 }, isMobile: true, hasTouch: true },
     },
+    {
+      // 接口级契约(商城价格排序需求变更 S1):无需浏览器,走 request fixture 直连 /api
+      name: 'shop-api',
+      testMatch: /shop-sort-api\.spec\.js$/,
+      use: { browserName: 'chromium' },
+    },
   ],
 };