Просмотр исходного кода

test(测试负责人·13:25轮): 浏览器自动化首次落地 11/11 全绿(admin5+frontend6) | 线上LangGraph问卷生成500实证(B-34复发/B-43) | 189派发通道根因收敛(B-42) | 新增B-44驾驶舱文案

- 新增 tests/e2e-live/ 两套线上 UI 用例(TC-A-U01~05 / TC-F-U01~06),带 TC 锚点,配置不触发 reset-db
- 管理端真实登录(滑块轨迹可自动化)推翻历史『仅探针可达』结论
- langgraph: 本地 pytest 51绿/12skip;线上 /health 200、/chat 200、/questionnaire/generate 500
- 前端 Jest 313/313、管理端 Jest 90/90、根级 23 skip
- 后端全量复跑进行中(50类/461例/98.92%,红类2个均属已知WIP遗留)
- 门禁:不通过(G10 线上问卷500、G12 接口层零执行、G6 CI软门禁)
E2E Test Bot 5 дней назад
Родитель
Сommit
504c3e5eb1

+ 221 - 0
docs/tdd/test/automation-run-2026-11-07-1325.md

@@ -0,0 +1,221 @@
+# XAF(浠艾福)测试闭环执行报告 · 2026-11-07 13:25
+
+- **执行人**:陆谨(软件测试专家团主理人 / 测试架构师)· 定时自动化,全程无人工干预
+- **执行档**:review(内网目标 + 已部署站点只读访问;未安装依赖、未改生产源码、未重置任何数据库)
+- **仓库/分支/HEAD**:`d:/cfc` · `cfclub` · `dab532903`(跑测前后一致)
+- **工作区口径**:主工作区含 57 项 WIP(其中 11 个后端测试文件 `M`),本轮后端结果为**「WIP 代入」口径**,非干净基线
+- **权威干净基线(对照)**:`7c3bdae58` 全量 clean = **286 类 / 2375 例 / 95.16%**;`7c3bdae58..HEAD` 的 `cfc-backend/src`、`cfc-web/src`、`cfc-frontend`、`cfc-langgraph/src` **源码零变更**(仅 docs/tools 提交)
+- **SRS 闸门基线**:`9daeaa660`
+
+---
+
+## 一、环境与执行通道(本轮实测,非沿用结论)
+
+| 目标 | 探测结果 | 判定 |
+|---|---|---|
+| `http://cfc.iwintrue.com/admin` | 302 → `/admin/` 200,`text/html`,`<title>浠艾福管理后台</title>` | ✅ 浏览器自动化可用 |
+| `http://cfc.iwintrue.com/frontend` | 302 → `/frontend/` 200,uni-app H5,落地 `#/pages/index-home/index` | ✅ 浏览器自动化可用 |
+| 后端 API | `POST /api/admin-auth/login-by-password` → 200 `{code:200,...token}` | ✅ 存活 |
+| 本机节点(浏览器自动化) | Chromium 1243 已装;4091(opencode web)、9082(backend)在听 | ✅ 已执行 2 个任务(串行) |
+| `192.168.16.189:4090`(浏览器自动化节点) | TCP **OPEN**,opencode `v2.0.31` 可连;但 `/openapi.json` → 401,`opencode run --server` 报 `Location not found: D:\cfc` | ⚠️ 节点在线、**派发通道不可用**(客户端 2.2.5 与服务端 2.0.31 协议/路径不匹配) |
+| `192.168.16.251`(langgraph 主机) | 22/80/443/3306/6379/8080 OPEN;**9000 CLOSED**、4090 CLOSED | ⚠️ 主机在线、**opencode 派发未暴露** |
+| LangGraph 服务(部署态) | `https://ai.etotem.com.cn/health` → `{"status":"ok"}`,`cfc-langgraph v0.3.0` | ✅ 线上服务可达,已做 API 级实测 |
+
+> 结论:**「每个节点只能跑一个浏览器自动化任务」已遵守**——两个浏览器任务在本机严格串行执行;189 节点因派发协议不可用未能承接,已如实标注,不记为通过。
+
+---
+
+## 二、用例总数 · 通过 · 失败
+
+| 层 | 套件 | 用例 | 通过 | 失败 | 错误 | 跳过 | 耗时 |
+|---|---|---|---|---|---|---|---|
+| 浏览器 E2E · 管理端(本机,/admin) | 1 | 5 | **5** | 0 | 0 | 0 | 2.7 min |
+| 浏览器 E2E · 小程序 H5(本机,/frontend) | 1 | 6 | **6** | 0 | 0 | 0 | 1.4 min |
+| 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 |
+| cfc-langgraph pytest(本地脚本级) | — | 63 | **51** | 0 | 0 | 12 | 352 s |
+| LangGraph 线上 API(部署态 vs 251) | — | 3 | 2 | **1** | 0 | 0 | — |
+| cfc-backend surefire(主工作区 WIP 口径) | 见 §三 | 见 §三 | 见 §三 | 见 §三 | 见 §三 | 见 §三 | 进行中 |
+
+**本轮已实测合计(不含后端与 skip)**:**467 例 / 466 通过 / 1 失败**(通过率 99.79%);另 23 例根级套件全 skip(0 执行,不计入通过)。
+
+---
+
+## 三、后端全量(进行中,如实标注)
+
+- 命令:`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 个:
+  - `GuideFamilyTaskControllerTest` 20 例 **2F 1E**
+  - `GuideRolePermissionTest` 21 例 **1F 1E**
+  - 两者均为**主工作区 WIP 中被修改的测试文件**,且与 B-29 遗留清单(GuideFamilyTask 3 + GuideRolePermission 2)逐例吻合 → 属已知遗留,非本轮新增回归
+- 对照口径:源码零变更 → 预期收敛于干净基线 **286 类 / 2375 例 / 95.16%**(WIP 代入理论值 96.93%,B-29 已验证过一次)
+- 节流说明:本机同时承载其他自动化会话,单类耗时为常态的 3–4 倍(`ActivityControllerTest` 461.7s、`AIChatControllerTest` 407.9s),全量预计需数小时,故本节按「进度快照」呈现
+- **本轮不以后端数字作为门禁达标依据**,仅作进度与红类观测;下一轮以跑完的全量结果替换本节
+
+---
+
+## 四、浏览器自动化明细(本轮核心增量)
+
+### 4.1 管理端 `/admin`(5/5)
+
+| 用例 | 断言要点 | 证据 |
+|---|---|---|
+| TC-A-U01 登录页可渲染 | 标题「浠艾福管理后台」、`用户名/手机号`+`密码` 输入框、`登 录` 按钮 | `_artifacts_live/admin-01-login.png` |
+| TC-A-U02 滑块人机验证 | `.slider-captcha` 存在,真实鼠标分步拖动到轨道末端后提示文案改变(验证通过) | `admin-02-captcha-passed.png` |
+| TC-A-U03 未登录拦截 | 未登录访问 `/admin/` 落到登录页 | — |
+| TC-A-U04 管理员真实登录 | 填账号+密码+拖滑块+点登录 → **离开登录页**、渲染后台主框架菜单 >3 项 | `admin-04-logged-in.png`(运营驾驶舱,14 组侧边菜单) |
+| TC-A-U05 菜单可访问 | 遍历一级菜单 ≥6 个,视图切换成功,**0 个未捕获 JS 异常** | `admin-05-menu-walk.png` |
+
+> **重要:本轮推翻了历史「管理端真实登录无法自动化」的判断**——登录页滑块是自研 `.slider-thumb` 组件,Playwright 鼠标轨迹分步拖动可通过;`cfc-web` 登录链路端到端可用(此前 12:25 轮记为「仅探针 302 可达」)。
+
+### 4.2 小程序 H5 `/frontend`(6/6)
+
+| 用例 | 断言要点 | 证据 |
+|---|---|---|
+| TC-F-U01 首页渲染 | hash 路由 `#/pages/*`、`uni-page` 根节点、TabBar 四入口(首页/健康/成长/我的)、0 未捕获异常 | `frontend-01-index.png` |
+| TC-F-U02 核心内容 | 五维(身/心/智/富/行)+ 能量沙盘 + 正文 >200 字 | `frontend-02-index-content.png` |
+| TC-F-U03 切「健康」 | 路由变化且仍渲染页面、0 异常 | `frontend-03-health.png` |
+| TC-F-U04 切「成长」 | 路由变化、0 异常 | `frontend-04-growth.png` |
+| TC-F-U05 未登录「我的」 | 出现登录/注册入口(鉴权引导) | `frontend-05-mine.png` |
+| TC-F-U06 首屏资源 | js/css 关键资源 0 加载失败 | — |
+
+### 4.3 新增用例与 SRS/场景映射(B-30 的锚点缺口部分闭环)
+
+新增自动化用例 **11 条**,全部带 `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` |
+
+配置 `tests/playwright.config.live.js` **刻意不带 globalSetup**——默认配置的 `globalSetup` 会执行 `reset-db.sh` 重置共享测试库,打已部署站点的 UI 套件绝不能触发。
+
+---
+
+## 五、LangGraph(251 部署态)实测
+
+| 探测 | 结果 |
+|---|---|
+| `GET /health` | 200 `{"status":"ok"}` |
+| `PUT /api/v1/chat`(POST,真实用户问题) | **200**,返回完整中文答复(74.8s)、`conversation_id`、`trace_id` 齐全 → 对话主链路可用 |
+| `POST /api/v1/questionnaire/generate` | **500** `{"error":"graph 执行失败: 'GenerateRequest' object has no attribute 'prompt_template'"}` |
+| 本地 `pytest tests --ignore=tests/qna` | **51 passed / 12 skipped / 0 failed**(352s,B-41 flaky 连续第 2 次满跑未复现) |
+
+> **本轮最高优先级发现(P0)**:线上 LangGraph **问卷生成 100% 失败**,且报错与 B-34 完全同源。台账在 67 轮以「本地 pytest 2 例转绿」把 B-34 判为「已解除」——**实测证明该结论只对仓库代码成立,对部署实例不成立**(线上 `GenerateRequest` schema 仍只有 `member_name/respondent_name/...`,无 `prompt_template` 字段,说明修复未发布)。已重开 B-34 为「未发布/未部署」,新增 B-43。
+
+---
+
+## 六、五要素问题清单
+
+### P0-1 · 线上 LangGraph 问卷生成 500(B-34 复发:修复未部署)
+1. **问题描述**:`POST https://ai.etotem.com.cn/api/v1/questionnaire/generate` 返回 500,错误 `'GenerateRequest' object has no attribute 'prompt_template'`;线上 `/openapi.json` 的 `GenerateRequest` 仍无该字段。
+2. **测试 bug 点**:`cfc-langgraph/src/schemas/questionnaire.py`(`GenerateRequest` 缺 `prompt_template`)与 `src/graphs/questionnaire.py:39 build_prompt` 直读该字段;**部署包为修复前版本**(线上 v0.3.0 schema 可证)。
+3. **最优修改方案**:① `GenerateRequest` 增 `prompt_template: Optional[str] = None`(与 `TongueRequest` 对齐);② `build_prompt` 用 `getattr(req, 'prompt_template', None)` 防御;③ **重新构建并发布 251 上的 LangGraph 镜像/进程**,发布后用同一条 curl 回归。
+4. **修改目的**:恢复 `/api/family/questionnaire` 的 AI 问卷生成(当前 100% 静默降级),使核心 AI 链路可用。
+5. **闭环方式**:同一 curl 返回 200 且问卷 JSON 非空;`pytest tests/test_graph.py` 2 例仍绿;线上 `/openapi.json` 的 `GenerateRequest` 出现 `prompt_template`。
+
+### P0-2 · 浏览器自动化节点 189 无法承接任务(B-42 根因收敛)
+1. **问题描述**:`192.168.16.189:4090` 网络与 opencode 服务均在线(v2.0.31),但 API 需鉴权(`/openapi.json` 401),`opencode run --server` 报 `Location not found: D:\cfc`——客户端把本地 cwd 当作远端目录下发,远端实际为 `c:/code/cfc`。
+2. **测试 bug 点**:`.tdd/config.json` 的 `opencode_sessions.production` 只声明 id/label,**未提供鉴权方式与远端目录映射**;本机 opencode CLI 2.2.5 与该服务端 2.0.31 版本不匹配。
+3. **最优修改方案**:① 在 189 上把 opencode 升级到 2.2.x 或锁本机 CLI 到 2.0.x;② 配置项补充 `auth`(token 环境变量)与 `remote_dir`;③ 修好前,浏览器任务统一由本机串行承接(本轮即如此)。
+4. **修改目的**:恢复「双节点并行」设计意图,让 /admin 与 /frontend 可真正并行而非串行。
+5. **闭环方式**:`opencode run --server http://192.168.16.189:4090 "echo ok" c:/code/cfc` 返回 OK;两节点同时各跑 1 个 Playwright 任务成功。
+
+### P1-1 · 根级接口套件 23 例全 skip(B-36 仍未闭环)
+1. **问题描述**:`tests/` 根级 jest 本轮 2 套件 23 例**全部 skipped,0 例执行**——接口层验收从「假红」变「真空」。
+2. **测试 bug 点**:`tests/integration/mini-games-api.spec.js:6-11`、`plan-chain.spec.js:21-22` 门控 `CFC_API_E2E==='1' && CFC_API_TOKEN`,且 `ADMIN_PHONE/ADMIN_PASSWORD` 为空。
+3. **最优修改方案**:新增 `tests/.env.test` + `globalSetup` 自动登录取 token 写入 `CFC_API_TOKEN`,`API_BASE_URL` 指向 `http://cfc.iwintrue.com`。
+4. **修改目的**:让 23 例在每轮默认执行,恢复接口层信号。
+5. **闭环方式**:`CFC_API_E2E=1 npm test` 实跑 23 例且失败均有归因;纳入 CI。
+
+### P1-2 · `pom.xml` 默认 `skipTests=true`(B-39 未修)
+1. **问题描述**:`cfc-backend/pom.xml:28` 默认跳过测试,`mvn test` 会 `BUILD SUCCESS` 但 0 用例执行(假绿)。
+2. **测试 bug 点**:`cfc-backend/pom.xml:28` + `:232` surefire 引用 `${skipTests}`。
+3. **最优修改方案**:改为 `false`,跳过由命令行 `-DskipTests` 显式控制;CI 增加「surefire-reports XML 数 > 0」守卫。
+4. **修改目的**:消除本地/CI「假绿」风险。
+5. **闭环方式**:默认 `mvn test` 产出 >0 XML;CI 守卫在 0 XML 时报红。
+
+### P2-1 · 管理端驾驶舱文案与统一用户模型不一致(新发现)
+1. **问题描述**:登录后「运营驾驶舱」统计卡显示「**孩子总数 4256**」,而 AGENTS.md 已明确「新功能禁止再引入『孩子』概念,统一用能登录用户/不能登录用户」。
+2. **测试 bug 点**:`cfc-web` 运营驾驶舱统计卡文案(`admin-04-logged-in.png` 可见)。
+3. **最优修改方案**:文案改为「未开通登录的子成员」或按口径拆为「能登录用户 / 不能登录用户」两卡。
+4. **修改目的**:产品口径统一,避免运营侧误读。
+5. **闭环方式**:TC-A-U04 增断言:驾驶舱不出现「孩子总数」字样(当前为红灯基线,改文案后转绿)。
+
+---
+
+## 七、20 类测试覆盖矩阵(三态)
+
+| # | 类型 | 状态 | 证据 |
+|---|---|---|---|
+| 1 | 单元测试 | 已发现 | 后端 290 测试文件 / 2366 `@Test`;前端 Jest 16 套 313 例 |
+| 2 | 测试覆盖率 | 未发现 | 无 JaCoCo(pom 无插件、离线不可得)→ 仅 surefire 口径 |
+| 3 | 变异测试 | 未发现 | 无 PIT |
+| 4 | 集成测试 | 已发现 | `@SpringBootTest` 类 + 根级 23 例(**全 skip**) |
+| 5 | 契约测试 | 已发现 | 多契约类(历史红类含 MapContract 等) |
+| 6 | 接口 API 测试 | 已发现 | 根级 API 套件 + 本轮 admin 登录接口实测 |
+| 7 | 系统/端到端 | **已发现(本轮新增)** | `tests/e2e-live/` 11 例真实浏览器 |
+| 8 | 验收 UAT | 待核实 | 无 UAT 记录 |
+| 9 | BDD 规格 | 待核实 | 未发现 feature 文件 |
+| 10 | 冒烟测试 | 已发现 | `ci-smoke.spec.js` + 本轮两站点冒烟 |
+| 11 | 回归测试 | 已发现 | 每轮全量复跑 + 零漂移对账 |
+| 12 | 探索性测试 | 未发现 | 无 charter 记录 |
+| 13 | 性能测试 | 未发现 | 无 k6/JMeter 资产 |
+| 14 | 可靠性测试 | 未发现 | 无故障注入/恢复演练 |
+| 15 | 安全测试 | 待核实 | 历史 `docs/SECURITY_REVIEW_2026-11.md` 静态结论;本轮未跑 SAST/DAST |
+| 16 | 兼容性测试 | **已发现(本轮)** | 桌面 1440×900 + 移动 375×812 双视口真实渲染 |
+| 17 | 可用性测试 | 待核实 | 无真实用户测试 |
+| 18 | 无障碍测试 | 未发现 | 无 axe/WCAG 用例 |
+| 19 | QA 流程 | 待核实 | CI 存在但 4 个测试 job 为软门禁(B-40) |
+| 20 | 质量指标 | 已发现 | 本轮报告 + `docs/tdd/tools/test_report_metrics.py` |
+
+---
+
+## 八、门禁结论
+
+**⛔ 不通过(有条件通过路径已明确)。**
+
+| 门禁 | 阈值 | 本轮实测 | 判定 |
+|---|---|---|---|
+| G1 后端用例通过率 | ≥95% | 未跑完(进度 48 类 98.88%);干净基线 95.16% | ⚠️ 待全量 |
+| G2 红例数 | 0 | 未跑完;基线 74 例(含环境红 8) | ❌ |
+| G3 前端 Jest | 100% | 313/313 + 90/90 | ✅ |
+| G4 测试可编译 | 0 错 | 全量 clean 已进入测试阶段(编译通过) | ✅ |
+| G5 用例集完整性 | 无沉默 | 48 类 48 报告,沉默 0(进行中) | ⚠️ 待全量 |
+| G6 CI 阻断门禁 | 4 个测试 job 必须阻断 | 4 个仍 `allow_failure`(B-40) | ❌ |
+| G7 状态机 RED 键 | 0 RED | 历史 3 伪 RED 键(本轮未变更) | ❌ |
+| 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) | ✅(首次达标) |
+| **G12 接口层零执行** | 23 例必须实跑 | 23/23 skip | ❌ |
+| **G13 测试资产入库** | 新增资产全部入库 | 本轮 11 例 + 配置待提交(见 §九) | ⚠️ 提交后转 ✅ |
+
+**转「通过」的必要条件**:① 发布 LangGraph 修复并复验问卷生成 200;② 后端全量跑完且红例=0(或环境红显式豁免);③ 4 个 CI job 转 blocking;④ 根级 23 例恢复执行;⑤ 189 派发通道修复后可双节点并行。
+
+---
+
+## 九、产出与提交
+
+| 产出 | 路径 | 状态 |
+|---|---|---|
+| 本报告 | `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` | 待提交 |
+| 线上 UI 配置(无 globalSetup) | `tests/playwright.config.live.js` | 待提交 |
+| 阻断台账 | `docs/tdd/test/blockers.md`(B-34 重开、B-42 根因收敛、新增 B-43) | 待提交 |
+| 截图证据 | `_artifacts_live/*.png`(9 张) | 本地留存,不入库 |
+| 日志 | `_r1125_backend.log`、`_r1125_agg.py`、`_r1125_partial.json`、`_lg_openapi.json`、`_lg_q.json` | 本地留存 |
+
+---
+
+## 十、未实测 / 待核实
+
+- 后端**其余 238 类未跑完**(本机负载导致,预计需更长时间),本轮不据此下达标结论
+- JaCoCo 行/分支覆盖率、PIT 变异:离线不可得
+- 性能/可靠性/无障碍/探索性:无工具、无资产(已给测试设计)
+- SAST/DAST/依赖漏洞深扫:需联网授权
+- `192.168.16.251` 上的 pytest **未在 251 本机执行**(opencode 派发未暴露、SSH 无凭据);本轮为「本机 pytest + 线上 API 级」替代口径,已在 §五 标注
+- 管理端其余 60+ UI spec 未在本轮执行(仅新增 5 例线上冒烟),历史 spec 依赖 `globalSetup` 重置库,需独立环境

