Commit a31056b09ed8586201669efea4dd4f98fd90aef9
1 parent
4dea0e57
fix(coding): 冒烟集断言不得依赖 UI 文案 / auth 实现细节 + storageState 必须写全登录态
实跑打脸(xlyClaude13,2026-07-23):4dea0e57 引入的冒烟集第 ③ 条规格写的是 「断言登录态有效(如页面出现仅登录后可见的导航元素)」,骨架照做生成了 `expect(getByTestId('user-box')).not.toHaveText(/未登录/)`。而该项目 `e2e/global-setup.ts` 写 storageState 时只写了 `erp_token`,`authSlice.ts` 却要求 `erp_token` + `erp_user` 两键齐备才回灌登录态——断言恒红。前端 test-gate 连跑 3 个 attempt(每次 7m17s,各含两遍建库副本)+ 仲裁循环,同代码同结果,证据文件三轮都写着 「重跑无法消除本失败」。冒烟集是增量模式下每轮必跑的那一份,它红一次就卡住整条流水线。 三处修正: - **冒烟 ③ 改为只断言「没有被踢回登录页」**:带 storageState 访问需登录的路由,断言 最终 URL 不是登录页 / 页面无登录表单。冒烟只回答「这份登录态被应用接受了吗」, **绝不**断言用户名、欢迎语、菜单项等 UI 文案——那依赖具体 auth 实现与文案,属业务 e2e 范畴。补通用红线:三条用例都不得出现业务字面量、不得依赖演示种子具体行、 不得随 FE 增减失效。冒烟红 = 栈或应用整体坏了,不该有第二种解读。 - **globalSetup 的 storageState 必须写「前端真正消费的完整登录态」**:生成前先 Grep frontend/src/ 定位 auth store 启动回灌读取的全部 key(token + 用户对象 + 权限等, 键名各项目不同),一个都不能少,写完把读取键集与写入键集逐项对账登记 decisions[]。 这是那个项目的真缺陷而不只是冒烟的问题——所有复用 storageState 的 e2e 都在未登录态 下跑,表现为「元素找得到但内容是未登录态」这种极难归因的红。 - **冒烟集加契约版本标记 `// erp-smoke-contract: v2`**:原 state 判据只查冒烟集存不存在, 坏的那份"存在"就永远判 exists:true 跳过重写,坏断言永久卡死。改为首行必须含 v2 标记, 找不到即判 exists:false 强制骨架按新规格重建(同 prototypeCommit 的判据套路)。 骨架自检也加一条:冒烟集逐条复核 + storageState 键集对账,别留到 test-gate 才炸。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing
1 changed file
with
23 additions
and
3 deletions
workflows/coding.mjs
| ... | ... | @@ -1299,6 +1299,11 @@ function frontendSkeletonPrompt(feItems, c) { |
| 1299 | 1299 | ' - **前端 dev server 必须由 Playwright 自己拥有并做就绪门(关键,防 e2e 连接竞态)**:在同一份配置里声明 `webServer` 段——`command` = 起前端 dev/preview 的命令(取自 `frontend/package.json` scripts),`url` = 前端 baseURL,`reuseExistingServer: true`,`timeout` 给足(≥120000)。Playwright 会**轮询 `url` 直到真正可连**才开始跑用例,这正是竞态的解药。', |
| 1300 | 1300 | ' - **绝不**用「并行起 vite + 直接跑 playwright」(如 `concurrently` / `&` 后台起再立刻跑)这类**无就绪门**的写法——那是本项目历史上最贵的一类假红灯。若项目约定必须自起,则**必须**在 `globalSetup` 里显式轮询前端端口直到返回 2xx/3xx 再返回,超时抛明确错误(`前端 dev server 在 <N>s 内未就绪 @ <url>`),**不允许**静默继续让用例去撞连接拒绝。', |
| 1301 | 1301 | ` - **globalSetup**(如 \`frontend/e2e/global-setup.*\`):\`node ${c.root}/scripts/setup-test-db.mjs\`(复制源库→一次性副本)→ 冷起后端 + 轮询健康端点就绪(Flyway 在此把本轮新迁移 apply 到副本)→ 执行 \`node ${c.root}/scripts/seed-demo-data.mjs\`(灌演示种子进副本)→ 用 \`config-vars.yaml\` 的 \`admin_init\` 凭据经 \`docs/05-API接口契约.md\` 登录端点取 JWT,写 \`storageState\`(admin 登录态供 e2e 复用)。`, |
| 1302 | + // storageState 写不全 = 整条 e2e 线的沉默塌陷:token 有了但 user 没写,前端 auth store 判「没登录」, | |
| 1303 | + // 于是每个复用 storageState 的用例都在未登录态下跑。2026-07-23 实测(xlyClaude13):authSlice 要求 | |
| 1304 | + // erp_token + erp_user 两键齐备才回灌,global-setup 只写了 erp_token,冒烟断言恒红且重跑三次不变。 | |
| 1305 | + // 「登录端点返回什么就写什么」不够——必须反查**前端真正读哪些键**,那才是登录态的判据。 | |
| 1306 | + ' - **storageState 必须写「前端真正消费的完整登录态」,不是只写 token(硬要求)**:生成 globalSetup 前,先用 Grep 在 `frontend/src/` 定位 auth store / 启动回灌逻辑(如 `store/**/auth*`、`localStorage.getItem(` 的全部调用点),**逐个列出**它恢复登录态时读取的 key(常见:token + 用户对象 + 权限/角色,键名各项目不同)。globalSetup 写 storageState 时**这些键一个都不能少**,值取自登录端点响应。少写任何一个 → 应用启动即判未登录,所有复用 storageState 的 e2e 都在未登录态下跑,且表现为「元素找得到但内容是未登录态」这种极难归因的红。写完自检:把读取键清单与实际写入键清单逐项对账,写进 `decisions[]`。', | |
| 1302 | 1307 | // globalTeardown 只 kill 进程、**不删副本**:test.mjs 的晋升(promote-to-source)在 e2e 之后跑、 |
| 1303 | 1308 | // 靠读副本的 flyway_schema_history 算 delta——teardown 若删副本,promote 会静默判"无迁移可晋升", |
| 1304 | 1309 | // 晋升链路断且无声。副本清理归各闸门的 finally(test.mjs / seed runner / 行为门 runner)在 promote 后做; |
| ... | ... | @@ -1313,8 +1318,17 @@ function frontendSkeletonPrompt(feItems, c) { |
| 1313 | 1318 | ' - 值为 `smoke` → 只匹配 `e2e/__smoke__/` 下的 spec。', |
| 1314 | 1319 | ' - 其它值 → 按逗号切分成路径片段列表,只匹配**路径含其中任一片段**的 spec(如 `order/create.spec.ts,order/list.spec.ts`)。', |
| 1315 | 1320 | ' - 实现用 `testMatch`(接受正则数组)或等价过滤;片段先做正则转义再拼,**绝不**把用户值当正则直接塞进去。', |
| 1316 | - ' - **冒烟集 `frontend/e2e/__smoke__/app-smoke.spec.*`(必须生成)**:3 条以内的最小用例——① 带 `storageState` 打开 baseURL 首页,断言应用根节点已挂载(非空白、无未捕获错误);② 访问 router 里**已指向真组件**的任一路由,断言其可达且未渲染 `data-fe-stub`;③ 断言登录态有效(如页面出现仅登录后可见的导航元素)。它是增量模式下后端 test-gate 的 e2e 段唯一会跑的东西,职责是「栈起得来 + 应用没整体炸」,**不是**业务断言,故必须与具体业务功能解耦、不随 FE 增减而失效。', | |
| 1317 | - ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 源库副本 + 本轮新迁移 + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐;**已存在但缺 `webServer` 就绪门(或用了无就绪门的并行起法)、缺 `ERP_E2E_SCOPE` 支持、缺冒烟集 → 必须补齐/改正**,这些都是已知会造成整轮 e2e 假红或增量模式失效的缺陷,不属于"已存在就不动"的范围。', | |
| 1321 | + // 冒烟集的唯一职责是「栈起得来 + 应用没整体炸」。它在增量模式下是后端 test-gate 唯一会跑的 e2e, | |
| 1322 | + // 一旦它自己红,整条流水线就被一条与业务无关的断言卡死——2026-07-23 实测正是如此:初版规格写了 | |
| 1323 | + // 「断言顶栏显示已登录用户名」,而该项目 storageState 缺 erp_user,断言恒红、连跑 3 个 attempt 无一例外。 | |
| 1324 | + // 教训:冒烟断言**只许依赖框架级不变量**(路由响应、重定向与否、根节点挂载),**绝不**依赖任何 | |
| 1325 | + // UI 文案字面量、组件内部结构、或某个 auth 实现细节——那些是业务 e2e 的活,不是冒烟的活。 | |
| 1326 | + ' - **冒烟集 `frontend/e2e/__smoke__/app-smoke.spec.*`(必须生成)**:3 条以内的最小用例,**文件首行逐字写 `// erp-smoke-contract: v2`**(版本标记,state 判据据此识别旧版冒烟集并重写)——', | |
| 1327 | + ' - ① 带 `storageState` 打开 baseURL 首页:断言应用根节点已挂载(容器内有子节点、非空白页)且无未捕获 JS 错误(监听 `pageerror`)。', | |
| 1328 | + ' - ② 访问 router 里**已指向真组件**的任一路由:断言 HTTP 响应非 4xx/5xx、页面未渲染 `data-fe-stub`。', | |
| 1329 | + ' - ③ 登录态冒烟**只断言「没有被踢回登录页」**:带 `storageState` 访问一个需登录才可进的路由,断言最终 URL **不是**登录页路径(或页面上不出现登录表单)。**绝不**断言顶栏用户名、欢迎语、菜单项等任何 UI 文案——那依赖具体 auth 实现与文案,属业务 e2e 范畴;冒烟只回答「这份登录态被应用接受了吗」。', | |
| 1330 | + ' - **通用红线**:三条用例都**不得**出现业务字面量(表名/字段名/中文文案/固定用户名)、不得依赖演示种子的具体行、不得随 FE 增减而失效。冒烟红 = 栈或应用整体坏了,不该有第二种解读。', | |
| 1331 | + ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 源库副本 + 本轮新迁移 + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐;**已存在但缺 `webServer` 就绪门(或用了无就绪门的并行起法)、缺 `ERP_E2E_SCOPE` 支持、缺冒烟集、storageState 未写全登录态、冒烟集无 `erp-smoke-contract: v2` 标记(旧版规格) → 必须补齐/改正/按 v2 重写**,这些都是已知会造成整轮 e2e 假红、恒红或增量模式失效的缺陷,不属于"已存在就不动"的范围。', | |
| 1318 | 1332 | '6. **单测基线(测试隔离接线点)**:vitest 配置(按 docs/04 § 零 `frontend.unit_test_runner` 约定,如 `frontend/vitest.config.*` 或 package.json 内配置段)`include` **限定** `tests/**/*.test.*`——单测一律落 `frontend/tests/**`(镜像 `src/` 结构,smoke 类归 `tests/__smoke__/` 且文件名同样以 `.test.*` 结尾,见 docs/04 § 2.1),`frontend/src/` 内的测试残留不被执行(约定漂移立即可见)。不存在才创建,已存在则只补齐 include 限定。**legacy 守卫**:若 `frontend/src/` 内已存在测试文件(旧约定 colocation 残留),**绝不**收窄 include(否则旧单测静默停跑、回归覆盖丢失)——保持现有 include 不动,登记 `decisions[]` 提示人工迁移(src/ 内测试迁至 tests/ 镜像路径后方可收窄)。', |
| 1319 | 1333 | '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', |
| 1320 | 1334 | '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', |
| ... | ... | @@ -1322,6 +1336,9 @@ function frontendSkeletonPrompt(feItems, c) { |
| 1322 | 1336 | '## 自检(可构建)', |
| 1323 | 1337 | '- 推断本项目前端 build / typecheck 命令(docs/04 § 零 / `frontend/package.json` scripts)。若可在子会话内安全跑(不挂死),**派 Agent 子会话**跑一次 build / dev-server 就绪探测确认骨架可构建可起;不可行则至少静态核对「docs/08 全量清单的每个 FE 路由都已声明 + 全 lazy + 导航无悬空 path + FeStub 存在」。', |
| 1324 | 1338 | '- **增量回归自检**:若本次是在已有 router 上补齐(往轮已实现过 FE),再核对一遍「本轮待实现清单**之外**的 FE 路由,component 指向与改动前一致」——用 `git diff` 看 router 文件,出现「真组件 → FeStub」的行即为误伤,必须改回。', |
| 1339 | + // 冒烟集恒红会把整条流水线卡在一条与业务无关的断言上(实测烧掉 3 个 attempt + 仲裁循环), | |
| 1340 | + // 而它每轮都跑,是所有 e2e 里最不该出错的一份。故在骨架期就把它跑通,别留到 test-gate 才炸。 | |
| 1341 | + '- **冒烟集自检(必做,别把它留给 test-gate 才发现)**:冒烟集是增量模式下每轮必跑的那 3 条,它红一次就卡住整条流水线。逐条复核:断言里有没有 UI 文案字面量 / 依赖某个 auth 实现细节 / 依赖演示种子具体行?有 → 按 v2 规格改写。再对账一遍 globalSetup 写入的 storageState 键集与前端 auth store 读取的键集是否一致——不一致就是「应用判未登录」的恒红源头。', | |
| 1325 | 1342 | '- 占位符扫描:`TBD` / `TODO` / `【人工填写:】` → 命中即修。', |
| 1326 | 1343 | '', |
| 1327 | 1344 | commitBlock('frontend/', 'feat(fe-skeleton): App 外壳 + router 全量 lazy 路由表 + FeStub 占位', |
| ... | ... | @@ -1349,11 +1366,14 @@ function frontendSkeletonStatePromptM(feItems, c) { |
| 1349 | 1366 | // 存量项目的 config 是在这两项存在之前生成的,必须靠本判据把它们拽回来补齐—— |
| 1350 | 1367 | // 否则 test-gate 设了 ERP_E2E_SCOPE 也没人消费,静默退化成全量(不假绿,但增量省时完全失效)。 |
| 1351 | 1368 | `- **e2e 范围开关判据**:Grep \`${c.root}/frontend/playwright.config.*\` 是否含 \`ERP_E2E_SCOPE\`,且 \`${c.root}/frontend/e2e/__smoke__/\` 下存在至少一个 spec 文件。任一缺失 → 判 \`exists:false\`(存量项目普遍缺这两项,需要重跑骨架 stage 补齐)。`, |
| 1369 | + // 版本标记而非内容启发式:坏冒烟集"存在"但断言不合规,只查存在性会永远判 exists:true 跳过重写。 | |
| 1370 | + // v1(无标记)里出现过依赖 UI 文案的登录断言,必然红且重跑无解——只能靠标记把它们全部拽回来重建。 | |
| 1371 | + `- **冒烟集契约版本判据**:Read \`${c.root}/frontend/e2e/__smoke__/\` 下的 spec,其中**必须**有一份首行逐字含 \`erp-smoke-contract: v2\`。找不到该标记(旧版冒烟集)→ 判 \`exists:false\`——旧版可能带着依赖 UI 文案 / auth 实现细节的断言,那类断言会恒红且重跑无解,必须由骨架 stage 按 v2 规格重写。`, | |
| 1352 | 1372 | '- 判据**只针对上面列出的 FE**:router 里其它 FE 路由(往轮已实现,指向真组件)一律视为满足,**不要**因为它们没指向 `FeStub` 就判缺失。', |
| 1353 | 1373 | '- 上面列出的 FE,其路由若已指向**真组件**(resume:上次已实现到一半)同样视为满足——本检测只问"路由在不在、是不是 lazy",不问指向谁。', |
| 1354 | 1374 | `- **legacy 豁免**:若 \`${c.root}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`, |
| 1355 | 1375 | '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`', |
| 1356 | - '- 任一缺失(无 router / 缺上面某个待实现 FE 的路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / **缺前端 dev server 就绪门** / **缺 `ERP_E2E_SCOPE` 支持或冒烟集** / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', | |
| 1376 | + '- 任一缺失(无 router / 缺上面某个待实现 FE 的路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / **缺前端 dev server 就绪门** / **缺 `ERP_E2E_SCOPE` 支持或冒烟集** / **冒烟集无 `erp-smoke-contract: v2` 标记** / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', | |
| 1357 | 1377 | '## 输出(EXISTS_SCHEMA)', |
| 1358 | 1378 | ].join('\n') |
| 1359 | 1379 | } | ... | ... |