Introduce a deterministic (no-LLM) coverage sub-gate that blocks coverage degradation before a task branch merges into `main`. Existing gates judge only by the FACT of passing (check_ci_green / check_tests_passed / merge-gate re-test), not by completeness — so a batch autonomous run (ORCH-088) silently erodes coverage. Pattern mirrors the security-gate (ORCH-022): leaf src/coverage_gate.py (never-raise) + thin check_coverage_gate in QG_CHECKS + _handle_coverage_gate splice in advance_stage, run AFTER merge-gate (measured on the caught-up HEAD that lands in main) and BEFORE image-freshness (fail before the expensive docker rebuild). - measure_coverage: pytest --cov=src --cov-report=json in the per-branch worktree -> line coverage %; None on tool error -> fail-open + WARNING by default (FR-6). - compute_coverage_verdict (pure): absolute | baseline | both + epsilon (NFR-4 anti-flap); baseline None -> bootstrap (absolute-only). - coverage_baseline DB table (additive, CREATE TABLE IF NOT EXISTS) + ratchet-up in _handle_merge_verify (deploy->done): atomic compare-and-set under merge-lease, never decreases; bootstrap on first merge. - Artefact 18-coverage-report.md (coverage_status: frontmatter, single source of truth); GET /queue `coverage` block; FAIL -> Telegram; optional POST /coverage/baseline override. - Flags ORCH_COVERAGE_* (kill-switch + self-hosting-only scope) -> enduro untouched; STAGE_TRANSITIONS / existing check_* / verdict keys byte-for-byte unchanged (NFR-5/AC-8). - pytest-cov==5.0.0 added to requirements.txt. Tests: tests/test_coverage_gate.py (TC-01..TC-15). Frozen QG-registry anti-regress tests + deploy-staging edge tests updated for the new sub-gate. Full suite green. Docs: README / adr-0029 / PIPELINE_DOCS / 18-coverage-report.md template (architecture stage) + CHANGELOG / CLAUDE.md / .env.example (this PR). Refs: ORCH-027 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
3.2 KiB
Python
72 lines
3.2 KiB
Python
"""ORCH-016 / TC-20 + AC-11: Quality Gates + stage machine are unchanged.
|
|
|
|
Smoke / change-detector test: the ORCH-016 PR touches comment formatting only.
|
|
The QG registry (src/qg/checks.QG_CHECKS) and the stage-machine table
|
|
(src/stages.STAGE_TRANSITIONS) MUST remain bit-identical to the contracts the
|
|
pipeline depends on. If a future change moves the comment hot path into these
|
|
files by accident, this guard breaks first.
|
|
"""
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
|
|
from src.qg.checks import QG_CHECKS # noqa: E402
|
|
from src.stages import STAGE_TRANSITIONS # noqa: E402
|
|
|
|
|
|
# The set of QG names the pipeline DEPLOYS on. Order doesn't matter, identity does.
|
|
_EXPECTED_QGS = {
|
|
"check_analysis_approved",
|
|
"check_analysis_complete",
|
|
"check_architecture_done",
|
|
"check_ci_green",
|
|
"check_review_approved",
|
|
"check_tests_passed",
|
|
"check_reviewer_verdict",
|
|
"check_tests_local",
|
|
"check_deploy_status",
|
|
"check_staging_status",
|
|
"check_branch_mergeable", # ORCH-043 merge-gate (deploy-staging -> deploy edge)
|
|
"check_staging_image_fresh", # ORCH-058 image-freshness sub-gate (same edge)
|
|
"check_security_gate", # ORCH-022 security sub-gate (same edge, run FIRST)
|
|
"check_coverage_gate", # ORCH-027 coverage sub-gate (same edge, after merge-gate)
|
|
}
|
|
|
|
|
|
def test_tc20_qg_registry_unchanged():
|
|
assert set(QG_CHECKS.keys()) == _EXPECTED_QGS
|
|
|
|
|
|
def test_tc20_qg_callables_unchanged():
|
|
# All entries must be callable — no stub / lambda / None.
|
|
for name, fn in QG_CHECKS.items():
|
|
assert callable(fn), f"QG {name} is not callable"
|
|
|
|
|
|
# Reference snapshot of STAGE_TRANSITIONS (mirrors what's in docs/architecture
|
|
# and src/stages.py — duplicated here on purpose as a regression yardstick).
|
|
_EXPECTED_TRANSITIONS = {
|
|
"created": {"next": "analysis", "agent": "analyst", "qg": None},
|
|
"analysis": {"next": "architecture", "agent": "architect", "qg": "check_analysis_approved"},
|
|
"architecture": {"next": "development", "agent": "developer", "qg": "check_architecture_done"},
|
|
"development": {"next": "review", "agent": "reviewer", "qg": "check_ci_green"},
|
|
"review": {"next": "testing", "agent": "tester", "qg": "check_reviewer_verdict"},
|
|
"testing": {"next": "deploy-staging", "agent": "deployer", "qg": "check_tests_passed"},
|
|
"deploy-staging": {"next": "deploy", "agent": "deployer", "qg": "check_staging_status"},
|
|
"deploy": {"next": "done", "agent": None, "qg": "check_deploy_status"},
|
|
"done": {"next": None, "agent": None, "qg": None},
|
|
# ORCH-090 (adr-0026): terminal SINK for a STOP-cancelled task (parallel to
|
|
# `done`; not a new edge — no exit-gate changed).
|
|
"cancelled": {"next": None, "agent": None, "qg": None},
|
|
}
|
|
|
|
|
|
def test_tc20_stage_transitions_unchanged():
|
|
assert STAGE_TRANSITIONS == _EXPECTED_TRANSITIONS, (
|
|
"STAGE_TRANSITIONS drift detected — ORCH-016 must not change the "
|
|
"stage machine. Touched stage_engine or stages.py? Update the snapshot "
|
|
"in a separate, intentional PR."
|
|
)
|