Skip to content

Commit a429edb

Browse files
authored
ci: enforce targeted coverage floors (#246) (#256)
Fixes #246
1 parent bab60eb commit a429edb

8 files changed

Lines changed: 304 additions & 1 deletion

File tree

‎.github/workflows/tests.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ jobs:
6969
python scripts/benchmark_runtime.py --check
7070
python -m compileall -q examples
7171
- name: Run tests with coverage threshold
72-
run: python -m pytest --cov=base_cli --cov-report=term-missing --cov-fail-under=80
72+
run: python -m pytest --cov=base_cli --cov-report=term-missing --cov-report=json:coverage.json --cov-fail-under=80
73+
- name: Enforce high-risk module coverage floors
74+
run: python scripts/validate_coverage.py coverage.json
7375
- name: Run static security checks
7476
run: |
7577
bandit -q -r lib/python/base_cli scripts -lll -iii

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ __pycache__/
66
dist/
77
build/
88
*.egg-info/
9+
coverage.json

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and versions are tracked in the repo-root `VERSION` file.
1313

1414
### Changed
1515

16+
- Add branch-aware coverage reporting with documented floors for lifecycle,
17+
filesystem, compatibility, history, and contract modules.
1618
- Decompose the application implementation behind a compatibility facade into
1719
focused core, lifecycle-installation, attachment, and invocation-runner
1820
modules without changing the public import surface.

‎docs/coverage-policy.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Coverage policy
2+
3+
The aggregate test gate requires at least 80% branch-aware coverage. The
4+
following high-risk modules also have explicit floors because regressions in
5+
these paths affect filesystem safety, lifecycle attachment, or machine-facing
6+
contracts:
7+
8+
| Module | Floor |
9+
| --- | ---: |
10+
| `base_cli._attach` | 75% |
11+
| `base_cli._click_compat` | 80% |
12+
| `base_cli._private_files` | 75% |
13+
| `base_cli._runtime` | 80% |
14+
| `base_cli.command_protocol` | 85% |
15+
| `base_cli.history` | 75% |
16+
| `base_cli.redaction` | 90% |
17+
18+
CI writes a branch-aware `coverage.json` report and runs
19+
`scripts/validate_coverage.py` against these floors. A floor change belongs in
20+
the same pull request as the tests that justify it. Platform-specific tests
21+
must cover both the native path and the safe fallback where the operating
22+
system provides a different filesystem primitive.

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ nav:
4949
- Typer adapter: typer-adapter.md
5050
- Optional integrations: integrations.md
5151
- Operations and security:
52+
- Coverage policy: coverage-policy.md
5253
- Local configuration: local-config.md
5354
- Cache ownership and layout: cache-ownership-and-layout.md
5455
- Performance: performance.md

‎scripts/validate_coverage.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env python3
2+
"""Enforce coverage floors for security- and contract-sensitive modules."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import sys
9+
from pathlib import Path
10+
from typing import Any, NoReturn
11+
12+
# These modules sit on filesystem, lifecycle, and machine-contract boundaries.
13+
# Their floors are intentionally stricter than the aggregate project gate.
14+
MODULE_FLOORS: dict[str, float] = {
15+
"lib/python/base_cli/_attach.py": 75.0,
16+
"lib/python/base_cli/_click_compat.py": 80.0,
17+
"lib/python/base_cli/_private_files.py": 75.0,
18+
"lib/python/base_cli/_runtime.py": 80.0,
19+
"lib/python/base_cli/command_protocol.py": 85.0,
20+
"lib/python/base_cli/history.py": 75.0,
21+
"lib/python/base_cli/redaction.py": 90.0,
22+
}
23+
24+
25+
def fail(message: str) -> NoReturn:
26+
print(f"coverage policy failed: {message}", file=sys.stderr)
27+
raise SystemExit(1)
28+
29+
30+
def _file_summary(files: dict[str, Any], expected_path: str) -> dict[str, Any]:
31+
for path, payload in files.items():
32+
if path == expected_path or path.endswith(expected_path):
33+
summary = payload.get("summary")
34+
if isinstance(summary, dict):
35+
return summary
36+
fail(f"coverage report is missing {expected_path}")
37+
38+
39+
def validate(report_path: Path) -> None:
40+
try:
41+
report = json.loads(report_path.read_text(encoding="utf-8"))
42+
except (OSError, json.JSONDecodeError) as exc:
43+
fail(f"cannot read {report_path}: {exc}")
44+
files = report.get("files") if isinstance(report, dict) else None
45+
if not isinstance(files, dict):
46+
fail(f"{report_path} does not contain a coverage 'files' mapping")
47+
48+
failures: list[str] = []
49+
for path, floor in MODULE_FLOORS.items():
50+
summary = _file_summary(files, path)
51+
covered = summary.get("percent_covered")
52+
if not isinstance(covered, (int, float)):
53+
failures.append(f"{path}: missing percent_covered")
54+
elif covered < floor:
55+
failures.append(f"{path}: {covered:.2f}% < {floor:.2f}%")
56+
if failures:
57+
fail("; ".join(failures))
58+
59+
for path, floor in MODULE_FLOORS.items():
60+
summary = _file_summary(files, path)
61+
print(f"{path}: {summary['percent_covered']:.2f}% (floor {floor:.2f}%)")
62+
63+
64+
def main() -> None:
65+
parser = argparse.ArgumentParser(description=__doc__)
66+
parser.add_argument("report", type=Path, help="coverage.py JSON report")
67+
args = parser.parse_args()
68+
validate(args.report)
69+
70+
71+
if __name__ == "__main__":
72+
main()

‎scripts/validate_docs.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"api-stability.md",
1919
"cache-ownership-and-layout.md",
2020
"consumer-profiles.md",
21+
"coverage-policy.md",
2122
"dependency-support.md",
2223
"extensions.md",
2324
"framework-choice.md",

‎tests/test_platform_edge_paths.py‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from __future__ import annotations
2+
3+
import tempfile
4+
import types
5+
import unittest
6+
from datetime import datetime, timezone
7+
from pathlib import Path
8+
from unittest import mock
9+
10+
import base_cli._click_compat as click_compat
11+
import base_cli._private_files as private_files
12+
import base_cli._runtime as runtime
13+
from base_cli import history
14+
from base_cli._attach import (
15+
_click_command_has_pending_children,
16+
_normalize_attached_option_declaration,
17+
_normalize_sensitive_parameters,
18+
_restore_attached_click_command,
19+
_restore_attached_click_main,
20+
_selected_click_path,
21+
_selected_click_paths,
22+
)
23+
24+
25+
class PrivateFileEdgeTests(unittest.TestCase):
26+
def test_permission_helpers_skip_posix_mode_changes_on_windows(self) -> None:
27+
with tempfile.TemporaryDirectory() as tmpdir:
28+
path = Path(tmpdir) / "file"
29+
path.write_text("payload", encoding="utf-8")
30+
with mock.patch.object(private_files.os, "name", "nt"):
31+
private_files.restrict_file(path)
32+
private_files.restrict_directory(path.parent)
33+
34+
def test_sync_directory_tolerates_filesystems_without_directory_fsync(self) -> None:
35+
with mock.patch.object(private_files.os, "fsync", side_effect=OSError("unsupported")):
36+
private_files._sync_directory(123) # pylint: disable=protected-access
37+
38+
def test_windows_replace_retries_transient_sharing_failure(self) -> None:
39+
with tempfile.TemporaryDirectory() as tmpdir:
40+
source = Path(tmpdir) / "source"
41+
destination = Path(tmpdir) / "destination"
42+
source.write_text("payload", encoding="utf-8")
43+
with (
44+
mock.patch.object(private_files.os, "name", "nt"),
45+
mock.patch.object(
46+
private_files.os,
47+
"replace",
48+
side_effect=[PermissionError("busy"), lambda src, dst: Path(dst).write_text(Path(src).read_text())],
49+
) as replace,
50+
mock.patch.object(private_files.time, "sleep") as sleep,
51+
):
52+
private_files._replace_with_retry(source, destination) # pylint: disable=protected-access
53+
self.assertEqual(replace.call_count, 2)
54+
sleep.assert_called_once()
55+
56+
def test_parent_directory_open_is_disabled_on_windows(self) -> None:
57+
path = Path("/tmp")
58+
with mock.patch.object(private_files.os, "name", "nt"):
59+
self.assertIsNone(private_files._open_parent_directory(path)) # pylint: disable=protected-access
60+
61+
62+
class ClickCompatibilityEdgeTests(unittest.TestCase):
63+
def test_dialect_falls_back_to_public_click_without_typer(self) -> None:
64+
with mock.patch.dict("sys.modules", {"typer": None}):
65+
command = object()
66+
self.assertIs(click_compat.dialect_for_command(command), __import__("click"))
67+
self.assertFalse(click_compat.is_command(command))
68+
69+
def test_vendored_dialect_requires_a_click_module(self) -> None:
70+
self.assertIsNone(click_compat._vendored_typer_dialect(types.SimpleNamespace())) # pylint: disable=protected-access
71+
self.assertIs(click_compat.dialect_for_typer(types.SimpleNamespace()), __import__("click"))
72+
73+
def test_marking_an_immutable_command_is_best_effort(self) -> None:
74+
class Immutable:
75+
__slots__ = ()
76+
77+
command = Immutable()
78+
self.assertIs(click_compat.mark_command_dialect(command, object()), command)
79+
80+
def test_vendor_version_option_requires_a_version(self) -> None:
81+
decorator = click_compat._vendor_version_option_factory(lambda **_: None, lambda *_args, **_kwargs: None) # pylint: disable=protected-access
82+
with self.assertRaisesRegex(RuntimeError, "version is required"):
83+
decorator()
84+
85+
86+
class RuntimeEdgeTests(unittest.TestCase):
87+
def test_owned_runtime_directory_collision_is_not_claimed(self) -> None:
88+
with tempfile.TemporaryDirectory() as tmpdir:
89+
path = Path(tmpdir) / "runs" / "run"
90+
path.mkdir(parents=True)
91+
with self.assertRaises(runtime.RuntimeDirectoryError):
92+
runtime.create_owned_runtime_directory(path, Path(tmpdir))
93+
94+
def test_owned_runtime_directory_uses_portable_fallback(self) -> None:
95+
with tempfile.TemporaryDirectory() as tmpdir:
96+
path = Path(tmpdir) / "runs" / "portable"
97+
with mock.patch.object(runtime, "_supports_secure_owned_directory_creation", return_value=False):
98+
identity, descriptor = runtime.create_owned_runtime_directory(path, Path(tmpdir))
99+
self.assertIsInstance(identity, tuple)
100+
self.assertIsNone(descriptor)
101+
self.assertTrue(path.is_dir())
102+
103+
def test_malformed_metadata_and_timestamps_are_ignored(self) -> None:
104+
with tempfile.TemporaryDirectory() as tmpdir:
105+
bundle = Path(tmpdir) / "bundle"
106+
bundle.mkdir()
107+
(bundle / "run.json").write_text("[]", encoding="utf-8")
108+
self.assertIsNone(runtime._read_bundle_metadata(bundle)) # pylint: disable=protected-access
109+
self.assertIsNone(runtime._timestamp_to_epoch("not-a-timestamp")) # pylint: disable=protected-access
110+
self.assertIsNone(runtime._timestamp_to_epoch(123)) # pylint: disable=protected-access
111+
112+
def test_retention_without_policy_is_a_noop(self) -> None:
113+
with tempfile.TemporaryDirectory() as tmpdir:
114+
logger = mock.Mock()
115+
runtime.prune_run_bundles(Path(tmpdir), logger=logger)
116+
logger.warning.assert_not_called()
117+
118+
def test_runtime_directory_failures_are_actionable(self) -> None:
119+
path = Path("/tmp/base-cli-edge/runtime")
120+
with mock.patch.object(Path, "mkdir", side_effect=OSError("read-only")):
121+
with self.assertRaisesRegex(runtime.RuntimeDirectoryError, "Check permissions"):
122+
runtime.create_runtime_directory(path, Path("/tmp/base-cli-edge"))
123+
124+
def test_safe_resolved_path_falls_back_when_resolution_fails(self) -> None:
125+
path = Path("relative/path")
126+
with mock.patch.object(Path, "resolve", side_effect=OSError("unavailable")):
127+
self.assertEqual(runtime._safe_resolved_path(path), path.absolute()) # pylint: disable=protected-access
128+
129+
130+
class AttachmentHelperEdgeTests(unittest.TestCase):
131+
def test_attachment_normalizers_accept_strings_and_reject_bad_values(self) -> None:
132+
self.assertEqual(_normalize_sensitive_parameters("token"), frozenset({"token"}))
133+
with self.assertRaises(TypeError):
134+
_normalize_sensitive_parameters([""])
135+
self.assertEqual(_normalize_attached_option_declaration("--NAME", str.lower), "--name")
136+
self.assertEqual(_normalize_attached_option_declaration("NAME", str.lower), "NAME")
137+
138+
def test_selected_paths_follow_resolved_click_children(self) -> None:
139+
root_command = object()
140+
child_command = object()
141+
root = types.SimpleNamespace(command=root_command, parent=None, info_name="root")
142+
child = types.SimpleNamespace(command=child_command, parent=root, info_name="child")
143+
resolutions = {id(root): [("child", child_command, child)]}
144+
self.assertEqual(_selected_click_path(root, child, resolutions), (("child", child_command),))
145+
self.assertEqual(_selected_click_paths(root, resolutions, {id(root): root}), ((("child", child_command),),))
146+
147+
def test_pending_children_and_restore_helpers_cover_fallbacks(self) -> None:
148+
command = types.SimpleNamespace(resolve_command=lambda *_args: None)
149+
context = types.SimpleNamespace(_protected_args=("child",), args=())
150+
self.assertTrue(_click_command_has_pending_children(context, command))
151+
self.assertFalse(_click_command_has_pending_children(types.SimpleNamespace(args=()), object()))
152+
153+
def original_invoke(_ctx: object) -> None:
154+
return None
155+
156+
def original_resolve(_ctx: object, _args: object) -> None:
157+
return None
158+
159+
command.invoke = lambda _ctx: "wrapped"
160+
command.resolve_command = lambda _ctx, _args: "wrapped"
161+
command.__base_cli_original_invoke__ = original_invoke
162+
command.__base_cli_original_resolve__ = original_resolve
163+
_restore_attached_click_command(command)
164+
self.assertIs(command.invoke, original_invoke)
165+
self.assertIs(command.resolve_command, original_resolve)
166+
167+
command.main = lambda: "wrapped"
168+
command.__base_cli_original_main__ = original_invoke
169+
_restore_attached_click_main(command)
170+
self.assertIs(command.main, original_invoke)
171+
172+
173+
class HistoryEdgeTests(unittest.TestCase):
174+
def test_primary_record_and_parser_cover_optional_and_invalid_fields(self) -> None:
175+
with tempfile.TemporaryDirectory() as tmpdir:
176+
root = Path(tmpdir)
177+
path = root / "history.jsonl"
178+
history.write_primary_record(
179+
path,
180+
"demo --token=secret",
181+
["demo", "--token=secret"],
182+
datetime(2026, 1, 1, tzinfo=timezone.utc),
183+
1,
184+
"run-1",
185+
project="demo",
186+
project_root=str(root),
187+
manifest=str(root / "manifest.yaml"),
188+
log_path=root / "primary.log",
189+
bundle_path=root / "bundle",
190+
)
191+
line = path.read_text(encoding="utf-8").splitlines()[0]
192+
self.assertIsNotNone(history.parse_finished_history_record_line(line))
193+
self.assertIsNone(history.parse_finished_history_record_line("not json"))
194+
self.assertIsNone(history.parse_finished_history_record_line("{}"))
195+
self.assertEqual(history.optional_string("value"), "value")
196+
self.assertIsNone(history.optional_string(1))
197+
self.assertEqual(history.optional_int(2), 2)
198+
self.assertIsNone(history.optional_int("2"))
199+
200+
201+
if __name__ == "__main__":
202+
unittest.main()

0 commit comments

Comments
 (0)