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
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import importlib.util
|
|
15
15
|
import json
|
|
16
|
+
import sys
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
import pytest
|
|
18
19
|
|
|
@@ -220,3 +221,1083 @@ def test_valid():
|
|
|
220
221
|
assert cov_check["status"] == "PASS"
|
|
221
222
|
assert "已声明 AC: 1" in cov_check["detail"]
|
|
222
223
|
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# D153:后端通道 AC × 覆盖维度
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
BACKEND_CHECK = "AC × 覆盖维度(后端通道)"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _check(res, name=BACKEND_CHECK):
|
|
233
|
+
return next((c for c in res.checks if c["check"] == name), None)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def create_backend_project(
|
|
237
|
+
tmp_path: Path,
|
|
238
|
+
prd_rows: list[tuple],
|
|
239
|
+
test_code: str | None,
|
|
240
|
+
*,
|
|
241
|
+
header: list[str] | None = None,
|
|
242
|
+
with_backend_test: bool = True,
|
|
243
|
+
framework: str | None = None,
|
|
244
|
+
extractor: str | None = None,
|
|
245
|
+
extractor_command: str | None = None,
|
|
246
|
+
mode: str = "advisory",
|
|
247
|
+
apps: list[dict] | None = None,
|
|
248
|
+
test_dir_files: dict[str, str] | None = None,
|
|
249
|
+
) -> Path:
|
|
250
|
+
prd_dir = tmp_path / "product" / "prd"
|
|
251
|
+
prd_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
header = header or ["AC ID", "场景", "回指规则", "适用覆盖维度", "后端适用覆盖维度"]
|
|
253
|
+
lines = [
|
|
254
|
+
"# 模块 PRD",
|
|
255
|
+
"## 五、验收标准",
|
|
256
|
+
"| " + " | ".join(header) + " |",
|
|
257
|
+
"|" + "---|" * len(header),
|
|
258
|
+
]
|
|
259
|
+
for row in prd_rows:
|
|
260
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
261
|
+
(prd_dir / "test-prd.md").write_text("\n".join(lines), encoding="utf-8")
|
|
262
|
+
|
|
263
|
+
apps = apps or [
|
|
264
|
+
{"name": "backend", "path": "backend", "kind": "backend", "role": "landing", "stack": "python"}
|
|
265
|
+
]
|
|
266
|
+
sp = ["product: 测试项目", "origin: greenfield", "apps:"]
|
|
267
|
+
for a in apps:
|
|
268
|
+
sp.extend([
|
|
269
|
+
f" - name: {a['name']}",
|
|
270
|
+
f" path: {a['path']}",
|
|
271
|
+
f" kind: {a['kind']}",
|
|
272
|
+
f" role: {a['role']}",
|
|
273
|
+
f" stack: {a['stack']}",
|
|
274
|
+
])
|
|
275
|
+
if with_backend_test:
|
|
276
|
+
sp.extend([
|
|
277
|
+
"backend_test:",
|
|
278
|
+
" test_dir: backend/tests",
|
|
279
|
+
f" mode: {mode}",
|
|
280
|
+
" ac_marker: pytest_mark",
|
|
281
|
+
])
|
|
282
|
+
if extractor_command:
|
|
283
|
+
sp.append(f' extractor_command: "{extractor_command}"')
|
|
284
|
+
elif extractor:
|
|
285
|
+
sp.append(f" extractor: {extractor}")
|
|
286
|
+
elif framework:
|
|
287
|
+
sp.append(f" framework: {framework}")
|
|
288
|
+
(tmp_path / "product" / "stack-profile.yaml").write_text("\n".join(sp) + "\n", encoding="utf-8")
|
|
289
|
+
|
|
290
|
+
test_dir = tmp_path / "backend" / "tests"
|
|
291
|
+
test_dir.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
if test_code is not None:
|
|
293
|
+
(test_dir / "test_sample.py").write_text(test_code, encoding="utf-8")
|
|
294
|
+
for name, code in (test_dir_files or {}).items():
|
|
295
|
+
p = test_dir / name
|
|
296
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
p.write_text(code, encoding="utf-8")
|
|
298
|
+
return tmp_path
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_backend_dimensions_pass(tmp_path: Path):
|
|
302
|
+
test_code = """
|
|
303
|
+
import pytest
|
|
304
|
+
|
|
305
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01", "C02"])
|
|
306
|
+
def test_ok():
|
|
307
|
+
assert True
|
|
308
|
+
"""
|
|
309
|
+
proj = create_backend_project(
|
|
310
|
+
tmp_path,
|
|
311
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01,C02")],
|
|
312
|
+
test_code,
|
|
313
|
+
)
|
|
314
|
+
res = validate_ac_alignment(proj)
|
|
315
|
+
check = _check(res)
|
|
316
|
+
assert check is not None and check["status"] == "PASS", check
|
|
317
|
+
assert res.status == "PASS"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_backend_dimensions_missing_warn(tmp_path: Path):
|
|
321
|
+
test_code = """
|
|
322
|
+
import pytest
|
|
323
|
+
|
|
324
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
325
|
+
def test_ok():
|
|
326
|
+
assert True
|
|
327
|
+
"""
|
|
328
|
+
proj = create_backend_project(
|
|
329
|
+
tmp_path,
|
|
330
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01,C02,C11")],
|
|
331
|
+
test_code,
|
|
332
|
+
)
|
|
333
|
+
res = validate_ac_alignment(proj)
|
|
334
|
+
check = _check(res)
|
|
335
|
+
assert check is not None and check["status"] == "WARN", check
|
|
336
|
+
assert "C02" in check["detail"] and "C11" in check["detail"], check
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_backend_zero_test_warn(tmp_path: Path):
|
|
340
|
+
proj = create_backend_project(
|
|
341
|
+
tmp_path,
|
|
342
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
343
|
+
"def test_unrelated():\n assert True\n",
|
|
344
|
+
)
|
|
345
|
+
res = validate_ac_alignment(proj)
|
|
346
|
+
check = _check(res)
|
|
347
|
+
assert check is not None and check["status"] == "WARN", check
|
|
348
|
+
assert "零测试" in check["detail"] and "AC-TEST-001" in check["detail"], check
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_backend_weak_only_warn(tmp_path: Path):
|
|
352
|
+
test_code = """
|
|
353
|
+
import pytest
|
|
354
|
+
|
|
355
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
356
|
+
@pytest.mark.skip(reason="waiver: 临时跳过")
|
|
357
|
+
def test_ok():
|
|
358
|
+
assert False
|
|
359
|
+
"""
|
|
360
|
+
proj = create_backend_project(
|
|
361
|
+
tmp_path,
|
|
362
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
363
|
+
test_code,
|
|
364
|
+
)
|
|
365
|
+
res = validate_ac_alignment(proj)
|
|
366
|
+
check = _check(res)
|
|
367
|
+
assert check is not None and check["status"] == "WARN", check
|
|
368
|
+
assert "弱证据" in check["detail"], check
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_backend_skipif_weak(tmp_path: Path):
|
|
372
|
+
test_code = """
|
|
373
|
+
import pytest
|
|
374
|
+
|
|
375
|
+
@pytest.mark.skipif(False, reason="off")
|
|
376
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
377
|
+
def test_ok():
|
|
378
|
+
assert True
|
|
379
|
+
"""
|
|
380
|
+
proj = create_backend_project(
|
|
381
|
+
tmp_path,
|
|
382
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
383
|
+
test_code,
|
|
384
|
+
)
|
|
385
|
+
res = validate_ac_alignment(proj)
|
|
386
|
+
check = _check(res)
|
|
387
|
+
assert check is not None and check["status"] == "WARN", check
|
|
388
|
+
assert "弱证据" in check["detail"], check
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def test_backend_multi_ac_dims_warn(tmp_path: Path):
|
|
392
|
+
test_code = """
|
|
393
|
+
import pytest
|
|
394
|
+
|
|
395
|
+
@pytest.mark.ac("AC-TEST-001", "AC-TEST-002", dims=["C01"])
|
|
396
|
+
def test_ok():
|
|
397
|
+
assert True
|
|
398
|
+
"""
|
|
399
|
+
proj = create_backend_project(
|
|
400
|
+
tmp_path,
|
|
401
|
+
[
|
|
402
|
+
("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01"),
|
|
403
|
+
("`AC-TEST-002`", "Given x When y Then z", "`R-001`", "", "C01"),
|
|
404
|
+
],
|
|
405
|
+
test_code,
|
|
406
|
+
)
|
|
407
|
+
res = validate_ac_alignment(proj)
|
|
408
|
+
check = _check(res)
|
|
409
|
+
assert check is not None and check["status"] == "WARN", check
|
|
410
|
+
assert "一测多AC" in check["detail"], check
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def test_backend_docstring_ac_not_dims(tmp_path: Path):
|
|
414
|
+
test_code = '''
|
|
415
|
+
import pytest
|
|
416
|
+
|
|
417
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
418
|
+
def test_ok():
|
|
419
|
+
"""也提到 AC-OTHER-001。"""
|
|
420
|
+
assert True
|
|
421
|
+
'''
|
|
422
|
+
proj = create_backend_project(
|
|
423
|
+
tmp_path,
|
|
424
|
+
[
|
|
425
|
+
("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01"),
|
|
426
|
+
("`AC-OTHER-001`", "Given x When y Then z", "`R-001`", "", ""),
|
|
427
|
+
],
|
|
428
|
+
test_code,
|
|
429
|
+
)
|
|
430
|
+
res = validate_ac_alignment(proj)
|
|
431
|
+
check = _check(res)
|
|
432
|
+
assert check is not None and check["status"] == "PASS", check
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def test_backend_dims_shape_warn(tmp_path: Path):
|
|
436
|
+
test_code = """
|
|
437
|
+
import pytest
|
|
438
|
+
|
|
439
|
+
@pytest.mark.ac("AC-TEST-001", dims={"C01"})
|
|
440
|
+
def test_ok():
|
|
441
|
+
assert True
|
|
442
|
+
"""
|
|
443
|
+
proj = create_backend_project(
|
|
444
|
+
tmp_path,
|
|
445
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
446
|
+
test_code,
|
|
447
|
+
)
|
|
448
|
+
res = validate_ac_alignment(proj)
|
|
449
|
+
check = _check(res)
|
|
450
|
+
assert check is not None and check["status"] == "WARN", check
|
|
451
|
+
assert "dims形态不支持" in check["detail"], check
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def test_backend_unknown_dim_warn(tmp_path: Path):
|
|
455
|
+
test_code = """
|
|
456
|
+
import pytest
|
|
457
|
+
|
|
458
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C99"])
|
|
459
|
+
def test_ok():
|
|
460
|
+
assert True
|
|
461
|
+
"""
|
|
462
|
+
proj = create_backend_project(
|
|
463
|
+
tmp_path,
|
|
464
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
465
|
+
test_code,
|
|
466
|
+
)
|
|
467
|
+
res = validate_ac_alignment(proj)
|
|
468
|
+
check = _check(res)
|
|
469
|
+
assert check is not None and check["status"] == "WARN", check
|
|
470
|
+
assert "未知维度" in check["detail"], check
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_backend_three_column_na(tmp_path: Path):
|
|
474
|
+
test_code = """
|
|
475
|
+
import pytest
|
|
476
|
+
|
|
477
|
+
@pytest.mark.ac("AC-TEST-001")
|
|
478
|
+
def test_ok():
|
|
479
|
+
assert True
|
|
480
|
+
"""
|
|
481
|
+
proj = create_backend_project(
|
|
482
|
+
tmp_path,
|
|
483
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`")],
|
|
484
|
+
test_code,
|
|
485
|
+
header=["AC ID", "场景", "回指规则"],
|
|
486
|
+
)
|
|
487
|
+
res = validate_ac_alignment(proj)
|
|
488
|
+
check = _check(res)
|
|
489
|
+
assert check is not None and check["status"] == "N/A", check
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_backend_standard_e2e_four_column_not_warn(tmp_path: Path):
|
|
493
|
+
test_code = """
|
|
494
|
+
import pytest
|
|
495
|
+
|
|
496
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
497
|
+
def test_ok():
|
|
498
|
+
assert True
|
|
499
|
+
"""
|
|
500
|
+
proj = create_backend_project(
|
|
501
|
+
tmp_path,
|
|
502
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "C01,C02")],
|
|
503
|
+
test_code,
|
|
504
|
+
header=["AC ID", "场景", "回指规则", "适用覆盖维度"],
|
|
505
|
+
)
|
|
506
|
+
res = validate_ac_alignment(proj)
|
|
507
|
+
check = _check(res)
|
|
508
|
+
assert check is not None and check["status"] == "N/A", check
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_backend_header_in_fourth_column_warns(tmp_path: Path):
|
|
512
|
+
test_code = """
|
|
513
|
+
import pytest
|
|
514
|
+
|
|
515
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
516
|
+
def test_ok():
|
|
517
|
+
assert True
|
|
518
|
+
"""
|
|
519
|
+
proj = create_backend_project(
|
|
520
|
+
tmp_path,
|
|
521
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "C01")],
|
|
522
|
+
test_code,
|
|
523
|
+
header=["AC ID", "场景", "回指规则", "后端适用覆盖维度"],
|
|
524
|
+
)
|
|
525
|
+
res = validate_ac_alignment(proj)
|
|
526
|
+
check = _check(res)
|
|
527
|
+
assert check is not None and check["status"] == "WARN", check
|
|
528
|
+
assert "第 4 列" in check["detail"], check
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_backend_notes_column_with_dims_warns(tmp_path: Path):
|
|
532
|
+
test_code = 'def test_unrelated():\n assert True\n'
|
|
533
|
+
proj = create_backend_project(
|
|
534
|
+
tmp_path,
|
|
535
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
536
|
+
test_code,
|
|
537
|
+
header=["AC ID", "场景", "回指规则", "适用覆盖维度", "备注"],
|
|
538
|
+
)
|
|
539
|
+
res = validate_ac_alignment(proj)
|
|
540
|
+
check = _check(res)
|
|
541
|
+
assert check is not None and check["status"] == "WARN", check
|
|
542
|
+
assert "疑似后端维度列" in check["detail"], check
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_backend_template_placeholder_na(tmp_path: Path):
|
|
546
|
+
proj = create_backend_project(
|
|
547
|
+
tmp_path,
|
|
548
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "N/A")],
|
|
549
|
+
"def test_unrelated():\n assert True\n",
|
|
550
|
+
)
|
|
551
|
+
res = validate_ac_alignment(proj)
|
|
552
|
+
check = _check(res)
|
|
553
|
+
assert check is not None and check["status"] == "N/A", check
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def test_backend_no_backend_test_na(tmp_path: Path):
|
|
557
|
+
test_code = """
|
|
558
|
+
import pytest
|
|
559
|
+
|
|
560
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
561
|
+
def test_ok():
|
|
562
|
+
assert True
|
|
563
|
+
"""
|
|
564
|
+
proj = create_backend_project(
|
|
565
|
+
tmp_path,
|
|
566
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
567
|
+
test_code,
|
|
568
|
+
with_backend_test=False,
|
|
569
|
+
)
|
|
570
|
+
res = validate_ac_alignment(proj)
|
|
571
|
+
check = _check(res)
|
|
572
|
+
assert check is not None and check["status"] == "N/A", check
|
|
573
|
+
assert "backend_test" in check["detail"], check
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def test_backend_unknown_extractor_unverified(tmp_path: Path):
|
|
577
|
+
test_code = 'def test_unrelated():\n assert True\n'
|
|
578
|
+
proj = create_backend_project(
|
|
579
|
+
tmp_path,
|
|
580
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
581
|
+
test_code,
|
|
582
|
+
extractor="unittest",
|
|
583
|
+
)
|
|
584
|
+
res = validate_ac_alignment(proj)
|
|
585
|
+
check = _check(res, "后端测试提取通道")
|
|
586
|
+
assert check is not None and check["status"] == "WARN", check
|
|
587
|
+
assert "[UNVERIFIED]" in check["detail"], check
|
|
588
|
+
assert res.status == "WARN", res.status
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_backend_multi_app_uses_global_test_dir(tmp_path: Path):
|
|
592
|
+
apps = [
|
|
593
|
+
{"name": "api1", "path": "api1", "kind": "backend", "role": "landing", "stack": "python"},
|
|
594
|
+
{"name": "api2", "path": "api2", "kind": "backend", "role": "landing", "stack": "python"},
|
|
595
|
+
]
|
|
596
|
+
test_code = """
|
|
597
|
+
import pytest
|
|
598
|
+
|
|
599
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
600
|
+
def test_ok():
|
|
601
|
+
assert True
|
|
602
|
+
"""
|
|
603
|
+
proj = create_backend_project(
|
|
604
|
+
tmp_path,
|
|
605
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
606
|
+
test_code,
|
|
607
|
+
apps=apps,
|
|
608
|
+
)
|
|
609
|
+
res = validate_ac_alignment(proj)
|
|
610
|
+
check = _check(res)
|
|
611
|
+
assert check is not None and check["status"] == "PASS", check
|
|
612
|
+
assert res.status == "PASS", res.status
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def test_backend_multi_app_root_test_dir_scans_all_apps(tmp_path: Path):
|
|
616
|
+
apps = [
|
|
617
|
+
{"name": "api1", "path": "api1", "kind": "backend", "role": "landing", "stack": "python"},
|
|
618
|
+
{"name": "api2", "path": "api2", "kind": "backend", "role": "landing", "stack": "python"},
|
|
619
|
+
]
|
|
620
|
+
proj = create_backend_project(
|
|
621
|
+
tmp_path,
|
|
622
|
+
[
|
|
623
|
+
("`AC-A-001`", "Given x When y Then z", "`R-001`", "", "C01"),
|
|
624
|
+
("`AC-B-001`", "Given x When y Then z", "`R-002`", "", "C01"),
|
|
625
|
+
],
|
|
626
|
+
None,
|
|
627
|
+
apps=apps,
|
|
628
|
+
)
|
|
629
|
+
profile = proj / "product" / "stack-profile.yaml"
|
|
630
|
+
profile.write_text(
|
|
631
|
+
profile.read_text(encoding="utf-8").replace("test_dir: backend/tests", "test_dir: ."),
|
|
632
|
+
encoding="utf-8",
|
|
633
|
+
)
|
|
634
|
+
for app, ac in (("api1", "AC-A-001"), ("api2", "AC-B-001")):
|
|
635
|
+
d = proj / app / "tests"
|
|
636
|
+
d.mkdir(parents=True)
|
|
637
|
+
(d / "test_x.py").write_text(
|
|
638
|
+
f"import pytest\n\n@pytest.mark.ac('{ac}', dims=['C01'])\ndef test_ok():\n assert True\n",
|
|
639
|
+
encoding="utf-8",
|
|
640
|
+
)
|
|
641
|
+
res = validate_ac_alignment(proj)
|
|
642
|
+
assert res.status == "PASS", [(c["check"], c["status"], c["detail"]) for c in res.checks]
|
|
643
|
+
cov = _check(res, "AC覆盖对账")
|
|
644
|
+
assert cov is not None and "覆盖: 2" in cov["detail"], cov
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def test_backend_star_test_scan_face(tmp_path: Path):
|
|
648
|
+
proj = create_backend_project(
|
|
649
|
+
tmp_path,
|
|
650
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
651
|
+
None,
|
|
652
|
+
test_dir_files={
|
|
653
|
+
"foo_test.py": 'import pytest\n\n@pytest.mark.ac("AC-TEST-001", dims=["C01"])\ndef test_x():\n assert True\n'
|
|
654
|
+
},
|
|
655
|
+
)
|
|
656
|
+
res = validate_ac_alignment(proj)
|
|
657
|
+
check = _check(res, "测试扫描面")
|
|
658
|
+
assert check is not None and check["status"] == "WARN", check
|
|
659
|
+
assert "[SCAN]" in check["detail"], check
|
|
660
|
+
assert "foo_test.py" in check["detail"], check
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def test_backend_syntax_error_warn(tmp_path: Path):
|
|
664
|
+
proj = create_backend_project(
|
|
665
|
+
tmp_path,
|
|
666
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
667
|
+
None,
|
|
668
|
+
test_dir_files={"test_bad.py": "def broken(:\n"},
|
|
669
|
+
)
|
|
670
|
+
res = validate_ac_alignment(proj)
|
|
671
|
+
check = _check(res, "测试提取完整性")
|
|
672
|
+
assert check is not None and check["status"] == "WARN", check
|
|
673
|
+
assert "[PARSE]" in check["detail"], check
|
|
674
|
+
assert "test_bad.py" in check["detail"], check
|
|
675
|
+
assert res.status == "WARN", res.status
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def test_backend_empty_root_na(tmp_path: Path):
|
|
679
|
+
proj = create_backend_project(tmp_path, [], None)
|
|
680
|
+
res = validate_ac_alignment(proj)
|
|
681
|
+
assert res.status == "N/A"
|
|
682
|
+
check = _check(res, "AC对账门禁运行态")
|
|
683
|
+
assert check is not None and check["status"] == "N/A", check
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def test_backend_dims_string_and_tuple(tmp_path: Path):
|
|
687
|
+
for idx, dims in enumerate(['"C01,C02"', '("C01", "C02")']):
|
|
688
|
+
root = tmp_path / str(idx)
|
|
689
|
+
test_code = f"""
|
|
690
|
+
import pytest
|
|
691
|
+
|
|
692
|
+
@pytest.mark.ac("AC-TEST-001", dims={dims})
|
|
693
|
+
def test_ok():
|
|
694
|
+
assert True
|
|
695
|
+
"""
|
|
696
|
+
proj = create_backend_project(
|
|
697
|
+
root,
|
|
698
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01,C02")],
|
|
699
|
+
test_code,
|
|
700
|
+
)
|
|
701
|
+
res = validate_ac_alignment(proj)
|
|
702
|
+
check = _check(res)
|
|
703
|
+
assert check is not None and check["status"] == "PASS", (dims, check)
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def test_backend_module_pytestmark_weak(tmp_path: Path):
|
|
707
|
+
test_code = """
|
|
708
|
+
import pytest
|
|
709
|
+
pytestmark = pytest.mark.skipif(False, reason="off")
|
|
710
|
+
|
|
711
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
712
|
+
def test_ok():
|
|
713
|
+
assert True
|
|
714
|
+
"""
|
|
715
|
+
proj = create_backend_project(
|
|
716
|
+
tmp_path,
|
|
717
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
718
|
+
test_code,
|
|
719
|
+
)
|
|
720
|
+
res = validate_ac_alignment(proj)
|
|
721
|
+
check = _check(res)
|
|
722
|
+
assert check is not None and check["status"] == "WARN", check
|
|
723
|
+
assert "弱证据" in check["detail"], check
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def test_backend_class_pytestmark_weak(tmp_path: Path):
|
|
727
|
+
test_code = """
|
|
728
|
+
import pytest
|
|
729
|
+
|
|
730
|
+
class TestX:
|
|
731
|
+
pytestmark = pytest.mark.skipif(False, reason="off")
|
|
732
|
+
|
|
733
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
734
|
+
def test_ok(self):
|
|
735
|
+
assert True
|
|
736
|
+
"""
|
|
737
|
+
proj = create_backend_project(
|
|
738
|
+
tmp_path,
|
|
739
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
740
|
+
test_code,
|
|
741
|
+
)
|
|
742
|
+
res = validate_ac_alignment(proj)
|
|
743
|
+
check = _check(res)
|
|
744
|
+
assert check is not None and check["status"] == "WARN", check
|
|
745
|
+
assert "弱证据" in check["detail"], check
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def test_backend_pytestmark_ac_unsupported(tmp_path: Path):
|
|
749
|
+
test_code = """
|
|
750
|
+
import pytest
|
|
751
|
+
pytestmark = pytest.mark.ac("AC-TEST-001")
|
|
752
|
+
|
|
753
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
754
|
+
def test_ok():
|
|
755
|
+
assert True
|
|
756
|
+
"""
|
|
757
|
+
proj = create_backend_project(
|
|
758
|
+
tmp_path,
|
|
759
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
760
|
+
test_code,
|
|
761
|
+
)
|
|
762
|
+
res = validate_ac_alignment(proj)
|
|
763
|
+
check = _check(res)
|
|
764
|
+
assert check is not None and check["status"] == "WARN", check
|
|
765
|
+
assert "模块/类级AC标记不支持" in check["detail"], check
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def test_backend_prd_unknown_token_warn(tmp_path: Path):
|
|
769
|
+
test_code = """
|
|
770
|
+
import pytest
|
|
771
|
+
|
|
772
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
773
|
+
def test_ok():
|
|
774
|
+
assert True
|
|
775
|
+
"""
|
|
776
|
+
proj = create_backend_project(
|
|
777
|
+
tmp_path,
|
|
778
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01,C99")],
|
|
779
|
+
test_code,
|
|
780
|
+
)
|
|
781
|
+
res = validate_ac_alignment(proj)
|
|
782
|
+
check = _check(res)
|
|
783
|
+
assert check is not None and check["status"] == "WARN", check
|
|
784
|
+
assert "未知 token" in check["detail"] and "C99" in check["detail"], check
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def test_backend_cross_prd_duplicate_warn(tmp_path: Path):
|
|
788
|
+
test_code = """
|
|
789
|
+
import pytest
|
|
790
|
+
|
|
791
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
792
|
+
def test_ok():
|
|
793
|
+
assert True
|
|
794
|
+
"""
|
|
795
|
+
proj = create_backend_project(
|
|
796
|
+
tmp_path,
|
|
797
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
798
|
+
test_code,
|
|
799
|
+
)
|
|
800
|
+
(proj / "product" / "prd" / "other-prd.md").write_text(
|
|
801
|
+
"# 另一域 PRD\n## 五、验收标准\n"
|
|
802
|
+
"| AC ID | 场景 | 回指规则 | 适用覆盖维度 | 后端适用覆盖维度 |\n"
|
|
803
|
+
"|---|---|---|---|---|\n"
|
|
804
|
+
"| `AC-TEST-001` | Given x When y Then z | `R-002` | | C01 |\n",
|
|
805
|
+
encoding="utf-8",
|
|
806
|
+
)
|
|
807
|
+
res = validate_ac_alignment(proj)
|
|
808
|
+
check = _check(res)
|
|
809
|
+
assert check is not None and check["status"] == "WARN", check
|
|
810
|
+
assert "重复声明" in check["detail"], check
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def test_backend_notes_ac_id_no_false_warn(tmp_path: Path):
|
|
814
|
+
proj = create_backend_project(
|
|
815
|
+
tmp_path,
|
|
816
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "AC-C01-001")],
|
|
817
|
+
"def test_unrelated():\n assert True\n",
|
|
818
|
+
header=["AC ID", "场景", "回指规则", "适用覆盖维度", "备注"],
|
|
819
|
+
)
|
|
820
|
+
res = validate_ac_alignment(proj)
|
|
821
|
+
check = _check(res)
|
|
822
|
+
assert check is not None and check["status"] == "N/A", check
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def test_backend_real_template_placeholder_na(tmp_path: Path):
|
|
826
|
+
template = (PKG_ROOT / "templates" / "prd" / "domain.md").read_text(encoding="utf-8")
|
|
827
|
+
proj = create_backend_project(
|
|
828
|
+
tmp_path,
|
|
829
|
+
[],
|
|
830
|
+
'import pytest\n\n@pytest.mark.ac("AC-TEST-001")\ndef test_ok():\n assert True\n',
|
|
831
|
+
)
|
|
832
|
+
(proj / "product" / "prd" / "test-prd.md").write_text(
|
|
833
|
+
template.replace("<CODE>", "TEST"), encoding="utf-8"
|
|
834
|
+
)
|
|
835
|
+
res = validate_ac_alignment(proj)
|
|
836
|
+
check = _check(res)
|
|
837
|
+
assert check is not None and check["status"] == "N/A", check
|
|
838
|
+
assert "零测试" not in check["detail"], check
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def test_backend_six_column_pass(tmp_path: Path):
|
|
842
|
+
test_code = """
|
|
843
|
+
import pytest
|
|
844
|
+
|
|
845
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
846
|
+
def test_ok():
|
|
847
|
+
assert True
|
|
848
|
+
"""
|
|
849
|
+
proj = create_backend_project(
|
|
850
|
+
tmp_path,
|
|
851
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "", "C01")],
|
|
852
|
+
test_code,
|
|
853
|
+
header=["AC ID", "场景", "回指规则", "适用覆盖维度", "备注", "后端适用覆盖维度"],
|
|
854
|
+
)
|
|
855
|
+
res = validate_ac_alignment(proj)
|
|
856
|
+
check = _check(res)
|
|
857
|
+
assert check is not None and check["status"] == "PASS", check
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def test_backend_empty_dims_warn(tmp_path: Path):
|
|
861
|
+
test_code = """
|
|
862
|
+
import pytest
|
|
863
|
+
|
|
864
|
+
@pytest.mark.ac("AC-TEST-001", dims=[])
|
|
865
|
+
def test_ok():
|
|
866
|
+
assert True
|
|
867
|
+
"""
|
|
868
|
+
proj = create_backend_project(
|
|
869
|
+
tmp_path,
|
|
870
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
871
|
+
test_code,
|
|
872
|
+
)
|
|
873
|
+
res = validate_ac_alignment(proj)
|
|
874
|
+
check = _check(res)
|
|
875
|
+
assert check is not None and check["status"] == "WARN", check
|
|
876
|
+
assert "空dims" in check["detail"], check
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def test_backend_parametrize_marks_weak(tmp_path: Path):
|
|
880
|
+
test_code = """
|
|
881
|
+
import pytest
|
|
882
|
+
|
|
883
|
+
@pytest.mark.parametrize("x", [pytest.param(1, marks=pytest.mark.skip)])
|
|
884
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
885
|
+
def test_ok(x):
|
|
886
|
+
assert x
|
|
887
|
+
"""
|
|
888
|
+
proj = create_backend_project(
|
|
889
|
+
tmp_path,
|
|
890
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
891
|
+
test_code,
|
|
892
|
+
)
|
|
893
|
+
res = validate_ac_alignment(proj)
|
|
894
|
+
check = _check(res)
|
|
895
|
+
assert check is not None and check["status"] == "WARN", check
|
|
896
|
+
assert "弱证据" in check["detail"], check
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def test_backend_enforce_dim_gap_warn(tmp_path: Path):
|
|
900
|
+
test_code = """
|
|
901
|
+
import pytest
|
|
902
|
+
|
|
903
|
+
@pytest.mark.ac("AC-TEST-001", dims=["C01"])
|
|
904
|
+
def test_ok():
|
|
905
|
+
assert True
|
|
906
|
+
"""
|
|
907
|
+
proj = create_backend_project(
|
|
908
|
+
tmp_path,
|
|
909
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01,C02")],
|
|
910
|
+
test_code,
|
|
911
|
+
mode="enforce",
|
|
912
|
+
)
|
|
913
|
+
res = validate_ac_alignment(proj)
|
|
914
|
+
check = _check(res)
|
|
915
|
+
assert check is not None and check["status"] == "WARN", check
|
|
916
|
+
assert res.status == "WARN", res.status
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
# ---------------------------------------------------------------------------
|
|
920
|
+
# D163:三形态适用性 / extractor 边界 / 诚实状态
|
|
921
|
+
# ---------------------------------------------------------------------------
|
|
922
|
+
|
|
923
|
+
def test_frontend_only_ac_gate_na(tmp_path: Path):
|
|
924
|
+
test_code = """
|
|
925
|
+
import pytest
|
|
926
|
+
|
|
927
|
+
@pytest.mark.ac("AC-TEST-001")
|
|
928
|
+
def test_ok():
|
|
929
|
+
assert True
|
|
930
|
+
"""
|
|
931
|
+
apps = [{"name": "web", "path": "frontend", "kind": "frontend", "role": "landing", "stack": "react"}]
|
|
932
|
+
proj = create_backend_project(
|
|
933
|
+
tmp_path,
|
|
934
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
935
|
+
test_code,
|
|
936
|
+
apps=apps,
|
|
937
|
+
)
|
|
938
|
+
res = validate_ac_alignment(proj)
|
|
939
|
+
assert res.status == "N/A", res.status
|
|
940
|
+
check = _check(res, "门控态")
|
|
941
|
+
assert check is not None and check["status"] == "N/A", check
|
|
942
|
+
assert "backend/fullstack" in check["detail"], check
|
|
943
|
+
assert not (proj / ".atlas" / "runs" / "backend-ac-alignment.json").exists()
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def test_missing_apps_dangling_ac_still_fails(tmp_path: Path):
|
|
947
|
+
test_code = """
|
|
948
|
+
import pytest
|
|
949
|
+
|
|
950
|
+
@pytest.mark.ac("AC-GHOST-999")
|
|
951
|
+
def test_fake():
|
|
952
|
+
assert True
|
|
953
|
+
"""
|
|
954
|
+
proj = create_backend_project(
|
|
955
|
+
tmp_path,
|
|
956
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
957
|
+
test_code,
|
|
958
|
+
)
|
|
959
|
+
(proj / "product" / "stack-profile.yaml").write_text("product: x\n", encoding="utf-8")
|
|
960
|
+
res = validate_ac_alignment(proj)
|
|
961
|
+
assert res.status == "FAIL", res.status
|
|
962
|
+
check = _check(res, "无主AC拦截")
|
|
963
|
+
assert check is not None and check["status"] == "FAIL", check
|
|
964
|
+
assert "AC-GHOST-999" in check["detail"], check
|
|
965
|
+
applicability = _check(res, "后端通道适用性")
|
|
966
|
+
assert applicability is not None and applicability["status"] == "WARN", applicability
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def test_missing_apps_without_input_unverified(tmp_path: Path):
|
|
970
|
+
proj = create_backend_project(
|
|
971
|
+
tmp_path,
|
|
972
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
973
|
+
None,
|
|
974
|
+
)
|
|
975
|
+
(proj / "product" / "stack-profile.yaml").write_text("product: x\n", encoding="utf-8")
|
|
976
|
+
res = validate_ac_alignment(proj)
|
|
977
|
+
applicability = _check(res, "后端通道适用性")
|
|
978
|
+
assert applicability is not None and applicability["status"] == "WARN", applicability
|
|
979
|
+
assert "[UNVERIFIED]" in applicability["detail"], applicability
|
|
980
|
+
assert res.status == "WARN", res.status
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def test_backend_no_extractor_without_input_unverified(tmp_path: Path):
|
|
984
|
+
proj = create_backend_project(
|
|
985
|
+
tmp_path,
|
|
986
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
987
|
+
None,
|
|
988
|
+
)
|
|
989
|
+
res = validate_ac_alignment(proj)
|
|
990
|
+
check = _check(res, "后端测试提取通道")
|
|
991
|
+
assert check is not None and check["status"] == "WARN", check
|
|
992
|
+
assert "[UNVERIFIED]" in check["detail"], check
|
|
993
|
+
assert res.status == "WARN", res.status
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def test_backend_non_python_without_extractor_unverified_not_zero_coverage(tmp_path: Path):
|
|
997
|
+
proj = create_backend_project(
|
|
998
|
+
tmp_path,
|
|
999
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1000
|
+
None,
|
|
1001
|
+
test_dir_files={"a.test.ts": "// no Python test input\n"},
|
|
1002
|
+
)
|
|
1003
|
+
res = validate_ac_alignment(proj)
|
|
1004
|
+
check = _check(res, "后端测试提取通道")
|
|
1005
|
+
assert check is not None and check["status"] == "WARN", check
|
|
1006
|
+
assert "[UNVERIFIED]" in check["detail"], check
|
|
1007
|
+
cov = _check(res, "AC覆盖对账")
|
|
1008
|
+
assert cov is None, cov # 未验证分支不应再产出 ZERO-COVERAGE
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def test_backend_zero_coverage_advisory_warns(tmp_path: Path):
|
|
1012
|
+
proj = create_backend_project(
|
|
1013
|
+
tmp_path,
|
|
1014
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1015
|
+
"def test_unrelated():\n assert True\n",
|
|
1016
|
+
with_backend_test=False,
|
|
1017
|
+
)
|
|
1018
|
+
res = validate_ac_alignment(proj)
|
|
1019
|
+
check = _check(res, "AC覆盖对账")
|
|
1020
|
+
assert check is not None and check["status"] == "WARN", check
|
|
1021
|
+
assert "[ZERO-COVERAGE]" in check["detail"], check
|
|
1022
|
+
assert res.status == "WARN", res.status
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def test_backend_enforce_parse_errors_fail(tmp_path: Path):
|
|
1026
|
+
proj = create_backend_project(
|
|
1027
|
+
tmp_path,
|
|
1028
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1029
|
+
None,
|
|
1030
|
+
test_dir_files={"test_bad.py": "def broken(:\n"},
|
|
1031
|
+
mode="enforce",
|
|
1032
|
+
)
|
|
1033
|
+
res = validate_ac_alignment(proj)
|
|
1034
|
+
check = _check(res, "测试提取完整性")
|
|
1035
|
+
assert check is not None and check["status"] == "FAIL", check
|
|
1036
|
+
assert "[PARSE]" in check["detail"], check
|
|
1037
|
+
assert res.status == "FAIL", res.status
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def test_record_schema_rejects_bad_records(tmp_path: Path):
|
|
1041
|
+
base = {
|
|
1042
|
+
"name": "test_x",
|
|
1043
|
+
"file": "backend/tests/test_x.py",
|
|
1044
|
+
"line": 1,
|
|
1045
|
+
"acs": {"AC-X-001"},
|
|
1046
|
+
"marker_acs": {"AC-X-001"},
|
|
1047
|
+
"doc_acs": set(),
|
|
1048
|
+
"dims": set(),
|
|
1049
|
+
"dims_bad": [],
|
|
1050
|
+
"dims_shape_ok": True,
|
|
1051
|
+
"dims_present": False,
|
|
1052
|
+
"skipped": False,
|
|
1053
|
+
"waiver": "",
|
|
1054
|
+
"weak": False,
|
|
1055
|
+
"marker_scope_unsupported": False,
|
|
1056
|
+
}
|
|
1057
|
+
assert vaa._record_schema_errors([base], tmp_path) == []
|
|
1058
|
+
bad_union = dict(base, acs={"AC-X-001"}, marker_acs=set())
|
|
1059
|
+
assert any("acs 必须等于" in e for e in vaa._record_schema_errors([bad_union], tmp_path))
|
|
1060
|
+
bad_line = dict(base, line=0)
|
|
1061
|
+
assert any("line 必须为正整数" in e for e in vaa._record_schema_errors([bad_line], tmp_path))
|
|
1062
|
+
dup = dict(base, name="test_y")
|
|
1063
|
+
assert any("(file,line) 重复" in e for e in vaa._record_schema_errors([base, dup], tmp_path))
|
|
1064
|
+
absolute = dict(base, file=str(tmp_path / "x.py"))
|
|
1065
|
+
assert any("相对路径" in e for e in vaa._record_schema_errors([absolute], tmp_path))
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
# ---------------------------------------------------------------------------
|
|
1069
|
+
# D164:TS / Vitest / Jest 内置 extractor(Batch C1)
|
|
1070
|
+
# ---------------------------------------------------------------------------
|
|
1071
|
+
|
|
1072
|
+
def test_backend_vitest_title_ac_and_dims_pass(tmp_path: Path):
|
|
1073
|
+
proj = create_backend_project(
|
|
1074
|
+
tmp_path,
|
|
1075
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1076
|
+
None,
|
|
1077
|
+
extractor="vitest",
|
|
1078
|
+
test_dir_files={
|
|
1079
|
+
"refund.test.ts": (
|
|
1080
|
+
"/** @atlas-dims C01 */\n"
|
|
1081
|
+
"it('[AC-TEST-001] 余额不足时拒绝退款', () => {});\n"
|
|
1082
|
+
),
|
|
1083
|
+
},
|
|
1084
|
+
)
|
|
1085
|
+
res = validate_ac_alignment(proj)
|
|
1086
|
+
assert res.status == "PASS", [(c["check"], c["status"], c["detail"]) for c in res.checks]
|
|
1087
|
+
cov = _check(res, "AC覆盖对账")
|
|
1088
|
+
assert cov is not None and "覆盖: 1" in cov["detail"], cov
|
|
1089
|
+
dim = _check(res)
|
|
1090
|
+
assert dim is not None and dim["status"] == "PASS", dim
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def test_backend_vitest_dangling_ac_fails(tmp_path: Path):
|
|
1094
|
+
proj = create_backend_project(
|
|
1095
|
+
tmp_path,
|
|
1096
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1097
|
+
None,
|
|
1098
|
+
extractor="vitest",
|
|
1099
|
+
test_dir_files={
|
|
1100
|
+
"refund.test.ts": "it('[AC-GHOST-999] 假挂载', () => {});\n",
|
|
1101
|
+
},
|
|
1102
|
+
)
|
|
1103
|
+
res = validate_ac_alignment(proj)
|
|
1104
|
+
assert res.status == "FAIL", res.status
|
|
1105
|
+
check = _check(res, "无主AC拦截")
|
|
1106
|
+
assert check is not None and "AC-GHOST-999" in check["detail"], check
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def test_backend_jest_skip_without_waiver_fails(tmp_path: Path):
|
|
1110
|
+
proj = create_backend_project(
|
|
1111
|
+
tmp_path,
|
|
1112
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1113
|
+
None,
|
|
1114
|
+
extractor="jest",
|
|
1115
|
+
test_dir_files={
|
|
1116
|
+
"__tests__/refund.spec.js": "test.skip('[AC-TEST-001] 暂不做', () => {});\n",
|
|
1117
|
+
},
|
|
1118
|
+
)
|
|
1119
|
+
res = validate_ac_alignment(proj)
|
|
1120
|
+
assert res.status == "FAIL", res.status
|
|
1121
|
+
check = _check(res, "无效跳过拦截")
|
|
1122
|
+
assert check is not None and check["status"] == "FAIL", check
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def test_backend_vitest_unwaived_describe_skip_fails(tmp_path: Path):
|
|
1126
|
+
proj = create_backend_project(
|
|
1127
|
+
tmp_path,
|
|
1128
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1129
|
+
None,
|
|
1130
|
+
extractor="vitest",
|
|
1131
|
+
test_dir_files={
|
|
1132
|
+
"refund.test.ts": (
|
|
1133
|
+
"describe.skip('group', () => {\n"
|
|
1134
|
+
" it('[AC-TEST-001] 暂不做', () => {});\n"
|
|
1135
|
+
"});\n"
|
|
1136
|
+
),
|
|
1137
|
+
},
|
|
1138
|
+
)
|
|
1139
|
+
res = validate_ac_alignment(proj)
|
|
1140
|
+
assert res.status == "FAIL", res.status
|
|
1141
|
+
check = _check(res, "无效跳过拦截")
|
|
1142
|
+
assert check is not None and "AC-TEST-001" in check["detail"], check
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
def test_backend_junit5_title_ac_and_dims_pass(tmp_path: Path):
|
|
1147
|
+
proj = create_backend_project(
|
|
1148
|
+
tmp_path,
|
|
1149
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1150
|
+
None,
|
|
1151
|
+
extractor="junit5",
|
|
1152
|
+
test_dir_files={
|
|
1153
|
+
"RefundTest.java": (
|
|
1154
|
+
"class RefundTest {\n"
|
|
1155
|
+
" @Test\n"
|
|
1156
|
+
" @DisplayName(\"[AC-TEST-001] 余额不足拒绝\")\n"
|
|
1157
|
+
" @Tag(\"atlas-dims-C01\")\n"
|
|
1158
|
+
" void refundRejected() {}\n"
|
|
1159
|
+
"}\n"
|
|
1160
|
+
),
|
|
1161
|
+
},
|
|
1162
|
+
)
|
|
1163
|
+
res = validate_ac_alignment(proj)
|
|
1164
|
+
assert res.status == "PASS", [(c["check"], c["status"], c["detail"]) for c in res.checks]
|
|
1165
|
+
dim = _check(res)
|
|
1166
|
+
assert dim is not None and dim["status"] == "PASS", dim
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def test_backend_junit5_dangling_ac_fails(tmp_path: Path):
|
|
1170
|
+
proj = create_backend_project(
|
|
1171
|
+
tmp_path,
|
|
1172
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1173
|
+
None,
|
|
1174
|
+
extractor="junit5",
|
|
1175
|
+
test_dir_files={
|
|
1176
|
+
"RefundTest.java": (
|
|
1177
|
+
"class RefundTest {\n"
|
|
1178
|
+
" @Test\n"
|
|
1179
|
+
" @DisplayName(\"[AC-GHOST-999] 假挂载\")\n"
|
|
1180
|
+
" void fake() {}\n"
|
|
1181
|
+
"}\n"
|
|
1182
|
+
),
|
|
1183
|
+
},
|
|
1184
|
+
)
|
|
1185
|
+
res = validate_ac_alignment(proj)
|
|
1186
|
+
assert res.status == "FAIL", res.status
|
|
1187
|
+
check = _check(res, "无主AC拦截")
|
|
1188
|
+
assert check is not None and "AC-GHOST-999" in check["detail"], check
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def test_backend_junit5_disabled_without_waiver_fails(tmp_path: Path):
|
|
1192
|
+
proj = create_backend_project(
|
|
1193
|
+
tmp_path,
|
|
1194
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1195
|
+
None,
|
|
1196
|
+
extractor="junit5",
|
|
1197
|
+
test_dir_files={
|
|
1198
|
+
"RefundTest.java": (
|
|
1199
|
+
"class RefundTest {\n"
|
|
1200
|
+
" @Test\n"
|
|
1201
|
+
" @DisplayName(\"[AC-TEST-001] 暂不做\")\n"
|
|
1202
|
+
" @Disabled(\"later\")\n"
|
|
1203
|
+
" void pending() {}\n"
|
|
1204
|
+
"}\n"
|
|
1205
|
+
),
|
|
1206
|
+
},
|
|
1207
|
+
)
|
|
1208
|
+
res = validate_ac_alignment(proj)
|
|
1209
|
+
assert res.status == "FAIL", res.status
|
|
1210
|
+
check = _check(res, "无效跳过拦截")
|
|
1211
|
+
assert check is not None and check["status"] == "FAIL", check
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
def _external_extractor(path: Path, body: str) -> str:
|
|
1215
|
+
path.write_text(body, encoding="utf-8")
|
|
1216
|
+
return f"{sys.executable} {path}"
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def test_backend_external_command_passes(tmp_path: Path):
|
|
1220
|
+
script = tmp_path / "extract.py"
|
|
1221
|
+
cmd = _external_extractor(script, (
|
|
1222
|
+
"import json\n"
|
|
1223
|
+
"print(json.dumps({'schema_version': 1, 'input_state': 'present', 'tests': [{\n"
|
|
1224
|
+
" 'name': 'T.ac', 'file': 'backend/tests/T.java', 'line': 3,\n"
|
|
1225
|
+
" 'acs': ['AC-TEST-001'], 'marker_acs': ['AC-TEST-001'], 'doc_acs': [],\n"
|
|
1226
|
+
" 'dims': ['C01'], 'dims_bad': [], 'dims_shape_ok': True, 'dims_present': True,\n"
|
|
1227
|
+
" 'skipped': False, 'waiver': '', 'weak': False, 'marker_scope_unsupported': False\n"
|
|
1228
|
+
"}], 'parse_errors': [], 'scan_face': []}))\n"
|
|
1229
|
+
))
|
|
1230
|
+
proj = create_backend_project(
|
|
1231
|
+
tmp_path,
|
|
1232
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1233
|
+
None,
|
|
1234
|
+
extractor_command=cmd,
|
|
1235
|
+
)
|
|
1236
|
+
res = validate_ac_alignment(proj)
|
|
1237
|
+
assert res.status == "PASS", [(c["check"], c["status"], c["detail"]) for c in res.checks]
|
|
1238
|
+
dim = _check(res)
|
|
1239
|
+
assert dim is not None and dim["status"] == "PASS", dim
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def test_backend_external_command_invalid_json_fails(tmp_path: Path):
|
|
1243
|
+
script = tmp_path / "extract.py"
|
|
1244
|
+
cmd = _external_extractor(script, "print('not json')\n")
|
|
1245
|
+
proj = create_backend_project(
|
|
1246
|
+
tmp_path,
|
|
1247
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1248
|
+
None,
|
|
1249
|
+
extractor_command=cmd,
|
|
1250
|
+
)
|
|
1251
|
+
res = validate_ac_alignment(proj)
|
|
1252
|
+
assert res.status == "FAIL", res.status
|
|
1253
|
+
check = _check(res, "测试提取通道")
|
|
1254
|
+
assert check is not None and "[EXTRACTOR]" in check["detail"], check
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def test_backend_external_command_nonzero_fails(tmp_path: Path):
|
|
1258
|
+
script = tmp_path / "extract.py"
|
|
1259
|
+
cmd = _external_extractor(script, "import sys\nsys.stderr.write('bad\\n')\nsys.exit(3)\n")
|
|
1260
|
+
proj = create_backend_project(
|
|
1261
|
+
tmp_path,
|
|
1262
|
+
[("`AC-TEST-001`", "Given x When y Then z", "`R-001`", "", "C01")],
|
|
1263
|
+
None,
|
|
1264
|
+
extractor_command=cmd,
|
|
1265
|
+
)
|
|
1266
|
+
res = validate_ac_alignment(proj)
|
|
1267
|
+
assert res.status == "FAIL", res.status
|
|
1268
|
+
check = _check(res, "测试提取通道")
|
|
1269
|
+
assert check is not None and "退出码 3" in check["detail"], check
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
def test_backend_junit5_multi_module_test_dir_root(tmp_path: Path):
|
|
1273
|
+
"""多模块 Maven/Gradle:单 app + test_dir: . 时递归盘点各模块测试。"""
|
|
1274
|
+
proj = create_backend_project(
|
|
1275
|
+
tmp_path,
|
|
1276
|
+
[
|
|
1277
|
+
("`AC-MODA-001`", "Given x When y Then z", "`R-001`", "", "C01"),
|
|
1278
|
+
("`AC-MODB-001`", "Given x When y Then z", "`R-002`", "", "C01"),
|
|
1279
|
+
],
|
|
1280
|
+
None,
|
|
1281
|
+
extractor="junit5",
|
|
1282
|
+
)
|
|
1283
|
+
profile = proj / "product" / "stack-profile.yaml"
|
|
1284
|
+
profile.write_text(
|
|
1285
|
+
profile.read_text(encoding="utf-8").replace("test_dir: backend/tests", "test_dir: ."),
|
|
1286
|
+
encoding="utf-8",
|
|
1287
|
+
)
|
|
1288
|
+
for module, ac in (("module-a", "AC-MODA-001"), ("module-b", "AC-MODB-001")):
|
|
1289
|
+
d = proj / module / "src" / "test" / "java" / "com" / "x"
|
|
1290
|
+
d.mkdir(parents=True)
|
|
1291
|
+
(d / f"{module.title()}Test.java").write_text(
|
|
1292
|
+
"class T {\n"
|
|
1293
|
+
" @Test\n"
|
|
1294
|
+
f" @DisplayName(\"[{ac}] case\")\n"
|
|
1295
|
+
" @Tag(\"atlas-dims-C01\")\n"
|
|
1296
|
+
" void t() {}\n"
|
|
1297
|
+
"}\n",
|
|
1298
|
+
encoding="utf-8",
|
|
1299
|
+
)
|
|
1300
|
+
res = validate_ac_alignment(proj)
|
|
1301
|
+
assert res.status == "PASS", [(c["check"], c["status"], c["detail"]) for c in res.checks]
|
|
1302
|
+
cov = _check(res, "AC覆盖对账")
|
|
1303
|
+
assert cov is not None and "覆盖: 2" in cov["detail"], cov
|