+ 4 - 2
docs/tdd/test/blockers.md

@@ -38,7 +38,7 @@
 | B-31 | `application.yml` 7 处明文凭据(安全) | 研发 | `cfc-backend/src/main/resources/application.yml` 存在 **7 处** `password/secret/api-key/appsecret` 明文赋值(本台账不复制取值),覆盖数据源口令、微信 appsecret 与第三方 key。任何具备仓库读权限者可直取测试/生产凭据;且历史提交已留在 git 历史中。建议:改 `${ENV}` 占位 + 本地 `.env`(gitignore)+ 配置中心/Secret 管理;**同时轮换已泄露凭据**;CI 加明文凭据扫描(gitleaks 规则集) | 测试(62 轮静态扫描登记) | 2026-11-06 23:10 | 待处理 |
 | B-32 | 场景/用例度量口径跨轮漂移(度量不可比) | 测试 | 本轮实测:场景 **215**、TC **784**(异常流 474);上一轮报告记「场景 244、TC 492(异常流 237)」。差异非增长(90 分钟内不可能新增 292 条 TC),而是**解析口径不同**(是否计入 `### 场景`(08/09 butler 17 条)、是否计入 `TC-*-N*` 负向编号、类型列是否含「权限」判异常)。与 B-15/B-27 同族:**度量脚本未入库、口径未固化** → 跨轮数字不可相减。建议:把 `_r2135_scene.py` 等脚本收进 `docs/tdd/tools/` 并入库,报告固定写「口径定义 + 脚本版本」,禁止手算 | 测试(62 轮登记) | 2026-11-06 23:10 | **部分解除(62 轮自处置)**:度量脚本已落地 `docs/tdd/tools/test_report_metrics.py`(聚合 + 逐例归因 + 场景覆盖 + 恒等式,纯只读、带 CLI 参数),本轮数字用该脚本复算完全一致(231 类/1847 例/1727 绿/93.50%/36 红类/116 红例;场景 215、TC 784、异常流 474、TC 锚点 0/784;恒等式 1838−20+3=1821,参数化净增 26 → 1847)。**但脚本无法入库**:被 `.gitignore:70 *.py` 全局忽略 → 见 B-33,需放行规则后才算真闭环 |
 | B-33 | `.gitignore:70 *.py` 全局忽略 Python 文件 —— 度量脚本「写了就丢」的真正根因 | 研发/流程 | 62 轮把度量工具落到 `docs/tdd/tools/test_report_metrics.py` 后 `git add` 被拒:`git check-ignore -v` → **`.gitignore:70:*.py`**。这解释了 B-15(`discover_tests.py`/`test_inventory.py` 全仓丢失、历史度量不可复现)与 B-32(口径跨轮漂移)为何反复发生:**任何 Python 度量/校验脚本在本仓库都不可能入库**,只存在于某个工作区,分支切换或清理即消失。影响面:度量脚本、数据 seed 脚本、报告解析脚本(`docs/scripts/fingerprint_engine.py` 等)同受此规则约束(后者是否被例外放行需核实)。建议:把规则细化为 `/*.py`(仅忽略仓库根临时脚本)或追加例外 `!docs/tdd/tools/*.py`、`!docs/scripts/**/*.py`,再由研发提交一次;测试侧脚本需随之入库。**注意:`.gitignore` 属研发所有,测试角色不代改** | 测试(62 轮实测登记) | 2026-11-06 23:10 | 待处理 |
