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
package/tests/test_validators.py
CHANGED
|
@@ -315,7 +315,13 @@ PRD_DEMO = """# demo 域 PRD
|
|
|
315
315
|
|
|
316
316
|
## 六、Assumptions & Defaults
|
|
317
317
|
|
|
318
|
+
无。
|
|
319
|
+
|
|
318
320
|
## 七、变更记录
|
|
321
|
+
|
|
322
|
+
| 版本 | 日期 | 变更内容及原因 | 影响 | 结论与后续 |
|
|
323
|
+
|---|---|---|---|---|
|
|
324
|
+
| 1.0 | 2026-09-25 | 初始 | 无(未触达验证面) | 无 |
|
|
319
325
|
"""
|
|
320
326
|
|
|
321
327
|
|
|
@@ -1350,6 +1356,36 @@ def test_e2e_index_coverage_dimensions_pass_and_warn() -> None:
|
|
|
1350
1356
|
assert "未知维度" in check_bad["detail"], check_bad
|
|
1351
1357
|
|
|
1352
1358
|
|
|
1359
|
+
def test_e2e_dimension_check_uses_shared_parser(monkeypatch) -> None:
|
|
1360
|
+
"""D153:E2E 门必须实际调用共享 parse_dims_cell(防保留别名、实际走本地副本)。"""
|
|
1361
|
+
INDEX_WITH_PAGE = INDEX_GOOD + "\n## 页面\n\n| 页面 | 路由 | 所属域 | 状态 |\n|---|---|---|---|\n| home | / | home | 已实现 |\n"
|
|
1362
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1363
|
+
root = Path(td)
|
|
1364
|
+
mk_prototype(root)
|
|
1365
|
+
mk_index(root, INDEX_WITH_PAGE)
|
|
1366
|
+
prd_dir = root / "product" / "prd"
|
|
1367
|
+
prd_dir.mkdir(parents=True, exist_ok=True)
|
|
1368
|
+
(prd_dir / "home-prd.md").write_text(
|
|
1369
|
+
"---\ndomain: home\n---\n\n"
|
|
1370
|
+
"## 五、验收标准\n\n"
|
|
1371
|
+
"| AC ID | 场景 | 回指规则 | 适用覆盖维度 |\n"
|
|
1372
|
+
"|---|---|---|---|\n"
|
|
1373
|
+
"| `AC-HOME-001` | 首页展示正常 | HOME-R-001 | C01 |\n",
|
|
1374
|
+
encoding="utf-8",
|
|
1375
|
+
)
|
|
1376
|
+
case_with_c01 = CASE_GOOD.strip()[:-3] + "覆盖维度:\n - C01\n```\n"
|
|
1377
|
+
mk_cases(root, case_with_c01)
|
|
1378
|
+
|
|
1379
|
+
base = ve.validate(root)
|
|
1380
|
+
base_check = next(c for c in base.checks if c["check"] == "AC × 覆盖维度")
|
|
1381
|
+
assert base_check["level"] == "PASS", base_check
|
|
1382
|
+
|
|
1383
|
+
monkeypatch.setattr(ve, "parse_dims_cell", lambda raw: (set(), False))
|
|
1384
|
+
res = ve.validate(root)
|
|
1385
|
+
check = next(c for c in res.checks if c["check"] == "AC × 覆盖维度")
|
|
1386
|
+
assert check["level"] == "N/A", check
|
|
1387
|
+
|
|
1388
|
+
|
|
1353
1389
|
def test_e2e_index_verb_fail() -> None:
|
|
1354
1390
|
"""新门(契约 §5.5,2026-09-23):step/expected 首 token 不在动词词表内 → FAIL。"""
|
|
1355
1391
|
with tempfile.TemporaryDirectory() as td:
|
|
@@ -1440,6 +1476,97 @@ def test_e2e_index_new_assert_verbs_render_and_validate() -> None:
|
|
|
1440
1476
|
), [(c["check"], c["level"]) for c in res.checks]
|
|
1441
1477
|
|
|
1442
1478
|
|
|
1479
|
+
def test_e2e_index_request_assertion_shape() -> None:
|
|
1480
|
+
"""D142:`request <api> <json-path> <expected>` 的形态门;api 不得被 testid 提取器误算。
|
|
1481
|
+
|
|
1482
|
+
正向:合法 request 断言 PASS;反向:缺 path/expected、api 无点、`(空)`、与
|
|
1483
|
+
initial/unchanged 叠写均 FAIL。两侧同测(校验器放行 + 生成器可渲染)。
|
|
1484
|
+
"""
|
|
1485
|
+
good = CASE_GOOD.replace(
|
|
1486
|
+
"- text home-title 首页 # 页面标题文案为「首页」",
|
|
1487
|
+
"- request app.updateApp Data.reqDTO.config.nodeParam.mailStatus (缺席) # 字段缺席")
|
|
1488
|
+
|
|
1489
|
+
def level(case_text: str) -> tuple[str, list[dict]]:
|
|
1490
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1491
|
+
root = Path(td)
|
|
1492
|
+
mk_prototype(root)
|
|
1493
|
+
mk_cases(root, case_text)
|
|
1494
|
+
mk_index(root)
|
|
1495
|
+
res = ve.validate(root)
|
|
1496
|
+
return res.status, res.checks
|
|
1497
|
+
|
|
1498
|
+
status, checks = level(good)
|
|
1499
|
+
assert not any(c["check"] == "request 断言形态" and c["level"] == "FAIL" for c in checks), checks
|
|
1500
|
+
assert any(c["check"] == "request 断言形态" and c["level"] == "PASS" for c in checks), checks
|
|
1501
|
+
assert gn.ts_case(
|
|
1502
|
+
{"id": "E2E-HOME-001", "_title": "t", "step": ["click home-cta-btn"],
|
|
1503
|
+
"expected": ["request app.updateApp Data.a (缺席)"], "testid": ["home-cta-btn"]},
|
|
1504
|
+
"/",
|
|
1505
|
+
).count("await atlasExpectRequest(") == 1
|
|
1506
|
+
|
|
1507
|
+
bad_cases = [
|
|
1508
|
+
("request app.updateApp", "形态"),
|
|
1509
|
+
("request app Data.a (缺席)", "api 须为带点后缀"),
|
|
1510
|
+
("request app.updateApp Data.a (空)", "(空) 三态不明"),
|
|
1511
|
+
("initial: request app.updateApp Data.a null", "叠写"),
|
|
1512
|
+
]
|
|
1513
|
+
for bad, needle in bad_cases:
|
|
1514
|
+
status, checks = level(CASE_GOOD.replace(
|
|
1515
|
+
"- text home-title 首页 # 页面标题文案为「首页」", f"- {bad} # bad"))
|
|
1516
|
+
row = next(c for c in checks if c["check"] == "request 断言形态")
|
|
1517
|
+
assert row["level"] == "FAIL", (bad, row)
|
|
1518
|
+
assert needle in row["detail"], (bad, row)
|
|
1519
|
+
|
|
1520
|
+
# testid 提取:api 不是 testid(不得报「原型无 app.updateApp」)
|
|
1521
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1522
|
+
root = Path(td)
|
|
1523
|
+
mk_prototype(root)
|
|
1524
|
+
mk_cases(root, good)
|
|
1525
|
+
mk_index(root)
|
|
1526
|
+
tres = vt.validate(root)
|
|
1527
|
+
assert not any("app.updateApp" in (c.get("detail") or "") for c in tres.checks), tres.checks
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
def test_e2e_index_manual_block_shape() -> None:
|
|
1531
|
+
"""D143:manual.md 的 `atlas-manual` 块必须 ID 唯一、页面在页面表、步骤含操作/观察;
|
|
1532
|
+
旧单行格式继续可用。坏块/坏页面/重复 ID ⇒ FAIL,解析失败也 FAIL。"""
|
|
1533
|
+
valid = """## MANUAL-HOME-001 结构化走查
|
|
1534
|
+
|
|
1535
|
+
```atlas-manual
|
|
1536
|
+
page: home
|
|
1537
|
+
step:
|
|
1538
|
+
- 打开首页 | 首页出现 |
|
|
1539
|
+
- 保存 | payload 不含字段 | const p = 1
|
|
1540
|
+
```
|
|
1541
|
+
"""
|
|
1542
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1543
|
+
root = Path(td)
|
|
1544
|
+
mk_prototype(root)
|
|
1545
|
+
mk_cases(root, CASE_GOOD)
|
|
1546
|
+
mk_index(root)
|
|
1547
|
+
manual = root / "product" / "e2e" / "manual.md"
|
|
1548
|
+
manual.write_text(valid, encoding="utf-8")
|
|
1549
|
+
res = ve.validate(root)
|
|
1550
|
+
row = next(c for c in res.checks if c["check"] == "manual 走查项形态")
|
|
1551
|
+
assert row["level"] == "PASS", row
|
|
1552
|
+
|
|
1553
|
+
def level(text: str) -> dict:
|
|
1554
|
+
with tempfile.TemporaryDirectory() as td:
|
|
1555
|
+
root = Path(td)
|
|
1556
|
+
mk_prototype(root)
|
|
1557
|
+
mk_cases(root, CASE_GOOD)
|
|
1558
|
+
mk_index(root)
|
|
1559
|
+
(root / "product" / "e2e" / "manual.md").write_text(text, encoding="utf-8")
|
|
1560
|
+
res = ve.validate(root)
|
|
1561
|
+
return next(c for c in res.checks if c["check"] == "manual 走查项形态")
|
|
1562
|
+
|
|
1563
|
+
assert level(valid.replace("page: home", "page: nope"))["level"] == "FAIL"
|
|
1564
|
+
assert level(valid.replace(" - 打开首页 | 首页出现 |", " - 打开首页 | |"))["level"] == "FAIL"
|
|
1565
|
+
dup = valid + "\n" + valid.replace("MANUAL-HOME-001", "MANUAL-HOME-001")
|
|
1566
|
+
assert level(dup)["level"] == "FAIL"
|
|
1567
|
+
assert level(valid.replace("page: home", "id: MANUAL-HOME-001\npage: home"))["detail"].startswith("解析失败")
|
|
1568
|
+
|
|
1569
|
+
|
|
1443
1570
|
def test_e2e_index_title_must_match_shard() -> None:
|
|
1444
1571
|
"""契约 §8.2(2026-09-25 补,`3509 §B31`):索引 `中文标题` == 分片
|
|
1445
1572
|
`## <ID> <标题>` 的**标题段**(空白归一后全等)。
|
|
@@ -1487,6 +1614,25 @@ def test_e2e_index_download_is_action_not_assertion() -> None:
|
|
|
1487
1614
|
assert bad["expected 动词合法"] == "FAIL", bad
|
|
1488
1615
|
|
|
1489
1616
|
|
|
1617
|
+
def test_e2e_index_type_is_action_not_assertion() -> None:
|
|
1618
|
+
"""契约 §5.5(D134,2026-10-23):`type` 属**动作**(写在 `step` 侧)。
|
|
1619
|
+
|
|
1620
|
+
两侧同测:写在 `step` ⇒ 校验器放行(`step 动词合法` PASS);写在 `expected` ⇒ 被拒
|
|
1621
|
+
(断言词表里没有它)——反向证据:它确实落在动作侧,不是两边都收。
|
|
1622
|
+
"""
|
|
1623
|
+
ok = _levels(CASE_GOOD.replace(
|
|
1624
|
+
"- click home-cta-btn # 点击主操作按钮",
|
|
1625
|
+
"- type home-title 首页 # 逐字键入标题",
|
|
1626
|
+
))
|
|
1627
|
+
assert ok["step 动词合法"] == "PASS", ok
|
|
1628
|
+
|
|
1629
|
+
bad = _levels(CASE_GOOD.replace(
|
|
1630
|
+
"- text home-title 首页 # 页面标题文案为「首页」",
|
|
1631
|
+
"- type home-title 首页",
|
|
1632
|
+
))
|
|
1633
|
+
assert bad["expected 动词合法"] == "FAIL", bad
|
|
1634
|
+
|
|
1635
|
+
|
|
1490
1636
|
def test_e2e_index_upload_is_action_and_fixture_reference_closed() -> None:
|
|
1491
1637
|
"""契约 §5.5(B201-1,2026-10-12):`upload` 属**动作**(写在 `step` 侧),
|
|
1492
1638
|
值 = **相对 `product/e2e/fixtures/` 的文件路径**(与 §5.8 mock 同一 fixtures 树)。
|
|
@@ -1995,6 +2141,88 @@ def test_stack_profile_origin_invalid_fails() -> None:
|
|
|
1995
2141
|
assert _sp_check(res, "origin 枚举")["level"] == "FAIL"
|
|
1996
2142
|
|
|
1997
2143
|
|
|
2144
|
+
def test_stack_profile_backend_test_extractor_is_generic() -> None:
|
|
2145
|
+
"""D163:extractor 是注册表名,不在 profile 层枚举具体框架。"""
|
|
2146
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2147
|
+
root = Path(td)
|
|
2148
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2149
|
+
"backend_test:\n extractor: vitest\n test_dir: web\n mode: advisory\n"
|
|
2150
|
+
)
|
|
2151
|
+
mk_profile(root, text)
|
|
2152
|
+
res = vsp.validate(root)
|
|
2153
|
+
assert _sp_check(res, "backend_test.extractor")["level"] == "PASS"
|
|
2154
|
+
assert res.status != "FAIL", [(c["check"], c["level"]) for c in res.checks]
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
def test_stack_profile_backend_test_extractor_alias_conflict_fails() -> None:
|
|
2158
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2159
|
+
root = Path(td)
|
|
2160
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2161
|
+
"backend_test:\n framework: pytest\n extractor: vitest\n test_dir: web\n"
|
|
2162
|
+
)
|
|
2163
|
+
mk_profile(root, text)
|
|
2164
|
+
res = vsp.validate(root)
|
|
2165
|
+
assert _sp_check(res, "backend_test.extractor 别名")["level"] == "FAIL"
|
|
2166
|
+
assert res.status == "FAIL"
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
def test_stack_profile_backend_test_extractor_shape_fails() -> None:
|
|
2170
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2171
|
+
root = Path(td)
|
|
2172
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2173
|
+
"backend_test:\n extractor: 'JUnit 5'\n test_dir: web\n"
|
|
2174
|
+
)
|
|
2175
|
+
mk_profile(root, text)
|
|
2176
|
+
res = vsp.validate(root)
|
|
2177
|
+
assert _sp_check(res, "backend_test.extractor 形态")["level"] == "FAIL"
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
def test_stack_profile_backend_test_legacy_framework_alias_passes() -> None:
|
|
2181
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2182
|
+
root = Path(td)
|
|
2183
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2184
|
+
"backend_test:\n framework: pytest\n test_dir: web\n"
|
|
2185
|
+
)
|
|
2186
|
+
mk_profile(root, text)
|
|
2187
|
+
res = vsp.validate(root)
|
|
2188
|
+
assert _sp_check(res, "backend_test.extractor")["level"] == "PASS"
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
def test_stack_profile_backend_test_external_command_passes() -> None:
|
|
2192
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2193
|
+
root = Path(td)
|
|
2194
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2195
|
+
"backend_test:\n extractor_command: java -jar tools/ac.jar\n test_dir: web\n"
|
|
2196
|
+
)
|
|
2197
|
+
mk_profile(root, text)
|
|
2198
|
+
res = vsp.validate(root)
|
|
2199
|
+
assert _sp_check(res, "backend_test.extractor_command")["level"] == "PASS"
|
|
2200
|
+
|
|
2201
|
+
|
|
2202
|
+
def test_stack_profile_backend_test_external_command_mutual_exclusion_fails() -> None:
|
|
2203
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2204
|
+
root = Path(td)
|
|
2205
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2206
|
+
"backend_test:\n extractor: pytest\n"
|
|
2207
|
+
" extractor_command: java -jar tools/ac.jar\n test_dir: web\n"
|
|
2208
|
+
)
|
|
2209
|
+
mk_profile(root, text)
|
|
2210
|
+
res = vsp.validate(root)
|
|
2211
|
+
assert _sp_check(res, "backend_test.extractor_command 互斥")["level"] == "FAIL"
|
|
2212
|
+
assert res.status == "FAIL"
|
|
2213
|
+
|
|
2214
|
+
|
|
2215
|
+
def test_stack_profile_backend_test_external_command_empty_fails() -> None:
|
|
2216
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2217
|
+
root = Path(td)
|
|
2218
|
+
text = PROFILE_BASE.format(origin="greenfield") + (
|
|
2219
|
+
"backend_test:\n extractor_command: ''\n test_dir: web\n"
|
|
2220
|
+
)
|
|
2221
|
+
mk_profile(root, text)
|
|
2222
|
+
res = vsp.validate(root)
|
|
2223
|
+
assert _sp_check(res, "backend_test.extractor_command 形态")["level"] == "FAIL"
|
|
2224
|
+
|
|
2225
|
+
|
|
1998
2226
|
def test_stack_profile_top_level_scalar_after_section_is_parsed() -> None:
|
|
1999
2227
|
"""段头之后的顶层标量必须被解析。
|
|
2000
2228
|
|
|
@@ -2467,6 +2695,89 @@ def test_prd_change_form_fields_and_revision_log_body() -> None:
|
|
|
2467
2695
|
assert c["status"] == "FAIL" and "正文为空" in c["detail"], c
|
|
2468
2696
|
|
|
2469
2697
|
|
|
2698
|
+
def test_rfc20261023_02_change_record_shape_and_impact_column() -> None:
|
|
2699
|
+
"""RFC-20261023-02 ③:§七 表头三态 + 「影响」单元格空 ⇒ FAIL(独立 check 行)。
|
|
2700
|
+
|
|
2701
|
+
变异:**M-new-3** 删 legacy 分支(3 列直接 FAIL)⇒ 第二侧红;摘「影响」空判据
|
|
2702
|
+
⇒ 第三侧红;表头不可辨分支摘掉 ⇒ 第四侧红。
|
|
2703
|
+
"""
|
|
2704
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2705
|
+
root = Path(td)
|
|
2706
|
+
dom = FM_DOMAIN + PRD_DEMO.format(ac="AC-HOME-001", rows="| `AC-HOME-001` | 纯渲染 |")
|
|
2707
|
+
plain = FM_GLOBAL + PRD_GLOBAL
|
|
2708
|
+
|
|
2709
|
+
def row() -> dict:
|
|
2710
|
+
return {c["check"]: c for c in vpr.validate(root).checks}["变更记录形态"]
|
|
2711
|
+
|
|
2712
|
+
mk_prd_raw(root, plain, dom)
|
|
2713
|
+
assert row()["status"] == "PASS", row()
|
|
2714
|
+
|
|
2715
|
+
legacy = dom.replace(
|
|
2716
|
+
"| 版本 | 日期 | 变更内容及原因 | 影响 | 结论与后续 |\n|---|---|---|---|---|\n"
|
|
2717
|
+
"| 1.0 | 2026-09-25 | 初始 | 无(未触达验证面) | 无 |",
|
|
2718
|
+
"| 版本 | 日期 | 变更说明 |\n|---|---|---|\n| 1.0 | 2026-09-25 | 初始 |")
|
|
2719
|
+
mk_prd_raw(root, plain, legacy)
|
|
2720
|
+
assert row()["status"] == "WARN" and "legacy" in row()["detail"], row()
|
|
2721
|
+
|
|
2722
|
+
blank = dom.replace("| 无(未触达验证面) | 无 |", "| | 无 |")
|
|
2723
|
+
mk_prd_raw(root, plain, blank)
|
|
2724
|
+
assert row()["status"] == "FAIL" and "影响" in row()["detail"], row()
|
|
2725
|
+
|
|
2726
|
+
weird = dom.replace("| 变更内容及原因 | 影响 | 结论与后续 |", "| 甲 | 乙 | 丙 |")
|
|
2727
|
+
mk_prd_raw(root, plain, weird)
|
|
2728
|
+
assert row()["status"] == "FAIL", row()
|
|
2729
|
+
|
|
2730
|
+
|
|
2731
|
+
def test_rfc20261023_02_optional_section_explicit_none() -> None:
|
|
2732
|
+
"""RFC-20261023-02 ④:§六 空表/空节且无显式结论 ⇒ WARN 首版;`无` ⇒ PASS;
|
|
2733
|
+
开户最小集(§六 整节不出现)⇒ 本检查不参与(PASS 行)。
|
|
2734
|
+
|
|
2735
|
+
变异:**M-new-2** 空表判据恒真(恒 PASS)⇒ 第一侧红。
|
|
2736
|
+
"""
|
|
2737
|
+
with tempfile.TemporaryDirectory() as td:
|
|
2738
|
+
root = Path(td)
|
|
2739
|
+
plain = FM_GLOBAL + PRD_GLOBAL
|
|
2740
|
+
dom = FM_DOMAIN + PRD_DEMO.format(ac="AC-HOME-001", rows="| `AC-HOME-001` | 纯渲染 |")
|
|
2741
|
+
|
|
2742
|
+
def row() -> dict:
|
|
2743
|
+
return {c["check"]: c for c in vpr.validate(root).checks}["可无项显式(§六)"]
|
|
2744
|
+
|
|
2745
|
+
empty = dom.replace("## 六、Assumptions & Defaults\n\n无。",
|
|
2746
|
+
"## 六、Assumptions & Defaults\n\n")
|
|
2747
|
+
mk_prd_raw(root, plain, empty)
|
|
2748
|
+
assert row()["status"] == "WARN" and "显式结论" in row()["detail"], row()
|
|
2749
|
+
|
|
2750
|
+
empty_tbl = dom.replace("## 六、Assumptions & Defaults\n\n无。",
|
|
2751
|
+
"## 六、Assumptions & Defaults\n\n"
|
|
2752
|
+
"| 假设内容 | 采用的默认值 | 影响面 | 何时需要澄清 |\n"
|
|
2753
|
+
"|---|---|---|---|\n")
|
|
2754
|
+
mk_prd_raw(root, plain, empty_tbl)
|
|
2755
|
+
assert row()["status"] == "WARN", row()
|
|
2756
|
+
|
|
2757
|
+
mk_prd_raw(root, plain, dom)
|
|
2758
|
+
assert row()["status"] == "PASS", row()
|
|
2759
|
+
|
|
2760
|
+
na = dom.replace("## 六、Assumptions & Defaults\n\n无。\n\n", "")
|
|
2761
|
+
mk_prd_raw(root, plain, na)
|
|
2762
|
+
assert row()["status"] == "PASS", row() # 节缺席 = 开户最小集,不参与
|
|
2763
|
+
|
|
2764
|
+
|
|
2765
|
+
def test_forbidden_vocabulary_single_source() -> None:
|
|
2766
|
+
"""RFC-20261023-02 ④ 词表守恒:两校验器 `FORBIDDEN` ⊆ `global-rules` §5 词表,
|
|
2767
|
+
且 `[必填` / `[条件必填` 三方齐在(改任一侧 ⇒ 红)。"""
|
|
2768
|
+
rules = (PKG_ROOT / "shared" / "global-rules.md").read_text(encoding="utf-8")
|
|
2769
|
+
m = re.search(r"## 5\. 占位符禁令.*?(?=\n## )", rules, re.S)
|
|
2770
|
+
assert m, "找不到 global-rules §5 占位符禁令"
|
|
2771
|
+
tokens = set(re.findall(r"`([^`\n]+)`", m.group(0)))
|
|
2772
|
+
vs = load("validate_structure", "validators/validate_structure.py")
|
|
2773
|
+
for mod, name in ((vpr, "validate_prd"), (vs, "validate_structure")):
|
|
2774
|
+
extra = set(mod.FORBIDDEN) - tokens
|
|
2775
|
+
assert not extra, f"{name} FORBIDDEN 超出 §5 单源:{sorted(extra)}"
|
|
2776
|
+
for tok in ("[必填", "[条件必填"):
|
|
2777
|
+
assert tok in mod.FORBIDDEN, (name, "缺", tok)
|
|
2778
|
+
assert tok in tokens, ("§5 缺", tok)
|
|
2779
|
+
|
|
2780
|
+
|
|
2470
2781
|
def test_testids_stub_missing_fails() -> None:
|
|
2471
2782
|
"""契约 §4.1(2026-09-26 补,`3509 §B73`)/F32(R7 升格为机器兜底):薄桩由生成器
|
|
2472
2783
|
产出;缺失 ⇒ 本器**自动再生 + PASS 一行知悉**(R6 机器兜底哲学——缺失的派生工件
|
|
@@ -2759,6 +3070,11 @@ def test_f22_imperative_testid_extraction_and_counts() -> None:
|
|
|
2759
3070
|
li.dataset.testid = "notes-item";
|
|
2760
3071
|
// setAttribute 字面量
|
|
2761
3072
|
btn.setAttribute("data-testid", "notes-submit");
|
|
3073
|
+
// setAttribute 多行 + Prettier 尾逗号
|
|
3074
|
+
img.setAttribute(
|
|
3075
|
+
"data-testid",
|
|
3076
|
+
"notes-avatar",
|
|
3077
|
+
);
|
|
2762
3078
|
// setAttribute 模板展开
|
|
2763
3079
|
el.setAttribute("data-testid", `${slug}-header`);
|
|
2764
3080
|
""",
|
|
@@ -2773,6 +3089,7 @@ def test_f22_imperative_testid_extraction_and_counts() -> None:
|
|
|
2773
3089
|
# 1. 抽取源覆盖:字面量与注入全部抽取
|
|
2774
3090
|
assert "notes-item" in counts
|
|
2775
3091
|
assert "notes-submit" in counts
|
|
3092
|
+
assert "notes-avatar" in counts
|
|
2776
3093
|
# 2. counts 为声明文件数:同文件出现多次 notes-item,文件数仍为 1
|
|
2777
3094
|
assert counts["notes-item"] == 1
|
|
2778
3095
|
assert counts["notes-submit"] == 1
|
|
@@ -3550,7 +3867,8 @@ def test_prd_unverifiable_list_ghost_fails() -> None:
|
|
|
3550
3867
|
|
|
3551
3868
|
def test_prd_illegal_nested_directories_fails() -> None:
|
|
3552
3869
|
"""契约 rings/prd/reference.md §2(平铺规范):
|
|
3553
|
-
product/prd/
|
|
3870
|
+
product/prd/ 除独立审查报告留痕 reviews/ 外严禁子目录(如 domains/)或散落非标准
|
|
3871
|
+
markdown 文件;reviews/ 是唯一保留子目录(issue #43)。
|
|
3554
3872
|
存在非法子目录或非标准文件时,参数0 目录规范门必须 FAIL(防静默穿透门控)。
|
|
3555
3873
|
"""
|
|
3556
3874
|
with tempfile.TemporaryDirectory() as td:
|
|
@@ -3565,6 +3883,15 @@ def test_prd_illegal_nested_directories_fails() -> None:
|
|
|
3565
3883
|
row0 = [c for c in res.checks if c["check"] == "参数0 目录规范"][0]
|
|
3566
3884
|
assert row0["status"] == "PASS"
|
|
3567
3885
|
|
|
3886
|
+
# reviews/ 是唯一保留子目录(独立审查报告留痕,issue #43):PASS
|
|
3887
|
+
reviews = root / "product" / "prd" / "reviews" / "2026-10-28-x"
|
|
3888
|
+
reviews.mkdir(parents=True)
|
|
3889
|
+
(reviews / "README.md").write_text("# review\n", encoding="utf-8")
|
|
3890
|
+
(reviews / "review.json").write_text("{}", encoding="utf-8")
|
|
3891
|
+
res_reviews = vpr.validate(root)
|
|
3892
|
+
row0_reviews = [c for c in res_reviews.checks if c["check"] == "参数0 目录规范"][0]
|
|
3893
|
+
assert row0_reviews["status"] == "PASS", row0_reviews
|
|
3894
|
+
|
|
3568
3895
|
# 变异注入:创建非法子目录 domains/ 并放置 markdown
|
|
3569
3896
|
nested_dir = root / "product" / "prd" / "domains"
|
|
3570
3897
|
nested_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -3607,6 +3934,41 @@ def test_validate_testids_strip_comments() -> None:
|
|
|
3607
3934
|
assert "notes-commented-html" not in counts
|
|
3608
3935
|
|
|
3609
3936
|
|
|
3937
|
+
def test_validate_testids_strip_comments_once_per_file() -> None:
|
|
3938
|
+
"""P5-A(D141):剥注释每文件只做一次;第二次循环复用第一次结果。
|
|
3939
|
+
|
|
3940
|
+
直接数 `strip_comments` 的调用次数:修复前每文件 2 次(第一次循环 + 第二次循环),
|
|
3941
|
+
修复后 1 次。输出仍要正确(静态属性 / dataset / setAttribute 都要抽到)。
|
|
3942
|
+
"""
|
|
3943
|
+
with tempfile.TemporaryDirectory() as td:
|
|
3944
|
+
root = Path(td)
|
|
3945
|
+
src = root / "src"
|
|
3946
|
+
src.mkdir(parents=True)
|
|
3947
|
+
(src / "a.js").write_text('el.dataset.testid = "p5a-a";', encoding="utf-8")
|
|
3948
|
+
(src / "b.js").write_text('<i data-testid="p5a-b"></i>', encoding="utf-8")
|
|
3949
|
+
(src / "c.js").write_text('el.setAttribute("data-testid", "p5a-c");', encoding="utf-8")
|
|
3950
|
+
(root / "product").mkdir(parents=True, exist_ok=True)
|
|
3951
|
+
(root / "product" / "stack-profile.yaml").write_text(
|
|
3952
|
+
"product: p5a-test\norigin: greenfield\napps:\n - name: web\n kind: frontend\n path: src\n",
|
|
3953
|
+
encoding="utf-8",
|
|
3954
|
+
)
|
|
3955
|
+
original = vt.strip_comments
|
|
3956
|
+
calls: list[str] = []
|
|
3957
|
+
|
|
3958
|
+
def counted(text: str) -> str:
|
|
3959
|
+
calls.append(text)
|
|
3960
|
+
return original(text)
|
|
3961
|
+
|
|
3962
|
+
vt.strip_comments = counted
|
|
3963
|
+
try:
|
|
3964
|
+
_per_page, counts, files, _expanded, _dups = vt.collect_testids_expanded(root, ["src"])
|
|
3965
|
+
finally:
|
|
3966
|
+
vt.strip_comments = original
|
|
3967
|
+
assert len(files) == 3 # 夹具自检:三个源文件
|
|
3968
|
+
assert len(calls) == len(files), f"每文件只应剥一次;实际 {len(calls)} 次(修复前为 6)"
|
|
3969
|
+
assert {"p5a-a", "p5a-b", "p5a-c"} <= set(counts), counts
|
|
3970
|
+
|
|
3971
|
+
|
|
3610
3972
|
def test_stack_profile_review_section_validation() -> None:
|
|
3611
3973
|
"""v0.1.0-beta.14: 校验 e2e.review 配置段(mode: gate | auto-baseline | off)。"""
|
|
3612
3974
|
with tempfile.TemporaryDirectory() as td:
|
|
@@ -3737,3 +4099,139 @@ testid:
|
|
|
3737
4099
|
lv = {c["check"]: c["level"] for c in res.checks}
|
|
3738
4100
|
assert lv["ID 格式"] == "PASS", lv["ID 格式"]
|
|
3739
4101
|
assert lv["索引↔分片标题一致"] == "PASS", lv["索引↔分片标题一致"]
|
|
4102
|
+
|
|
4103
|
+
|
|
4104
|
+
def test_testids_scan_skips_runtime_artifact_dirs() -> None:
|
|
4105
|
+
"""D150 合成靶场咬合批(2026-10-25):实然扫描**必须跳过运行期产物目录**
|
|
4106
|
+
(`.atlas-run/` / `test-results/` 等,SKIP_DIRS 新增段)。
|
|
4107
|
+
|
|
4108
|
+
现场:执行器时序下「失败批 ⇒ 立即重验」成为常态路径,而 Playwright 失败批会在
|
|
4109
|
+
`.atlas-run/<用例>/error-context.md` 留下含 testid 字面的快照文本——不排除则
|
|
4110
|
+
`validate_testids` 扫出「testid 全站唯一 —— 重复」**假红**(真源只声明一次)。
|
|
4111
|
+
产物是**回声**,与测试文件同族(issue #27/#33 同判据)。
|
|
4112
|
+
变异 **MB21**(摘 `.atlas-run` 排除 ⇒ 本钉红)。
|
|
4113
|
+
"""
|
|
4114
|
+
with tempfile.TemporaryDirectory() as td:
|
|
4115
|
+
root = Path(td)
|
|
4116
|
+
# 复现 playground 场景:apps[].path = "."(整仓扫描)——只有这时 .atlas-run/ 才落在扫描域内
|
|
4117
|
+
(root / "product").mkdir(parents=True, exist_ok=True)
|
|
4118
|
+
(root / "product/stack-profile.yaml").write_text(
|
|
4119
|
+
"product: demo\napps:\n - name: web\n path: .\n kind: frontend\n"
|
|
4120
|
+
" role: landing\n stack: demo\n", encoding="utf-8")
|
|
4121
|
+
(root / "page.html").write_text(
|
|
4122
|
+
'<button data-testid="home-title">ok</button>', encoding="utf-8")
|
|
4123
|
+
mk_stub(root)
|
|
4124
|
+
mk_cases(root)
|
|
4125
|
+
mk_index(root)
|
|
4126
|
+
art = root / ".atlas-run" / "some-case-chromium"
|
|
4127
|
+
art.mkdir(parents=True)
|
|
4128
|
+
(art / "error-context.md").write_text(
|
|
4129
|
+
'<h1 data-testid="home-title">快照回声</h1>', encoding="utf-8")
|
|
4130
|
+
tr = root / "test-results" / "x"
|
|
4131
|
+
tr.mkdir(parents=True)
|
|
4132
|
+
(tr / "trace.md").write_text('data-testid="home-title"', encoding="utf-8")
|
|
4133
|
+
import importlib.util as _iu
|
|
4134
|
+
spec_ = _iu.spec_from_file_location(
|
|
4135
|
+
"vtid_scan", PKG_ROOT / "validators" / "validate_testids.py")
|
|
4136
|
+
vtid_ = _iu.module_from_spec(spec_)
|
|
4137
|
+
spec_.loader.exec_module(vtid_)
|
|
4138
|
+
files = {str(f.relative_to(root)) for f in vtid_._scan_files([root])}
|
|
4139
|
+
assert not any(f.startswith((".atlas-run/", "test-results/")) for f in files), \
|
|
4140
|
+
sorted(f for f in files if f.startswith((".atlas-run/", "test-results/")))
|
|
4141
|
+
|
|
4142
|
+
|
|
4143
|
+
# ── D157(2026-10-27):`靶场:` 具名靶场闭集门 + 路径门放行已登记靶场 ─────────────
|
|
4144
|
+
_TARGET_PROFILE = "e2e:\n targets:\n kb: /kb-canvas\n"
|
|
4145
|
+
_TARGET_CASE = CASE_GOOD.replace("step:", "靶场: kb\nstep:", 1)
|
|
4146
|
+
|
|
4147
|
+
|
|
4148
|
+
def test_named_target_in_registry_passes() -> None:
|
|
4149
|
+
"""正向(D157):`靶场: kb` 且 `e2e.targets` 已登记该名 ⇒ 「靶场 取值合法」PASS。"""
|
|
4150
|
+
rows = _ve_rows(_TARGET_CASE, _TARGET_PROFILE)
|
|
4151
|
+
assert rows["靶场 取值合法"]["level"] == "PASS", rows["靶场 取值合法"]
|
|
4152
|
+
|
|
4153
|
+
|
|
4154
|
+
def test_named_target_without_registry_fails() -> None:
|
|
4155
|
+
"""坏输入 1(D157):项目未声明 `e2e.targets` 而用例钉名 ⇒ FAIL(不猜)。"""
|
|
4156
|
+
rows = _ve_rows(_TARGET_CASE)
|
|
4157
|
+
assert rows["靶场 取值合法"]["level"] == "FAIL", rows["靶场 取值合法"]
|
|
4158
|
+
|
|
4159
|
+
|
|
4160
|
+
def test_named_target_unknown_name_fails() -> None:
|
|
4161
|
+
"""坏输入 2(D157):钉名不在闭集内(白名单只放了别的名)⇒ FAIL。"""
|
|
4162
|
+
rows = _ve_rows(_TARGET_CASE, "e2e:\n targets:\n other: /other-canvas\n")
|
|
4163
|
+
assert rows["靶场 取值合法"]["level"] == "FAIL", rows["靶场 取值合法"]
|
|
4164
|
+
|
|
4165
|
+
|
|
4166
|
+
def _ve_rows_with_page_table(case: str, profile_extra: str = "") -> dict:
|
|
4167
|
+
"""`_ve_rows` + 带页面表的索引(路径门需要非空 `page_routes` 才启用)。"""
|
|
4168
|
+
with tempfile.TemporaryDirectory() as td:
|
|
4169
|
+
root = Path(td)
|
|
4170
|
+
mk_prototype(root)
|
|
4171
|
+
if profile_extra:
|
|
4172
|
+
p = root / "product" / "stack-profile.yaml"
|
|
4173
|
+
p.write_text(p.read_text(encoding="utf-8") + profile_extra, encoding="utf-8")
|
|
4174
|
+
mk_cases(root, case)
|
|
4175
|
+
mk_index(root, _with_page_table("已实现"))
|
|
4176
|
+
return {c["check"]: c for c in ve.validate(root).checks}
|
|
4177
|
+
|
|
4178
|
+
|
|
4179
|
+
def test_named_target_route_accepted_by_goto_and_guard_gates() -> None:
|
|
4180
|
+
"""正向(D157):`goto`/`守卫` 显式路径命中**已登记靶场路由** ⇒ 两道路径门放行。"""
|
|
4181
|
+
body = _TARGET_CASE.replace(
|
|
4182
|
+
"- click home-cta-btn",
|
|
4183
|
+
"- goto /kb-canvas").replace(
|
|
4184
|
+
"靶场: kb\n", "靶场: kb\n守卫: /kb-canvas\n", 1)
|
|
4185
|
+
rows = _ve_rows_with_page_table(body, _TARGET_PROFILE)
|
|
4186
|
+
assert rows["goto 路径 ∈ 页面表"]["level"] == "PASS", rows["goto 路径 ∈ 页面表"]
|
|
4187
|
+
assert rows["守卫 路径 ∈ 页面表"]["level"] == "PASS", rows["守卫 路径 ∈ 页面表"]
|
|
4188
|
+
|
|
4189
|
+
|
|
4190
|
+
def test_unregistered_route_still_fails_gates() -> None:
|
|
4191
|
+
"""反向(D157):路径门放行的只有**登记表内**路由——自由路径仍 FAIL(单源不破)。"""
|
|
4192
|
+
body = CASE_GOOD.replace("- click home-cta-btn", "- goto /free-canvas")
|
|
4193
|
+
rows = _ve_rows_with_page_table(body, _TARGET_PROFILE)
|
|
4194
|
+
assert rows["goto 路径 ∈ 页面表"]["level"] == "FAIL", rows["goto 路径 ∈ 页面表"]
|
|
4195
|
+
body2 = CASE_GOOD.replace("step:", "守卫: /free-canvas\nstep:", 1)
|
|
4196
|
+
rows2 = _ve_rows_with_page_table(body2, _TARGET_PROFILE)
|
|
4197
|
+
assert rows2["守卫 路径 ∈ 页面表"]["level"] == "FAIL", rows2["守卫 路径 ∈ 页面表"]
|
|
4198
|
+
|
|
4199
|
+
|
|
4200
|
+
# ── D157(2026-10-27):`e2e.targets` 形状门(stack-profile 校验器)───────────────
|
|
4201
|
+
def test_stack_profile_targets_shape_pass() -> None:
|
|
4202
|
+
"""正向(D157):合法 `targets` 登记 ⇒ 「e2e.targets 形态」PASS。"""
|
|
4203
|
+
with tempfile.TemporaryDirectory() as td:
|
|
4204
|
+
root = Path(td)
|
|
4205
|
+
mk_profile(root, PROFILE_BASE.format(origin="greenfield")
|
|
4206
|
+
+ " targets:\n kb: /kb-canvas\n")
|
|
4207
|
+
res = vsp.validate(root)
|
|
4208
|
+
assert _sp_check(res, "e2e.targets 形态")["level"] == "PASS", \
|
|
4209
|
+
[(c["check"], c["level"], c["detail"]) for c in res.checks]
|
|
4210
|
+
|
|
4211
|
+
|
|
4212
|
+
def test_stack_profile_targets_bad_shape_fails() -> None:
|
|
4213
|
+
"""坏输入(D157):路由不以 / 开头 ⇒ FAIL;名字含非法字符 ⇒ FAIL。"""
|
|
4214
|
+
with tempfile.TemporaryDirectory() as td:
|
|
4215
|
+
root = Path(td)
|
|
4216
|
+
mk_profile(root, PROFILE_BASE.format(origin="greenfield")
|
|
4217
|
+
+ " targets:\n kb: kb-canvas\n")
|
|
4218
|
+
res = vsp.validate(root)
|
|
4219
|
+
assert _sp_check(res, "e2e.targets 形态")["level"] == "FAIL", \
|
|
4220
|
+
[(c["check"], c["level"], c["detail"]) for c in res.checks]
|
|
4221
|
+
with tempfile.TemporaryDirectory() as td:
|
|
4222
|
+
root = Path(td)
|
|
4223
|
+
mk_profile(root, PROFILE_BASE.format(origin="greenfield")
|
|
4224
|
+
+ " targets:\n Bad_Name: /kb-canvas\n")
|
|
4225
|
+
res = vsp.validate(root)
|
|
4226
|
+
assert _sp_check(res, "e2e.targets 形态")["level"] == "FAIL", \
|
|
4227
|
+
[(c["check"], c["level"], c["detail"]) for c in res.checks]
|
|
4228
|
+
|
|
4229
|
+
|
|
4230
|
+
def test_stack_profile_targets_absent_pass() -> None:
|
|
4231
|
+
"""零变化承诺(D157):未声明 `targets` ⇒ 可选段 PASS(单靶场现行为)。"""
|
|
4232
|
+
with tempfile.TemporaryDirectory() as td:
|
|
4233
|
+
root = Path(td)
|
|
4234
|
+
mk_profile(root, PROFILE_BASE.format(origin="greenfield"))
|
|
4235
|
+
res = vsp.validate(root)
|
|
4236
|
+
assert _sp_check(res, "e2e.targets(可选段)")["level"] == "PASS", \
|
|
4237
|
+
[(c["check"], c["level"], c["detail"]) for c in res.checks]
|