atlas-project-harness 0.2.0-beta.6 → 0.2.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/DESIGN.md +4 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/rings/e2e/reference.md +3 -3
- package/scripts/__pycache__/atlas_verify.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
- package/scripts/atlas_verify.py +201 -0
- package/scripts/gen_e2e_scripts.py +24 -15
- package/shared/gates.md +3 -2
- package/tests/__pycache__/test_atlas_verify.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_map_anchors.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/test_atlas_verify.py +183 -0
- package/tests/test_gen_e2e_scripts.py +28 -11
- package/tests/test_map_anchors.py +469 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [v0.2.0-beta.7] - 2026-10-25
|
|
8
|
+
|
|
9
|
+
### 🏃 真跑日批:D80② 升档兑现(D149)+ 真跑日协议(D148)+ D108 落点勘误
|
|
10
|
+
|
|
11
|
+
* **D80② 升档兑现(D149)**:`e2e.app_login` 未声明 ⇒ 生成器推 **`NOT_READY`(退出码 3 + 机读键 `"app_login"`)**,与 `app_base_url` 同一通道族(`--json` 面 `{"ok": false, "not_ready": "app_login", …}`),不再「占位生成 + WARN」半生成——硬前提的输入不得靠「缺失即静默放行」吸收;文案按 D103 相抵点指路:「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / 登录响应体无令牌)⇒ 人工会话取出,不是「补声明」的对象。RFC-20261015-02 预告的时间锚「下一次真跑准备日」到期执行。钉:旧钉改判(半生成 ⇒ 全不生成)+ 新 `--json` 面钉;变异 **MB17**(退回 WARN 半生成)/ **MB18**(机读键写错或缺 message)。
|
|
12
|
+
* **真跑日协议(D148)**:runbook `docs/guide/real-run-day.md`——detached worktree 隔离执行(不碰用户 checkout、不提交)/ D145 manifest 配对先行 / 七项咬合清单 / **注红条款**(注红 red 不计缺陷判据)。首轮已执行出库,回写见 duty-review 三件(QUEUE / README / bugs.md)。
|
|
13
|
+
* **D108 落点勘误 + 留存窗口补定**:失败留证实际落 **`.atlas-run/`**(生成物早有显式 `outputDir`,Playwright 构件目录跟着 outputDir 走——D108 首版契约误写缺省 `test-results/`,2026-10-25 真跑日实咬勘误,构件形态 = `trace.zip + test-failed-*.png` 成对);**留存窗口 = 到下一次 `playwright test` 起跑**(Playwright 启动即清空 outputDir ⇒ 跨轮取证须起跑前归档)。契约 §7.1 / 生成物注释 / RFC 勘误节同批改口;咬合点不变 = 重装配(`install --update` / 首装),不溯及已装配副本。
|
|
14
|
+
* **真跑日首轮三发现入档**(均为观察、不计缺陷):① 落点勘误(上行);② 留证留存窗口(升格条件 = 首个有缺陷指控价值的构件在复跑起跑后被清空);③ 「栈起没起」探针盲区首个活体实证(前端 200 / 内网后端不可达 ⇒ 白屏全红、形似用例失败实为环境红)+ 半就绪变体(网关 200 / 业务 API 悬挂,唯有打真实业务 API 才能判)。另:案 9(`e2e_index` WARN 基线 7 条)升室待拍板;WS-C 回执状态收窄、WS-D 观察锚写作约定随批落地;消融首跑复核留档 `research/2026-10-23-ablation-first-run-recheck.md`。
|
|
15
|
+
|
|
7
16
|
## [v0.2.0-beta.6] - 2026-10-23
|
|
8
17
|
|
|
9
18
|
### 🧱 装配基线 manifest:漂移逐文件指名(D145)
|
package/DESIGN.md
CHANGED
|
@@ -574,7 +574,7 @@ adapters:
|
|
|
574
574
|
| D105 | 生成的运行器配置**必须声明 `expect.timeout` 下限**(2026-10-17,issue #28;真跑现场驱动) | ① **病灶**:#27 落地后真跑仍 `3 failed 1 passed`,而**元素真在**(探针实测 `disabled=true` + 合规 `aria-label`)、三处导航 URL 全对 ⇒ 红因是**断言超时**:生成的 `playwright.config.ts` **没有 `expect` 段** ⇒ 继承 Playwright **默认 5s**,而该应用首屏实测 **14,184ms**(dev bundle 13.8MB)⇒「元素还没渲染」被误判成「元素不存在」= **假红**。② **定案**:生成器在两处 config 分支都发射 `expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) }`(缺省 20s,仍远小于 test 超时;`ATLAS_EXPECT_TIMEOUT` 为运维覆盖口,与 `ATLAS_SLOWMO` 同族)。**它不是「固定等待」**:`expect` 是轮询式 auto-wait,本改只抬高**上限**,不引入 sleep,不破 §5.5「不得固定等待」。③ **被否**:只给守卫 `toHaveCount` 加 timeout(其余断言仍 5s,慢一点又撞);调 test 超时(那只是兜底,不解决断言级假红);固定 sleep(契约明禁,且共享靶场必飘)。④ **代价(如实)**:真失败从 5s 变 20s 才红;可在慢环境调低。⑤ **钉与变异**:`test_runner_config_declares_expect_timeout_floor`(配置含 `expect: {` + `20_000` + 覆盖口),变异 **MB5**(摘掉发射 ⇒ 红,还原逐字节一致)。 |
|
|
575
575
|
| D106 | **真跑门的执行 cwd = 生成物目录**(2026-10-17,issue #29;真跑现场驱动) | ① **病灶**:#27/#28 落地后真跑仍 `3 failed 1 passed`,而同一份产物 **`cd scripts` 直跑 8.2s 全绿** (`--grep E2E-APP-AGENT-DETAIL-001`)——差异 = **cwd**:`atlas_check` 用 `cwd=项目根` + `-c <相对路径>` 跑,而生成物里的相对路径全是 **cwd 相对**的(`storageState: ".atlas-auth/state.json"`、`outputDir: ".atlas-run"`、mock fixture)⇒ Playwright 把 storageState 解析到 `<root>/.atlas-auth/`(**并在那儿写了一个空 state**)⇒ **读不到人的会话** ⇒ 未登录态跑用例 ⇒ 断言 20s 超时**假红**。控制台(`e2e_console`)本来就是 `cd scripts` 再跑。② **定案**:本门改为 **`cwd=<scripts_dir>` + `-c playwright.config.ts`**(与执行器同一口径,两个入口不再各说各话)。③ **被否**:把生成物里的相对路径全改成绝对路径(派生件带机器绝对路径 = 不可移植、且破「生成物 = 确定性派生物」);只修 setup 的 state 路径(其余相对路径仍会漂)。④ **教训**:**「同一份产物在两个入口下跑出不同结论」必须先查 cwd / 环境**,否则会把执行器差异误判成用例或实现缺陷(本轮依次误判为「超时不足」→ 已由 D105 兜住超时面,再查出真因 = cwd)。⑤ **钉与变异**:`test_check_e2e_runs_with_scripts_dir_as_cwd`(截 `run()` 的 (cmd, cwd),断言 cwd = scripts 且 cmd 里无相对配置路径),变异 **MB6**(退回 root + 相对 `-c` ⇒ 红,还原逐字节一致)。 |
|
|
576
576
|
| D107 | 零步基线**每个探针都必须「缺席即判不成立」,不得等挂载**(2026-10-17,issue #30;真跑现场驱动) | ① **病灶**:`atlas_check --page app-agent-detail` ⇒ 两条真用例全绿,唯零步基线 `Test timeout of 30000ms exceeded`。带参页面的页级路由本就不渲染详情(`D104` 允许的形态),而 `attr` 探针发射的是 `getByTestId(t).first().getAttribute(n)` —— **无 `count()` 守卫**,Playwright 对**未挂载**元素的 `getAttribute` 会等到默认超时 ⇒ 4 探针 × 2 趟撑爆 test 预算。契约 §5.5 早已明写「基线探不到元素按 `null = 不成立` 处理」⇒ **等挂载 = 违约**(A1 判错,非项目缺陷)。② **定案**:`attr` 探针加 `count() > 0 &&` 前置(缺席 ⇒ `false` = 不成立);并把该性质上升为**生成器的通用不变式**:**任何探针表达式都必须带缺席守卫**(`count()) > 0` 或 `tsVis(` / `tsVisible(` / `atlasValueOf(`)——由通用钉守着,防未来新增动词再犯。③ **被否**:给基线设更大的 test 超时(把「等挂载」合法化,掩盖违约);带参页面跳过基线(契约 §5.5 已选择「回 null」而非「跳过」);只在 v2 侧声明 `守卫:`(那是**资产选择题**,不修 atlas 违约面)。④ **钉与变异**:`test_zero_step_probes_never_wait_for_attachment`(对**全部**探针做守卫检查)+ 变异 **MB7**(摘 `attr` 的守卫 ⇒ 红,还原逐字节一致)。⑤ **出库判定**:探针行为属**运行面** ⇒ 待下一轮真跑复验后出库。 |
|
|
577
|
-
| D108 | E2E 真跑失败**自动留证**——trace / screenshot 只在失败落盘(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;外部发版雷达首跑 5 条机制级情报中唯一采纳项,独立评审两轮 + 维护者拍板「按推荐继续」) | ① **病灶**:真跑偶发红(节拍类)现场的取证上限 = 文本尾部 80 行(`e2e_console.py:520`(采集即截)/ `:582`(docstring 失败才带尾)/ `:591-592`(落账条件)),元素几何 / 命中点 / 时序类判据只能靠 `/tmp` 临时探针(跨会话不可复算;D98 动词批与案 6 两次付费先例);`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`,3 连跑全绿)的**升判条件**「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)⇒ 转 DR4」悬空在等证据源(`bugs.md:17`)。② **定案**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动落 trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图到 Playwright 缺省 `test-results/`,**全绿轮零新增构件**;不溯及已装配副本 ⇒ **咬合点 = 下一次 `install.sh --update` / 首装重装配**,重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报(对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);留证含业务内容,只落采纳仓本地、不进 atlas 上报通道(§15 零业务上下文出站面不破);不解决 D103「人工会话取出 ⇒ CI 不可复现」盲区(trace 只重放**已发生**的失败,真身是登录态不是取证手段)。③ **被否**:`video` 缓行(trace 自带回放器,video 是粗粒度投影,多一键 = 双轨同构面 +2 无收益;缓行条件 = 实跑出现 trace 覆盖不了的取证需求才另起决策行);全量常录 `on`(磁盘 × 每用例,成功轮留证无消费方);留证键加环境开关(= 给「成功轮也留证」开后门);独立录制管理器 / 上传通道 / 轮转清理(运行时平台机制,Atlas 零运行时);**失败自动重试 N 次**(与「节拍类首跑不坐实、复跑复现才改判」(`bugs.md` E2E 段)方向相反,会把真缺陷静默吸收成绿——**防复读机裁决**已落 `duty-review/QUEUE.md` 归档区,外部情报再提 retry 引用可径行否决);托管云沙盒会话(违「项目自足」,同归档区裁决)。④ **钉与变异**:钉 3 条——`test_failure_evidence_both_tracks_declare_trace`(双轨同构:`mode:none` 轨与预置登录轨都必须含两键)/ `test_failure_evidence_values_are_closed`(值形态封闭:恰为 retain-on-failure / only-on-failure,`video` 混入 / `on` 常录 ⇒ 红)/ `test_failure_evidence_has_no_override_switch`(无覆盖口),`tests/test_gen_e2e_scripts.py` 74 → **77** 条;变异 **MB10**(摘 none 轨 trace/screenshot 行)/ **MB11**(值改 `on`),`mutation_proof` 四段协议双绿(基线 3 passed → 注入 rc=1 → 还原逐字节一致 → 复绿),注入 diff 摘录于 `proposals/REVIEW-2026-10-18-e2e-failure-evidence.md`;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬,失败才落盘)」,门行 = `shared/gates.md` 2.2。⑤
|
|
577
|
+
| D108 | E2E 真跑失败**自动留证**——trace / screenshot 只在失败落盘(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;外部发版雷达首跑 5 条机制级情报中唯一采纳项,独立评审两轮 + 维护者拍板「按推荐继续」) | ① **病灶**:真跑偶发红(节拍类)现场的取证上限 = 文本尾部 80 行(`e2e_console.py:520`(采集即截)/ `:582`(docstring 失败才带尾)/ `:591-592`(落账条件)),元素几何 / 命中点 / 时序类判据只能靠 `/tmp` 临时探针(跨会话不可复算;D98 动词批与案 6 两次付费先例);`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`,3 连跑全绿)的**升判条件**「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)⇒ 转 DR4」悬空在等证据源(`bugs.md:17`)。② **定案**:生成物两轨 playwright 配置补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动落 trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图到 Playwright 缺省 `test-results/`,**全绿轮零新增构件**;不溯及已装配副本 ⇒ **咬合点 = 下一次 `install.sh --update` / 首装重装配**,重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报(对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);留证含业务内容,只落采纳仓本地、不进 atlas 上报通道(§15 零业务上下文出站面不破);不解决 D103「人工会话取出 ⇒ CI 不可复现」盲区(trace 只重放**已发生**的失败,真身是登录态不是取证手段)。③ **被否**:`video` 缓行(trace 自带回放器,video 是粗粒度投影,多一键 = 双轨同构面 +2 无收益;缓行条件 = 实跑出现 trace 覆盖不了的取证需求才另起决策行);全量常录 `on`(磁盘 × 每用例,成功轮留证无消费方);留证键加环境开关(= 给「成功轮也留证」开后门);独立录制管理器 / 上传通道 / 轮转清理(运行时平台机制,Atlas 零运行时);**失败自动重试 N 次**(与「节拍类首跑不坐实、复跑复现才改判」(`bugs.md` E2E 段)方向相反,会把真缺陷静默吸收成绿——**防复读机裁决**已落 `duty-review/QUEUE.md` 归档区,外部情报再提 retry 引用可径行否决);托管云沙盒会话(违「项目自足」,同归档区裁决)。④ **钉与变异**:钉 3 条——`test_failure_evidence_both_tracks_declare_trace`(双轨同构:`mode:none` 轨与预置登录轨都必须含两键)/ `test_failure_evidence_values_are_closed`(值形态封闭:恰为 retain-on-failure / only-on-failure,`video` 混入 / `on` 常录 ⇒ 红)/ `test_failure_evidence_has_no_override_switch`(无覆盖口),`tests/test_gen_e2e_scripts.py` 74 → **77** 条;变异 **MB10**(摘 none 轨 trace/screenshot 行)/ **MB11**(值改 `on`),`mutation_proof` 四段协议双绿(基线 3 passed → 注入 rc=1 → 还原逐字节一致 → 复绿),注入 diff 摘录于 `proposals/REVIEW-2026-10-18-e2e-failure-evidence.md`;契约条 = `rings/e2e/reference.md` §7.1「失败留证(硬,失败才落盘)」,门行 = `shared/gates.md` 2.2。⑤ **出库判定(2026-10-25 真跑日出库,D148 首轮)**:红轮 28 失败构件(trace.zip + 截图成对)/ 绿轮零构件,两面实证(咬合记录 = `shared/gates.md` 2.2「D108 首次咬合」);**落点勘误随批**(实际 `.atlas-run/`,非首版误写的缺省 `test-results/`)+ **留存窗口观察在案**(Playwright 起动清空 outputDir ⇒ 留证只存到下一次起跑,`duty-review/bugs.md` E2E 段带升格条件)。 |
|
|
578
578
|
| D109 | **逐用例宿主注入 URL 参数通道**:`goto` 查询门**重靶** + 项目级白名单(2026-10-18;字段与门落在 `rings/e2e/reference.md` §5.5 / §8.2;采用方现场驱动) | ① **病灶**:`会话:`(D89)解决的是 **sessionStorage** 那一个消费面;而某类应用的被测参数**只从载入地址的常规查询串读**(`window.location.search`)⇒ **无合法写法**(`goto` 带 `?` 是硬 FAIL、`auth` 只管登录态、`seed:` 绑数据实体)—— D104 ① 已把这条记为「三条各自成立 ⇒ 无合法写法」,当日改选 B′(base query 全局转发)并把「`会话:` 扩 URL 参数档」列入被否清单。采用方现场(2026-10-18):两条用例被迫写成**同一前提**(都走 sessionStorage)⇒ 重复,而查询串那一面**零覆盖**。② **重靶取证(关键,本行的依据)**:原门(§5.7,2026-09-23;事故 `3509 §B20` = `E2E-LOGIN-003` 断言 `?state=forbidden` 演示横幅**自身文案**)的**靶子随 E1 原型环退役而消失** —— `?state=` 只出现在本文件 `:460` 的**已作废**决策行(D8 两段式 / D56 原型交互分级);E1 后**无任何决策行**重新论证该门;`duty-review/bugs.md` 与 `CHANGELOG.md` **零起**该门事故;今日它在 D104 ① 里的身份是「无合法写法」的**阻碍** = 制造成本,而非拦事故。该门今日真正剩下的(`reference.md` 旧 §5.7 邻域的那条)是**另一族、不限于原型**的纪律:「前置数据态只能用真实界面动作达到」—— **而这族边界在重靶前后都由项目级白名单守**,与「`goto` 带不带 `?`」无关 ⇒ 取舍只剩成本与表达力。③ **定案**:(a)门**重靶** `goto 不带查询参数` → **`goto 查询键须已声明`**(键须 ① ∈ `e2e.query_preset.keys` ∧ ② ∈ 本用例 `参数:`;**原靶仍在 FAIL 面内**:未声明键照样红);(b)用例块新增可选 `参数:`(**只声明键**,值内联在 `step` 的 `goto` 查询串里 = 单一来源),**守卫导航取该用例首个带参 `goto` 的查询串**(否则守卫读的是**无前提的 DOM**:参数决定「哪些元素在场」的页面上守卫会在 `goto` 后即红);(c)`_support/url_merge.merge_base_query` 的「目标自带 `?` ⇒ 原样返回」**暗位转正为并集档**(base 在前、**同键 ⇒ 用例胜**)——该分支原注释即写着「§5.7 下不可达,**为未来扩展留位**」,且既有生成物无一带 query 的 `goto` ⇒ **零回归**;(d)三道新门(声明有效 / 值形态 / 同键双载体)+ 生成期同源中止(双保险)。④ **被否**:(A)「值只写 `参数:` 行、`goto` 保持无 query」—— 其立论之一是「保住 §5.7 事故防护」,取证后该论据**作废**(见 ②),取舍只剩成本与表达力,B 胜;其余照 D104 ⑤(运行期 TS 助手 / 把「同名键以目标为准」写成通则)。⑤ **边界(如实登记,不得 oversell)**:允许带白名单查询串后,「数据态直达」那族纪律的**机器面只剩白名单本身**(「项目说合法就合法」)—— 与 `会话:` 同族、是本设计的本来形态;登记以防后人以为还有别的网在兜。⑥ **钉与变异**:`tests/test_validators.py` **116 → 123**(重靶钉改名 1 + `参数:` 门 6);`tests/test_gen_e2e_scripts.py` **+6**(两处导航面承载 / 基线不施加 / 无白名单中止 / 白名单外中止 / 死声明中止 / 值形态中止);`_support/url_merge.py` 并集档(原「原样保留」单元钉随之改判)。变异 **MB12**(并集档退回「原样返回」⇒ 单元钉 + 守卫承载钉齐红)、**MB13**(守卫不追加用例参数 ⇒ 守卫面钉红),两枚均四段协议通过、还原逐字节一致,注入 diff 见任务件 `.trellis/tasks/10-18-atlas-url-param-channel/check-report.md`。设计依据 = 采用方现场(`src/entry/main/app.tsx:18/:36` 两个消费面 + `validators/validate_e2e_index.py` 旧门 + `_support/url_merge.py:48`)。 |
|
|
579
579
|
| D110 | 控制台**逐用例数据准备钩子**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 执行前必跑、失败即拒(2026-10-18,v3 现场实证移植批;病灶账 = `duty-review/bugs.md` 005/006 链 companion;契约条 = `rings/e2e/reference.md` §10「执行」行 / `e2e_console` docstring) | ① **病灶**:共享状态型靶场(SPA + 服务端态)的用例 `precondition` 属**文档性声明**、运行器不自动归零 ⇒ 残留实体使 count 类断言系统性偏移(现场实测 Expected 1 · Received 5);且控制台此前对「执行前重置」零消费——`e2e.reset`(`shared/stack-profile.md` §2)只被 `atlas_check` 真跑门执行(**批级**:整轮一次),逐条执行面无 per-case 前提通道。② **定案**:执行器在每条用例**计时起点之前**跑该脚本(`node <e2e.scripts_dir>/atlas-prepare.mjs`,cwd = 项目根,180s 上限,**不占**用例超时窗);失败(非零)/ 超时 / 起不动 ⇒ **拒绝执行该条**:`done` 事件按完整形状写入(`ok=False` / `exit` 如实 / `prep_failed=True`),失败原文进「输出」面板——不静默跑注定偏移的用例(对齐执行器「解析不成立 ⇒ 诚实拒绝」同族口径)。③ **与 `e2e.reset` 的关系(登记,防后人重复起案)**:**不并轨**——`reset` 是批级且值形态为 shell 命令串(项目在 profile 声明)、本钩子是逐条且文件约定(项目在 `<e2e.scripts_dir>` 放脚本),现场 `reset: null`、两粒度各占一格;控制台若要消费 `reset` 须另起决策行。④ **被否**:由生成脚本内自查归零(把环境初值达序藏进生成物 = 静默吸收);自动执行 precondition 声明(人读文档,无机器可执行形态);prepare 失败仍照跑(偏离移用例 = 假绿通道)。⑤ **钉与变异**:`tests/test_e2e_console.py` **13 → 14**(钩子先于真跑 / 失败拒绝并带 `prep_failed` / 起不动 `exit=-2` 三型一钉);变异 **M94**(摘 prepare 钩子段 ⇒ 钉红,还原逐字节一致)。设计依据 = v3 装配副本手改(现场实证可用);主仓移植时审校掉装配副本失败路径的 `self._lines` 属性错拼(会 AttributeError 让执行线程挂死、`running` 永真),统一改走 `self._emit`(持锁 + notify)。 |
|
|
580
580
|
| D111 | `refresh` 落盘节拍参数化:发射 `ATLAS_REFRESH_SETTLE` 注入的 `waitForTimeout`(先等后刷),默认 0 行为不变(2026-10-18,v3 现场实证移植批;病灶账 = `duty-review/bugs.md` 005 链;契约条 = `rings/e2e/reference.md` §7.1 / §5.5 `refresh` 行) | ① **病灶**:`refresh` 裸 `reload` 对启用**节流型自动保存**的项目(距上次成功保存 < 节流窗则延迟落库)必丢步骤编辑内容(现场实测 Expected 1 · Received 0——刷新发生在保存落库之前)。② **定案**:发射**先等后刷**两行——`await page.waitForTimeout(Number(process.env.ATLAS_REFRESH_SETTLE || "0"));` + `await page.reload();`;**默认 0 ⇒ 生成物行为与既有契约一致**。通道选择同 F11:Playwright `test` CLI 无逐用例注入面,env 是唯一通道。③ **零业务常数(硬)**:毫秒数**不进生成器**——它属项目侧**运行时**前提,现场以 `ATLAS_REFRESH_SETTLE=<毫秒>` 于启动面注入(对齐 `ATLAS_SLOWMO` / `ATLAS_EXPECT_TIMEOUT` 同族);它也不是 §5.5「固定 sleep」禁令所指(该禁令管**用例正文**不把共享靶场节拍写死;本行是执行环境配置面)。④ **被否**:硬编码任何项目常数进源包(§15 零业务上下文纪律);生成期读项目配置算节拍(生成物 = 确定性派生物,节拍是运行时动态值)。⑤ **钉与变异**:`tests/test_gen_e2e_scripts.py` **83 → 84**(先等后刷顺序钉);变异 **M95**(摘 `waitForTimeout` 发射行 ⇒ 钉红,还原逐字节一致)。 |
|
|
@@ -614,6 +614,9 @@ adapters:
|
|
|
614
614
|
| D145 | **装配基线 manifest**:`.atlas/MANIFEST.json` 逐文件摘要 + 配对校验 + 「未解释差异不得判一致」(2026-10-23;`RFC-20261023-02` ②;`scripts/pkg_digest.py`、`scripts/atlas_check.py`、`install.sh`、`shared/gates.md` §2.2) | ① **病灶**:聚合摘要只回答「变没变」,回答不了「变在哪」——`check_version` 的漂移 WARN 自证「差异明细跑 `backflow.py` 三态对账」;而外部流程规范给的机制是**逐项内容摘要 + 解释制一致性**(缺失字段 / 采集失败 / 未解释差异均不得判一致)。② **定案**:`manifest()` 与 `digest()` 共用同一次遍历产 `{digest, files}`;`stamp_file()` **先 MANIFEST 后 VERSION**(VERSION 在场即 manifest 已齐 ⇒ 半态可判定);`check_version` **配对校验先行**(manifest.digest ≠ 装配戳 ⇒ WARN 指名重装),配对后漂移**逐文件指名**(改/增/删各 cap 10 + 计数);旧装配无 manifest ⇒ 聚合口径 + ok 行尾注「正式判据未齐」(不新增红);两分支 WARN 保 M68 三词;`install.sh` 落戳后硬校验 manifest 在场;判定「一致」= 摘要相等 ∧ manifest 齐,未解释差异只能记「不一致(已解释)」(运行时仍 WARN,漂移是常态信号)。③ **被否**:扩 VERSION 为多行/JSON(旧副本全军 WARN,干跑守卫重走);漂移直接升红门(打断正常开发循环,解释制只属收口/发布判定);接线 `release.py`(过度设计,列为后续可选项)。④ **钉与变异**:`tests/test_tools.py` manifest 形态/配对/空装配三钉;`tests/test_atlas_check.py` 逐文件指名 / cap 10 / 不配套 / 形态非法 / 旧装配尾注五钉;F54 干跑扩 `MANIFEST.json` 逐字节断言;变异 **M-new-1**(摘 diff 分支)、**M-new-4**(摘配对校验)、**M-new-5**(摘干跑闸),四段协议还原逐字节一致。 |
|
|
615
615
|
| D146 | **域级 PRD 变更记录 5 列** + 「变更后只重验受影响项」通则(2026-10-23;`RFC-20261023-02` ③;`templates/prd/domain.md` §六/§七、`validators/validate_prd.py`、`rings/prd/reference.md`、`shared/global-rules.md` §16) | ① **病灶**:§七 只有「版本/日期/变更说明」,无影响、无结论与后续;「受影响才重验」原则散落 `apply` §3.2 / e2e 收口三值② / D128 `stale` 三处而未收拢,变更后哪些结论可沿用无处承载。② **定案**:表头升级 `版本/日期/变更内容及原因/影响/结论与后续`;「影响」点名受影响域/AC/用例,未触达验证面写 `无(未触达验证面)`;validator 独立 check 行:5 列 PASS / legacy(含`变更说明`)WARN「下次真实变更时随迁」/ 表头不可辨或「影响」空 FAIL / 节在无表 WARN;通则落 `global-rules` §16(只重验受影响项、未受影响结论沿用并留痕),机器落点指认 D128 stale / `backflow` 三态 / apply §3.2,不新增门;全局 `十一` 修订记录 4 列**不扩**(边界句)。③ **被否**:语义塞进「变更说明」单元格(无判据);新增「变更→受影响用例集」对账门(语义依赖,先立字段与通则);全局修订记录同步扩列(无消费者)。④ **钉与变异**:`test_rfc20261023_02_change_record_shape_and_impact_column` 四态;**M-new-3** 删 legacy 分支 ⇒ 钉红;测试夹具随迁 5 列(不为迁移单跑一轮)。 |
|
|
616
616
|
| D147 | **显式「无」通则** + `FORBIDDEN` 三处单源 + 两处机器检查首版 WARN(2026-10-23;`RFC-20261023-02` ④;`shared/global-rules.md` §5、`validators/validate_prd.py`、`validators/validate_structure.py`、`templates/prd/domain.md`、`rings/prd/reference.md` §7、`rings/e2e/reference.md` §10.1) | ① **病灶**:`无` / `不适用(理由:…)` / `未覆盖(沿用实然)` / 控制台「未创建」四套写法并存,「空表/空节」与「评估过为无」同形;`FORBIDDEN` 不抓空表,也不抓 `[必填:…]` 占位残留;两校验器与 §5 的词表各说各话。② **定案**:通则 = 「可无内容不得留空白,必须有结论」(`无`(理由可选)/既有语义词,词表单源 = §5);`FORBIDDEN` 增补 `[必填` / `[条件必填` 并三处同步 + 守恒钉 `test_forbidden_vocabulary_single_source`;机器落点只两处(§六 空表/空节无 `无` ⇒ **WARN 首版**;§七「影响」空 ⇒ FAIL),均独立 check 行、登记门册;模板占位指令统一「无则写 无(理由可选)」;翻转触发器 = 连续两收口周期真实遗漏 ≥1 且误报 0 ⇒ 升 FAIL,统计落 per-域收口留痕 README「可无项空表:N / 误报:N」。③ **被否**:全量扫描所有可无节 FAIL(违反「落门先问语料」);把四词语义合并成单一写法(丢语义边界)。④ **钉与变异**:`test_rfc20261023_02_optional_section_explicit_none`(空节 / 空表 / 显式 `无` / 开户最小集四态);**M-new-2** 判据恒真 ⇒ 钉红。 |
|
|
617
|
+
| D148 | **真跑日协议**:一次「重装配 + 真跑」把已立待咬的能力一次咬合,**detached worktree 隔离执行**(2026-10-24;runbook = `docs/guide/real-run-day.md`;维护者拍板「都按你推荐的来」+ 边界「v3 用户分支 `dev_20261120` 不碰、自建 worktree、不提交」;源自设计优化方案 v2 WS-B,评审结论已并入) | ① **病灶**:咬合点分散且都挂在「下一次真跑」上(D108 留证 / adapters 面 / hover 与 goto 未解观察 / WARN 基线 / D80② 升档 / 副本漂移正式解法),逐个等真跑 ⇒ 凑不齐;且用户 checkout 常被占用(现场:v3 `dev_20261120` 在用)。② **定案**:runbook 六步(worktree 隔离 → 重装配 → D145 manifest 配对校验 → 重生成对账 → 净场协议 → 真跑留档 `product/e2e/reviews/<日期>/`)+ 七项咬合清单(逐项勾、咬合一处回写一处:gates 对应行 / bugs.md / QUEUE)+ **注红条款**(注红 = 取证动作,red 不计缺陷判据、不进 bugs.md 复发计数、不触发巡检升判——与「首跑不坐实」纪律的摩擦预先写死)+ 不做清单(不顺手改用例语义/产品代码、不在用户 checkout 留痕、不提交 worktree 产物;登录态只读复制不覆盖)。③ **被否**:逐项单等真跑(凑不齐);用户 checkout 直跑(分支占用 + 痕迹);GUI/看板类真跑仪表(运行时平台机制,Atlas 零运行时)。④ **钉与变异**:本条为协议页 + 决策行,无新机器面(咬合证据回写既有登记面);D149(app_login 升档)与 D108(失败留证)是首批两个咬合对象。⑤ **出库判定(2026-10-25 首轮执行完,出库)**:七项结果——① D108 两面咬到(红 28 构件/绿零构件;**附带三发现**:落点勘误〔`.atlas-run/` 非缺省 `test-results/`〕、留存窗口〔Playwright 起动清空 outputDir,观察入 bugs.md〕、「栈起没起」探针盲区首个活体实证〔前端 200/后端不可达⇒门放行全红〕);② adapters 面实况已采集(models 渲染层 82/1859 行、routes 19/55——与既有记录同族,无升档动作);③ hover 未咬到(靶场白屏无从复现);④ goto 计数未咬到(同因);⑤ WARN 基线实测 **7 条**(≥3 且形态重复 ⇒ 观察室升室条件满足 ⇒ QUEUE 待拍板案 9);⑥ D149 已声明面咬到;⑦ worktree 面咬到(首装即最新 + D145 manifest 配对 ok;用户 checkout 漂移副本未动,留用户自行 `--update`);D124 就绪门顺带 ✓(chromium launch 实跑)。协议页随首轮实况修订(落点/留存窗口)。 |
|
|
618
|
+
| D149 | **D80② 升档兑现:`e2e.app_login` 未声明 ⇒ 生成器推 `NOT_READY`(退出码 3 + 机读键 `"app_login"`)**,不再「占位生成 + WARN」半生成(2026-10-24;RFC-20261015-02 预告的时间锚「下一次真跑准备日」到期执行;真跑日批) | ① **病灶**:占位形态 = 硬前提的输入被「缺失即静默放行」吸收(未登录态跑用例 ⇒ 假红/假绿都解释不了)。② **定案**:与 `app_base_url` 同一通道族(`--json` 面 `{"ok": false, "not_ready": "app_login", "message": …}`);文案按 **D103 相抵点**指路——「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / 登录响应体无令牌)⇒ 人工会话取出,是描述路径、不是「补声明」的对象(排除类不被罚 not_ready,出路写进文案);`atlas_check` 消费面走既有 `GEN_NOT_READY` 三态 SKIP 中继(message 透传,无需改);`validate_stack_profile` WARN 保留(两道机器面都看得见)。③ **被否**:直接 FAIL(「未走到这一步」≠ 失败,`3509 §B110`);文案只说「请补声明」(丢 D103 排除类出路 ⇒ 把「如实未声明」罚成必填错)。④ **钉与变异**:`test_setup_without_app_login_pushes_not_ready`(改判自旧钉「degrades_loudly」:rc=3 + stderr 指路含 `mode: none` 出口与「人工会话取出」+ **不写盘**)+ `test_app_login_not_ready_json_face_matches_channel`(机读键 + message 面);变异 **MB17**(退回 WARN 半生成 ⇒ 红)/ **MB18**(机读键写错/缺 message ⇒ 红)。⑤ **出库判定(2026-10-25 出库)**:已声明靶场(v3 fabricated)行为不变实证——真跑日 regen 正常、setup 预置照跑、无 not_ready(D148 首轮第 6 项);未声明面由钉 MB17/MB18 守。 |
|
|
619
|
+
| D150 | **验收执行器 = 执行层最小立项面 ①**(2026-10-25;`proposals/RFC-20261025-02` v2 + 评审 `REVIEW-20261025-02`;前置链 = R-1 表态 → RFC-20261025-01 前置研究(两道闸)→ 三张清单(评核过);维护者「继续」拍板进实现批,同日落刀) | ① **病灶**:真跑验收六步是 runbook 非机器行为(prepare 调用/结果汇总/留档/断点续跑靠纪律);runbook 六步中唯一无机器承载的 = 编排 + 统一报告 + 留档三件事(评审建设性判定)。② **定案**:`scripts/atlas_verify.py` 四步——pre(项目钩子 `atlas-prepare.mjs`,cwd=项目根、180s、**批级 `ATLAS_CASE_ID=verify-batch` 显式批标识**〔新声明:非用例 ID,逐用例分段仍归控制台〕、`node_modules` 存在 ⇒ 注入 `NODE_PATH`);**run = `atlas_check` 默认模式为执行体(全批只跑一次真跑——v1 评审 MUST:双跑 = 首跑 D108 留证被 Playwright 二次起跑清空;`--fast` 分支被否因门行缺席)**;report(stdout/`--json` 沿行形态 + 留档 `reviews/<日期>-verify/` **仅全链绿落档**——失败批不落档故重跑不被短路);backfill **缓行**(写面零触碰:不写 e2e-index、不进评审账本、不取 claim——第二写手四题另案)。续跑语义无独立状态文件:留档在 ⇒ 整批短路;pre 恒重跑(如实);run 按 `results.json` 产物判(新鲜度依赖 Playwright 起跑清空行为,明写)。失败停环沿 apply §7 条文化。③ **被否**:执行器自起 Playwright(双真跑);独立净场步(净场已机制化于 prepare,D110);复用 `ATLAS_REFRESH_SETTLE` 当静置窗(换用途复用旋钮);独立状态机文件(死结同族);零变化钉的「全仓逐字节」口径(收窄为既有套件全绿 + 行集零新增红)。④ **钉与变异**:`tests/test_atlas_verify.py` 七钉(pre 失败停环 / 批级 ID 值形态 / run 失败含门行+留证路径 / 留档短路 / 断点语义·pre 恒重跑·run 按产物 / **全批恰一次真跑** / 参数错不写盘);变异 **MB19**(摘留档短路)/ **MB20**(摘 run 续跑判据)四段协议双咬、还原逐字节一致。⑤ **出库判定**:首轮真跑日经它起跑(四步各有如实状态 + 单次真跑 + 留证不蒸发 + 留档在)后出库;实现批验收线一句话版在 RFC §4 尾。 |
|
|
617
620
|
|
|
618
621
|
|
|
619
622
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Atlas — 践行 Project Harness 的全流程 AI 编程工作流
|
|
2
2
|
|
|
3
3
|
<p align="left">
|
|
4
|
-
<img src="https://img.shields.io/badge/version-0.2.0--beta.
|
|
4
|
+
<img src="https://img.shields.io/badge/version-0.2.0--beta.8-blue.svg" alt="Version" />
|
|
5
5
|
<img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" />
|
|
6
6
|
<img src="https://img.shields.io/badge/tests-passing-success.svg" alt="Tests" />
|
|
7
7
|
<a href="https://sunwenzhe-git.github.io/atlas/"><img src="https://img.shields.io/badge/docs-VitePress-green.svg" alt="Docs" /></a>
|
package/package.json
CHANGED
package/rings/e2e/reference.md
CHANGED
|
@@ -501,7 +501,7 @@ testid:
|
|
|
501
501
|
- **断言超时下限(硬)**(2026-10-17,issue #28 / `DESIGN.md` D105):生成的运行器配置**必须**声明 `expect.timeout`(缺省 **20s**,`ATLAS_EXPECT_TIMEOUT` 可覆盖)。理由:Playwright 的 `expect` 默认只等 **5s**,而 SPA 首屏(dev bundle 可达 10MB+,现场实测 14s)会让「元素还没渲染」被误判成「元素不存在」= **假红**(本轮实测:导航 URL 全对、元素真在,却判 3 failed)。**它不是「固定等待」**(§5.5 的禁令指 `sleep`):`expect` 是轮询式 auto-wait,本条只抬**上限**。
|
|
502
502
|
- **等待超时下限(硬)**(2026-10-19,`DESIGN.md` D118;v2 真跑现场驱动):生成物里的 `ATLAS_WAIT_TIMEOUT_MS`(**页面就绪锚点** / `waitFor` / `download` / `select` 就绪轮询**共用同一个常量**)**缺省 20s**、且**必须留环境覆盖口** `ATLAS_WAIT_TIMEOUT_MS`。理由与上一条同源:该应用首屏实测 **14,184ms**,而此常量原**写死 10s、无覆盖口** ⇒ 同一份生成物里「等首屏」比「等断言」还短 ⇒ 冷启 / 负载下**间歇性假红**(v2 现场:整份首跑 9 passed / **1 failed**,红的那条倒在锚点 10s 超时 `元素齐全守卫:声明的 testid 无一在场`;同口径探针实测该元素 **2.2~2.5s** 出现且 `disabled=true` + 合规 `aria-label` 全对;复跑 `--repeat-each=3` ⇒ 3/3 绿、全量 **28 passed / 0 failed / 0 flaky**)。**它不是「固定等待」**(只抬上限,轮询语义不变);**它仍会咬人**:`ATLAS_WAIT_TIMEOUT_MS=200` 跑同一条用例必红(现场实测:同一页同一 URL 各轮渲染耗时在 **<1.5s ~ 2.2s** 之间波动,故取远低于它的值;实测 `=1500` 那轮碰巧绿 —— **这种逐轮波动正是 10s 写死会成为枚硬币投掷的原因**)。
|
|
503
503
|
- **refresh 落盘节拍(`ATLAS_REFRESH_SETTLE`,2026-10-18 补,`DESIGN.md` D111;v3 现场实证移植批)**:`refresh` 动词发射**先等后刷**两行——`await page.waitForTimeout(Number(process.env.ATLAS_REFRESH_SETTLE || "0"));` 先于 `await page.reload();`;接续在 `rings/e2e/reference.md` §5.5 动词表 `refresh` 行、发射面见 `tests/test_gen_e2e_scripts.py` 顺序钉。三条口径:① **默认 0 ⇒ 生成物行为与既有契约一致**(毫秒数属项目侧**运行时**前提,运行时设 `ATLAS_REFRESH_SETTLE=<毫秒>`;零业务常数进生成器,对齐 F11 `ATLAS_SLOWMO` 注入先例与 §5.5 的 `ATLAS_EXPECT_TIMEOUT` 同族);② 它**不是 §5.5「固定 sleep」禁令所指**——该禁令禁的是**用例侧**把共享靶场节拍写死进用例;本行是执行**环境配置**面,不进用例正文;③ 单一注入面 = 生成物字面内联(`process.env`),无生成期开关/无覆盖口。
|
|
504
|
-
- **失败留证(硬,失败才落盘)**(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;独立评审两轮 + 维护者拍板「按推荐继续」,源自 OpenHands `browser record`
|
|
504
|
+
- **失败留证(硬,失败才落盘)**(2026-10-18,`proposals/RFC-20261018-01-e2e-failure-evidence`;独立评审两轮 + 维护者拍板「按推荐继续」,源自 OpenHands `browser record` 机制级审查;**落点勘误 + 留存窗口 2026-10-25 真跑日实咬补定**):生成的运行器配置(两轨)**必须**补 `trace: 'retain-on-failure'` + `screenshot: 'only-on-failure'`——失败现场自动留 Playwright trace(DOM 快照 + 网络 + 操作日志,trace viewer 可逐帧回放)与截图,落 **`.atlas-run/`**(生成物显式 `outputDir` 覆盖 Playwright 缺省 `test-results/`——**D108 首版契约误写缺省目录,2026-10-25 实咬勘误**,构件形态 = `.atlas-run/<用例目录>/trace.zip + test-failed-*.png` 成对);**全绿轮零新增构件**;**留存窗口 = 到下一次 `playwright test` 起跑**(Playwright 启动即清空 outputDir——「首跑红→复跑绿」场景复跑启动瞬间旧构件蒸发,跨轮取证须在起跑前归档;观察与升格条件见 `duty-review/bugs.md` E2E 段);`video` 缓行(trace 就是回放器,粗粒度投影无收益)。三条口径:① **不溯及已装配副本**(`playwright.config.ts` 由装配的生成器发射 ⇒ 本能力的**咬合点 = 下一次 `install.sh --update` / 首装重装配**;重装配前「契约已声明留证而现场无 trace」属**如实态、非缺陷**,巡检不得误报——对齐 `shared/single-source.md` §3 迁移仪式「新源生效 ⇔ 旧源显式失效」);② **留证含业务内容,只落采纳仓本地**(`.atlas-run/`),不进 atlas 上报通道(§15 零业务上下文出站面不受牵连);**例外(2026-10-23 D142)**:含 `request` 断言的用例默认 `trace: off`(trace 会保存请求体,与 request 的「载荷不落盘」承诺冲突),`ATLAS_REQUEST_TRACE=1` 才恢复 `retain-on-failure`;③ **它不解决 D103 盲区**(人工会话取出类的「CI 不可复现」真身是登录态,不是取证手段——trace 只重放**已发生**的失败)。**判据接线(动机)**:`duty-review/bugs.md` 未解观察(hover 禁用按钮首跑偶发 `Element is not visible`)的升判条件「复现 ≥2 次且能给出固定节拍证据(trace / 连续截图)」由此取得**常驻证据源**,缺陷调查不再依赖落 `/tmp` 的临时探针(不可复算,D98 / 案 6 两次付费先例)。钉 3 条(双轨同构 / 值形态封闭 / 无覆盖口)+ 变异 **MB10**(摘任一轨 trace 键)/ **MB11**(值改 `on`),四段协议含注入 diff、还原逐字节一致;证据与升问题通见门行:`shared/gates.md` 2.2「E2E 真跑」。
|
|
505
505
|
- **零步探针的缺席语义(硬)**(2026-10-17,issue #30 / `DESIGN.md` D107):基线探针**必须在元素缺席时立即判「不成立」**(`count() > 0` 或经 `tsVis` / `atlasValueOf`),**不得调用会等挂载的 actionability API 裸用**(`getAttribute` / `isDisabled` / `isChecked` / `inputValue` 直接挂在 locator 上且无 `count()` 前置)——带参页面上基线本就探不到元素,等挂载会把 `null = 不成立`(§5.5)变成「挂到 test 超时」。
|
|
506
506
|
- **宿主注入型 base query 的转发(硬)**(2026-10-17,issue #27 / `DESIGN.md` D104):`e2e.app_base_url` 携带的 **query 是宿主注入型前提**(现场:应用只从 `window.location.search` 读 tenant,无 sessionStorage 回落,见 `DESIGN.md` D104)——`会话:` 通道写的是 sessionStorage(`D89`)、**送不到**这类消费点;而 `goto` 自带 `?` 在 `D109` 之前一律 FAIL(§5.7)。故由**生成物负责把它带到每一跳**:三处导航发射面(`step` 的 `goto`、元素齐全守卫、零步基线 `data.url`)在**生成期**把 base 的 query 并进目标路径(插在 path 与 hash 之间;实现单源 = `_support/url_merge.py`)。三条口径:① **base 无 query ⇒ 生成物逐字节零变化**(`生成物最新` 门看守派生件,钉见 `tests/test_gen_e2e_scripts.py`);② **`ATLAS_BASE_URL` 只覆盖 host、不覆盖 base 的 query**(换 tenant 请改 `stack-profile.yaml`);③ **边界(明示,不假装支持)**:base query 一旦成为前提,就**无法用同一份用例表达「它缺席时」的行为**——`会话:`(sessionStorage)与 `auth: none`(storageState)**都不在同一消费面**、不构成逃逸口;该场景当前**超出本契约范围**,需要时另起决策行(新增 opt-out 键 = 触 `D99` 字段集冻结)。**消费者侧**:控制台手工项「打开」的地址构造同样走该口径(`e2e_console.manual_open_url`;旧实现 `base + path` 在 base 带 query 时产畸形 URL)。④ **D109 追加(2026-10-18)**:**用例级**宿主注入参数走 §5.5 `参数:` + 内联 `goto` 查询串,与 base 的 query **求并集**(base 在前、**同键 ⇒ 用例胜**);**守卫导航同样带**(取该用例首个带参 `goto` 的查询串);**零步基线不施加**;本函数原本的「目标自带 `?` ⇒ 原样返回」暗位随之**转正为并集档**(既有生成物无一带 query 的 `goto` ⇒ 零回归)。
|
|
507
507
|
- **运行器前置(硬)**(issue #24 / `DESIGN.md` D101):生成物 import `@playwright/test` ⇒ **从 `e2e.scripts_dir` 出发必须可解析**(判据 = 依赖可导入性,不按路径存在;与门同口径)。生成器**不校正**它(生成期代表不了运行机器),由消费者在**起跑前**判:`atlas_check`「E2E 真跑」门不成立 ⇒ `FAIL` 点名;控制台不成立 ⇒ 诚实拒绝,不起注定失败的子进程。**全局兜底**(本机只 `npm i -g` 了 playwright)⇒ 执行器在 Node 的 `node_modules` 搜索路径上补**幂等软链**并复检,且把「用了全局什么」输出(门侧同步降 `WARN`——跑得起来,但结论依赖机器全局状态);链接位被非本工具占用 ⇒ **拒绝覆盖**。`NODE_PATH` 保留(CJS 项目的真通道)但**不作为判据**:ESM 项目下它无效(Node 的 ESM 解析器不读它)。
|
|
@@ -642,7 +642,7 @@ testid:
|
|
|
642
642
|
2. **反偏见**:绝不改用例迁就实现;绝不无根因删用例;**绝不用 `skipped` 绕过失败**;绝不弱化 `expected` 求绿;改用例必须写明理由(AC 变了 / 假设错了 / 非确定)。
|
|
643
643
|
3. **去重 3-Gate**:判重必须过「同目标 / 同意图 / 同层」三关并附证据;文件名或标题相似不算证据。
|
|
644
644
|
4. **完成门**:需求相关用例在真实应用上全 `green`,或标 `blocked` 并写明原因与去向。
|
|
645
|
-
5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16;D143 2026-10-23 结构化)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`:旧行 `- <ID> | <页面> | <标题> | <操作说明>` 继续可用;新项可用 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`,`step` 每行 `操作 | 观察 | 命令(可空)`),id/title
|
|
645
|
+
5. **手工项盘点(`RFC-20261015-01` / issue #10,2026-10-16;D143 2026-10-23 结构化)**:分片与索引页面表就绪后,识别**机器覆盖不到的人工作业**(控制台盖不住的项)⇒ 落 `product/e2e/manual.md`:旧行 `- <ID> | <页面> | <标题> | <操作说明>` 继续可用;新项可用 `## <ID> <标题>` + `atlas-manual` 块(`page / precondition / step / evidence`,`step` 每行 `操作 | 观察 | 命令(可空)`),id/title 只从标题行取。**观察锚写作约定(2026-10-24,设计优化 v2 WS-D;非字段、不进校验器)**:`step` 行「观察」列建议首 token 写产物锚 `→<用例ID> S<步骤号>`(如 `→E2E-APP-WORKFLOW-DETAIL-001 S3`),把人侧反馈钉到产物坐标(「点哪改哪」的文本化);升格为机器字段的前提 = 「观察项引用错位」现场实证 ≥2 例(QUEUE 观察室,届时走独立决策行)。评估了却没有 ⇒ 域收口留痕 README 写「已评估:0」;**没盘点过** = §5.4 留痕缺该行,采用者由此看到「忘了」。**验收走查现场**新识别的手工项 ⇒ 授权**人侧当场直写** `manual.md`(格式同 §10.1),下场 e2e 报告披露增量;不为此新增设施(控制台的唯一写盘点仍是账本 + 索引,§10.1)。
|
|
646
646
|
|
|
647
647
|
---
|
|
648
648
|
|
|
@@ -660,7 +660,7 @@ testid:
|
|
|
660
660
|
| 启动 | `python3 .atlas/scripts/e2e_console.py --root .`(默认端口 4190;`--slowmo` / `--timeout` / `--no-head` / `--auto-close` 可选) |
|
|
661
661
|
| 静态资源 | `.atlas/templates/e2e/console/`(`index.html` / `app.js` / `console.css`);环境变量 `ATLAS_CONSOLE_ASSETS` 可覆盖 |
|
|
662
662
|
| 只读输入 | `product/e2e/e2e-index.md` + `product/e2e/cases/*.md`;格式解析**复用** `validate_e2e_index.py`,不写第二份 |
|
|
663
|
-
| 执行 | 一次一条(**并发 = 1**)、node 单轨(`<e2e.node_modules>/.bin/playwright test -c playwright.config.ts <file>.spec.ts:<line> --headed`(慢动作经 `ATLAS_SLOWMO` 注入配置的 `launchOptions.slowMo`;F11,2026-10-14),可执行解析与 `atlas_check`「E2E 真跑」门同一规则(含**浏览器就绪判据**,2026-10-20,`DESIGN.md` D124:`ATLAS_PLAYWRIGHT_CHANNEL` 未设时,playwright 自算的 chromium 可执行路径必须**存在**——npm 装包不自动下载浏览器,缺 ⇒ 起跑前诚实拒绝并点名 `npx playwright install chromium`);缺或不可达 ⇒ 服务端诚实拒绝,不起注定失败的子进程;「跑完留窗 + 结果哨兵」由 `atlas-hold.ts` 承载,2026-10-12 迁移批;**持窗模式向运行器传 `--timeout=0`(2026-10-21,`DESIGN.md` D126)**——Playwright 默认 test timeout(30s)**计入 fixture teardown**,留窗等待会在约 30s 处被判超时并强制关窗(浏览器「跑完仍自动关闭」的真因;哨兵此时已写 `exitstatus:0`,界面还会误示「已保留」);控制台的 wall-clock `--timeout` 是唯一超时真相源(到点 kill 并如实报「超时」),自动关窗模式不传该旗标、保留运行器原生 30s 兜底);**用例执行前数据准备钩子(硬,2026-10-18,`DESIGN.md` D110;v3 现场实证移植批)**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 每条用例执行前先跑它(`node`,cwd = 项目根,180s 上限、**不占**用例超时窗——在计时起点之前;运行时 env 必带 `ATLAS_CASE_ID` = 当前用例 ID 且同步注入 `NODE_PATH`(与真跑子进程依赖解析环境一致,2026-10-20,`DESIGN.md` D125);项目脚本可据此只准备该用例依赖的数据,不读该变量的存量脚本行为不变——2026-10-20,`DESIGN.md` D121);失败/超时/起不动 ⇒ **诚实拒绝执行该条并在 UI 显式分流为「前置失败」**(`done` 事件完整形状 + `prep_failed` 标记,UI 呈现为 warn 级 pill 与「与用例脚本本身无关」通告,消除误导;失败原文进「输出」面板),不静默跑注定偏移的用例(共享状态型靶场的残留实体会让 count 类断言系统性偏移);地址 = `e2e.app_base_url`(E1 单段;来源下拉随 E1 删除,`#155`——meta 行保留 base_url 展示)。**已提交视图(2026-10-21,`DESIGN.md` D127)**:控制台「已提交」= 事件流中**所有 submit 快照按序累积合并**(后提交覆盖同名用例),不是最后一条事件;单次提交只携带页面草稿(差量),避免部分提交抹掉历史用例。**blocked / skipped 用例执行按钮禁用**(title 带索引状态原因;服务端同判诚实拒绝,`#158`——生成物为 `test.skip` 本无 node id
|
|
663
|
+
| 执行 | 一次一条(**并发 = 1**)、node 单轨(`<e2e.node_modules>/.bin/playwright test -c playwright.config.ts <file>.spec.ts:<line> --headed`(慢动作经 `ATLAS_SLOWMO` 注入配置的 `launchOptions.slowMo`;F11,2026-10-14),可执行解析与 `atlas_check`「E2E 真跑」门同一规则(含**浏览器就绪判据**,2026-10-20,`DESIGN.md` D124:`ATLAS_PLAYWRIGHT_CHANNEL` 未设时,playwright 自算的 chromium 可执行路径必须**存在**——npm 装包不自动下载浏览器,缺 ⇒ 起跑前诚实拒绝并点名 `npx playwright install chromium`);缺或不可达 ⇒ 服务端诚实拒绝,不起注定失败的子进程;「跑完留窗 + 结果哨兵」由 `atlas-hold.ts` 承载,2026-10-12 迁移批;**持窗模式向运行器传 `--timeout=0`(2026-10-21,`DESIGN.md` D126)**——Playwright 默认 test timeout(30s)**计入 fixture teardown**,留窗等待会在约 30s 处被判超时并强制关窗(浏览器「跑完仍自动关闭」的真因;哨兵此时已写 `exitstatus:0`,界面还会误示「已保留」);控制台的 wall-clock `--timeout` 是唯一超时真相源(到点 kill 并如实报「超时」),自动关窗模式不传该旗标、保留运行器原生 30s 兜底);**用例执行前数据准备钩子(硬,2026-10-18,`DESIGN.md` D110;v3 现场实证移植批)**:项目自带 `product/e2e/scripts/atlas-prepare.mjs` ⇒ 每条用例执行前先跑它(`node`,cwd = 项目根,180s 上限、**不占**用例超时窗——在计时起点之前;运行时 env 必带 `ATLAS_CASE_ID` = 当前用例 ID 且同步注入 `NODE_PATH`(与真跑子进程依赖解析环境一致,2026-10-20,`DESIGN.md` D125);项目脚本可据此只准备该用例依赖的数据,不读该变量的存量脚本行为不变——2026-10-20,`DESIGN.md` D121);失败/超时/起不动 ⇒ **诚实拒绝执行该条并在 UI 显式分流为「前置失败」**(`done` 事件完整形状 + `prep_failed` 标记,UI 呈现为 warn 级 pill 与「与用例脚本本身无关」通告,消除误导;失败原文进「输出」面板),不静默跑注定偏移的用例(共享状态型靶场的残留实体会让 count 类断言系统性偏移);地址 = `e2e.app_base_url`(E1 单段;来源下拉随 E1 删除,`#155`——meta 行保留 base_url 展示)。**已提交视图(2026-10-21,`DESIGN.md` D127)**:控制台「已提交」= 事件流中**所有 submit 快照按序累积合并**(后提交覆盖同名用例),不是最后一条事件;单次提交只携带页面草稿(差量),避免部分提交抹掉历史用例。**blocked / skipped 用例执行按钮禁用**(title 带索引状态原因;服务端同判诚实拒绝,`#158`——生成物为 `test.skip` 本无 node id,不得误报「重跑生成」);**验收执行器(2026-10-25,`RFC-20261025-02` / `DESIGN.md` D150)**:真跑可经 `scripts/atlas_verify.py` 四步起跑(pre/run=atlas_check/report 留档/backfill 缓行·写面零触碰;**全批只跑一次真跑**);**批级 `ATLAS_CASE_ID` 语义 = 显式串 `verify-batch`(非用例 ID——逐用例分段准备仍归控制台逐条执行,D121 不变;不读该变量的存量脚本行为逐字节不变)** |
|
|
664
664
|
| **桩披露(2026-10-12,§5.8)** | 含 `mock:` 行的用例卡片渲染 **MOCKED 徽标**(分片静态事实)+ 展开区 mock 清单;执行输出的 `[atlas-mock]` 命中行随日志流呈现(运行时事实)——人工走查者必须能看见「这条绿是桩撑的」 |
|
|
665
665
|
| **评审指纹与陈旧度(2026-10-21,`DESIGN.md` D128)** | 账本快照与每条提交绑定 **HEAD 提交与脚本散列指纹**(`prov: {head_sha, scripts_hash, git_dirty, actor_type}`)。`scripts_hash` 白名单覆盖 `scripts/` 下源码及 `atlas-prepare.mjs`(**候选顺序 = Runner 实际执行面:`product/e2e/scripts/` 优先**),排除运行时产物(`test-results/`、trace、png 等)——否则跑一轮就脏一次。控制台按当前工作区指纹实时重算 `review_state`(`fresh` / `stale` / `legacy` / `unreviewed`):代码或脚本变动 ⇒ **`stale`**,旧账本无指纹 ⇒ **`legacy`**;**两者同等按「需重审」处理**(剔除已评审、禁用复选框、顶栏单列计数、纳入「只看未评审」筛选),**读不到当前指纹时按 `legacy` 而非 `fresh`**(fail-closed,不冒充已核验);重新执行该用例自动清空旧评审态。`actor_type` ∈ `human \| agent`,由环境变量 **`ATLAS_ACTOR_TYPE`** 声明(缺省 `human`;自动化跑请显式设 `agent`)——它是**操作者自述、不是密码学证明**,故本环只如实暴露该字段,收口阻断仍由门与 `shared/closeout.md` 承担。提交另记 `reviewed_scope`(本次签字覆盖的用例集),消费者 = 人读账本 `ledger.md` 的「本次评审范围」行与控制台顶栏(不写不读 = 死字段,禁止)。**当前无 git / 生成脚本未落盘** ⇒ 相应维度记 `null`,不得用空串哈希冒充「匹配」 |
|
|
666
666
|
| **评审维度与执行状态正交** | 页面 `localStorage` 草稿 = 人的勾选;索引 `状态` 列(`red` / `green` / `blocked` / `skipped`;E0 历史态 `prototype-pass` 见 §5.4)= 执行结果。**两者不得互相推导**;「人工 review 通过」只在**执行通过**后解锁 |
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""atlas 验收执行器(执行层最小立项面 ①)——`RFC-20261025-02` v2 / `DESIGN.md` D150。
|
|
3
|
+
|
|
4
|
+
四步(**执行器 = 编排者 + 统一报告者 + 留档者,不重造真跑**):
|
|
5
|
+
|
|
6
|
+
pre 项目数据准备钩子 `product/e2e/scripts/atlas-prepare.mjs`(缺 ⇒ SKIP+原因,不算失败);
|
|
7
|
+
cwd = 项目根、180s 上限(控制台 D110 同款);env 带 `ATLAS_CASE_ID=verify-batch`
|
|
8
|
+
(显式批标识——RFC-20261025-02 批级语义:非用例 ID,D121 的逐用例分段仍归控制台);
|
|
9
|
+
`<root>/node_modules` 存在 ⇒ 同步注入 `NODE_PATH`(与真跑子进程依赖解析环境一致)。
|
|
10
|
+
run = `python3 .atlas/scripts/atlas_check.py --root <root>`(默认模式含全量真跑与装配门)
|
|
11
|
+
——**全批只跑一次真跑**(双跑 = 首跑 D108 留证被 Playwright 二次起跑清空)。
|
|
12
|
+
report 汇总行集 → stdout(+`--json`)并落档 `product/e2e/reviews/<日期>-verify/`
|
|
13
|
+
(README + report.json);**仅全链绿时落档**——失败批只出结构化报告不落档,
|
|
14
|
+
重跑因此不被短路。
|
|
15
|
+
backfill **缓行**(首批写面零触碰:不写 e2e-index 状态列、不进评审账本、不取写面 claim)。
|
|
16
|
+
|
|
17
|
+
续跑语义(无独立状态文件——产物存在性 = 唯一判据,失败停环语义沿 `apply/reference.md` §7):
|
|
18
|
+
- 留档目录在 ⇒ 整批短路(已成功的批不重跑);
|
|
19
|
+
- pre 无文件产物(改的是靶场服务端态)⇒ **恒重跑**(幂等廉价,如实);
|
|
20
|
+
- run 产物 = `product/e2e/scripts/.atlas-run/results.json`:缺 ⇒ 执行;在 ⇒ 跳过并注记
|
|
21
|
+
「沿用既有 results,原批退出码未知」(正常中途失败时 Playwright 起跑清空 `.atlas-run`
|
|
22
|
+
⇒ 产物必缺;在而留档无 = 上批死于 run 成功与落档之间的窄窗);
|
|
23
|
+
- 任一步失败 ⇒ 停该步、结构化报告(哪步/为何/已完步产物清单),下游不静默跳过。
|
|
24
|
+
|
|
25
|
+
退出码:**0** 全链绿或短路;**1** 失败(结构化报告);**2** 参数错(不写盘)。纯标准库。
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import time
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
PRE_TIMEOUT_S = 180
|
|
38
|
+
BATCH_CASE_ID = "verify-batch" # RFC-20261025-02 批级语义:显式批标识,非用例 ID(D121)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _prep_env(root: Path) -> dict:
|
|
42
|
+
env = dict(__import__("os").environ)
|
|
43
|
+
env["ATLAS_CASE_ID"] = BATCH_CASE_ID
|
|
44
|
+
nm = root / "node_modules"
|
|
45
|
+
if nm.is_dir():
|
|
46
|
+
env["NODE_PATH"] = str(nm)
|
|
47
|
+
return env
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def step_pre(root: Path) -> dict:
|
|
51
|
+
"""项目数据准备钩子;缺 ⇒ SKIP,失败/超时 ⇒ 失败(停环)。"""
|
|
52
|
+
prep = root / "product" / "e2e" / "scripts" / "atlas-prepare.mjs"
|
|
53
|
+
started = time.perf_counter()
|
|
54
|
+
if not prep.is_file():
|
|
55
|
+
return {"name": "pre", "status": "SKIP",
|
|
56
|
+
"detail": "无项目数据准备钩子 atlas-prepare.mjs(合法态,不算失败)",
|
|
57
|
+
"duration_ms": int((time.perf_counter() - started) * 1000)}
|
|
58
|
+
try:
|
|
59
|
+
pr = subprocess.run(["node", str(prep)], cwd=str(root), capture_output=True,
|
|
60
|
+
text=True, timeout=PRE_TIMEOUT_S, env=_prep_env(root))
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
return {"name": "pre", "status": "FAIL", "detail": "node 可执行不存在", "duration_ms": 0}
|
|
63
|
+
except subprocess.TimeoutExpired:
|
|
64
|
+
return {"name": "pre", "status": "FAIL",
|
|
65
|
+
"detail": f"atlas-prepare 超时(>{PRE_TIMEOUT_S}s,不占真跑超时窗)",
|
|
66
|
+
"duration_ms": PRE_TIMEOUT_S * 1000}
|
|
67
|
+
dur = int((time.perf_counter() - started) * 1000)
|
|
68
|
+
if pr.returncode != 0:
|
|
69
|
+
return {"name": "pre", "status": "FAIL",
|
|
70
|
+
"detail": f"atlas-prepare 失败(exit={pr.returncode}):{(pr.stderr or pr.stdout or '')[-800:]}",
|
|
71
|
+
"duration_ms": dur}
|
|
72
|
+
return {"name": "pre", "status": "ok",
|
|
73
|
+
"detail": ((pr.stdout or "").strip()[-400:] or "(无输出)"), "duration_ms": dur}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _parse_rows(output: str) -> list[dict]:
|
|
77
|
+
"""解析 atlas_check 行形态(`ok|WARN|FAIL|SKIP <名称> <摘要>`,与门行原样)。"""
|
|
78
|
+
rows = []
|
|
79
|
+
for line in output.splitlines():
|
|
80
|
+
toks = line.split(None, 2)
|
|
81
|
+
if toks and toks[0] in {"ok", "WARN", "FAIL", "SKIP"} and len(toks) >= 2:
|
|
82
|
+
rows.append({"status": toks[0], "name": toks[1],
|
|
83
|
+
"summary": (toks[2] if len(toks) > 2 else "").strip()})
|
|
84
|
+
return rows
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def step_run(root: Path, py: str) -> dict:
|
|
88
|
+
"""run = atlas_check 默认模式(含全量真跑);产物在 ⇒ 跳过(见模块 docstring 续跑语义)。"""
|
|
89
|
+
started = time.perf_counter()
|
|
90
|
+
results = root / "product" / "e2e" / "scripts" / ".atlas-run" / "results.json"
|
|
91
|
+
if results.is_file():
|
|
92
|
+
return {"name": "run", "status": "SKIP",
|
|
93
|
+
"detail": "沿用既有 results.json(原批退出码未知;强制重跑请先删留档与 .atlas-run)",
|
|
94
|
+
"duration_ms": 0, "rows": [], "real_run_count": 0}
|
|
95
|
+
check = root / ".atlas" / "scripts" / "atlas_check.py"
|
|
96
|
+
if not check.is_file():
|
|
97
|
+
return {"name": "run", "status": "FAIL", "detail": "atlas_check.py 不存在(未装配?)",
|
|
98
|
+
"duration_ms": 0, "rows": [], "real_run_count": 0}
|
|
99
|
+
pr = subprocess.run([py, str(check), "--root", str(root)], capture_output=True, text=True)
|
|
100
|
+
dur = int((time.perf_counter() - started) * 1000)
|
|
101
|
+
out = (pr.stdout or "") + (pr.stderr or "")
|
|
102
|
+
return {"name": "run", "status": "ok" if pr.returncode == 0 else "FAIL",
|
|
103
|
+
"detail": f"atlas_check exit={pr.returncode}" + ("" if pr.returncode == 0 else f":{out.strip()[-400:]}"),
|
|
104
|
+
"duration_ms": dur, "rows": _parse_rows(out), "real_run_count": 1}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _evidence_paths(root: Path) -> list[str]:
|
|
108
|
+
d = root / "product" / "e2e" / "scripts" / ".atlas-run"
|
|
109
|
+
if not d.is_dir():
|
|
110
|
+
return []
|
|
111
|
+
hits = [str(p.relative_to(root)) for p in sorted(d.rglob("*"))
|
|
112
|
+
if p.is_file() and (p.name == "trace.zip" or p.name.startswith("test-failed"))]
|
|
113
|
+
return hits[:20]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def archive_dir(root: Path) -> Path:
|
|
117
|
+
return root / "product" / "e2e" / "reviews" / f"{datetime.now().date().isoformat()}-verify"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_archive(root: Path, report: dict) -> str:
|
|
121
|
+
d = archive_dir(root)
|
|
122
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
(d / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
124
|
+
lines = [f"# 验收执行器留档({datetime.now().isoformat(timespec='seconds')};RFC-20261025-02/D150)",
|
|
125
|
+
"", "| 步 | 态 | 用时(ms) | 说明 |", "|---|---|---|---|"]
|
|
126
|
+
for st in report["steps"]:
|
|
127
|
+
lines.append(f"| {st['name']} | {st['status']} | {st['duration_ms']} | {st['detail'][:120]} |")
|
|
128
|
+
lines += ["", "| 门行 | 态 | 摘要 |", "|---|---|---|"]
|
|
129
|
+
for r in report["rows"]:
|
|
130
|
+
lines.append(f"| {r['name']} | {r['status']} | {r['summary'][:120]} |")
|
|
131
|
+
(d / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
132
|
+
return str(d.relative_to(root))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main(argv: list[str] | None = None) -> int:
|
|
136
|
+
ap = argparse.ArgumentParser(description="atlas 验收执行器(编排+统一报告+留档;不重造真跑)")
|
|
137
|
+
ap.add_argument("--root", required=True, help="项目根")
|
|
138
|
+
ap.add_argument("--json", action="store_true", help="机读报告(stdout)")
|
|
139
|
+
args = ap.parse_args(argv)
|
|
140
|
+
root = Path(args.root).resolve()
|
|
141
|
+
if not root.is_dir():
|
|
142
|
+
print("ERROR: --root 不是目录", file=sys.stderr)
|
|
143
|
+
return 2
|
|
144
|
+
|
|
145
|
+
# 留档在 ⇒ 整批短路(已成功的批不重跑)
|
|
146
|
+
if archive_dir(root).is_dir():
|
|
147
|
+
report = {"ok": True, "short_circuit": True,
|
|
148
|
+
"detail": f"留档 {archive_dir(root).relative_to(root)} 已存在——已成功的批不重跑",
|
|
149
|
+
"steps": [], "rows": [], "real_run_count": 0, "archive": str(archive_dir(root).relative_to(root))}
|
|
150
|
+
print(json.dumps(report, ensure_ascii=False, indent=2) if args.json
|
|
151
|
+
else f"[短路] {report['detail']}")
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
steps: list[dict] = []
|
|
155
|
+
real_run = 0
|
|
156
|
+
|
|
157
|
+
pre = step_pre(root)
|
|
158
|
+
steps.append(pre)
|
|
159
|
+
if pre["status"] == "FAIL":
|
|
160
|
+
return _fail(root, steps, [], 0, args.json)
|
|
161
|
+
|
|
162
|
+
run = step_run(root, py=sys.executable)
|
|
163
|
+
steps.append(run)
|
|
164
|
+
real_run += run.get("real_run_count", 0)
|
|
165
|
+
if run["status"] == "FAIL":
|
|
166
|
+
return _fail(root, steps, run.get("rows", []), real_run, args.json)
|
|
167
|
+
|
|
168
|
+
rows = run.get("rows", [])
|
|
169
|
+
report = {"ok": True, "short_circuit": False, "steps": steps, "rows": rows,
|
|
170
|
+
"real_run_count": real_run, "archive": None}
|
|
171
|
+
report["archive"] = write_archive(root, report)
|
|
172
|
+
if args.json:
|
|
173
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
174
|
+
else:
|
|
175
|
+
for st in steps:
|
|
176
|
+
print(f"[{st['status']}] {st['name']} ({st['duration_ms']}ms) {st['detail'][:160]}")
|
|
177
|
+
for r in rows:
|
|
178
|
+
print(f" {r['status']:<4} {r['name']:<24} {r['summary'][:100]}")
|
|
179
|
+
print(f"[留档] {report['archive']}")
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _fail(root: Path, steps: list[dict], rows: list[dict], real_run: int, as_json: bool) -> int:
|
|
184
|
+
"""失败停环:停在该步、结构化报告、下游不静默跳过;失败不落档(重跑不被短路)。"""
|
|
185
|
+
report = {"ok": False, "short_circuit": False, "steps": steps, "rows": rows,
|
|
186
|
+
"real_run_count": real_run, "archive": None,
|
|
187
|
+
"failed_step": next((s["name"] for s in reversed(steps) if s["status"] == "FAIL"), None),
|
|
188
|
+
"completed_steps": [s["name"] for s in steps if s["status"] in {"ok", "SKIP"}],
|
|
189
|
+
"evidence": _evidence_paths(root)}
|
|
190
|
+
if as_json:
|
|
191
|
+
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
192
|
+
else:
|
|
193
|
+
print(f"[FAIL] 停在 {report['failed_step']} 步(下游不执行);已完步:{report['completed_steps']}",
|
|
194
|
+
file=sys.stderr)
|
|
195
|
+
if report["evidence"]:
|
|
196
|
+
print(f"[留证] 失败构件:{report['evidence']}", file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
凭据只来自运行期环境变量 `ATLAS_APP_USER` / `ATLAS_APP_PASSWORD`(不读项目 `.env`、不硬编码);
|
|
34
34
|
未设置 ⇒ 跳过预置 + 明确警告;换取令牌失败 ⇒ 报错中止
|
|
35
35
|
(不得静默续跑——那会退化成「未登录态跑用例」的假绿)。
|
|
36
|
-
**段缺失 =
|
|
37
|
-
|
|
38
|
-
`not_ready
|
|
36
|
+
**段缺失 = 未声明(不再是「合法降级」,D80② 升档已落地 2026-10-24 / `DESIGN.md` D149)**:
|
|
37
|
+
`validate_stack_profile` WARN 指名在前;生成器直接推 `not_ready`:退出码 3 + 机读
|
|
38
|
+
`not_ready: "app_login"`(文案按 D103 指路「有门但三态都装不下 ⇒ 人工会话取出」)。
|
|
39
39
|
* **网络边界打桩**(契约 §5.8,2026-10-12):`mock:` 行(用例级)在 `e2e.mock.mode: on`
|
|
40
40
|
时编译为**单条** `context.route` 派发表(pathname+method 命中、query 免疫;前缀内未命中
|
|
41
41
|
fail-loud abort + 用例末审计断言点名;OPTIONS 自动 204 + CORS 头);fixture 内容**生成期
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
退出码(`3509 §B110`):**0** 正常 · **2** 真错(缺 profile / 动词越界 / 审查门不过…) ·
|
|
52
52
|
**3** 前置未声明 —— **3 不是失败**:调用方应判「未就绪」而不是「生成失败」,
|
|
53
53
|
且 `--json` 下 stdout 给出 `{"ok": false, "not_ready": <键>, …}`。
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
键 = `app_base_url`(地址未声明)/ `app_login`(登录形态未声明;D80② 升档 2026-10-24 落地,
|
|
55
|
+
`DESIGN.md` D149,升档记录见 `shared/gates.md`「E2E 真跑」行)。
|
|
56
56
|
"""
|
|
57
57
|
from __future__ import annotations
|
|
58
58
|
|
|
@@ -1402,14 +1402,21 @@ def main() -> int:
|
|
|
1402
1402
|
|
|
1403
1403
|
app_login = e2e.get("app_login") or None
|
|
1404
1404
|
if app_login is None:
|
|
1405
|
-
# RFC-20261015-02(issue #11
|
|
1406
|
-
#
|
|
1407
|
-
#
|
|
1408
|
-
#
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1405
|
+
# RFC-20261015-02(issue #11)/ D80② 升档落地(2026-10-24,D149,真跑日批):
|
|
1406
|
+
# 段缺失不再是「合法降级」——直接推 NOT_READY(退出码 3 + 机读键 "app_login",
|
|
1407
|
+
# 与 app_base_url 同一通道)。硬前提的输入不得靠「缺失即静默放行」吸收。
|
|
1408
|
+
# 文案按 D103 相抵点指路:「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 /
|
|
1409
|
+
# 登录响应体无令牌)⇒ 人工会话取出路径,不是「补声明」能解决的形态,不属本判据。
|
|
1410
|
+
msg = ("e2e.app_login 未声明(E2E 硬前提:登录形态。三选一:real 取证四值 / "
|
|
1411
|
+
"mode: none(靶场无门,显式声明)/ mode: fabricated(桩令牌,必填收窄 "
|
|
1412
|
+
"storage_key);「有门但三态都装不下」(图形验证码 / 服务端 cookie 会话 / "
|
|
1413
|
+
"登录响应体无令牌)⇒ 人工会话取出(rings/e2e/reference.md §7.1 / "
|
|
1414
|
+
"DESIGN.md D103),是描述路径、不是本判据的对象)")
|
|
1415
|
+
if args.json:
|
|
1416
|
+
print(json.dumps({"ok": False, "not_ready": "app_login", "message": msg},
|
|
1417
|
+
ensure_ascii=False, indent=2))
|
|
1418
|
+
print(f"NOT_READY: {msg}", file=sys.stderr)
|
|
1419
|
+
return NOT_READY
|
|
1413
1420
|
else:
|
|
1414
1421
|
login_mode = app_login.get("mode") or "real"
|
|
1415
1422
|
if login_mode not in APP_LOGIN_MODES:
|
|
@@ -2107,7 +2114,8 @@ def main() -> int:
|
|
|
2107
2114
|
" // 3 failed 1 passed)。提升到 20s(仍远小于 test 超时),并留 `ATLAS_EXPECT_TIMEOUT` 覆盖口。\n"
|
|
2108
2115
|
" expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) },\n"
|
|
2109
2116
|
f" use: {{ baseURL: process.env.ATLAS_BASE_URL ?? '{base}',\n"
|
|
2110
|
-
" // 失败留证(2026-10-18,RFC-20261018-01):失败才落
|
|
2117
|
+
" // 失败留证(2026-10-18,RFC-20261018-01;落点勘误 2026-10-25):失败才落 .atlas-run/(本配置 outputDir),\n"
|
|
2118
|
+
" // 全绿轮零产物;留存窗口 = 到下一次起跑(Playwright 启动清空 outputDir)。\n"
|
|
2111
2119
|
" // 生效咬合点 = 重装配(install --update / 首装),不溯及已装配副本。\n"
|
|
2112
2120
|
" trace: 'retain-on-failure',\n"
|
|
2113
2121
|
" screenshot: 'only-on-failure',\n"
|
|
@@ -2142,7 +2150,8 @@ def main() -> int:
|
|
|
2142
2150
|
" // 3 failed 1 passed)。提升到 20s(仍远小于 test 超时),并留 `ATLAS_EXPECT_TIMEOUT` 覆盖口。\n"
|
|
2143
2151
|
" expect: { timeout: Number(process.env.ATLAS_EXPECT_TIMEOUT ?? 20_000) },\n"
|
|
2144
2152
|
f" use: {{ baseURL: process.env.ATLAS_BASE_URL ?? '{base}', // E1 单段\n"
|
|
2145
|
-
" // 失败留证(2026-10-18,RFC-20261018-01):失败才落
|
|
2153
|
+
" // 失败留证(2026-10-18,RFC-20261018-01;落点勘误 2026-10-25):失败才落 .atlas-run/(本配置 outputDir),\n"
|
|
2154
|
+
" // 全绿轮零产物;留存窗口 = 到下一次起跑(Playwright 启动清空 outputDir)。\n"
|
|
2146
2155
|
" // 生效咬合点 = 重装配(install --update / 首装),不溯及已装配副本。\n"
|
|
2147
2156
|
" trace: 'retain-on-failure',\n"
|
|
2148
2157
|
" screenshot: 'only-on-failure',\n"
|