-| B-34 | LangGraph 问卷生成 100% 失败降级(`GenerateRequest` 缺 `prompt_template`) | 研发 | **P0 生产缺陷**。`c3925c97e feat(langgraph): 全部提示词迁移到Java后端可配置` 只给 `cfc-langgraph/src/schemas/tongue.py:14` 的 `TongueRequest` 加了 `prompt_template`,**未给 `src/schemas/questionnaire.py:39 GenerateRequest` 加**;而 `src/graphs/questionnaire.py:39 build_prompt` 直接读 `req.prompt_template` → 每次调用必抛 `AttributeError`。链路:Java `AiGateway:321` 恒发送 `prompt_template` → pydantic 静默丢弃额外字段 → 图节点必抛 → 接口 500 → `AiGateway` catch 后返回 null → 调用方**静默降级模板**。即 `/api/family/questionnaire` 的 AI 生成能力 100% 不可用。`c3925c97e` 已在 `origin/cfclub` 祖先链 → 现网同样受影响。65 轮 pytest 实测 2 例必红即此因。修复:`GenerateRequest` 增 `prompt_template: Optional[str] = Field(default=None, ...)`(与 `TongueRequest:14` 对齐);`build_prompt` 可加 `getattr` 防御。闭环:`pytest tests/test_graph.py` 2 例转绿 + `/api/v1/questionnaire/generate` 返回 200 且 `questionnaire_json` 非空 + `AiGateway` 不再打「调用失败」warn | 测试(65 轮实测登记) | 2026-11-07 02:15 | **已解除(67 轮实测复验,2026-11-07 09:30)**:cfc-langgraph pytest 全量 `51 passed / 12 skipped / 0 failed`(302s),`test_graph` 2 例复原、此前 2 红清零 → 修复生效实证;API 端 `/api/v1/questionnaire/generate` 200 复验待下轮(TR-10 跟踪) |
+| B-34 | LangGraph 问卷生成 100% 失败降级(`GenerateRequest` 缺 `prompt_template`) | 研发 | **P0 生产缺陷**。`c3925c97e feat(langgraph): 全部提示词迁移到Java后端可配置` 只给 `cfc-langgraph/src/schemas/tongue.py:14` 的 `TongueRequest` 加了 `prompt_template`,**未给 `src/schemas/questionnaire.py:39 GenerateRequest` 加**;而 `src/graphs/questionnaire.py:39 build_prompt` 直接读 `req.prompt_template` → 每次调用必抛 `AttributeError`。链路:Java `AiGateway:321` 恒发送 `prompt_template` → pydantic 静默丢弃额外字段 → 图节点必抛 → 接口 500 → `AiGateway` catch 后返回 null → 调用方**静默降级模板**。即 `/api/family/questionnaire` 的 AI 生成能力 100% 不可用。`c3925c97e` 已在 `origin/cfclub` 祖先链 → 现网同样受影响。65 轮 pytest 实测 2 例必红即此因。修复:`GenerateRequest` 增 `prompt_template: Optional[str] = Field(default=None, ...)`(与 `TongueRequest:14` 对齐);`build_prompt` 可加 `getattr` 防御。闭环:`pytest tests/test_graph.py` 2 例转绿 + `/api/v1/questionnaire/generate` 返回 200 且 `questionnaire_json` 非空 + `AiGateway` 不再打「调用失败」warn | 测试(65 轮实测登记) | 2026-11-07 02:15 | **⚠️ 重新打开(2026-11-07 13:25 线上实测)**:67 轮的「已解除」只对**仓库代码**成立,对**部署实例不成立**。本轮直连部署态 LangGraph 实测:`POST https://ai.etotem.com.cn/api/v1/questionnaire/generate` → **HTTP 500** `{"error":"graph 执行失败: 'GenerateRequest' object has no attribute 'prompt_template'"}`;线上 `/openapi.json`(cfc-langgraph v0.3.0)的 `GenerateRequest` 仍只有 `member_name/respondent_name/relationship_type/member_profile/respondent_profile/family_context`,**无 `prompt_template`** → 部署包为修复前版本。B-43 为其发布侧跟踪条目 |
 | B-35 | DAN 报告解析 A1/A2/B4 仍失效(修复提交未覆盖该版式) | 研发 | `DanReportParseServiceTest` 65 轮全量实测 **4 例红**:`testParseA2_standardCoreLiteracyFormat:262`(姓名 `expected 俊辰 but null`)、`testParseB4_regression:280`(仍走降级 `expected false but true`)、`testParseA1_shouldFallbackToGenericPattern`、`testParseA1_shouldExtractCognitiveDimensionFromDetailPage`(`expected not null`)。而 `c611ec55d fix(dan): 修复 A1/B4 认知维度解析失效` 声称已修 → **修复未覆盖这两种版式**(`testParseA2_regression` 同批绿、`standardCoreLiteracy` 版式红,说明是版式分支漏配)。健康报告解析为核心链路(AGENTS.md「健康报告 PDF 上传与解析」)。建议按真实样本补版式分支 + 回归样本入库。闭环:4 例转绿 + 用 3 份真实报告样本走 `/api/health/report/parse` 人工比对字段 | 测试(65 轮实测登记) | 2026-11-07 02:15 | **已解除(67 轮实测复验,2026-11-07 09:30)**:`DanReportParseServiceTest` 全量 **38/38 GREEN**,此前 A1/A2/B4 4 红清零 → 版式分支补配生效;真实样本人工比对待下轮(TR-11 跟踪) |
 | B-36 | 根级黑盒 API 套件由「12 恒红」转为「默认 skip」,接口层覆盖归零 | 测试/环境 | WIP 将 `tests/integration/mini-games-api.spec.js:6-11`、`plan-chain.spec.js:21-22` 改为 `CFC_API_E2E==='1' && CFC_API_TOKEN` 门控(原写死 `Bearer test-token` 打真服务 → 恒 401/500)。方向正确(消除 12 个伪红),但 65 轮实测 **23 例全部 skip、0 执行**(`Tests: 23 skipped`)→ 接口层验收从「假红」变「真空」,且 `plan-chain` 仍依赖空的 `ADMIN_PHONE/ADMIN_PASSWORD`。建议:新增 `tests/.env.test` + seed 账号,`globalSetup` 自动登录取 token 写入 `process.env.CFC_API_TOKEN`,CI 注入 `API_BASE_URL=http://127.0.0.1:9082`。闭环:本地 9082 在听时 `CFC_API_E2E=1 npm test` 23 例实跑且红例有归因;CI 出绿灯记录 | 测试(65 轮实测登记) | 2026-11-07 02:15 | 待处理 |
 | B-37 | sleep.config.tags blank-key 实现缺口(契约测试 1 红,阻断该键转 GREEN) | 研发 | 复验轮(TR-6 派发 10 键复验)`SleepConfigTagsMechanismContractTest` **9/10 绿 + 1 红**:`publicValue_blankKey_returnsError:99` 期望 blank key(`"   "`)→ 500 但实得 200。实现 `PublicConfigController.getValue`(L21-28)只查 `key==null||isEmpty()`,未 reject 全空白串 → blank key 被当作正常查询放行。测试由 `f11062b42`(提交信息注明「21绿/1红 blank-key 缺陷」)引入,属**已知实现缺口、非测试误报**。`HealthSleepControllerContractTest` 12/12 绿不受影响。该键 `--mark-green` 已**暂缓**、状态机保持 YELLOW。修复:`getValue` 校验改为 `key==null || key.trim().isEmpty()`(或判 `matches("\\s*")`),并确认无调用方依赖空白 key 语义。闭环:`SleepConfigTagsMechanismContractTest` 10/10 绿后再 `--mark-green sleep.config.tags` | 测试(复验派发登记) | 2026-11-07 05:50 | **已解除(67 轮实测复验,2026-11-07 09:30)**:`SleepConfigTagsMechanismContractTest` 全量 **10/10 GREEN**,blank-key 红清零 → 修复生效;`--mark-green sleep.config.tags` 键不在本工作区账本,由研发负责人侧执行 |
