Commit 4dea0e57287187155d0912143a34e5b96419b3a5
1 parent
543ea15a
perf(coding): test-gate 缺省只跑本轮改动关联的 e2e,全量回归拆成 /full-test 按需触发
起因:增量改一个功能仍会跑全量 e2e——用户改的功能与被跑的用例(如打印按钮那条) 毫无关系。实测 test-gate 占全流程墙钟 13.3%,且后端每个模块的 scripts/test.mjs 内部还会各跑一遍全量 playwright。 coding.mjs 新增 args.testScope(缺省 'affected',逃生口 'full'): - **只收窄 e2e 段**。build / lint / 后端单测+集成 / 前端 vitest 一律仍全量——它们是 分钟级的,却是编译期与逻辑回归的第一道网,砍掉省不下多少却漏掉最常见的连带破坏。 - 前端阶段 test-gate:范围 = 本轮待跑 FE 各自 plan 里 `测试先行类型 = e2e` 的 test_file 并集(verify 段早有同款推导,line 659);取不到则退冒烟集。 - 后端模块 test-gate(本轮无 FE 改动):退到 frontend/e2e/__smoke__/ 冒烟集。 不跳过 e2e 段——它同时是起栈健康检查。 - 首跑(全新项目)时「本轮待跑 FE」天然等于全部 FE,affected 与 full 重合;收窄只在 /add-req 后的增量重跑里真的生效,这正是它存在的场景。 载体是 env ERP_E2E_SCOPE,**scripts/test.mjs 一个字未动**——它的 setup→build→lint→ test→e2e→promote→drop 是迁移晋升链路的契约,且模板改动不落已生成项目(见 preflight 硬门);env 经 spawnSync 天然继承到 playwright 进程,前后端两条路径同一个开关。 消费端由 fe-skeleton 生成的 playwright.config 实现(未设=全量,绝不默认收窄), 同时生成与业务解耦的冒烟集;state 判据同步加严,存量项目会重跑一次骨架 stage 补齐。 防假绿(收窄的唯一真风险):范围推导错的表现是「0 条用例 + exit 0」,与真绿不可区分。 GATE_SCHEMA 加 e2eScope/e2ePassed,编排层 guardEmptyE2e() 硬判——green 且明确 0 条一律 翻 red 走既有 retry/仲裁链。module-report 被要求如实记录本次范围,禁止拔高成「全量回归」。 新增 /erp-workflow:full-test(skills/coding/full-test):跑 scripts/test.mjs 全链 + 显式清空 ERP_E2E_SCOPE 的全量 e2e,出 full-regression-<日期>.md 证据并提交。 前置硬门:工作树干净(绿后会晋升迁移到源库)、不与正在跑的 coding 流程撞端口。 后台子进程 + 回合内轮询;只跑测试、不改源码、红了只报告不修复。 代价明说:增量轮里跨功能连带回归(改共享组件 / router / 全局状态 / API 契约 / DB 迁移 打破没动过的功能)不再有网,由 /full-test 按需兜底。逃生口 testScope: "full"。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Showing
6 changed files
with
254 additions
and
21 deletions
.claude-plugin/plugin.json
| 1 | { | 1 | { |
| 2 | "name": "erp-workflow", | 2 | "name": "erp-workflow", |
| 3 | - "description": "ERP 项目全流程框架:阶段 A 计划(plan-start 入口 + A0~A5 共 6 个 skill + 增量需求入口 add-req + 微改快速通道 quick-field/quick-ui + B 阶段瘦入口 coding-start = 11 个 skill;plan-start 终结闸 4 项前移硬校验;add-req 经 req-ledger 哈希台账识别后续新增/变更需求只跑增量;quick-field 对「加列 + 界面呈现」、quick-ui 对「已有页面纯前端小交互」保 SSoT 跳流程、不作废 tag、不重跑 coding) + 阶段 B 编码(单个静默 Workflow 脚本 coding.mjs,子代理自动跑后端+前端功能循环、测试闸门、本地里程碑 tag)。", | 3 | + "description": "ERP 项目全流程框架:阶段 A 计划(plan-start 入口 + A0~A5 共 6 个 skill + 增量需求入口 add-req + 微改快速通道 quick-field/quick-ui + B 阶段瘦入口 coding-start + 全量回归入口 full-test = 12 个 skill;plan-start 终结闸 4 项前移硬校验;add-req 经 req-ledger 哈希台账识别后续新增/变更需求只跑增量;quick-field 对「加列 + 界面呈现」、quick-ui 对「已有页面纯前端小交互」保 SSoT 跳流程、不作废 tag、不重跑 coding) + 阶段 B 编码(单个静默 Workflow 脚本 coding.mjs,子代理自动跑后端+前端功能循环、测试闸门、本地里程碑 tag;测试闸缺省 affected 增量模式——e2e 只跑本轮改动关联用例,跨功能全量回归由 full-test 按需触发)。", |
| 4 | "version": "0.3.0", | 4 | "version": "0.3.0", |
| 5 | "skills": [ | 5 | "skills": [ |
| 6 | "./skills/plan/plan-start", | 6 | "./skills/plan/plan-start", |
| @@ -13,6 +13,7 @@ | @@ -13,6 +13,7 @@ | ||
| 13 | "./skills/plan/add-req", | 13 | "./skills/plan/add-req", |
| 14 | "./skills/plan/quick-field", | 14 | "./skills/plan/quick-field", |
| 15 | "./skills/plan/quick-ui", | 15 | "./skills/plan/quick-ui", |
| 16 | - "./skills/coding/coding-start" | 16 | + "./skills/coding/coding-start", |
| 17 | + "./skills/coding/full-test" | ||
| 17 | ] | 18 | ] |
| 18 | } | 19 | } |
README.md
| @@ -70,7 +70,8 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 | @@ -70,7 +70,8 @@ Claude Code 插件:ERP / 后端管理系统全流程开发框架。 | ||
| 70 | 按全部 FE spec 聚合的作用域并集枚举路由,交互/文字/样式三层断言;交互失效 | 70 | 按全部 FE spec 聚合的作用域并集枚举路由,交互/文字/样式三层断言;交互失效 |
| 71 | /sentinel 错/样式违规(非 token 色、横向溢出、控件重叠等)转可 fix must-fix | 71 | /sentinel 错/样式违规(非 token 色、横向溢出、控件重叠等)转可 fix must-fix |
| 72 | →fix→单测复验→重跑门(≤3 轮),软文字按来源仲裁,green 才放行) | 72 | →fix→单测复验→重跑门(≤3 轮),软文字按来源仲裁,green 才放行) |
| 73 | - → testGate(frontend,全量回归 vitest+playwright,兜底行为 fix 引入的回归) | 73 | + → testGate(frontend,vitest 全量 + playwright 按 testScope 定宽;缺省 affected 只跑本轮 |
| 74 | + 改动关联的 e2e,跨功能全量回归由 /erp-workflow:full-test 按需触发) | ||
| 74 | → runMilestone(milestone/frontend-phase) | 75 | → runMilestone(milestone/frontend-phase) |
| 75 | 76 | ||
| 76 | · 前端 jsdom-only 重叠(可选,parallel.frontendOverlap,缺省关):前端**不依赖后端**的部分 | 77 | · 前端 jsdom-only 重叠(可选,parallel.frontendOverlap,缺省关):前端**不依赖后端**的部分 |
| @@ -143,18 +144,20 @@ erp-workflow-plugin/ | @@ -143,18 +144,20 @@ erp-workflow-plugin/ | ||
| 143 | │ ├── add-req/ # 增量需求入口(Plan 完结后追加/修改需求,经 req-ledger 哈希台账只跑增量) | 144 | │ ├── add-req/ # 增量需求入口(Plan 完结后追加/修改需求,经 req-ledger 哈希台账只跑增量) |
| 144 | │ ├── quick-field/ # 字段微改快速通道(加列 + 界面呈现:保 SSoT 跳流程,不作废 tag、不重跑 coding) | 145 | │ ├── quick-field/ # 字段微改快速通道(加列 + 界面呈现:保 SSoT 跳流程,不作废 tag、不重跑 coding) |
| 145 | │ └── quick-ui/ # 前端小功能快速通道(已有页面局部小交互,只消费既有端点:直改 frontend/,不作废 tag、不重跑 coding) | 146 | │ └── quick-ui/ # 前端小功能快速通道(已有页面局部小交互,只消费既有端点:直改 frontend/,不作废 tag、不重跑 coding) |
| 146 | - └── coding/ # 阶段 B:1 个 skill(瘦入口) | ||
| 147 | - └── coding-start/ # 启动 workflows/coding.mjs | 147 | + └── coding/ # 阶段 B:2 个 skill |
| 148 | + ├── coding-start/ # 启动 workflows/coding.mjs | ||
| 149 | + └── full-test/ # 全量回归闸(按需触发,跑 scripts/test.mjs 全链 + 全量 e2e) | ||
| 148 | ``` | 150 | ``` |
| 149 | 151 | ||
| 150 | -## Skill 清单(11 个) | 152 | +## Skill 清单(12 个) |
| 151 | 153 | ||
| 152 | -### 入口(5 个) | 154 | +### 入口(6 个) |
| 153 | 155 | ||
| 154 | | Skill | 作用 | 谁调用 | | 156 | | Skill | 作用 | 谁调用 | |
| 155 | |---|---|---| | 157 | |---|---|---| |
| 156 | | `plan-start` | **A 阶段入口 + Plan 终结硬闸**。读 docs/08 § 一 找第一个未勾 A 子项 → 派发对应 A skill;A 全部完成时校验 4 项前移闸门(REQ 真实数据、`config-vars.yaml` 全部配置(含 DB 凭据 / 密钥)全锁、docs/04 § 零 命令齐、docs/05+02 已评审),全过才提示运行 `/erp-workflow:coding-start`,否则指出缺口不放行 | **用户手动** `/erp-workflow:plan-start` | | 158 | | `plan-start` | **A 阶段入口 + Plan 终结硬闸**。读 docs/08 § 一 找第一个未勾 A 子项 → 派发对应 A skill;A 全部完成时校验 4 项前移闸门(REQ 真实数据、`config-vars.yaml` 全部配置(含 DB 凭据 / 密钥)全锁、docs/04 § 零 命令齐、docs/05+02 已评审),全过才提示运行 `/erp-workflow:coding-start`,否则指出缺口不放行 | **用户手动** `/erp-workflow:plan-start` | |
| 157 | | `coding-start` | **B 阶段瘦入口**(`allowed-tools: Read Glob Workflow Bash(git ...)`)。校验 Plan 完成态(docs/08 § 一 全勾)+ 取得 projectRoot(git 就绪由 coding.mjs runBranchSetup 在运行时把守)→ 读 docs/08 § 二/§ 三 + `git tag -l 'milestone/*'` 概述阶段进度(Workflow Router 再用 `req-done/*` 判定功能级 resume)→ 若有 `docs/superpowers/RESUME.md` 则读末尾条目复盘上次 halt 原因/待跑模块 → 调用 `Workflow({scriptPath:"${CLAUDE_PLUGIN_ROOT}/workflows/coding.mjs", args:{projectRoot}})` 启动整个编码阶段 → 告知"已在后台启动" | **用户手动** `/erp-workflow:coding-start` | | 159 | | `coding-start` | **B 阶段瘦入口**(`allowed-tools: Read Glob Workflow Bash(git ...)`)。校验 Plan 完成态(docs/08 § 一 全勾)+ 取得 projectRoot(git 就绪由 coding.mjs runBranchSetup 在运行时把守)→ 读 docs/08 § 二/§ 三 + `git tag -l 'milestone/*'` 概述阶段进度(Workflow Router 再用 `req-done/*` 判定功能级 resume)→ 若有 `docs/superpowers/RESUME.md` 则读末尾条目复盘上次 halt 原因/待跑模块 → 调用 `Workflow({scriptPath:"${CLAUDE_PLUGIN_ROOT}/workflows/coding.mjs", args:{projectRoot}})` 启动整个编码阶段 → 告知"已在后台启动" | **用户手动** `/erp-workflow:coding-start` | |
| 160 | +| `full-test` | **全量回归闸**(按需触发)。跑项目 `scripts/test.mjs` 全链(setup-test-db → build → lint → 后端单测+集成 + 前端 vitest → **全量 e2e**(显式清空 `ERP_E2E_SCOPE`)→ 绿则晋升迁移 → 删副本),出 `docs/superpowers/module-reports/full-regression-<日期>.md` 证据报告并提交。存在的理由:`coding-start` 缺省 affected 增量模式只跑本轮改动关联的 e2e(省时),**跨功能连带回归的网就是本命令**——一批需求做完 / 合并前 / 改过共享组件·router·全局状态·API 契约·DB 迁移时手动跑。只跑测试、不改源码、红了不修。 | **用户手动** `/erp-workflow:full-test` | | ||
| 158 | | `add-req` | **增量需求入口**(Plan 完结后追加 / 修改需求时用)。`node lib/req-ledger.mjs scan` 用内容哈希台账(`.req-ledger.json`)识别 docs/01 REQ 卡片 + docs/08 §三 FE 行的**新增 / 变更**;只对增量做 **A3-delta**(写新 `V_n` migration + 同步 docs/03,**绝不改 V1**)+ **A5-delta**(补 docs/05 端点 / docs/02 顺序 / docs/08 模块行 / FE 行),并 `git tag -d` 作废**变更**单元已有的 `req-done/*`(及所属 `milestone/*`,复位 docs/08 里程碑字段),使 coding.mjs Router 只重跑增量。首跑无台账则先建基线。 | **用户手动** `/erp-workflow:add-req` | | 161 | | `add-req` | **增量需求入口**(Plan 完结后追加 / 修改需求时用)。`node lib/req-ledger.mjs scan` 用内容哈希台账(`.req-ledger.json`)识别 docs/01 REQ 卡片 + docs/08 §三 FE 行的**新增 / 变更**;只对增量做 **A3-delta**(写新 `V_n` migration + 同步 docs/03,**绝不改 V1**)+ **A5-delta**(补 docs/05 端点 / docs/02 顺序 / docs/08 模块行 / FE 行),并 `git tag -d` 作废**变更**单元已有的 `req-done/*`(及所属 `milestone/*`,复位 docs/08 里程碑字段),使 coding.mjs Router 只重跑增量。首跑无台账则先建基线。 | **用户手动** `/erp-workflow:add-req` | |
| 159 | | `quick-field` | **字段微改快速通道**(已完成需求追加纯展示/录入字段时替代 add-req 全流程)。保 schema SSoT 不缩水(`V_n` ALTER + docs/03 同步 + validate-ddl fail-closed),后端/前端代码由主会话直改(grep 清单 + 轻量编译/lint 自检),台账 `req-ledger commit` 重基线但**绝不作废任何 req-done/milestone tag**——Router 视一切已完成,不重跑 coding.mjs(不冷起栈、不跑测试门/行为门)。代价是字段未过行为验收:登记到 docs/08 §四 待验清单,攒批人工验收。仅限追加式 ADD COLUMN(可空/带 DEFAULT)+ 既有链路多带一个字段;超界即停下回落 `/add-req`。 | **用户手动** `/erp-workflow:quick-field` | | 162 | | `quick-field` | **字段微改快速通道**(已完成需求追加纯展示/录入字段时替代 add-req 全流程)。保 schema SSoT 不缩水(`V_n` ALTER + docs/03 同步 + validate-ddl fail-closed),后端/前端代码由主会话直改(grep 清单 + 轻量编译/lint 自检),台账 `req-ledger commit` 重基线但**绝不作废任何 req-done/milestone tag**——Router 视一切已完成,不重跑 coding.mjs(不冷起栈、不跑测试门/行为门)。代价是字段未过行为验收:登记到 docs/08 §四 待验清单,攒批人工验收。仅限追加式 ADD COLUMN(可空/带 DEFAULT)+ 既有链路多带一个字段;超界即停下回落 `/add-req`。 | **用户手动** `/erp-workflow:quick-field` | |
| 160 | | `quick-ui` | **前端小功能快速通道**(quick-field 的姊妹通道,已完成页面追加局部小交互时替代 add-req)。代码由主会话直改 `frontend/`(lint/类型检查 + grep 自检,不起栈、不跑行为门/e2e),可见布局变化同步 `prototype/`、按需更新 docs/08 §三 FE 行文字,台账 `req-ledger commit` 重基线但**绝不作废任何 tag**——前端阶段不重跑。待验条目登记 docs/08 §四。硬边界:纯前端、只消费 docs/05 既有端点、不新增页面/路由/FE 行;超界即停下回落 `/add-req`。 | **用户手动** `/erp-workflow:quick-ui` | | 163 | | `quick-ui` | **前端小功能快速通道**(quick-field 的姊妹通道,已完成页面追加局部小交互时替代 add-req)。代码由主会话直改 `frontend/`(lint/类型检查 + grep 自检,不起栈、不跑行为门/e2e),可见布局变化同步 `prototype/`、按需更新 docs/08 §三 FE 行文字,台账 `req-ledger commit` 重基线但**绝不作废任何 tag**——前端阶段不重跑。待验条目登记 docs/08 §四。硬边界:纯前端、只消费 docs/05 既有端点、不新增页面/路由/FE 行;超界即停下回落 `/add-req`。 | **用户手动** `/erp-workflow:quick-ui` | |
skills/coding/coding-start/SKILL.md
| @@ -26,7 +26,8 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) | @@ -26,7 +26,8 @@ allowed-tools: Read Glob Workflow Bash(git rev-parse *) Bash(git tag *) | ||
| 26 | approve 即打 req-done | 26 | approve 即打 req-done |
| 27 | 前端行为门 阶段级 behavior(整个前端只跑一次:起全栈验「按钮真生效/文字对/样式合规」, | 27 | 前端行为门 阶段级 behavior(整个前端只跑一次:起全栈验「按钮真生效/文字对/样式合规」, |
| 28 | 硬问题可 fix→单测复验→重跑门(≤3 轮),green 才进测试闸) | 28 | 硬问题可 fix→单测复验→重跑门(≤3 轮),green 才进测试闸) |
| 29 | - 前端测试闸 test-gate(全量回归,兜底行为 fix 引入的回归) | 29 | + 前端测试闸 test-gate(e2e 只跑本轮改动关联用例 = affected 增量模式; |
| 30 | + build/lint/单测 仍全量。跨功能全量回归请按需跑 /erp-workflow:full-test) | ||
| 30 | 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/<id> tag) | 31 | 跨模块记录 → 模块报告 → 里程碑(merge --no-ff + milestone/<id> tag) |
| 31 | 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑 | 32 | 任一模块 halt → fail-fast 停在该模块,修复后重跑本入口即可续跑 |
| 32 | 33 | ||
| @@ -103,6 +104,15 @@ Workflow({ | @@ -103,6 +104,15 @@ Workflow({ | ||
| 103 | }) | 104 | }) |
| 104 | ``` | 105 | ``` |
| 105 | 106 | ||
| 107 | +> `testScope` **刻意不传**——缺省即 `"affected"`(增量模式):test-gate 的 **e2e 段**只跑本轮改动关联的用例 | ||
| 108 | +> (前端取本轮待跑 FE 各自 plan 里 `测试先行类型 = e2e` 的 `test_file` 并集;后端模块本轮无待跑 FE 时退到 | ||
| 109 | +> `frontend/e2e/__smoke__/` 冒烟集),载体是 env `ERP_E2E_SCOPE`,由 fe-skeleton 生成的 `playwright.config` | ||
| 110 | +> 消费。**收窄只作用于 e2e**:build / lint / 后端单测+集成 / 前端 vitest 一律仍全量。首跑(全新项目)时 | ||
| 111 | +> "本轮待跑 FE" 天然等于全部 FE,与全量重合——收窄只在 `/add-req` 后的增量重跑里真的生效。 | ||
| 112 | +> **代价明说**:跨功能连带回归(改共享组件 / router / 全局状态 / API 契约 / DB 迁移打破了没动过的功能) | ||
| 113 | +> 在增量轮里不再有网,由用户按需跑 `/erp-workflow:full-test` 兜底。**逃生口**:传 `testScope: "full"` | ||
| 114 | +> 即回到每轮全量回归的现行行为。防假绿:范围推导错导致 e2e 匹配 0 条时,编排层硬判 red(`guardEmptyE2e`)。 | ||
| 115 | + | ||
| 106 | > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 | 116 | > `pluginRoot` 供 coding.mjs 调用插件 `lib/req-ledger.mjs` 在首跑时自动建立需求台账基线(P1#3,使日后 `/add-req` 能识别增量、免空跑);缺省则该增强静默跳过。 |
| 107 | > | 117 | > |
| 108 | > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。 | 118 | > `parallel` 是模块级并行总开关:`modules: true` 时 coding.mjs 先跑调度器(LLM 只输出模块间依赖边,JS 做确定性拓扑波次),同波次 ≥2 个后端模块各自在 lane worktree + lane 测试库 + 专属 migration 版本段内并行推进;任何调度/隔离环节失败一律 fail-open 降级串行,绝不因并行机制 halt。**逃生口**:改传 `parallel: { modules: false }`(或整个不传 `parallel`)即完全回退现行串行路径。 |
skills/coding/full-test/SKILL.md
0 → 100644
| 1 | +--- | ||
| 2 | +name: full-test | ||
| 3 | +description: 全量回归入口——按需跑一次完整测试闸(build + lint + 后端单测/集成 + 前端 vitest + **全量 e2e**),出证据报告并提交。coding-start 的增量模式只跑本轮改动关联的 e2e(省时),跨功能连带回归靠本命令兜底:改完一批需求、合并前、发布前、或怀疑历史功能被打破时手动运行。只跑测试、不改任何源码;红了只报告不修复。 | ||
| 4 | +user-invocable: true | ||
| 5 | +allowed-tools: Read Write Edit Glob Grep Bash(node *) Bash(git *) Bash(ls *) Bash(cat *) Bash(sleep *) Bash(pnpm *) Bash(npm *) Bash(npx *) | ||
| 6 | +--- | ||
| 7 | + | ||
| 8 | +**所有输出必须使用中文。** | ||
| 9 | + | ||
| 10 | +# full-test — 全量回归闸(按需触发) | ||
| 11 | + | ||
| 12 | +`/erp-workflow:coding-start` 缺省跑 **affected 增量模式**:test-gate 的 e2e 段只覆盖本轮改动关联的用例, | ||
| 13 | +其余 e2e 不跑(`testScope: "affected"`,见 `workflows/coding.mjs` 顶部 `TEST_SCOPE`)。 | ||
| 14 | +这是为省时刻意留的缺口——**跨功能连带回归由本命令兜底**。 | ||
| 15 | + | ||
| 16 | +**什么时候该跑:** 一批需求做完准备合并 / 发布前 / 改过共享组件·router·全局状态·API 契约·DB 迁移 / | ||
| 17 | +怀疑某个没动过的功能被打破了 / 距上次全量回归已过去若干轮增量。 | ||
| 18 | + | ||
| 19 | +**本命令不做的事:** 不改任何源码、不修红灯、不打 tag、不合并分支、不动 `docs/08`。红了只出报告。 | ||
| 20 | + | ||
| 21 | +## 步骤 0:打印横幅 | ||
| 22 | + | ||
| 23 | +``` | ||
| 24 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 25 | + [full-test] 全量回归闸 | ||
| 26 | + | ||
| 27 | + 跑 scripts/test.mjs 完整链: | ||
| 28 | + setup-test-db(复制源库→一次性副本) | ||
| 29 | + → build(前后端)→ lint(前后端) | ||
| 30 | + → 单测 + 集成(后端 + 前端 vitest) | ||
| 31 | + → e2e(**全量** playwright,不设 ERP_E2E_SCOPE) | ||
| 32 | + → 绿则晋升本轮新迁移到源库 → 无论红绿删副本 | ||
| 33 | + | ||
| 34 | + 只跑测试、不改源码。预计耗时较长(含冷起全栈),跑完出证据报告。 | ||
| 35 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 36 | +``` | ||
| 37 | + | ||
| 38 | +## 步骤 1:前置校验(任一不满足即停下,不启动测试) | ||
| 39 | + | ||
| 40 | +1. **取 projectRoot**:`Bash(git rev-parse --show-toplevel)`,stdout 记为 `projectRoot`(绝对路径)。失败 → 缺口「当前目录不在 git 仓库内」。 | ||
| 41 | +2. **闸门脚本存在**:`Glob` `scripts/test.mjs`。不存在 → 缺口「项目缺 scripts/test.mjs,先跑 /erp-workflow:plan-start 生成骨架」。 | ||
| 42 | +3. **工作树干净**:`Bash(git status --porcelain)` 无输出。有输出 → 缺口「工作树不干净:<摘要>」。 | ||
| 43 | + > 为什么是硬门:`test.mjs` 绿后会把副本上验证过的新迁移 **晋升到源库**(`promote-to-source.mjs`)。 | ||
| 44 | + > 带着未提交改动跑,等于让一份没进版本库的状态去改源库 schema,事后无法复现是哪次改动导致的。 | ||
| 45 | +4. **不在跑其它闸门**:`Glob` `.tmp/behavior-gate/` / `.tmp/seed-gen/` 下是否有本次之外的活动痕迹,并确认没有正在运行的 `coding.mjs`。有 → 提示用户「检测到可能正在运行的编码流程,全量回归会冷起同端口的栈、互相击杀,请等它结束再跑」并停下。 | ||
| 46 | + | ||
| 47 | +任一缺口 → 输出拦截横幅(逐条列缺口 + 处理位置),停下,末尾附 `[ERP-HALT]`。 | ||
| 48 | + | ||
| 49 | +## 步骤 2:确认 e2e 范围开关不会误伤 | ||
| 50 | + | ||
| 51 | +`Grep` `frontend/playwright.config.*` 是否含 `ERP_E2E_SCOPE`: | ||
| 52 | + | ||
| 53 | +- **含** → 本命令**必须显式清空**该变量再跑(见步骤 3),保证跑的是全量而不是继承了外壳里的残留值。 | ||
| 54 | +- **不含** → 说明该项目的 config 还没接范围开关(存量项目),此时 e2e 本来就恒为全量,直接跑即可;在报告里注记一句即可,不必修 config(补齐由 coding.mjs 的 fe-skeleton stage 负责)。 | ||
| 55 | + | ||
| 56 | +## 步骤 3:跑闸门(后台子进程 + 轮询,绝不在主推理流里同步跑) | ||
| 57 | + | ||
| 58 | +`scripts/test.mjs` 动辄几十分钟,**必须**派**隔离后台子进程**跑,主会话只轮询、只读回结构化摘要: | ||
| 59 | + | ||
| 60 | +1. 建日志目录 `.tmp/full-test/`(`Bash(node -e "...")` 用 `fs.mkdirSync(..., {recursive:true})`,跨平台)。 | ||
| 61 | +2. 用 `Bash` 的 `run_in_background` 起 detached 子进程,**显式清空** `ERP_E2E_SCOPE`,输出重定向到 `.tmp/full-test/run.log`,退出码写 `.tmp/full-test/exit`: | ||
| 62 | + | ||
| 63 | + ``` | ||
| 64 | + ERP_E2E_SCOPE= node scripts/test.mjs > .tmp/full-test/run.log 2>&1; echo $? > .tmp/full-test/exit | ||
| 65 | + ``` | ||
| 66 | + | ||
| 67 | + > Windows/PowerShell 环境下改用 `node -e` 起一个 `spawn` 包装(`env: {...process.env, ERP_E2E_SCOPE: ''}`), | ||
| 68 | + > 效果等价——关键是**子进程 env 里 `ERP_E2E_SCOPE` 为空**,且日志落盘、退出码落盘。 | ||
| 69 | +3. **在同一回合内轮询到终态**:反复 `Bash` 调用(每轮 `sleep 60`~`120` 后读 `.tmp/full-test/exit` 是否存在 + 读 `run.log` 尾部若干行),多少轮都行。 | ||
| 70 | + **绝不**用 Monitor / ScheduleWakeup / 「装个监听器然后结束回合等通知」——结束回合就等于宣告完成。 | ||
| 71 | +4. 每轮向用户播报一行当前进度(从 `run.log` 里 `[test.mjs] N/5 <阶段>` 行取),让长跑可见。 | ||
| 72 | +5. 拿到 exit 码后,从 `run.log` **只**提取结构化摘要(**不要**把全文灌进上下文): | ||
| 73 | + - 各阶段(build / lint / 单测 / e2e)的通过与否; | ||
| 74 | + - 失败用例名 + 断言摘要(≤30 行); | ||
| 75 | + - e2e 实跑条数(playwright 的 `N passed` 行)——**0 条即异常**,说明范围开关被误设,按红处理。 | ||
| 76 | + | ||
| 77 | +## 步骤 4:证据报告 + 提交 | ||
| 78 | + | ||
| 79 | +写 `docs/superpowers/module-reports/full-regression-<YYYY-MM-DD>.md`(同日多次跑加 `-r<N>` 后缀,**绝不覆盖**前一次): | ||
| 80 | + | ||
| 81 | +```markdown | ||
| 82 | +# 全量回归报告 <YYYY-MM-DD>[-r<N>] | ||
| 83 | + | ||
| 84 | +- 触发:/erp-workflow:full-test(人工按需) | ||
| 85 | +- 分支 / HEAD:<branch> @ <短哈希> | ||
| 86 | +- 命令:ERP_E2E_SCOPE=(空,全量) node scripts/test.mjs | ||
| 87 | +- 结论:GREEN / RED | ||
| 88 | +- e2e 实跑条数:<N>(0 条 = 范围开关误设,按 RED 处理) | ||
| 89 | +- 各阶段:build <ok/fail> / lint <ok/fail> / 后端单测集成 <ok/fail> / 前端 vitest <ok/fail> / e2e <ok/fail> | ||
| 90 | +- 迁移晋升:<已晋升 V<n>... / 无新迁移 / 未晋升(红)> | ||
| 91 | + | ||
| 92 | +## 失败摘要(RED 时必填) | ||
| 93 | +<失败用例 + 断言摘要,≤30 行> | ||
| 94 | + | ||
| 95 | +## 距上次全量回归的增量 | ||
| 96 | +<git log --oneline <上次 full-regression 报告的提交>..HEAD 摘要;首次跑写「首次」> | ||
| 97 | +``` | ||
| 98 | + | ||
| 99 | +`Bash(git add <报告> && git commit -m "docs(full-test): 全量回归报告 <日期> <GREEN|RED>")`。 | ||
| 100 | +commit 失败 → 报告已落盘即可,把 stderr 摘要告诉用户,不 halt。 | ||
| 101 | + | ||
| 102 | +## 步骤 5:输出结论横幅 | ||
| 103 | + | ||
| 104 | +绿: | ||
| 105 | + | ||
| 106 | +``` | ||
| 107 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 108 | + [full-test] ✅ 全量回归 GREEN | ||
| 109 | + | ||
| 110 | + e2e 实跑:<N> 条(全量) | ||
| 111 | + 报告:docs/superpowers/module-reports/full-regression-<日期>.md | ||
| 112 | + 迁移晋升:<已晋升 V<n>... / 无新迁移> | ||
| 113 | + | ||
| 114 | + [ERP-HALT] 全量回归通过。 | ||
| 115 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 116 | +``` | ||
| 117 | + | ||
| 118 | +红: | ||
| 119 | + | ||
| 120 | +``` | ||
| 121 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 122 | + [full-test] ⛔ 全量回归 RED | ||
| 123 | + | ||
| 124 | + 失败阶段:<build / lint / 单测 / e2e> | ||
| 125 | + 失败用例:<逐条列出,不截断关键断言> | ||
| 126 | + 报告:docs/superpowers/module-reports/full-regression-<日期>.md | ||
| 127 | + | ||
| 128 | + 这些失败**不在**最近几轮增量的 e2e 覆盖范围内——属于跨功能连带回归。 | ||
| 129 | + 下一步:把失败功能作为变更单元跑 /erp-workflow:add-req 重新走编码, | ||
| 130 | + 或直接修复后再跑一次 /erp-workflow:full-test。 | ||
| 131 | + [ERP-HALT] 已停下,本命令不修复。 | ||
| 132 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
| 133 | +``` | ||
| 134 | + | ||
| 135 | +本 skill 到此结束,**不调用任何下游 skill**、**不自动修复**。 | ||
| 136 | + | ||
| 137 | +## 参考 | ||
| 138 | + | ||
| 139 | +- `scripts/test.mjs`(项目内闸门脚本:setup→build→lint→test→e2e→promote→drop 全时序) | ||
| 140 | +- `frontend/playwright.config.*`(`ERP_E2E_SCOPE` 范围开关的消费端,由 coding.mjs 的 fe-skeleton stage 生成/补齐) | ||
| 141 | +- `workflows/coding.mjs` 顶部 `TEST_SCOPE`(增量模式定义与收窄边界) | ||
| 142 | +- `coding-start`(编码入口,缺省 affected 增量模式;传 `testScope: "full"` 可让它自己跑全量) | ||
| 143 | +- `add-req`(增量需求入口,本命令报出的连带回归从这里回流) |
skills/plan/add-req/SKILL.md
| @@ -166,6 +166,8 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | @@ -166,6 +166,8 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/req-ledger.mjs scan <root> | ||
| 166 | 166 | ||
| 167 | 台账已更新并随全部增量产物 git 提交(工作树干净)。 | 167 | 台账已更新并随全部增量产物 git 提交(工作树干净)。 |
| 168 | 运行 /erp-workflow:coding-start 增量编码(Router 只跑缺 tag 的模块/功能)。 | 168 | 运行 /erp-workflow:coding-start 增量编码(Router 只跑缺 tag 的模块/功能)。 |
| 169 | + 注:增量编码的 test-gate 只跑本轮改动关联的 e2e(省时);跨功能连带回归 | ||
| 170 | + 请在一批需求做完 / 合并前跑 /erp-workflow:full-test 兜一次全量。 | ||
| 169 | [ERP-HALT] 增量处理完成,已停下(不自动进编码)。 | 171 | [ERP-HALT] 增量处理完成,已停下(不自动进编码)。 |
| 170 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | 172 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 171 | ``` | 173 | ``` |
workflows/coding.mjs
| @@ -13,7 +13,8 @@ export const meta = { | @@ -13,7 +13,8 @@ export const meta = { | ||
| 13 | ], | 13 | ], |
| 14 | // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** | 14 | // 注:'Behavior' = 阶段级行为验收门(v3)——整个前端阶段在 featureLoop 全部 FE 完成后只跑**一次** |
| 15 | // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。 | 15 | // 行为验收(含 fix 循环),不再在每个 FE 的 review 循环内做 approve 子门(v2 per-FE 形态已撤销)。 |
| 16 | - // 时序:featureLoop(frontend) → Behavior(行为门+fix)→ Gate(testGate 全量回归,兜底行为 fix 引入的回归)。 | 16 | + // 时序:featureLoop(frontend) → Behavior(行为门+fix)→ Gate(testGate,兜底行为 fix 引入的回归; |
| 17 | + // e2e 宽度见 TEST_SCOPE:缺省 affected 只跑本轮改动关联用例,跨功能全量回归归 /erp-workflow:full-test)。 | ||
| 17 | // 注:'Preview' = 静态原型渲染门——前端段开头一次性渲染 prototype/**/*.html 本身(Playwright headless | 18 | // 注:'Preview' = 静态原型渲染门——前端段开头一次性渲染 prototype/**/*.html 本身(Playwright headless |
| 18 | // 截图归档 + 原型自带交互点击冒烟)。与 Behavior 正交:Preview 测**静态原型**、Behavior 测**实现**。 | 19 | // 截图归档 + 原型自带交互点击冒烟)。与 Behavior 正交:Preview 测**静态原型**、Behavior 测**实现**。 |
| 19 | // 硬门:原型渲染失败 / 脚本未捕获异常(jsErrors) → halt(原型属上游 plan 权威,coding 不改原型源码, | 20 | // 硬门:原型渲染失败 / 脚本未捕获异常(jsErrors) → halt(原型属上游 plan 权威,coding 不改原型源码, |
| @@ -83,9 +84,14 @@ const ADJUDICATE_SCHEMA = { type:'object', additionalProperties:false, | @@ -83,9 +84,14 @@ const ADJUDICATE_SCHEMA = { type:'object', additionalProperties:false, | ||
| 83 | guidance:{type:'string'}, | 84 | guidance:{type:'string'}, |
| 84 | rationale:{type:'string'} } } | 85 | rationale:{type:'string'} } } |
| 85 | 86 | ||
| 87 | +// e2eScope / e2ePassed:本次 e2e 段实际跑了什么范围、匹配到多少条用例。 | ||
| 88 | +// 存在的理由是**防假绿**:增量模式把 e2e 收窄成 env 过滤后,范围推导错误的表现是"0 条用例、exit 0", | ||
| 89 | +// 与真绿不可区分。故让子代理据实回报条数,JS 侧硬判 0 条即 red(见 testGate),不给沉默通过留口子。 | ||
| 86 | const GATE_SCHEMA = { type:'object', additionalProperties:false, | 90 | const GATE_SCHEMA = { type:'object', additionalProperties:false, |
| 87 | required:['status'], properties:{ status:{type:'string',enum:['green','red']}, | 91 | required:['status'], properties:{ status:{type:'string',enum:['green','red']}, |
| 88 | - failures:{type:'array',items:{type:'string'}} } } | 92 | + failures:{type:'array',items:{type:'string'}}, |
| 93 | + e2eScope:{type:'string'}, | ||
| 94 | + e2ePassed:{type:'integer'} } } | ||
| 89 | 95 | ||
| 90 | // BEHAVIOR_GATE_SCHEMA:前端行为门(阶段级,frontend-phase 末尾一次)返回。 | 96 | // BEHAVIOR_GATE_SCHEMA:前端行为门(阶段级,frontend-phase 末尾一次)返回。 |
| 91 | // 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 样式层 / 覆盖率 / 环境错误分别结构化, | 97 | // 不杂交 GATE×STAGE_RESULT——复用既有词汇但独立成型:交互层 / 文字层 / 样式层 / 覆盖率 / 环境错误分别结构化, |
| @@ -287,6 +293,18 @@ if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { | @@ -287,6 +293,18 @@ if (ROOT === '.' || !(/^(?:\/|[A-Za-z]:[\\/])/.test(ROOT))) { | ||
| 287 | // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 | 293 | // 缺省(旧 coding-start 未传)则相关增强静默跳过——由 /add-req 首跑兜底建基线。 |
| 288 | const PLUGIN = ARGS?.pluginRoot || '' | 294 | const PLUGIN = ARGS?.pluginRoot || '' |
| 289 | 295 | ||
| 296 | +// 测试范围(testScope):test-gate 的 **e2e 段**跑多宽。缺省 'affected'。 | ||
| 297 | +// - 'affected'(缺省):只跑本轮改动关联的 e2e——前端阶段取本轮 feItems 各自 plan 里 | ||
| 298 | +// `测试先行类型 = e2e` 的 test_file 并集;后端模块(本轮无前端改动)退到冒烟集。 | ||
| 299 | +// - 'full':现行全量回归。 | ||
| 300 | +// **收窄只作用于 e2e 段**:build / lint / 后端单测+集成 / 前端 vitest 一律仍是全量——它们是分钟级的, | ||
| 301 | +// 却是编译期与逻辑回归的第一道网,砍掉省不下多少时间反而漏掉最常见的连带破坏。 | ||
| 302 | +// 载体是 env `ERP_E2E_SCOPE`(fe-skeleton 生成的 playwright.config 消费,缺省不过滤)。**绝不**改 | ||
| 303 | +// `scripts/test.mjs` 的调用形态——它的 setup→build→lint→test→e2e→promote→drop 时序是迁移晋升链路的 | ||
| 304 | +// 契约(且模板改动不落已生成项目,见 preflight 硬门),加 env 是唯一既生效又不动安全边界的接口。 | ||
| 305 | +// 全量回归改为按需触发:`/erp-workflow:full-test`。 | ||
| 306 | +const TEST_SCOPE = ARGS?.testScope === 'full' ? 'full' : 'affected' | ||
| 307 | + | ||
| 290 | // 模块执行上下文:串行/主根模式 root=ROOT、lane=null;并行 lane 模式由 Phase C/D 填充。 | 308 | // 模块执行上下文:串行/主根模式 root=ROOT、lane=null;并行 lane 模式由 Phase C/D 填充。 |
| 291 | // 所有"模块作用域"的 prompt builder 与 runner 一律经 C 取根,不再直接闭包 ROOT。 | 309 | // 所有"模块作用域"的 prompt builder 与 runner 一律经 C 取根,不再直接闭包 ROOT。 |
| 292 | // ROOT 仍保留:仅供"主根专属"操作(milestone merge / docs/08 字段 / RESUME / ledger / preflight)使用。 | 310 | // ROOT 仍保留:仅供"主根专属"操作(milestone merge / docs/08 字段 / RESUME / ledger / preflight)使用。 |
| @@ -743,10 +761,16 @@ function fixPrompt(id, phase, issues, c) { | @@ -743,10 +761,16 @@ function fixPrompt(id, phase, issues, c) { | ||
| 743 | // ---- 测试闸(原 test-gate)---- | 761 | // ---- 测试闸(原 test-gate)---- |
| 744 | // attempt:1 = 首次跑;2 = 上轮 red 后的 flake 重试。每次 attempt 写到独立证据文件,避免 retry | 762 | // attempt:1 = 首次跑;2 = 上轮 red 后的 flake 重试。每次 attempt 写到独立证据文件,避免 retry |
| 745 | // 把首次 red 证据覆盖掉(report § ⑤ 失去 flake 信号)。 | 763 | // 把首次 red 证据覆盖掉(report § ⑤ 失去 flake 信号)。 |
| 764 | +// e2e 范围见 TEST_SCOPE 注释:affected(缺省)只收窄 **e2e 段**,其余全量不动。 | ||
| 765 | +// 首跑(全新项目)时"本轮 feItems"天然等于全部 FE,affected 与 full 的 e2e 范围重合—— | ||
| 766 | +// 收窄只在增量重跑(/add-req 后只有少数 FE 待跑)时才真的生效,这正是它存在的场景。 | ||
| 746 | function gatePrompt(module, phase, attempt = 1, c) { | 767 | function gatePrompt(module, phase, attempt = 1, c) { |
| 747 | const fe = isFrontend(phase) | 768 | const fe = isFrontend(phase) |
| 748 | const id = module?.id ?? '<module>' | 769 | const id = module?.id ?? '<module>' |
| 749 | const phaseId = fe ? 'frontend-phase' : id | 770 | const phaseId = fe ? 'frontend-phase' : id |
| 771 | + const affected = TEST_SCOPE !== 'full' | ||
| 772 | + const feItems = (module?.feItems) || [] | ||
| 773 | + const feList = feItems.map(x => `\`${x}\``).join(', ') | ||
| 750 | return [ | 774 | return [ |
| 751 | `# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase}, attempt=${attempt})`, | 775 | `# test-gate — ${fe ? '前端阶段' : `模块 ${id}`} 硬测试闸(phase=${phase}, attempt=${attempt})`, |
| 752 | '', | 776 | '', |
| @@ -758,8 +782,30 @@ function gatePrompt(module, phase, attempt = 1, c) { | @@ -758,8 +782,30 @@ function gatePrompt(module, phase, attempt = 1, c) { | ||
| 758 | '', | 782 | '', |
| 759 | '## 命令', | 783 | '## 命令', |
| 760 | fe | 784 | fe |
| 761 | - ? `- 前端:命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 拼接(缺失则 \`pnpm test:ci && pnpm e2e:ci\`),跑 vitest + playwright(含全 FE 回归)。**收尾(无论红绿)**:测试跑完后执行 \`node ${c.root}/scripts/drop-test-db.mjs\` 删测试副本(前端阶段不走 test.mjs,副本清理落在本门;源库不动)。` | ||
| 762 | - : `- 后端:跑 \`${c.root}/scripts/test.mjs\`(跨平台 Node 测试入口;含本模块新增 + 已合并模块回归;其内部自带 复制副本→测试→绿后晋升→删副本 全时序)。`, | 785 | + ? `- 前端:命令从 \`${c.root}/docs/04-技术规范.md § 零 frontend.test_command\` / \`frontend.e2e_command\` 拼接(缺失则 \`pnpm test:ci && pnpm e2e:ci\`),跑 vitest + playwright。**收尾(无论红绿)**:测试跑完后执行 \`node ${c.root}/scripts/drop-test-db.mjs\` 删测试副本(前端阶段不走 test.mjs,副本清理落在本门;源库不动)。` |
| 786 | + : `- 后端:跑 \`${c.root}/scripts/test.mjs\`(跨平台 Node 测试入口;含本模块新增 + 已合并模块回归;其内部自带 复制副本→测试→绿后晋升→删副本 全时序)。**命令形态一字不改**——不传参、不拆步骤、不跳过其中任何一步(那套时序是迁移晋升链路的契约)。`, | ||
| 787 | + // e2e 范围收窄(增量省时):唯一接口是 env ERP_E2E_SCOPE,由 fe-skeleton 生成的 playwright.config 消费。 | ||
| 788 | + // 之所以不改命令本身:后端走 test.mjs(内部固定调 {{e2e_cmd}},改不到),前端命令来自 docs/04 约定; | ||
| 789 | + // env 则天然经 spawnSync 继承打穿到 playwright 进程,两条路径同一个开关,且旧项目补齐 config 即可生效。 | ||
| 790 | + ...(affected ? [ | ||
| 791 | + '', | ||
| 792 | + '### e2e 范围(本次 = **affected 增量模式**,非全量回归)', | ||
| 793 | + '- **只收窄 e2e 段**:build / lint / 后端单测+集成 / 前端 vitest 一律照常**全量**跑,一条都不许跳。', | ||
| 794 | + fe && feItems.length | ||
| 795 | + ? [ | ||
| 796 | + `- 本轮待跑 FE:${feList}。逐个 Read 其 plan(\`${c.root}/docs/superpowers/plans/<日期>-<FE-NN>.md\`,同一 FE 多份取最新日期),取出所有 \`测试先行类型 = e2e\` 任务的 \`test_file\`,去重后得到本次 e2e 范围。`, | ||
| 797 | + `- 把它们相对 \`frontend/e2e/\` 的路径片段用逗号连成一串,作为 env \`ERP_E2E_SCOPE\` 前置到 e2e 命令上(例:\`ERP_E2E_SCOPE=order/create.spec.ts,order/list.spec.ts pnpm e2e:ci\`;Windows 下用 \`cross-env\` 或在派发的子进程 env 里设,**不要**写成 PowerShell 语法而在 sh 里跑)。`, | ||
| 798 | + '- 一个 test_file 都取不到(plan 缺失 / 无 e2e 任务)→ 退到 `ERP_E2E_SCOPE=smoke`(骨架期生成的冒烟集),**不要**因此改成全量、也不要跳过 e2e 段。', | ||
| 799 | + ].join('\n') | ||
| 800 | + : `- 本模块本轮无待跑 FE(后端模块 / 本轮无前端改动),e2e 范围退到冒烟集:\`ERP_E2E_SCOPE=smoke\`(骨架期生成的 \`frontend/e2e/__smoke__/\`,验"栈起得来 + 首页可达 + 登录态有效")。**不要**跳过 e2e 段——它同时是起栈健康检查。`, | ||
| 801 | + '- **禁止空跑**:e2e 段匹配到 **0 条**用例即视为**范围推导错误**,按 `red` 返回(`failures` 写明推导出的范围与来源),**绝不**当成绿。这是收窄唯一真正的风险,宁可报错重来。', | ||
| 802 | + `- 证据里必须逐字写一行 \`e2eScope: <你实际用的 ERP_E2E_SCOPE 值>\`,并在返回里回填 \`e2eScope\` / \`e2ePassed\`(e2e 段通过的用例条数)。`, | ||
| 803 | + '- **全量回归不在本门**:本次跳过的那些 e2e 由用户按需跑 `/erp-workflow:full-test` 兜底,你**不需要**、也**不许**为"回归覆盖不足"halt 或改回全量。', | ||
| 804 | + ] : [ | ||
| 805 | + '', | ||
| 806 | + '### e2e 范围(本次 = **full 全量回归**)', | ||
| 807 | + '- 不设 `ERP_E2E_SCOPE`(或显式置空),playwright 跑全部 spec。返回里 `e2eScope` 填 `full`。', | ||
| 808 | + ]), | ||
| 763 | '- 子会话只返回结构化 JSON:`{command, exit_code, passed, failed, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行含 FAIL 摘要)。', | 809 | '- 子会话只返回结构化 JSON:`{command, exit_code, passed, failed, stdout_excerpt}`(`stdout_excerpt` ≤ 30 行含 FAIL 摘要)。', |
| 764 | // 补 9:testGate 的 e2e(Playwright globalSetup 按固定端口冷起后端)与 Seed 同属起栈类 stage—— | 810 | // 补 9:testGate 的 e2e(Playwright globalSetup 按固定端口冷起后端)与 Seed 同属起栈类 stage—— |
| 765 | // 整段由编排层包进全局起栈互斥(stackLock,Phase D 定义);lane 测试库硬约束已由 featureStageContract 注入。 | 811 | // 整段由编排层包进全局起栈互斥(stackLock,Phase D 定义);lane 测试库硬约束已由 featureStageContract 注入。 |
| @@ -773,6 +819,7 @@ function gatePrompt(module, phase, attempt = 1, c) { | @@ -773,6 +819,7 @@ function gatePrompt(module, phase, attempt = 1, c) { | ||
| 773 | '', | 819 | '', |
| 774 | '## 输出(必须符合下发的 GATE JSON schema)', | 820 | '## 输出(必须符合下发的 GATE JSON schema)', |
| 775 | '- `status`: `green`(`exit_code = 0` 且 `failed = 0`)| `red`;`failures`: 失败用例摘要(green 时可省略 / 空数组)。', | 821 | '- `status`: `green`(`exit_code = 0` 且 `failed = 0`)| `red`;`failures`: 失败用例摘要(green 时可省略 / 空数组)。', |
| 822 | + `- \`e2eScope\`: 本次 e2e 实际范围(${affected ? '你算出的 ERP_E2E_SCOPE 值,如 `order/create.spec.ts,...` 或 `smoke`' : '`full`'});\`e2ePassed\`: e2e 段通过条数(**据实填**,编排层据此判空跑)。`, | ||
| 776 | '- 不要返回额外字段。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', | 823 | '- 不要返回额外字段。**不要在本步骤内自动重试**——重试由上层 Workflow 控制。', |
| 777 | ].filter(Boolean).join('\n') | 824 | ].filter(Boolean).join('\n') |
| 778 | } | 825 | } |
| @@ -1173,7 +1220,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | @@ -1173,7 +1220,7 @@ function behaviorGatePrompt(feItems, behaviorRound, attempt, c) { | ||
| 1173 | 1220 | ||
| 1174 | // behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归—— | 1221 | // behaviorReverifyPrompt:阶段级行为 fix 后的功能复验。fix 改的是 frontend/ UI 源码,可能引入功能回归—— |
| 1175 | // 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮 | 1222 | // 在下一 behaviorRound 重起全栈之前,先派子会话跑**全量前端单测**(vitest,不跑 e2e——e2e/行为维度由下一轮 |
| 1176 | -// 行为门重跑 + 阶段 testGate 全量回归兜底),红则当功能回归硬边界(调用方 allowContinue:false)。 | 1223 | +// 行为门重跑 + 阶段 testGate 兜底),红则当功能回归硬边界(调用方 allowContinue:false)。 |
| 1177 | function behaviorReverifyPrompt(behaviorRound, fixedCount, c) { | 1224 | function behaviorReverifyPrompt(behaviorRound, fixedCount, c) { |
| 1178 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md` | 1225 | const evidence = `docs/superpowers/module-reports/frontend-phase-behavior-reverify-r${behaviorRound}.md` |
| 1179 | return [ | 1226 | return [ |
| @@ -1257,7 +1304,17 @@ function frontendSkeletonPrompt(feItems, c) { | @@ -1257,7 +1304,17 @@ function frontendSkeletonPrompt(feItems, c) { | ||
| 1257 | // 晋升链路断且无声。副本清理归各闸门的 finally(test.mjs / seed runner / 行为门 runner)在 promote 后做; | 1304 | // 晋升链路断且无声。副本清理归各闸门的 finally(test.mjs / seed runner / 行为门 runner)在 promote 后做; |
| 1258 | // tdd 内循环单独跑 e2e 时副本留存到下次 globalSetup 重新复制,无害。 | 1305 | // tdd 内循环单独跑 e2e 时副本留存到下次 globalSetup 重新复制,无害。 |
| 1259 | ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树(**不**删副本——副本清理由外层闸门在晋升后负责)。', | 1306 | ' - **globalTeardown**(如 `frontend/e2e/global-teardown.*`):kill globalSetup 起的后端进程树(**不**删副本——副本清理由外层闸门在晋升后负责)。', |
| 1260 | - ' - **说明**:这是 **e2e 基线契约**(前端 e2e 基线 = 源库副本 + 本轮新迁移 + 演示种子 + admin storageState)的**唯一接线点**——per-FE tdd 的 e2e 与阶段级 testGate 跑的 e2e 共用此 globalSetup。**骨架期只需静态成立 + 不破坏 build,无需真跑 e2e。** 幂等:已存在则按需补齐;**已存在但缺 `webServer` 就绪门(或用了无就绪门的并行起法)→ 必须补齐/改正**,这是已知会造成整轮 e2e 假红的缺陷,不属于"已存在就不动"的范围。', | 1307 | + // e2e 范围开关(增量省时的落点):编排层唯一能打穿到 playwright 进程的接口就是 env—— |
| 1308 | + // 后端 test-gate 走 scripts/test.mjs,内部固定调 {{e2e_cmd}},命令行参数加不进去;而 env 经 | ||
| 1309 | + // spawnSync 天然继承。故把过滤逻辑放进**由本 stage 生成的 config**里,前后端两条路径同一个开关。 | ||
| 1310 | + // 缺省(env 未设)必须是全量——任何"默认收窄"都会让不知情的人拿到一份假绿。 | ||
| 1311 | + ' - **e2e 范围开关 `ERP_E2E_SCOPE`(必须实现,增量模式靠它省时)**:在同一份配置里读 `process.env.ERP_E2E_SCOPE`——', | ||
| 1312 | + ' - 未设 / 空串 → **不过滤,跑全部 spec**(缺省即全量,绝不能反过来)。', | ||
| 1313 | + ' - 值为 `smoke` → 只匹配 `e2e/__smoke__/` 下的 spec。', | ||
| 1314 | + ' - 其它值 → 按逗号切分成路径片段列表,只匹配**路径含其中任一片段**的 spec(如 `order/create.spec.ts,order/list.spec.ts`)。', | ||
| 1315 | + ' - 实现用 `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 假红或增量模式失效的缺陷,不属于"已存在就不动"的范围。', | ||
| 1261 | '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/ 镜像路径后方可收窄)。', | 1318 | '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/ 镜像路径后方可收窄)。', |
| 1262 | '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', | 1319 | '- **lazy 硬护栏**:router 表里**任何** FE 路由都不得用顶部静态 `import`;必须 `() => import(...)`。自检:Grep 路由文件,确认每个 FE 路由的 `component` 都是动态 import 形态。', |
| 1263 | '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', | 1320 | '- **路径硬护栏**:所有产出文件必须以 `frontend/` 开头;命中 `backend/` / `sql/` / `scripts/` → 越界硬停。', |
| @@ -1289,11 +1346,14 @@ function frontendSkeletonStatePromptM(feItems, c) { | @@ -1289,11 +1346,14 @@ function frontendSkeletonStatePromptM(feItems, c) { | ||
| 1289 | '', | 1346 | '', |
| 1290 | `用 Grep / Read 检查 \`${c.root}/frontend/\`:是否已存在 router 配置文件,且其中**本轮待实现的 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`)+ **前端 dev server 就绪门**(配置内有 \`webServer\` 段且带 \`url\`,或 globalSetup 内有显式轮询前端端口至可连的逻辑),**且单测基线存在**——vitest 配置 \`include\` 限定 \`tests/**/*.test.*\`。`, | 1347 | `用 Grep / Read 检查 \`${c.root}/frontend/\`:是否已存在 router 配置文件,且其中**本轮待实现的 FE 路由**(对应 FE:${list})都已声明、全部为 lazy import(\`() => import(...)\`),占位组件 \`FeStub\`(\`frontend/src/views/_stub/FeStub.*\`)存在,**且 e2e 基线脚手架存在**——Playwright 配置文件(\`frontend/playwright.config.*\`)+ globalSetup 文件(如 \`frontend/e2e/global-setup.*\`)+ **前端 dev server 就绪门**(配置内有 \`webServer\` 段且带 \`url\`,或 globalSetup 内有显式轮询前端端口至可连的逻辑),**且单测基线存在**——vitest 配置 \`include\` 限定 \`tests/**/*.test.*\`。`, |
| 1291 | '- **就绪门判据从严**:只要找不到 `webServer.url`、也找不到 globalSetup 内对前端端口的显式轮询(哪怕 e2e 目前跑得通)→ 判 `exists:false`。这是已知会让整轮 e2e 假红 40+ 分钟的竞态缺陷,值得多跑一次骨架 stage 去补。', | 1348 | '- **就绪门判据从严**:只要找不到 `webServer.url`、也找不到 globalSetup 内对前端端口的显式轮询(哪怕 e2e 目前跑得通)→ 判 `exists:false`。这是已知会让整轮 e2e 假红 40+ 分钟的竞态缺陷,值得多跑一次骨架 stage 去补。', |
| 1349 | + // 存量项目的 config 是在这两项存在之前生成的,必须靠本判据把它们拽回来补齐—— | ||
| 1350 | + // 否则 test-gate 设了 ERP_E2E_SCOPE 也没人消费,静默退化成全量(不假绿,但增量省时完全失效)。 | ||
| 1351 | + `- **e2e 范围开关判据**:Grep \`${c.root}/frontend/playwright.config.*\` 是否含 \`ERP_E2E_SCOPE\`,且 \`${c.root}/frontend/e2e/__smoke__/\` 下存在至少一个 spec 文件。任一缺失 → 判 \`exists:false\`(存量项目普遍缺这两项,需要重跑骨架 stage 补齐)。`, | ||
| 1292 | '- 判据**只针对上面列出的 FE**:router 里其它 FE 路由(往轮已实现,指向真组件)一律视为满足,**不要**因为它们没指向 `FeStub` 就判缺失。', | 1352 | '- 判据**只针对上面列出的 FE**:router 里其它 FE 路由(往轮已实现,指向真组件)一律视为满足,**不要**因为它们没指向 `FeStub` 就判缺失。', |
| 1293 | '- 上面列出的 FE,其路由若已指向**真组件**(resume:上次已实现到一半)同样视为满足——本检测只问"路由在不在、是不是 lazy",不问指向谁。', | 1353 | '- 上面列出的 FE,其路由若已指向**真组件**(resume:上次已实现到一半)同样视为满足——本检测只问"路由在不在、是不是 lazy",不问指向谁。', |
| 1294 | `- **legacy 豁免**:若 \`${c.root}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`, | 1354 | `- **legacy 豁免**:若 \`${c.root}/frontend/src/\` 内已存在测试文件(\`*.test.*\` / \`*.spec.*\`,旧约定 colocation 残留),vitest include 项**不作为缺失判据**(视为满足)——该状态须人工迁移决策,绝不由骨架重跑静默收窄 include 停跑旧测试。`, |
| 1295 | '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`', | 1355 | '- 全部满足(骨架已建齐,含 e2e 基线脚手架 + 单测基线)→ `{ "exists": true }`', |
| 1296 | - '- 任一缺失(无 router / 缺上面某个待实现 FE 的路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / **缺前端 dev server 就绪门** / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', | 1356 | + '- 任一缺失(无 router / 缺上面某个待实现 FE 的路由 / 存在 eager import / 无 FeStub / 缺 Playwright 配置 / 缺 globalSetup / **缺前端 dev server 就绪门** / **缺 `ERP_E2E_SCOPE` 支持或冒烟集** / vitest include 未限定 tests/**/*.test.*(且非 legacy 豁免))→ `{ "exists": false }`', |
| 1297 | '## 输出(EXISTS_SCHEMA)', | 1357 | '## 输出(EXISTS_SCHEMA)', |
| 1298 | ].join('\n') | 1358 | ].join('\n') |
| 1299 | } | 1359 | } |
| @@ -2386,6 +2446,9 @@ function reportPrompt(module, c, facts = {}) { | @@ -2386,6 +2446,9 @@ function reportPrompt(module, c, facts = {}) { | ||
| 2386 | `- 上游 test-gate 已 green(编排层判定:共 ${gAttempts} 个 attempt,末次 \`${gateEvidence}\` = green)。红态根本走不到本步——testGate 会先 halt,故**不需要**你重新校验绿不绿。`, | 2446 | `- 上游 test-gate 已 green(编排层判定:共 ${gAttempts} 个 attempt,末次 \`${gateEvidence}\` = green)。红态根本走不到本步——testGate 会先 halt,故**不需要**你重新校验绿不绿。`, |
| 2387 | `- 只需 Read 末次证据 \`${c.root}/docs/superpowers/module-reports/${gateEvidence}\` 取 § ⑤ 要用的摘要${gateFlake ? `;另 Read 首次 \`${phaseId}-test-gate-r1.md\`(attempt>1 = flake,§ ⑤ 需要首次失败用例)` : '(单 attempt,无 flake,无需读其它 attempt 文件)'}。该文件缺失 → halt(证据链断裂)。`, | 2447 | `- 只需 Read 末次证据 \`${c.root}/docs/superpowers/module-reports/${gateEvidence}\` 取 § ⑤ 要用的摘要${gateFlake ? `;另 Read 首次 \`${phaseId}-test-gate-r1.md\`(attempt>1 = flake,§ ⑤ 需要首次失败用例)` : '(单 attempt,无 flake,无需读其它 attempt 文件)'}。该文件缺失 → halt(证据链断裂)。`, |
| 2388 | gateFlake ? `- **flake 标注(编排层已判定)**:attempt 数 = ${gAttempts} 且末次 green → § ⑤ 顶部必须写 \`flake-detected: r1 red, r${gAttempts} green\`,并附首次失败用例与末次绿色记录链接。` : '', | 2448 | gateFlake ? `- **flake 标注(编排层已判定)**:attempt 数 = ${gAttempts} 且末次 green → § ⑤ 顶部必须写 \`flake-detected: r1 red, r${gAttempts} green\`,并附首次失败用例与末次绿色记录链接。` : '', |
| 2449 | + // 报告必须说清"这次到底测了多宽"——增量模式下 e2e 只覆盖本轮改动,报告若仍写"全量回归通过" | ||
| 2450 | + // 就是伪造覆盖面。全量回归改由用户按需跑 /erp-workflow:full-test,报告只如实记录本次范围。 | ||
| 2451 | + `- **本次 e2e 范围(编排层给定,如实写进 § ⑤,不要拔高成"全量回归")**:\`${gate.e2eScope || (TEST_SCOPE === 'full' ? 'full' : '(子代理未回填,按证据文件里的 e2eScope 行为准)')}\`${TEST_SCOPE === 'full' ? '(全量)' : `(增量模式:只跑本轮改动关联的 e2e${Number.isInteger(gate.e2ePassed) ? `,通过 ${gate.e2ePassed} 条` : ''};未覆盖部分由 \`/erp-workflow:full-test\` 按需兜底)`}。`, | ||
| 2389 | fe | 2452 | fe |
| 2390 | ? `- 阶段级行为门已 green(编排层判定:behaviorRound=${bhRounds},末次证据 \`${bhEvidence}\`,routesPlanned/Reached=${bh.routesPlanned ?? '?'}/${bh.routesReached ?? '?'},controlsEnumerated=${bh.controlsEnumerated ?? '?'},authState=${bh.authState || '?'})。同样**不要**再 Glob 全部 \`frontend-phase-behavior-r*-a*.md\` 自证——只 Read 末次那一份取 § ⑤/§ ⑧ 需要的逐 FE 小节${bhRounds > 1 ? `;另 Read \`frontend-phase-behavior-reverify-r*.md\`(有 ${bhRounds - 1} 轮行为 fix,§ ⑤ 要记录 fix 轮数与复验结果)` : '(一轮即绿,无 fix、无复验文件)'}。` | 2453 | ? `- 阶段级行为门已 green(编排层判定:behaviorRound=${bhRounds},末次证据 \`${bhEvidence}\`,routesPlanned/Reached=${bh.routesPlanned ?? '?'}/${bh.routesReached ?? '?'},controlsEnumerated=${bh.controlsEnumerated ?? '?'},authState=${bh.authState || '?'})。同样**不要**再 Glob 全部 \`frontend-phase-behavior-r*-a*.md\` 自证——只 Read 末次那一份取 § ⑤/§ ⑧ 需要的逐 FE 小节${bhRounds > 1 ? `;另 Read \`frontend-phase-behavior-reverify-r*.md\`(有 ${bhRounds - 1} 轮行为 fix,§ ⑤ 要记录 fix 轮数与复验结果)` : '(一轮即绿,无 fix、无复验文件)'}。` |
| 2391 | : '', | 2454 | : '', |
| @@ -3132,15 +3195,26 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath, c, { flipDoc | @@ -3132,15 +3195,26 @@ async function reviewWithFixLoop(id, phase, verifyResult, specPath, c, { flipDoc | ||
| 3132 | 3195 | ||
| 3133 | // flake 重试:每个 attempt 写独立证据文件 `<id>-test-gate-r<attempt>.md`,不覆盖前一次 red 证据(report § ⑤ 用得到)。 | 3196 | // flake 重试:每个 attempt 写独立证据文件 `<id>-test-gate-r<attempt>.md`,不覆盖前一次 red 证据(report § ⑤ 用得到)。 |
| 3134 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 | 3197 | // red 是硬正确性边界——**绝不** continue 跳过;只让仲裁在"再跑一次辨 flake"与"确属真失败 → halt"间裁决。 |
| 3198 | +// 空跑守卫(affected 模式专用):e2e 收窄的唯一真风险是"范围推导错 → 0 条用例 → exit 0", | ||
| 3199 | +// 表现与真绿完全一致。子代理侧已被要求禁止空跑,但那是自律;这里再做一次**编排层硬判**—— | ||
| 3200 | +// green 且 e2ePassed 明确为 0 一律翻成 red,走既有 retry/仲裁链(重跑一次通常会带出正确范围)。 | ||
| 3201 | +// e2ePassed 缺席(旧行为 / 未回填)不翻——只惩罚"明确说了 0 条",不猜。 | ||
| 3202 | +function guardEmptyE2e(g) { | ||
| 3203 | + if (TEST_SCOPE === 'full') return g | ||
| 3204 | + if (g.status !== 'green' || g.e2ePassed !== 0) return g | ||
| 3205 | + return { ...g, status:'red', | ||
| 3206 | + failures: [...(g.failures || []), `e2e 收窄范围匹配到 0 条用例(e2eScope="${g.e2eScope || '?'}")——空跑不算绿,范围推导有误`] } | ||
| 3207 | +} | ||
| 3208 | + | ||
| 3135 | async function testGate(module, phase, c) { | 3209 | async function testGate(module, phase, c) { |
| 3136 | let attempt = 1 | 3210 | let attempt = 1 |
| 3137 | const site = `gate:${phase}:${module.id}` | 3211 | const site = `gate:${phase}:${module.id}` |
| 3138 | - etaLog(site, 1, '全量回归 + 冷起栈') | ||
| 3139 | - let g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) | 3212 | + etaLog(site, 1, TEST_SCOPE === 'full' ? '全量回归 + 冷起栈' : '增量回归(e2e 收窄)+ 冷起栈') |
| 3213 | + let g = guardEmptyE2e(await agentR(gatePrompt(module, phase, attempt, c), {label:`gate:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})) | ||
| 3140 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) | 3214 | if (g.status === 'red') { // 自动重试 1 次(防 flaky) |
| 3141 | attempt = 2 | 3215 | attempt = 2 |
| 3142 | etaLog(site, 2, 'red → 防 flaky 自动重跑') | 3216 | etaLog(site, 2, 'red → 防 flaky 自动重跑') |
| 3143 | - g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA}) | 3217 | + g = guardEmptyE2e(await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}`, phase:'Gate', schema: GATE_SCHEMA})) |
| 3144 | } | 3218 | } |
| 3145 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 | 3219 | // 仍 red:经仲裁辨识 flake。allowContinue:false → 红色不可跳过,仲裁只在 retry / halt 间选。 |
| 3146 | for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { | 3220 | for (let adj = 1; g.status === 'red' && adj <= ADJUDICATE_MAX; adj++) { |
| @@ -3150,7 +3224,7 @@ async function testGate(module, phase, c) { | @@ -3150,7 +3224,7 @@ async function testGate(module, phase, c) { | ||
| 3150 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) | 3224 | throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${verdict.rationale || (g.failures||[]).join('; ')}`) |
| 3151 | attempt += 1 // retry:再跑一个独立 attempt 证据文件 | 3225 | attempt += 1 // retry:再跑一个独立 attempt 证据文件 |
| 3152 | etaLog(site, attempt, '仲裁判 retry(辨 flake)') | 3226 | etaLog(site, attempt, '仲裁判 retry(辨 flake)') |
| 3153 | - g = await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA}) | 3227 | + g = guardEmptyE2e(await agentR(gatePrompt(module, phase, attempt, c), {label:`gate-retry:${phase}:${module.id}:a${attempt}`, phase:'Gate', schema: GATE_SCHEMA})) |
| 3154 | } | 3228 | } |
| 3155 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) | 3229 | if (g.status === 'red') throw new Error(`HALT test-gate-red ${phase}:${module.id}: ${ADJUDICATE_MAX} 轮仲裁后仍 red:${(g.failures||[]).join('; ')}`) |
| 3156 | // attempts 回传给 module-report:报告不必再 Glob + 通读全部 attempt 证据去自证"最后一份绿", | 3230 | // attempts 回传给 module-report:报告不必再 Glob + 通读全部 attempt 证据去自证"最后一份绿", |
| @@ -3747,13 +3821,13 @@ async function runModule(module, c) { | @@ -3747,13 +3821,13 @@ async function runModule(module, c) { | ||
| 3747 | await featureLoop(module.feItems, 'frontend', c, module.feStage || 'all') | 3821 | await featureLoop(module.feItems, 'frontend', c, module.feStage || 'all') |
| 3748 | // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 | 3822 | // 阶段级行为门(v3):整个前端阶段只跑一次行为验收——起全栈 + 演示/sentinel 种子,按全部 FE spec 聚合 |
| 3749 | // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 | 3823 | // 作用域并集验「按钮真生效/文字对」;硬问题转 must-fix→fix→单测复验→重跑门(≤BEHAVIOR_STAGE_MAX 轮)。 |
| 3750 | - // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 全量回归兜底,不让回归证据过期。 | 3824 | + // 放在 testGate 之前:行为 fix 改动 frontend/ 源码,绿后由 testGate 兜底(单测全量 + 本轮 e2e),不让回归证据过期。 |
| 3751 | if (c.lane == null) phase('Behavior') | 3825 | if (c.lane == null) phase('Behavior') |
| 3752 | // 行为门虽也冷起全栈,但只在 frontend-phase 终波(宽 1、主根)跑——无兄弟 lane 可互杀, | 3826 | // 行为门虽也冷起全栈,但只在 frontend-phase 终波(宽 1、主根)跑——无兄弟 lane 可互杀, |
| 3753 | // 不进起栈互斥(补 9 范围仅 Seed / testGate)。 | 3827 | // 不进起栈互斥(补 9 范围仅 Seed / testGate)。 |
| 3754 | reportFacts.behavior = await runBehaviorGate(module.feItems, c) | 3828 | reportFacts.behavior = await runBehaviorGate(module.feItems, c) |
| 3755 | if (c.lane == null) phase('Gate') | 3829 | if (c.lane == null) phase('Gate') |
| 3756 | - // 阶段级 testGate(全量回归 vitest+playwright),与行为门职责正交;起栈互斥(补 9)同后端段整段包锁。 | 3830 | + // 阶段级 testGate(vitest 全量 + playwright 按 TEST_SCOPE 定宽),与行为门职责正交;起栈互斥(补 9)同后端段整段包锁。 |
| 3757 | reportFacts.gate = await withStackLock(() => testGate(module, 'frontend', c)) | 3831 | reportFacts.gate = await withStackLock(() => testGate(module, 'frontend', c)) |
| 3758 | } | 3832 | } |
| 3759 | if (c.lane == null) phase('Milestone') | 3833 | if (c.lane == null) phase('Milestone') |