2026-04-20 21:39:53 -03:00
|
|
|
"""Unit tests for the repository testing contract helpers."""
|
|
|
|
|
|
2026-04-10 17:06:53 -03:00
|
|
|
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
|
fix(hermes): close the zero-evidence fail-open in absence checks
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>
2026-08-17 16:24:37 +00:00
|
|
|
from testing.quality_hygiene import run_check as run_hygiene_check
|
2026-04-10 17:06:53 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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"]
|
|
|
|
|
|
|
|
|
|
|
2026-08-17 13:11:34 +00:00
|
|
|
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")
|
|
|
|
|
}
|
fix(hermes): close the zero-evidence fail-open in absence checks
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>
2026-08-17 16:24:37 +00:00
|
|
|
assert len(expected) == 16
|
2026-08-17 13:11:34 +00:00
|
|
|
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"]
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 17:06:53 -03:00
|
|
|
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 = {
|
2026-08-17 13:11:34 +00:00
|
|
|
"required_docs": [
|
|
|
|
|
{"path": "README.md", "description": ""},
|
|
|
|
|
{"path": "missing.md", "description": "Missing"},
|
|
|
|
|
],
|
2026-04-10 17:06:53 -03:00
|
|
|
"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()
|
2026-08-17 13:11:34 +00:00
|
|
|
(tests_dir / "conftest.py").write_text("value = 1\n", encoding="utf-8")
|
2026-04-10 17:06:53 -03:00
|
|
|
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)
|
2026-08-17 13:11:34 +00:00
|
|
|
assert any(
|
|
|
|
|
"naming rule failed" in issue and "bad-name.py" in issue for issue in issues
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 17:06:53 -03:00
|
|
|
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>
|
2026-08-17 15:10:17 -03:00
|
|
|
<class filename="ok.py" line-rate="1.0" branch-rate="1.0" />
|
|
|
|
|
<class filename="low.py" line-rate="0.90" branch-rate="1.0" />
|
|
|
|
|
<class filename="weak_branches.py" line-rate="1.0" branch-rate="0.90" />
|
2026-04-10 17:06:53 -03:00
|
|
|
</classes>
|
|
|
|
|
</package>
|
|
|
|
|
</packages>
|
|
|
|
|
</coverage>
|
|
|
|
|
"""
|
|
|
|
|
),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
contract = {
|
|
|
|
|
"coverage": {
|
|
|
|
|
"minimum_percent": 95.0,
|
2026-08-17 15:10:17 -03:00
|
|
|
"tracked_files": ["ok.py", "low.py", "weak_branches.py", "missing.py"],
|
2026-04-10 17:06:53 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issues = run_coverage_check(contract, tmp_path, coverage_xml)
|
|
|
|
|
|
2026-08-17 15:10:17 -03:00
|
|
|
assert "line coverage below 95.0%: low.py (90.0%)" in issues
|
|
|
|
|
assert "branch coverage below 95.0%: weak_branches.py (90.0%)" in issues
|
2026-04-10 17:06:53 -03:00
|
|
|
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"
|
2026-08-17 13:11:34 +00:00
|
|
|
assert run_coverage_check(
|
|
|
|
|
{"coverage": {"tracked_files": []}}, tmp_path, missing_xml
|
|
|
|
|
) == ["coverage xml missing: missing.xml"]
|
2026-04-10 17:06:53 -03:00
|
|
|
|
|
|
|
|
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>
|
2026-08-17 15:10:17 -03:00
|
|
|
<class filename="mapped.py" line-rate="1.0" branch-rate="1.0" />
|
|
|
|
|
<class filename="{(tmp_path / 'absolute.py').as_posix()}" line-rate="1.0" branch-rate="1.0" />
|
2026-04-10 17:06:53 -03:00
|
|
|
<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 == []
|