@@ -46,4 +46,6 @@
 | B-39 | `cfc-backend/pom.xml` 默认 `skipTests=true`(测试零执行假绿) | 研发 | **P0 流程黑洞**。`cfc-backend/pom.xml:28` 声明 `<skipTests>true</skipTests>`,`:232` 的 surefire 配置引用 `${skipTests}`。本轮(2026-11-07 10:59)实测:在干净隔离树 `D:\_wt_r1054`@`9359de39a` 执行文档标准命令 `mvn -o -B clean test` → **9:49 BUILD SUCCESS,日志明确 `[INFO] Tests are skipped.`,surefire-reports 下 0 个 XML**——即“构建成功但零用例执行”。影响:① 任何按标准 Maven 命令接入的本地验证/CI 都会得到**假绿**,覆盖率与通过率无从谈起;② 本台账历史上多轮“全量通过”结论必须显式带 `-DskipTests=false` 才成立;③ 新入职/新流水线极易踩坑。`.gitlab-ci.yml:16` 注释已记录该陷阱并以 `-DskipTests=false` 绕过,但**根因未修** → 每个使用方都要自己记得加参数。建议:去掉 `:28` 的默认 `<skipTests>true</skipTests>`(或改为 `<skipTests>false</skipTests>`,把“跳过”交给命令行 `-DskipTests` 显式控制);同时在 CI 增加一条守卫:`mvn -o -B test -DskipTests=false` 后断言 `surefire-reports/*.xml` 数量 > 0,否则 job 失败。闭环:默认 `mvn test` 能跑出用例;或 CI 守卫在“0 XML”时报红 | 测试(本轮实测登记) | 2026-11-07 11:10 | 待处理 |
 | B-40 | CI 4 个测试 job 为软门禁 + 集成层被排除 | 研发/流程 | `.gitlab-ci.yml` 现有 7 个 job,其中 **4 个 `allow_failure` 语义**:`backend-contract-junit:134`、`langgraph-pytest:165`、`tests-jest:225`(e2e-playwright 亦按注释兜底),即**测试失败不阻断流水线**;且 `backend-contract-junit:142` 使用 `-Dtest='!**/integration/**'` 排除整个 `integration` 包(1794/2219 例)——而本轮 26 个红类中 **14 个位于 `com.etotem.cfc.integration.*`**(含 `EnergySystemFlowTest` 6F、`DimensionRegistryMapContractTest` 3F、`PlannerBindInviteFlowTest` 4F 等),**恰是 CI 视野之外的部分**。后果:CI 绿灯 ≠ 可发布,红灯也不拦人。建议:① 分两级门禁——`test-blocking`(全绿子集,必须过)与 `test-observed`(已知红,允许失败但产出趋势);② 把 `integration` 包按类白名单逐批转 blocking;③ 报告 G6 口径从“有无 CI”升级为“blocking job 覆盖例数占比”。闭环:任一 blocking job 红 → 流水线红;且 blocking 覆盖 ≥ 当前全量 80% | 测试(本轮实测登记) | 2026-11-07 11:10 | 待处理 |
 | B-41 | langgraph.test_analysis_graph_offline_run_returns_interpretation 历史 flaky 需固化 | 研发 | 本轮(2026-11-07 12:57)cfc-langgraph pytest 全量 51 passed / 0 failed / 12 skipped(EXIT 0);历史满跑失败、隔离单跑 PASS 的 `TestGrowthReportInterpretB04::test_analysis_graph_offline_run_returns_interpretation` 本轮未复现,但属顺序相关/模块级 state 污染型 flaky(疑似 chromadb 原生依赖 + 模块级 fixture),存在回弹风险。建议:mock chromadb + 确定性 fixture + 单文件隔离运行(`--forked`);CI 固定隔离。闭环:连续 3 轮满跑全绿后再视为修复生效 | 测试(第11轮 巡检登记) | 2026-11-07 12:57 | 待处理 |
