evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
191 lines
6.4 KiB
Python
191 lines
6.4 KiB
Python
"""Unit tests for the repository testing contract helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import textwrap
|
|
|
|
from testing.quality_contract import load_contract
|
|
from testing.quality_coverage import run_check as run_coverage_check
|
|
from testing.quality_docs import run_check as run_docs_check
|
|
from testing.quality_hygiene import run_check as run_hygiene_check
|
|
|
|
|
|
def test_bundled_contract_exposes_local_and_jenkins_profiles():
|
|
contract = load_contract()
|
|
assert "local" in contract["profiles"]
|
|
assert "jenkins" in contract["profiles"]
|
|
assert contract["pytest_suites"]["unit"]["paths"]
|
|
|
|
|
|
def test_handoff_modules_are_exactly_managed_linted_and_covered():
|
|
"""The canonical contract must not silently drop a handoff module."""
|
|
contract = load_contract()
|
|
expected = {
|
|
path.as_posix() for path in Path("scripts/ops").glob("hermes_handoff_*.py")
|
|
}
|
|
assert len(expected) == 16
|
|
assert expected <= set(contract["managed_modules"])
|
|
assert expected <= set(contract["lint_paths"])
|
|
assert expected <= set(contract["coverage"]["tracked_files"])
|
|
assert expected <= set(contract["coverage"]["branch_tracked_files"])
|
|
assert "scripts/ops" in contract["pytest_suites"]["unit"]["coverage_sources"]
|
|
assert "scripts/ops/hermes_handoff_*.py" in contract["hygiene"]["line_limit_globs"]
|
|
|
|
|
|
def test_docs_check_reports_missing_docstring_and_missing_path(tmp_path: Path):
|
|
module_path = tmp_path / "managed.py"
|
|
module_path.write_text("value = 1\n", encoding="utf-8")
|
|
(tmp_path / "README.md").write_text("repo docs\n", encoding="utf-8")
|
|
|
|
contract = {
|
|
"required_docs": [{"path": "README.md", "description": "Docs"}],
|
|
"managed_modules": ["managed.py"],
|
|
"lint_paths": ["missing-dir"],
|
|
"pytest_suites": {"unit": {"description": "Unit", "paths": ["missing-tests"]}},
|
|
"manual_scripts": [{"path": "missing-script.py", "description": "Manual"}],
|
|
}
|
|
|
|
issues = run_docs_check(contract, tmp_path)
|
|
|
|
assert "module docstring missing: managed.py" in issues
|
|
assert "contract path missing: missing-dir" in issues
|
|
assert "contract path missing: missing-tests" in issues
|
|
assert "contract path missing: missing-script.py" in issues
|
|
|
|
|
|
def test_docs_check_reports_missing_required_doc_metadata(tmp_path: Path):
|
|
(tmp_path / "README.md").write_text("", encoding="utf-8")
|
|
|
|
contract = {
|
|
"required_docs": [
|
|
{"path": "README.md", "description": ""},
|
|
{"path": "missing.md", "description": "Missing"},
|
|
],
|
|
"managed_modules": [],
|
|
"lint_paths": [],
|
|
"pytest_suites": {"unit": {"description": "", "paths": []}},
|
|
"manual_scripts": [{"path": "manual.py", "description": ""}],
|
|
}
|
|
|
|
issues = run_docs_check(contract, tmp_path)
|
|
|
|
assert "required doc empty: README.md" in issues
|
|
assert "required doc missing description: README.md" in issues
|
|
assert "required doc missing: missing.md" in issues
|
|
assert "pytest suite missing description: unit" in issues
|
|
assert "manual script missing description: manual.py" in issues
|
|
|
|
|
|
def test_hygiene_check_enforces_line_limit_and_name_rules(tmp_path: Path):
|
|
tests_dir = tmp_path / "tests"
|
|
tests_dir.mkdir()
|
|
(tests_dir / "conftest.py").write_text("value = 1\n", encoding="utf-8")
|
|
bad_name = tests_dir / "bad-name.py"
|
|
bad_name.write_text("x = 1\n", encoding="utf-8")
|
|
long_file = tests_dir / "test_too_long.py"
|
|
long_file.write_text("line\n" * 4, encoding="utf-8")
|
|
|
|
contract = {
|
|
"hygiene": {
|
|
"max_lines": 3,
|
|
"line_limit_globs": ["tests/*.py"],
|
|
"naming_rules": [
|
|
{
|
|
"glob": "tests/*.py",
|
|
"pattern": r"^test_[a-z0-9_]+\.py$",
|
|
"description": "pytest files use test_*.py names.",
|
|
}
|
|
],
|
|
}
|
|
}
|
|
|
|
issues = run_hygiene_check(contract, tmp_path)
|
|
|
|
assert any("file exceeds 3 LOC" in issue for issue in issues)
|
|
assert any(
|
|
"naming rule failed" in issue and "bad-name.py" in issue for issue in issues
|
|
)
|
|
|
|
|
|
def test_coverage_check_enforces_per_file_floor(tmp_path: Path):
|
|
build_dir = tmp_path / "build"
|
|
build_dir.mkdir()
|
|
coverage_xml = build_dir / "coverage.xml"
|
|
coverage_xml.write_text(
|
|
textwrap.dedent(
|
|
"""\
|
|
<coverage>
|
|
<packages>
|
|
<package>
|
|
<classes>
|
|
<class filename="ok.py" line-rate="1.0" />
|
|
<class filename="low.py" line-rate="0.90" />
|
|
</classes>
|
|
</package>
|
|
</packages>
|
|
</coverage>
|
|
"""
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
contract = {
|
|
"coverage": {
|
|
"minimum_percent": 95.0,
|
|
"tracked_files": ["ok.py", "low.py", "missing.py"],
|
|
}
|
|
}
|
|
|
|
issues = run_coverage_check(contract, tmp_path, coverage_xml)
|
|
|
|
assert "coverage below 95.0%: low.py (90.0%)" in issues
|
|
assert "coverage missing for tracked file: missing.py" in issues
|
|
|
|
|
|
def test_coverage_check_handles_missing_xml_and_source_root_mapping(tmp_path: Path):
|
|
missing_xml = tmp_path / "missing.xml"
|
|
assert run_coverage_check(
|
|
{"coverage": {"tracked_files": []}}, tmp_path, missing_xml
|
|
) == ["coverage xml missing: missing.xml"]
|
|
|
|
source_dir = tmp_path / "pkg"
|
|
source_dir.mkdir()
|
|
(source_dir / "mapped.py").write_text("value = 1\n", encoding="utf-8")
|
|
coverage_xml = tmp_path / "coverage.xml"
|
|
coverage_xml.write_text(
|
|
textwrap.dedent(
|
|
f"""\
|
|
<coverage>
|
|
<sources>
|
|
<source>{source_dir}</source>
|
|
</sources>
|
|
<packages>
|
|
<package>
|
|
<classes>
|
|
<class filename="mapped.py" line-rate="1.0" />
|
|
<class filename="{(tmp_path / "absolute.py").as_posix()}" line-rate="1.0" />
|
|
<class filename="skip.py" />
|
|
</classes>
|
|
</package>
|
|
</packages>
|
|
</coverage>
|
|
"""
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(tmp_path / "absolute.py").write_text("value = 2\n", encoding="utf-8")
|
|
|
|
issues = run_coverage_check(
|
|
{
|
|
"coverage": {
|
|
"minimum_percent": 95.0,
|
|
"tracked_files": ["pkg/mapped.py", "absolute.py"],
|
|
}
|
|
},
|
|
tmp_path,
|
|
coverage_xml,
|
|
)
|
|
|
|
assert issues == []
|