atlas-project-harness 0.2.0-beta.2 → 0.2.0-beta.21
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 +267 -0
- package/DESIGN.md +46 -3
- package/README.md +24 -14
- package/_support/__pycache__/ac_coverage.cpython-312.pyc +0 -0
- package/_support/__pycache__/case_block.cpython-312.pyc +0 -0
- package/_support/__pycache__/layout.cpython-312.pyc +0 -0
- package/_support/__pycache__/manual_block.cpython-312.pyc +0 -0
- package/_support/__pycache__/stack_profile.cpython-312.pyc +0 -0
- package/_support/ac_coverage.py +120 -0
- package/_support/case_block.py +3 -3
- package/_support/layout.py +411 -0
- package/_support/manual_block.py +67 -0
- package/_support/stack_profile.py +3 -1
- package/adapters/__pycache__/_graph.cpython-312.pyc +0 -0
- package/adapters/__pycache__/_jpa.cpython-312.pyc +0 -0
- package/adapters/__pycache__/_lib.cpython-312.pyc +0 -0
- package/adapters/__pycache__/api.cpython-312.pyc +0 -0
- package/adapters/__pycache__/models.cpython-312.pyc +0 -0
- package/adapters/__pycache__/tests.cpython-312.pyc +0 -0
- package/adapters/_graph.py +7 -2
- package/adapters/_jpa.py +291 -0
- package/adapters/_lib.py +8 -3
- package/adapters/ac/__init__.py +80 -0
- package/adapters/ac/__pycache__/__init__.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/command.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/js.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/junit.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/pytest.cpython-312.pyc +0 -0
- package/adapters/ac/command.py +88 -0
- package/adapters/ac/js.py +334 -0
- package/adapters/ac/junit.py +266 -0
- package/adapters/ac/pytest.py +380 -0
- package/adapters/api.py +331 -8
- package/adapters/models.py +126 -2
- package/adapters/routes.py +7 -3
- package/adapters/tests.py +43 -5
- package/apply/reference.md +6 -3
- package/bin/atlas.js +66 -28
- package/extensions/pi.ts +39 -8
- package/install.sh +179 -19
- package/package.json +1 -1
- package/patches/project-wiring/README.md +4 -3
- package/patches/project-wiring/__pycache__/apply-wiring.cpython-312.pyc +0 -0
- package/patches/project-wiring/agent-pointer.md +1 -1
- package/patches/project-wiring/apply-wiring.py +146 -153
- package/patches/workflow-plan-apply/1.6-plan-apply.insert.md +1 -1
- package/rings/e2e/reference.md +37 -20
- package/rings/prd/reference.md +26 -8
- package/rings/structure/reference.md +3 -2
- package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
- package/scripts/__pycache__/atlas_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/atlas_verify.cpython-312.pyc +0 -0
- package/scripts/__pycache__/closeout_derive.cpython-312.pyc +0 -0
- package/scripts/__pycache__/e2e_console.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_testid_stub.cpython-312.pyc +0 -0
- package/scripts/__pycache__/make_review_inputs.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
- package/scripts/__pycache__/refresh_structure.cpython-312.pyc +0 -0
- package/scripts/__pycache__/release.cpython-312.pyc +0 -0
- package/scripts/__pycache__/write_claim.cpython-312.pyc +0 -0
- package/scripts/atlas_check.py +329 -86
- package/scripts/atlas_report.py +10 -2
- package/scripts/atlas_verify.py +435 -0
- package/scripts/closeout_derive.py +296 -0
- package/scripts/e2e_console.py +46 -31
- package/scripts/gen_e2e_scripts.py +424 -66
- package/scripts/gen_testid_stub.py +8 -3
- package/scripts/make_review_inputs.py +20 -6
- package/scripts/pkg_digest.py +42 -5
- package/scripts/refresh_structure.py +29 -8
- package/scripts/release.py +238 -7
- package/scripts/write_claim.py +204 -0
- package/shared/closeout.md +7 -0
- package/shared/gates.md +24 -19
- package/shared/global-rules.md +8 -1
- package/shared/independent-review.md +4 -3
- package/shared/layout.md +28 -3
- package/shared/single-source.md +2 -0
- package/shared/stack-profile.md +19 -10
- package/skills/atlas-apply/SKILL.md +1 -0
- package/skills/atlas-design-review/SKILL.md +3 -3
- package/skills/atlas-e2e/SKILL.md +1 -0
- package/skills/atlas-prd/SKILL.md +1 -0
- package/skills/atlas-release/SKILL.md +9 -3
- package/skills/atlas-structure/SKILL.md +1 -0
- package/skills/frontend-design/SKILL.md +2 -2
- package/skills/frontend-design/references/tuning-verbs.md +1 -1
- package/templates/e2e/case-shard.md +1 -1
- package/templates/e2e/console/app.js +25 -4
- package/templates/e2e/console/console.css +2 -0
- package/templates/e2e/console/index.html +2 -2
- package/templates/prd/domain.md +14 -12
- package/templates/prd/global.md +2 -2
- package/templates/stack-profile.yaml +12 -11
- package/tests/__pycache__/test_ac_adapters.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_adapter_api.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_adapter_models.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_apply_wiring.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_verify.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_verify_playground.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_canonical_consistency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_closeout_derive.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_duty_bug_families.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_e2e_console.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_gen_e2e_scripts.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_install_selfcheck.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_layout.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/__pycache__/test_no_framework_names.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_project_wiring.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_refresh_facts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_shared_kernel.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_tools.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_tools.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_validate_ac_alignment.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validate_adr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validate_walkthrough.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_write_claim.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/fixtures/e2e_coverage_dims_golden.json +12 -0
- package/tests/fixtures/verify_playground/product/e2e/cases/home.md +40 -0
- package/tests/fixtures/verify_playground/product/e2e/e2e-index.md +18 -0
- package/tests/fixtures/verify_playground/product/e2e/reviews/2026-01-01-fixture/README.md +3 -0
- package/tests/fixtures/verify_playground/product/e2e/reviews/2026-01-01-fixture/review.json +17 -0
- package/tests/fixtures/verify_playground/product/e2e/scripts/atlas-prepare.mjs +4 -0
- package/tests/fixtures/verify_playground/product/prd/README.md +5 -0
- package/tests/fixtures/verify_playground/product/prd/home-prd.md +62 -0
- package/tests/fixtures/verify_playground/product/prd/prd.md +52 -0
- package/tests/fixtures/verify_playground/product/stack-profile.yaml.tpl +18 -0
- package/tests/fixtures/verify_playground/site/index.html +6 -0
- package/tests/test_ac_adapters.py +312 -0
- package/tests/test_adapter_api.py +253 -0
- package/tests/test_adapter_models.py +236 -0
- package/tests/test_apply_wiring.py +136 -23
- package/tests/test_atlas_check.py +371 -0
- package/tests/test_atlas_verify.py +335 -0
- package/tests/test_atlas_verify_playground.py +162 -0
- package/tests/test_canonical_consistency.py +5 -2
- package/tests/test_closeout_derive.py +264 -0
- package/tests/test_duty_bug_families.py +355 -3
- package/tests/test_e2e_console.py +49 -0
- package/tests/test_gen_e2e_scripts.py +650 -17
- package/tests/test_install_selfcheck.py +76 -0
- package/tests/test_layout.py +252 -0
- package/tests/test_map_anchors.py +475 -0
- package/tests/test_no_framework_names.py +86 -0
- package/tests/test_project_wiring.py +12 -4
- package/tests/test_refresh_facts.py +18 -3
- package/tests/test_shared_kernel.py +50 -0
- package/tests/test_tools.py +257 -1
- package/tests/test_validate_ac_alignment.py +1081 -0
- package/tests/test_validate_adr.py +24 -0
- package/tests/test_validate_walkthrough.py +313 -0
- package/tests/test_validators.py +499 -1
- package/tests/test_write_claim.py +236 -0
- package/validators/__pycache__/validate_ac_alignment.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_adr.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_design_exemptions.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_e2e_index.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_e2e_mocks.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_prd.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_stack_profile.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_structure.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_testids.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_walkthrough.cpython-312.pyc +0 -0
- package/validators/validate_ac_alignment.py +439 -183
- package/validators/validate_adr.py +15 -5
- package/validators/validate_design_exemptions.py +15 -6
- package/validators/validate_e2e_index.py +109 -38
- package/validators/validate_e2e_mocks.py +12 -5
- package/validators/validate_prd.py +109 -14
- package/validators/validate_stack_profile.py +62 -8
- package/validators/validate_structure.py +9 -2
- package/validators/validate_testids.py +36 -23
- package/validators/validate_walkthrough.py +300 -0
|
@@ -153,3 +153,27 @@ def test_validate_adr_warn_cycle_detected(tmp_path: Path):
|
|
|
153
153
|
assert "演进链存在循环引用环路: 0001->0002->0003->0001" in cycle_checks[0]["detail"], cycle_checks
|
|
154
154
|
# F36(R8 收束):WARN 必须随身携带**机器可读修复指令**(superseded_by 移出环外)。
|
|
155
155
|
assert "superseded_by" in cycle_checks[0]["detail"] and "移出环外" in cycle_checks[0]["detail"], cycle_checks
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_validate_adr_override_docs_root(tmp_path: Path):
|
|
159
|
+
"""RFC-20261026-01:`layout.assets_root=docs` + `layout.adr=docs/decisions` 生效。"""
|
|
160
|
+
(tmp_path / "atlas.config.yaml").write_text(
|
|
161
|
+
"version: 1\nlayout:\n assets_root: docs\n adr: docs/decisions\n",
|
|
162
|
+
encoding="utf-8",
|
|
163
|
+
)
|
|
164
|
+
d = tmp_path / "docs" / "decisions"
|
|
165
|
+
d.mkdir(parents=True)
|
|
166
|
+
(d / "0001-ffmpeg-audio.md").write_text(VALID_ADR_SAMPLE, encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
res = MOD.validate_adr(tmp_path)
|
|
169
|
+
assert res.status == "PASS", res.checks
|
|
170
|
+
assert not (tmp_path / "product").exists()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_validate_adr_invalid_layout_fails_loud(tmp_path: Path):
|
|
174
|
+
"""RFC-20261026-01:非法布局配置 ⇒ 单行 FAIL,不回落到 product/adr。"""
|
|
175
|
+
(tmp_path / "atlas.config.yaml").write_text(
|
|
176
|
+
"version: 1\nlayout:\n assets_root: ../escape\n", encoding="utf-8")
|
|
177
|
+
res = MOD.validate_adr(tmp_path)
|
|
178
|
+
assert res.status == "FAIL"
|
|
179
|
+
assert res.checks[0]["check"] == "项目布局配置"
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""`validators/validate_walkthrough.py` 回归测试(M1,`RFC-20261022-01` 批 1)。
|
|
3
|
+
|
|
4
|
+
对账平面 = **声明 ⇔ 凭据**:apply 的三问第 2 问答「是」⇒ 受影响域级 PRD 的走查节
|
|
5
|
+
必须有 `> 已确认:<日期>` 行。四态出口逐条钉死(评审 P1-1 / P1-2):
|
|
6
|
+
|
|
7
|
+
正控:确认行齐 ⇒ PASS;
|
|
8
|
+
坏输入:是 + 无确认行 ⇒ FAIL / 确认行形态非法 ⇒ FAIL / 缺走查节 ⇒ FAIL /
|
|
9
|
+
域级 PRD 不存在 ⇒ FAIL / 重派生只留说明行 ⇒ FAIL;
|
|
10
|
+
解析面:第 2 问不可解析 ⇒ WARN(不是 SKIP——防写歪措辞静默旁路)/ 无第 2 问行 ⇒ WARN /
|
|
11
|
+
是但受影响域解析不到 ⇒ WARN;
|
|
12
|
+
时空域:已归档任务不扫 / 无 apply 记录 ⇒ SKIP / 第 2 问否 ⇒ SKIP / task 记录损坏 ⇒ SKIP。
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import importlib.util
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
PKG_ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
V_WALK = PKG_ROOT / "validators" / "validate_walkthrough.py"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load():
|
|
25
|
+
spec = importlib.util.spec_from_file_location("validate_walkthrough_under_test", V_WALK)
|
|
26
|
+
mod = importlib.util.module_from_spec(spec)
|
|
27
|
+
spec.loader.exec_module(mod)
|
|
28
|
+
return mod
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
MOD = _load()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _task(root: Path, tid: str = "T1", status: str = "planning") -> Path:
|
|
35
|
+
d = root / ".trellis" / "tasks" / tid
|
|
36
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
(d / "task.json").write_text(
|
|
38
|
+
json.dumps({"id": tid, "title": "夹具", "status": status}, ensure_ascii=False),
|
|
39
|
+
encoding="utf-8")
|
|
40
|
+
return d
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _apply(d: Path, text: str) -> Path:
|
|
44
|
+
p = d / "atlas-apply.md"
|
|
45
|
+
p.write_text(text, encoding="utf-8")
|
|
46
|
+
return p
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
APPLY_YES = """# T1 · 交付报告
|
|
50
|
+
|
|
51
|
+
## 2. 三问门判定
|
|
52
|
+
|
|
53
|
+
1. **这次是否改动既有能力**?是 → 有 PRD 基线
|
|
54
|
+
2. **这次是否需要人来确认应然 UI / 流程**?**是** ⇒ 派生走查并取得确认
|
|
55
|
+
3. **这次是否需要可重复的验收**?是
|
|
56
|
+
|
|
57
|
+
## 3. 影响面清单
|
|
58
|
+
|
|
59
|
+
- `product/prd/demo-prd.md`:新增旅程 1 条
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
APPLY_UNCLEAR = APPLY_YES.replace(
|
|
63
|
+
"2. **这次是否需要人来确认应然 UI / 流程**?**是** ⇒ 派生走查并取得确认",
|
|
64
|
+
"2. **这次是否需要人来确认应然 UI / 流程**?看情况而定")
|
|
65
|
+
APPLY_NO = APPLY_YES.replace(
|
|
66
|
+
"2. **这次是否需要人来确认应然 UI / 流程**?**是** ⇒ 派生走查并取得确认",
|
|
67
|
+
"2. **这次是否需要人来确认应然 UI / 流程**?否(纯后端)")
|
|
68
|
+
APPLY_NO_Q2 = """## 2. 三问门判定
|
|
69
|
+
|
|
70
|
+
1. 是否改动既有能力?是
|
|
71
|
+
|
|
72
|
+
## 3. 影响面清单
|
|
73
|
+
- `product/prd/demo-prd.md`
|
|
74
|
+
"""
|
|
75
|
+
APPLY_NO_SECTION = """# 报告
|
|
76
|
+
|
|
77
|
+
## 1. 模式与需求 ID
|
|
78
|
+
|
|
79
|
+
apply T1
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _prd(root: Path, domain: str = "demo", confirm: str | None = "2026-10-23",
|
|
84
|
+
with_section: bool = True, template_note: bool = False) -> Path:
|
|
85
|
+
d = root / "product" / "prd"
|
|
86
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
lines = [f"# {domain} 域 PRD", "", "## 七、变更记录", "", "- 2026-10-23 初稿", ""]
|
|
88
|
+
if with_section:
|
|
89
|
+
lines += ["## 八、用户旅程走查", ""]
|
|
90
|
+
if confirm is not None:
|
|
91
|
+
lines += [f"> 已确认:{confirm}", ""]
|
|
92
|
+
if template_note:
|
|
93
|
+
lines += ["> [初稿无「已确认」行 = 未确认;重派生本节时必须删除旧的「已确认」行待重确认]", ""]
|
|
94
|
+
lines += ["### 旅程 1:查看列表(FN-01)", "", "1. 入口:`/demo` ⇒ 列表可见"]
|
|
95
|
+
p = d / f"{domain}-prd.md"
|
|
96
|
+
p.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
97
|
+
return p
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _checks(res) -> list[dict]:
|
|
101
|
+
return res.checks
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------- 正控
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_confirmed_walkthrough_passes(tmp_path) -> None:
|
|
108
|
+
d = _task(tmp_path)
|
|
109
|
+
_apply(d, APPLY_YES)
|
|
110
|
+
_prd(tmp_path)
|
|
111
|
+
res = MOD.run(tmp_path)
|
|
112
|
+
assert res.status == "PASS", _checks(res)
|
|
113
|
+
assert any(c["status"] == "PASS" and "demo" in c["check"] for c in res.checks)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_main_exit_zero_and_json(tmp_path, capsys) -> None:
|
|
117
|
+
d = _task(tmp_path)
|
|
118
|
+
_apply(d, APPLY_YES)
|
|
119
|
+
_prd(tmp_path)
|
|
120
|
+
rc = MOD.main(["--root", str(tmp_path), "--json"])
|
|
121
|
+
assert rc == 0
|
|
122
|
+
payload = json.loads(capsys.readouterr().out)
|
|
123
|
+
assert payload["ok"] is True and payload["status"] == "PASS"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_relative_root_does_not_crash(tmp_path, monkeypatch) -> None:
|
|
127
|
+
"""issue #45:atlas_check 以 `--root .` 调用;入口必须先归一为绝对根再 relative_to。"""
|
|
128
|
+
d = _task(tmp_path)
|
|
129
|
+
_apply(d, APPLY_YES)
|
|
130
|
+
_prd(tmp_path)
|
|
131
|
+
monkeypatch.chdir(tmp_path)
|
|
132
|
+
res = MOD.run(Path("."))
|
|
133
|
+
assert res.status == "PASS", _checks(res)
|
|
134
|
+
assert MOD.main(["--root", ".", "--json"]) == 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------- 坏输入(必红)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_yes_without_confirmation_line_fails(tmp_path) -> None:
|
|
141
|
+
d = _task(tmp_path)
|
|
142
|
+
_apply(d, APPLY_YES)
|
|
143
|
+
_prd(tmp_path, confirm=None)
|
|
144
|
+
res = MOD.run(tmp_path)
|
|
145
|
+
assert res.status == "FAIL", _checks(res)
|
|
146
|
+
bad = [c for c in res.checks if c["status"] == "FAIL"]
|
|
147
|
+
assert bad and "demo" in bad[0]["check"] and "demo-prd.md" in bad[0]["detail"]
|
|
148
|
+
assert MOD.main(["--root", str(tmp_path)]) == 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_rederived_section_with_only_template_note_fails(tmp_path) -> None:
|
|
152
|
+
"""重派生删确认行后只剩模板说明行 —— 说明行不得被当成确认凭据。"""
|
|
153
|
+
d = _task(tmp_path)
|
|
154
|
+
_apply(d, APPLY_YES)
|
|
155
|
+
_prd(tmp_path, confirm=None, template_note=True)
|
|
156
|
+
res = MOD.run(tmp_path)
|
|
157
|
+
assert res.status == "FAIL", _checks(res)
|
|
158
|
+
assert any("未确认" in c["detail"] or "已确认" in c["detail"]
|
|
159
|
+
for c in res.checks if c["status"] == "FAIL")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_malformed_confirmation_line_fails(tmp_path) -> None:
|
|
163
|
+
d = _task(tmp_path)
|
|
164
|
+
_apply(d, APPLY_YES)
|
|
165
|
+
_prd(tmp_path, confirm="待用户拍板")
|
|
166
|
+
res = MOD.run(tmp_path)
|
|
167
|
+
assert res.status == "FAIL", _checks(res)
|
|
168
|
+
assert any("形态非法" in c["detail"] for c in res.checks if c["status"] == "FAIL")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_missing_walkthrough_section_fails(tmp_path) -> None:
|
|
172
|
+
d = _task(tmp_path)
|
|
173
|
+
_apply(d, APPLY_YES)
|
|
174
|
+
_prd(tmp_path, with_section=False)
|
|
175
|
+
res = MOD.run(tmp_path)
|
|
176
|
+
assert res.status == "FAIL", _checks(res)
|
|
177
|
+
assert any("缺「用户旅程走查」节" in c["detail"]
|
|
178
|
+
for c in res.checks if c["status"] == "FAIL")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_missing_domain_prd_fails(tmp_path) -> None:
|
|
182
|
+
d = _task(tmp_path)
|
|
183
|
+
_apply(d, APPLY_YES)
|
|
184
|
+
# 不建 demo-prd.md
|
|
185
|
+
res = MOD.run(tmp_path)
|
|
186
|
+
assert res.status == "FAIL", _checks(res)
|
|
187
|
+
assert any("不存在" in c["detail"] for c in res.checks if c["status"] == "FAIL")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------- 解析面(WARN 不静默)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_unparseable_second_question_warns_not_skips(tmp_path) -> None:
|
|
194
|
+
d = _task(tmp_path)
|
|
195
|
+
_apply(d, APPLY_UNCLEAR)
|
|
196
|
+
_prd(tmp_path, confirm=None)
|
|
197
|
+
res = MOD.run(tmp_path)
|
|
198
|
+
assert res.status == "WARN", _checks(res)
|
|
199
|
+
assert any("不可解析" in c["detail"] for c in res.checks if c["status"] == "WARN")
|
|
200
|
+
assert MOD.main(["--root", str(tmp_path)]) == 0 # WARN 不置红
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_missing_second_question_line_warns(tmp_path) -> None:
|
|
204
|
+
d = _task(tmp_path)
|
|
205
|
+
_apply(d, APPLY_NO_Q2)
|
|
206
|
+
_prd(tmp_path, confirm=None)
|
|
207
|
+
res = MOD.run(tmp_path)
|
|
208
|
+
assert res.status == "WARN", _checks(res)
|
|
209
|
+
assert any("无第 2 问行" in c["detail"] for c in res.checks if c["status"] == "WARN")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_yes_without_domain_reference_warns(tmp_path) -> None:
|
|
213
|
+
d = _task(tmp_path)
|
|
214
|
+
_apply(d, APPLY_YES.replace("`product/prd/demo-prd.md`:新增旅程 1 条", "(影响面见会话)"))
|
|
215
|
+
_prd(tmp_path, confirm=None)
|
|
216
|
+
res = MOD.run(tmp_path)
|
|
217
|
+
assert res.status == "WARN", _checks(res)
|
|
218
|
+
assert any("受影响域不可解析" in c["detail"] for c in res.checks if c["status"] == "WARN")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------- 时空域 / SKIP
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_archived_task_is_not_scanned(tmp_path) -> None:
|
|
225
|
+
d = _task(tmp_path, status="completed")
|
|
226
|
+
_apply(d, APPLY_YES)
|
|
227
|
+
_prd(tmp_path, confirm=None) # 确认行缺失,但在归档域外 ⇒ 不得红
|
|
228
|
+
res = MOD.run(tmp_path)
|
|
229
|
+
assert res.status == "SKIP", _checks(res)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_no_apply_record_skips(tmp_path) -> None:
|
|
233
|
+
_task(tmp_path)
|
|
234
|
+
res = MOD.run(tmp_path)
|
|
235
|
+
assert res.status == "SKIP", _checks(res)
|
|
236
|
+
assert any("无 atlas-apply.md" in c["detail"] for c in res.checks)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_second_question_no_skips(tmp_path) -> None:
|
|
240
|
+
d = _task(tmp_path)
|
|
241
|
+
_apply(d, APPLY_NO)
|
|
242
|
+
_prd(tmp_path, confirm=None)
|
|
243
|
+
res = MOD.run(tmp_path)
|
|
244
|
+
assert res.status == "SKIP", _checks(res)
|
|
245
|
+
assert any("第 2 问=否" in c["detail"] for c in res.checks)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_no_question_section_skips(tmp_path) -> None:
|
|
249
|
+
d = _task(tmp_path)
|
|
250
|
+
_apply(d, APPLY_NO_SECTION)
|
|
251
|
+
res = MOD.run(tmp_path)
|
|
252
|
+
assert res.status == "SKIP", _checks(res)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_broken_task_record_skips_loudly_and_other_task_still_checked(tmp_path) -> None:
|
|
256
|
+
broken = tmp_path / ".trellis" / "tasks" / "BAD"
|
|
257
|
+
broken.mkdir(parents=True)
|
|
258
|
+
(broken / "task.json").write_text("{not json", encoding="utf-8")
|
|
259
|
+
(broken / "atlas-apply.md").write_text(APPLY_YES, encoding="utf-8")
|
|
260
|
+
_prd(tmp_path, confirm=None)
|
|
261
|
+
|
|
262
|
+
d = _task(tmp_path, tid="GOOD")
|
|
263
|
+
_apply(d, APPLY_YES)
|
|
264
|
+
res = MOD.run(tmp_path)
|
|
265
|
+
skips = [c for c in res.checks if c["status"] == "SKIP" and "BAD" in c["check"]]
|
|
266
|
+
fails = [c for c in res.checks if c["status"] == "FAIL" and "GOOD" in c["check"]]
|
|
267
|
+
assert skips and "不可解析" in skips[0]["detail"], _checks(res)
|
|
268
|
+
assert fails, _checks(res) # 坏记录不吞掉健康任务的红色
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_no_tasks_dir_skips(tmp_path) -> None:
|
|
272
|
+
res = MOD.run(tmp_path)
|
|
273
|
+
assert res.status == "SKIP", _checks(res)
|
|
274
|
+
assert "未就绪" in res.checks[0]["detail"]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_unknown_status_is_visible_skip(tmp_path) -> None:
|
|
278
|
+
"""评审 P1-1:未知 / 变体 status 不得静默出域(手滑 `in-progress` 会整体绕过锁)。"""
|
|
279
|
+
d = _task(tmp_path, status="in-progress")
|
|
280
|
+
_apply(d, APPLY_YES)
|
|
281
|
+
_prd(tmp_path, confirm=None)
|
|
282
|
+
res = MOD.run(tmp_path)
|
|
283
|
+
assert res.status == "SKIP", _checks(res)
|
|
284
|
+
named = [c for c in res.checks if "in-progress" in c["detail"]]
|
|
285
|
+
assert named and named[0]["status"] == "SKIP", _checks(res)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_conflicting_second_question_lines_warn_not_bypass(tmp_path) -> None:
|
|
289
|
+
"""评审 P1-2:小节里模板行「否」在前、真实判定「是」在后 ⇒ WARN,不得按首行 SKIP。"""
|
|
290
|
+
text = APPLY_YES.replace(
|
|
291
|
+
"## 2. 三问门判定\n\n",
|
|
292
|
+
"## 2. 三问门判定\n\n"
|
|
293
|
+
"2. **这次是否需要人来确认应然 UI / 流程**?否(引用的模板行)\n")
|
|
294
|
+
d = _task(tmp_path)
|
|
295
|
+
_apply(d, text)
|
|
296
|
+
_prd(tmp_path, confirm=None)
|
|
297
|
+
res = MOD.run(tmp_path)
|
|
298
|
+
assert res.status == "WARN", _checks(res)
|
|
299
|
+
assert any("不一致" in c["detail"] for c in res.checks if c["status"] == "WARN")
|
|
300
|
+
assert not any(c["status"] == "SKIP" and "第 2 问=否" in c["detail"] for c in res.checks)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_consistent_duplicate_second_question_lines_resolve(tmp_path) -> None:
|
|
304
|
+
"""多行一致(都答「是」)⇒ 正常取值并进入对账,不误判 WARN。"""
|
|
305
|
+
text = APPLY_YES.replace(
|
|
306
|
+
"## 2. 三问门判定\n\n",
|
|
307
|
+
"## 2. 三问门判定\n\n"
|
|
308
|
+
"2. **这次是否需要人来确认应然 UI / 流程**?是(引用模板)\n")
|
|
309
|
+
d = _task(tmp_path)
|
|
310
|
+
_apply(d, text)
|
|
311
|
+
_prd(tmp_path, confirm=None)
|
|
312
|
+
res = MOD.run(tmp_path)
|
|
313
|
+
assert res.status == "FAIL", _checks(res)
|