-| 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 | 待处理 |
+| 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 | 待处理 |

+ 160 - 0
tests/e2e-live/admin-live.spec.js

@@ -0,0 +1,160 @@
+/**
+ * ================================================================
+ * 管理端(cfc-web)线上 UI 级 E2E · TC-A-U01 ~ TC-A-U06
+ * ================================================================
+ * 目标站点:http://cfc.iwintrue.com/admin
+ * 场景来源:docs/flows/admin-overview-flow.md(管理员登录 → 后台主框架 → 核心菜单)
+ *
+ * 只读约束:不新增/修改/删除任何业务数据;仅执行登录(会话建立)。
+ * 凭据来源:环境变量 CFC_ADMIN_PHONE / CFC_ADMIN_PASSWORD(缺省用测试环境公共测试账号),
+ *          禁止在报告/日志中打印明文口令。
+ *
+ * DOM 依据(2026-11-07 实测):
+ *   input[placeholder="用户名 / 手机号"]、input[placeholder="密码"]、
+ *   input[placeholder="登录身份(输入手机号后自动带出)"]、
+ *   滑块验证 .slider-captcha > .slider-track / .slider-thumb、按钮「登 录」
+ * ================================================================
+ */
+const { test, expect } = require('@playwright/test');
+const fs = require('fs');
+const path = require('path');
+
+const ART = path.join(__dirname, '..', '..', '_artifacts_live');
+if (!fs.existsSync(ART)) fs.mkdirSync(ART, { recursive: true });
+
+const PHONE = process.env.CFC_ADMIN_PHONE || '13800138000';
+const PASSWORD = process.env.CFC_ADMIN_PASSWORD || 'admin123';
+
+/** 拖动滑块到最右侧(Element UI 滑块验证) */
+async function dragSliderToEnd(page) {
+  const thumb = page.locator('.slider-thumb').first();
+  const track = page.locator('.slider-track').first();
+  await thumb.waitFor({ state: 'visible', timeout: 15000 });
+  const tb = await thumb.boundingBox();
+  const rb = await track.boundingBox();
+  if (!tb || !rb) throw new Error('滑块/轨道 boundingBox 不可得');
+  const y = tb.y + tb.height / 2;
+  const endX = rb.x + rb.width - tb.width / 2 - 2;
+  await page.mouse.move(tb.x + tb.width / 2, y);
+  await page.mouse.down();
+  // 分步移动,模拟真人拖动(部分实现按轨迹校验)
+  const steps = 18;
+  for (let i = 1; i <= steps; i++) {
+    const x = tb.x + tb.width / 2 + ((endX - (tb.x + tb.width / 2)) * i) / steps;
+    await page.mouse.move(x, y, { steps: 2 });
+    await page.waitForTimeout(18);
+  }
+  await page.mouse.up();
+}
+
+test.describe('管理端 UI · 登录与主框架', () => {
+  test('TC-A-U01 登录页可渲染(标题/账号/密码/登录按钮)', async ({ page }) => {
+    const resp = await page.goto('/admin/login', { waitUntil: 'domcontentloaded' });
+    expect(resp, '登录页应有 HTTP 响应').not.toBeNull();
+    expect(resp.status(), '登录页状态码应 < 400').toBeLessThan(400);
+
+    await expect(page).toHaveTitle(/浠艾福管理后台/);
+    await expect(page.locator('input[placeholder="用户名 / 手机号"]')).toBeVisible();
+    await expect(page.locator('input[placeholder="密码"]')).toBeVisible();
+    await expect(page.getByRole('button', { name: /登\s*录/ })).toBeVisible();
+    await page.screenshot({ path: path.join(ART, 'admin-01-login.png') });
+  });
+
+  test('TC-A-U02 滑块人机验证存在且可拖动通过', async ({ page }) => {
+    await page.goto('/admin/login', { waitUntil: 'domcontentloaded' });
+    await expect(page.locator('.slider-captcha')).toBeVisible();
+    await expect(page.locator('.captcha-hint')).toContainText(/拖动到最右侧/);
+
+    await dragSliderToEnd(page);
+    // 成功后提示文案变化(验证通过/成功),或滑块填充色铺满
+    const hint = page.locator('.captcha-hint');
+    await expect(hint).not.toContainText(/拖动到最右侧/, { timeout: 10000 });
+    const text = (await hint.innerText()).trim();
+    expect(text.length, '滑块通过后应有反馈文案').toBeGreaterThan(0);
+    await page.screenshot({ path: path.join(ART, 'admin-02-captcha-passed.png') });
+  });
+
+  test('TC-A-U03 未登录访问后台受保护路由被拦截到登录页', async ({ page }) => {
+    await page.goto('/admin/', { waitUntil: 'domcontentloaded' });
+    await page.waitForTimeout(2500);
+    const url = page.url();
+    const loginVisible = await page.locator('input[placeholder="密码"]').isVisible().catch(() => false);
+    expect(
+      url.includes('/login') || loginVisible,
+      `未登录访问 /admin/ 应落到登录页,实际 URL=${url} 登录表单可见=${loginVisible}`
+    ).toBeTruthy();
+  });
+
+  test('TC-A-U04 管理员真实登录(滑块 + 凭据)进入后台主框架', async ({ page }) => {
+    await page.goto('/admin/login', { waitUntil: 'domcontentloaded' });
+    await page.locator('input[placeholder="用户名 / 手机号"]').fill(PHONE);
+    await page.locator('input[placeholder="密码"]').fill(PASSWORD);
+
+    // 「登录身份」为只读带出项,若为空则尝试点选首个身份选项
+    const roleInput = page.locator('input[placeholder="登录身份(输入手机号后自动带出)"]');
+    if (await roleInput.isVisible().catch(() => false)) {
+      const roleVal = await roleInput.inputValue().catch(() => '');
+      if (!roleVal) {
+        await roleInput.click();
+        const opt = page.locator('.el-select-dropdown__item, .el-option').first();
+        if (await opt.isVisible({ timeout: 3000 }).catch(() => false)) await opt.click();
+      }
+    }
+
+    await dragSliderToEnd(page);
+    await page.waitForTimeout(600);
+
+    await page.getByRole('button', { name: /登\s*录/ }).click();
+    await page.waitForTimeout(5000);
+
+    const afterUrl = page.url();
+    const loggedIn = !afterUrl.includes('/login');
+    // 兼容后端拒绝(例如身份未选)时给出可诊断的信息
+    expect(loggedIn, `登录后应离开登录页,实际 URL=${afterUrl}`).toBeTruthy();
+
+    // 后台主框架特征:侧边菜单项存在
+    const menuCount = await page.locator('.el-menu-item, .el-submenu__title, .sidebar-container a').count();
+    expect(menuCount, '登录后应渲染后台菜单').toBeGreaterThan(3);
+    await page.screenshot({ path: path.join(ART, 'admin-04-logged-in.png'), fullPage: false });
+  });
+
+  test('TC-A-U05 后台核心菜单可访问(用户管理/数据看板)', async ({ page }) => {
+    await page.goto('/admin/login', { waitUntil: 'domcontentloaded' });
+    await page.locator('input[placeholder="用户名 / 手机号"]').fill(PHONE);
+    await page.locator('input[placeholder="密码"]').fill(PASSWORD);
+    const roleInput = page.locator('input[placeholder="登录身份(输入手机号后自动带出)"]');
+    if (await roleInput.isVisible().catch(() => false)) {
+      const v = await roleInput.inputValue().catch(() => '');
+      if (!v) {
+        await roleInput.click();
+        const opt = page.locator('.el-select-dropdown__item, .el-option').first();
+        if (await opt.isVisible({ timeout: 3000 }).catch(() => false)) await opt.click();
+      }
+    }
+    await dragSliderToEnd(page);
+    await page.waitForTimeout(600);
+    await page.getByRole('button', { name: /登\s*录/ }).click();
+    await page.waitForTimeout(5000);
+    test.skip(page.url().includes('/login'), '登录未通过(见 TC-A-U04),本用例无法在无会话下判定');
+
+    const menuTexts = await page.locator('.el-menu-item, .el-submenu__title').allInnerTexts();
+    const joined = menuTexts.join('|').replace(/\s+/g, '');
+    expect(joined.length, '菜单文案不应为空').toBeGreaterThan(0);
+
+    const errors = [];
+    page.on('pageerror', (e) => errors.push(String(e)));
+    // 逐个点击前 6 个一级菜单,断言 URL/内容变化且无未捕获异常
+    const items = page.locator('.el-menu-item');
+    const n = Math.min(await items.count(), 6);
+    let navigated = 0;
+    for (let i = 0; i < n; i++) {
+      const before = page.url();
+      await items.nth(i).click({ timeout: 8000 }).catch(() => {});
+      await page.waitForTimeout(1200);
+      if (page.url() !== before || (await page.locator('.el-main, .main-container, #app > div').count()) > 0) navigated++;
+    }
+    expect(navigated, '至少应有一个菜单可切换视图').toBeGreaterThan(0);
+    expect(errors, '菜单遍历期间不应出现未捕获 JS 异常').toHaveLength(0);
+    await page.screenshot({ path: path.join(ART, 'admin-05-menu-walk.png') });
+  });
+});

