Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a96dc49
test: harden package removal checks and portable runner (#275, #276)
codeasier Nov 1, 2026
f59fe08
test: add bounded generated pagination and validation properties (#280)
codeasier Nov 1, 2026
d98c95b
test: keep runner fixture defaults and package formatting lint-clean
codeasier Nov 1, 2026
7210555
test: enforce semantic CI gates and visible browser execution (#278, …
codeasier Nov 1, 2026
a45365a
test: guard critical publication and rollback coverage independently …
codeasier Nov 1, 2026
9da523d
test: resolve the current lazy snapshot loader in each property example
codeasier Nov 1, 2026
8432100
test: keep retired-package fixtures independent of historical branding
codeasier Nov 1, 2026
de04a9e
test: close coverage evidence connections on corrupt-data failures
codeasier Nov 1, 2026
2f3c171
test: integrate accepted CI foundation into package and runner regres…
codeasier Nov 1, 2026
9be1dd8
test: integrate accepted CI foundation into generated property regres…
codeasier Nov 1, 2026
5b6cbca
test: validate the planned package-runner and property test integrati…
codeasier Nov 1, 2026
5db7f98
fix: close failed focus checkpoints before verified cleanup
codeasier Nov 1, 2026
203950a
test: scope checkpoint identity failure injection to owned descriptor
codeasier Nov 1, 2026
8df9428
fix: normalize malformed YAML and snapshot input failures
codeasier Nov 1, 2026
3fbab2a
fix: validate declared block states with precise literal types
codeasier Nov 1, 2026
14404c7
Merge planned runtime repair stack for critical coverage integration …
codeasier Nov 1, 2026
a35dfb4
test: isolate critical coverage from unmerged input validation
codeasier Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/scripts/check_critical_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Fail closed when a critical module loses measured line/branch coverage."""

from __future__ import annotations

import io
import sys
from pathlib import Path

from coverage import Coverage, CoverageData
from coverage.exceptions import CoverageException

# These are independent floors, not an average that another module can offset.
# Keep the original three floors unchanged when extending the risk inventory.
CRITICAL_FLOORS = {
"src/pt_snap_cli/core/snapshot_import_backend.py": 90,
"src/pt_snap_cli/snapshot/simulate/simulated_caching_allocator.py": 88,
"src/pt_snap_cli/snapshot/simulate/snapshot_mutator.py": 84,
"src/pt_snap_cli/core/import_metadata_contract.py": 100,
"src/pt_snap_cli/core/import_metadata.py": 90,
"src/pt_snap_cli/core/split_service.py": 90,
"src/pt_snap_cli/core/focus_service.py": 90,
"src/pt_snap_cli/core/skill_service.py": 90,
"src/pt_snap_cli/core/snapshot_cache.py": 90,
"src/pt_snap_cli/snapshot/representation.py": 95,
}


class CoverageGuardError(ValueError):
"""Coverage evidence is absent, invalid, or below an independent floor."""


def check_coverage(root: Path) -> None:
root = root.resolve()
if not callable(getattr(CoverageData, "close", None)):
raise CoverageGuardError("Critical coverage requires coverage>=7.10.3; install .[dev]")
data_file = root / ".coverage"
if not data_file.is_file():
raise CoverageGuardError(f"Missing coverage data: {data_file}")
data: CoverageData | None = None
try:
coverage = Coverage(data_file=str(data_file), config_file=str(root / "pyproject.toml"))
# Own the data before load(): a corrupt SQLite file can fail inside
# coverage.py's connection __enter__, before its normal cleanup runs.
data = coverage.get_data()
coverage.load()
if not data.has_arcs():
raise CoverageGuardError("Critical coverage requires branch data, not line-only data")
measured = {Path(name).resolve() for name in data.measured_files()}
failures = []
for relative, floor in CRITICAL_FLOORS.items():
source = root / relative
if not source.is_file():
failures.append(f"{relative}: missing critical source file")
continue
if source not in measured:
failures.append(f"{relative}: missing measured coverage data")
continue
if not coverage.analysis2(str(source))[1]:
failures.append(f"{relative}: no executable statements")
continue
# report() returns the unrounded line+branch percentage. Never compare
# the rounded terminal percentage, which could hide a regression.
percent = coverage.report(
morfs=[str(source)],
file=io.StringIO(),
ignore_errors=False,
skip_empty=False,
skip_covered=False,
)
print(f"{relative}: {percent:.4f}% (required {floor}%)")
if percent < floor:
failures.append(f"{relative}: {percent:.4f}% is below {floor}%")
if failures:
raise CoverageGuardError("\n".join(failures))
except (CoverageException, OSError) as error:
raise CoverageGuardError(f"Cannot validate critical coverage: {error}") from error
finally:
if data is not None:
data.close()


def main() -> int:
try:
check_coverage(Path(__file__).resolve().parents[2])
except CoverageGuardError as error:
print(f"Critical coverage gate failed:\n{error}", file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
30 changes: 29 additions & 1 deletion .github/workflows/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,41 @@ Concurrency is keyed by GitHub ref and superseded runs are cancelled.
| Compatibility matrix | Python 3.10–3.12; fixture provenance first, then `pytest -m "not slow and not nightly"` |
| Full PR test tier | Python 3.13; `pytest -m "not nightly"` with branch coverage, preserving the existing slow tests |
| Overall coverage | `pyproject.toml` fail-under 90 with branches enabled |
| Critical runtime coverage | Import backend 90; simulated allocator 88; snapshot mutator 84, checked independently |
| Critical runtime coverage | Ten independent line+branch floors, enforced by the fail-closed checker below |
| Python 3.13 packaging | Upload coverage, build wheel, run offline clean-wheel skill/CLI acceptance including both sharded formats |
| Nightly/manual dataset acceptance | Python 3.13; provenance, wheel build and audit, then `nightly` tests including the required installed-wheel CLI/API test |

Million-event deep-offset pagination tests are marked `slow`; their small
pagination contracts still run across the compatibility matrix.

### Independent critical-file coverage

After full Python 3.13 coverage, `check_critical_coverage.py` enforces this complete
inventory. A high average cannot compensate for a single file below its floor.

| Runtime module (`src/pt_snap_cli/`) | Minimum line + branch coverage |
| --- | --- |
| `core/snapshot_import_backend.py` | 90% |
| `snapshot/simulate/simulated_caching_allocator.py` | 88% |
| `snapshot/simulate/snapshot_mutator.py` | 84% |
| `core/import_metadata_contract.py` | 100% |
| `core/import_metadata.py` | 90% |
| `core/split_service.py` | 90% |
| `core/focus_service.py` | 90% |
| `core/skill_service.py` | 90% |
| `core/snapshot_cache.py` | 90% |
| `snapshot/representation.py` | 95% |

The checker requires `.coverage`, branch data, every listed source file, measured
coverage for each exact path, and executable statements. Missing/corrupt evidence
fails; percentages are compared before display rounding. It has no selector or
floor overrides. The overall 90% gate remains separate and unchanged. CI contract
mutation tests reject an omitted/conditional checker, alternate coverage commands,
ignored failures, reordered execution, and narrowed source/omit/exclusion settings;
`tests/test_critical_coverage_gate.py` separately protects the exact inventory and
exercises real coverage-data success/failure cases. Raise floors only with actual
CI evidence; do not delete code, add exclusions, or weaken assertions to meet them.

`tests/test_ci_tiers.py` checks parsed commands, matrix rows, marker truth tables,
conditions, independent file floors and gate ordering, not step display names or
complete shell text. `tests/_ci_contract.py` intentionally supports only a bounded
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,7 @@ jobs:
- name: Guard critical runtime coverage
if: matrix.python-version == '3.13'
# Separate floors prevent strong modules from masking a weak boundary.
run: |
python -m coverage report --include='*/core/snapshot_import_backend.py' --fail-under=90
python -m coverage report --include='*/simulate/simulated_caching_allocator.py' --fail-under=88
python -m coverage report --include='*/simulate/snapshot_mutator.py' --fail-under=84
run: python .github/scripts/check_critical_coverage.py

- name: Upload coverage report
uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Issues = "https://github.com/codeasier/pt-snap-cli/issues"
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
# The coverage gate owns/explicitly closes CoverageData, including bad SQLite input.
"coverage[toml]>=7.10.3",
"hypothesis>=6.100.0,<7",
"playwright>=1.40.0",
"black>=23.0.0",
Expand Down
Loading
Loading