+ 134 - 0
tests/e2e-live/frontend-live.spec.js

@@ -0,0 +1,134 @@
+/**
+ * ================================================================
+ * 小程序 H5(cfc-frontend uni-app)线上 UI 级 E2E · TC-F-U01 ~ TC-F-U06
+ * ================================================================
+ * 目标站点:http://cfc.iwintrue.com/frontend/(uni-app H5,hash 路由)
+ * 场景来源:用户场景「游客浏览首页 → 五维能量沙盘 → 商品/活动 → TabBar 切换 → 我的(未登录)」
+ *
+ * DOM 依据(2026-11-07 实测):
+ *   落地路由 #/pages/index-home/index,title=首页,
+ *   TabBar 文案:首页 / 健康 / 成长 / 我的(uni-app 原生 tabbar)
+ *
+ * 只读约束:游客态浏览,不登录、不下单、不提交表单。
+ * ================================================================
+ */
+const { test, expect } = require('@playwright/test');
+const fs = require('fs');
+const path = require('path');
+
+const ART = path.join(__dirname, '..', '..', '_artifacts_live');
+if (!fs.existsSync(ART)) fs.mkdirSync(ART, { recursive: true });
+
+/** 采集页面运行期异常与失败请求 */
+function watch(page) {
+  const errors = [];
+  const failed = [];
+  page.on('pageerror', (e) => errors.push(String(e).slice(0, 300)));
+  page.on('requestfailed', (r) => failed.push(`${r.method()} ${r.url().slice(0, 120)}`));
+  return { errors, failed };
+}
+
+/** 点击 TabBar 项(uni-app H5 渲染为 uni-tabbar > .uni-tabbar__item) */
+async function clickTab(page, label) {
+  const inTabbar = page.locator('uni-tabbar .uni-tabbar__item', { hasText: label }).first();
+  if (await inTabbar.count()) {
+    await inTabbar.click();
+    return true;
+  }
+  const byText = page.getByText(label, { exact: true }).last();
+  if (await byText.count()) {
+    await byText.click();
+    return true;
+  }
+  return false;
+}
+
+test.describe('小程序 H5 UI · 游客浏览与导航', () => {
+  test('TC-F-U01 首页可渲染(标题/落地路由/TabBar 四项)', async ({ page }) => {
+    const w = watch(page);
+    const resp = await page.goto('/frontend/', { waitUntil: 'domcontentloaded' });
+    expect(resp, '首页应有 HTTP 响应').not.toBeNull();
+    expect(resp.status(), '首页状态码应 < 400').toBeLessThan(400);
+
+    await page.waitForTimeout(5000);
+    expect(page.url(), '应落到 hash 路由首页').toMatch(/#\/pages\//);
+    await expect(page).toHaveTitle(/首页|浠艾福/);
+
+    // uni-app 页面根节点已挂载
+    expect(await page.locator('uni-page, .uni-page').count(), '应渲染 uni-page 根节点').toBeGreaterThan(0);
+
+    // TabBar 四项
+    const tabTexts = await page.locator('uni-tabbar .uni-tabbar__item, .uni-tabbar__label').allInnerTexts();
+    const joined = tabTexts.join('|').replace(/\s+/g, '');
+    expect(joined, 'TabBar 应包含 首页/健康/成长/我的').toMatch(/首页/);
+    expect(joined).toMatch(/健康/);
+    expect(joined).toMatch(/成长/);
+    expect(joined).toMatch(/我的/);
+
+    await page.screenshot({ path: path.join(ART, 'frontend-01-index.png') });
+    expect(w.errors, `首页不应有未捕获 JS 异常:${w.errors.join(' ;; ')}`).toHaveLength(0);
+  });
+
+  test('TC-F-U02 首页核心内容渲染(五维沙盘 / 三步引导)', async ({ page }) => {
+    await page.goto('/frontend/', { waitUntil: 'domcontentloaded' });
+    await page.waitForTimeout(5000);
+    const body = (await page.locator('body').innerText()).replace(/\s+/g, '');
+
+    // 五维能量(身/心/智/行/富)与能量沙盘区块
+    for (const dim of ['身', '心', '智', '富', '行']) {
+      expect(body, `首页应出现五维之一「${dim}」`).toContain(dim);
+    }
+    expect(body).toMatch(/沙盘|五维/);
+    expect(body.length, '首页正文应有实质内容').toBeGreaterThan(200);
+    await page.screenshot({ path: path.join(ART, 'frontend-02-index-content.png') });
+  });
+
+  test('TC-F-U03 TabBar 切换到「健康」正常跳转且无异常', async ({ page }) => {
+    const w = watch(page);
+    await page.goto('/frontend/', { waitUntil: 'domcontentloaded' });
+    await page.waitForTimeout(4500);
+    const before = page.url();
+
+    const clicked = await clickTab(page, '健康');
+    test.skip(!clicked, 'TabBar「健康」不可点(DOM 结构与预期不符),需更新选择器');
+    await page.waitForTimeout(3500);
+
+    expect(page.url(), '切 Tab 后路由应变化').not.toBe(before);
+    expect(page.url()).toMatch(/#\/pages\//);
+    const pageCount = await page.locator('uni-page, .uni-page').count();
+    expect(pageCount, '切换后仍应渲染页面根节点').toBeGreaterThan(0);
+    await page.screenshot({ path: path.join(ART, 'frontend-03-health.png') });
+    expect(w.errors, `切 Tab 过程不应有未捕获异常:${w.errors.join(' ;; ')}`).toHaveLength(0);
+  });
+
+  test('TC-F-U04 TabBar 切换到「成长」正常跳转', async ({ page }) => {
+    const w = watch(page);
+    await page.goto('/frontend/', { waitUntil: 'domcontentloaded' });
+    await page.waitForTimeout(4500);
+    const before = page.url();
+    const clicked = await clickTab(page, '成长');
+    test.skip(!clicked, 'TabBar「成长」不可点,需更新选择器');
+    await page.waitForTimeout(3500);
+    expect(page.url()).not.toBe(before);
+    await page.screenshot({ path: path.join(ART, 'frontend-04-growth.png') });
+    expect(w.errors, `切 Tab 过程不应有未捕获异常:${w.errors.join(' ;; ')}`).toHaveLength(0);
+  });
+
+  test('TC-F-U05 未登录访问「我的」应出现登录/注册入口(鉴权引导)', async ({ page }) => {
+    await page.goto('/frontend/#/pages/mine/index', { waitUntil: 'domcontentloaded' });
+    await page.waitForTimeout(4500);
+    const body = (await page.locator('body').innerText()).replace(/\s+/g, '');
+    const hasLoginEntry = /登录|注册|立即登录/.test(body);
+    expect(hasLoginEntry, '未登录态「我的」页应提供登录入口').toBeTruthy();
+    await page.screenshot({ path: path.join(ART, 'frontend-05-mine.png') });
+  });
+
+  test('TC-F-U06 首屏关键资源加载无失败(无 requestfailed)', async ({ page }) => {
+    const w = watch(page);
+    await page.goto('/frontend/', { waitUntil: 'networkidle', timeout: 60000 }).catch(() => {});
+    await page.waitForTimeout(2000);
+    // 过滤掉已知的媒体/统计类非关键失败不做断言,只统计 js/css 类资源
+    const critical = w.failed.filter((u) => /\.(js|css)(\?|$)/i.test(u));
+    expect(critical, `关键静态资源不应加载失败:${critical.join(' ;; ')}`).toHaveLength(0);
+  });
+});

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

@@ -0,0 +1,50 @@
+/**
+ * Playwright 线上 UI 级 E2E 配置(针对已部署站点 cfc.iwintrue.com)
+ * ================================================================
+ * 与 playwright.config.js 的关键区别:
+ *   - **无 globalSetup**:默认配置的 globalSetup 会执行 reset-db.sh 重置共享测试库,
+ *     对本配置(打生产/测试已部署站点)绝对禁止。
+ *   - 只做只读浏览器自动化:不写业务数据(唯一例外是登录,属只读会话建立)。
+ *   - workers=1、fullyParallel=false:单节点串行,符合「每个节点只能跑一个浏览器自动化任务」。
+ *   - 两个 project 按 spec 分流:admin 走桌面视口,frontend(小程序 H5)走移动视口。
+ *
+ * 运行(单节点、串行):
+ *   npx playwright test -c playwright.config.live.js --project=admin-desktop
+ *   npx playwright test -c playwright.config.live.js --project=frontend-mobile
+ */
+var path = require('path');
+
+var LIVE_BASE = process.env.LIVE_BASE_URL || 'http://cfc.iwintrue.com';
+
+module.exports = {
+  testDir: path.join(__dirname, 'e2e-live'),
+  timeout: 150000,
+  expect: { timeout: 15000 },
+  retries: 0,
+  workers: 1,
+  fullyParallel: false,
+  outputDir: path.join(__dirname, '.live-artifacts'),
+  reporter: [['list']],
+  use: {
+    baseURL: LIVE_BASE,
+    headless: true,
+    ignoreHTTPSErrors: true,
+    actionTimeout: 20000,
+    navigationTimeout: 60000,
+    screenshot: 'only-on-failure',
+    trace: 'off',
+    video: 'off',
+  },
+  projects: [
+    {
+      name: 'admin-desktop',
+      testMatch: /admin-live\.spec\.js$/,
+      use: { browserName: 'chromium', viewport: { width: 1440, height: 900 } },
+    },
+    {
+      name: 'frontend-mobile',
+      testMatch: /frontend-live\.spec\.js$/,
+      use: { browserName: 'chromium', viewport: { width: 375, height: 812 }, isMobile: true, hasTouch: true },
+    },
+  ],
+};