diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 94586043..5c369e61 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1132,32 +1132,7 @@ jobs: rust_coverage_fail_under_lines() { local manifest="$1" - python3 - "$manifest" <<'PY' - import sys - import tomllib - from pathlib import Path - - manifest = Path(sys.argv[1]) - metadata = ( - tomllib.loads(manifest.read_text(encoding="utf-8")) - .get("package", {}) - .get("metadata", {}) - .get("opencode", {}) - .get("coverage", {}) - ) - value = metadata.get("minimum_lines") - if value is None: - sys.exit(0) - if isinstance(value, bool) or not isinstance(value, (int, float)): - raise SystemExit( - "package.metadata.opencode.coverage.minimum_lines must be a number from 0 to 100" - ) - if not 0 <= float(value) <= 100: - raise SystemExit( - "package.metadata.opencode.coverage.minimum_lines must be between 0 and 100" - ) - print(f"{float(value):g}") - PY + python3 "${GITHUB_WORKSPACE}/scripts/ci/rust_coverage_threshold.py" "$manifest" } run_rust_test_coverage() { @@ -1182,8 +1157,8 @@ jobs: append "### Rust coverage threshold (${manifest})" append "" append "- Result: FAIL" - append "- Reason: ${manifest} defines an invalid package.metadata.opencode.coverage.minimum_lines value." - append "- Fix: set package.metadata.opencode.coverage.minimum_lines to a numeric line-coverage percentage from 0 to 100." + append "- Reason: ${manifest} defines an invalid package.metadata.opencode.coverage.minimum_lines or workspace.metadata.opencode.coverage.minimum_lines value." + append "- Fix: set the matching package or workspace metadata key to a numeric line-coverage percentage from 0 to 100." append "" failures=$((failures + 1)) continue @@ -1194,7 +1169,7 @@ jobs: append "### Rust coverage threshold (${manifest})" append "" append "- Result: PASS" - append "- Reason: ${manifest} sets package.metadata.opencode.coverage.minimum_lines to ${threshold}%, so Rust coverage enforces the repository-owned baseline instead of the central default." + append "- Reason: ${manifest} sets a package/workspace opencode coverage minimum_lines value to ${threshold}%, so Rust coverage enforces the repository-owned baseline instead of the central default." append "" fi if ! ensure_tauri_frontend_dist "$manifest"; then diff --git a/scripts/ci/rust_coverage_threshold.py b/scripts/ci/rust_coverage_threshold.py new file mode 100644 index 00000000..910d7f10 --- /dev/null +++ b/scripts/ci/rust_coverage_threshold.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Resolve a repository-owned Rust line-coverage baseline from Cargo metadata.""" + +from __future__ import annotations + +import argparse +import tomllib +from pathlib import Path +from typing import Any + +METADATA_PATHS = ( + "package.metadata.opencode.coverage.minimum_lines", + "workspace.metadata.opencode.coverage.minimum_lines", +) + + +def _nested_value(document: dict[str, Any], path: str) -> Any: + """Return a dotted TOML value, or None when any segment is absent.""" + value: Any = document + for segment in path.split("."): + if not isinstance(value, dict) or segment not in value: + return None + value = value[segment] + return value + + +def resolve_minimum_lines(document: dict[str, Any]) -> float | None: + """Return package or virtual-workspace coverage metadata after validation.""" + value = None + selected_path = None + for path in METADATA_PATHS: + candidate = _nested_value(document, path) + if candidate is not None: + value = candidate + selected_path = path + break + if selected_path is None: + return None + if isinstance(value, bool) or not isinstance(value, (int, float)): + raise ValueError(f"{selected_path} must be a number from 0 to 100") + threshold = float(value) + if not 0 <= threshold <= 100: + raise ValueError(f"{selected_path} must be between 0 and 100") + return threshold + + +def read_minimum_lines(manifest: Path) -> float | None: + """Read and resolve one Cargo manifest's line-coverage baseline.""" + document = tomllib.loads(manifest.read_text(encoding="utf-8")) + return resolve_minimum_lines(document) + + +def main() -> int: + """Print the normalized threshold when configured; print nothing otherwise.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("manifest", type=Path) + args = parser.parse_args() + try: + threshold = read_minimum_lines(args.manifest) + except (OSError, tomllib.TOMLDecodeError, ValueError) as exc: + parser.error(str(exc)) + if threshold is not None: + print(f"{threshold:g}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/safe_pytest_command.py b/scripts/ci/safe_pytest_command.py index f361fa13..4143c083 100644 --- a/scripts/ci/safe_pytest_command.py +++ b/scripts/ci/safe_pytest_command.py @@ -81,6 +81,14 @@ def execute_command(project_dir: pathlib.Path, argv: Sequence[str]) -> int: raise ValueError("configured command is not a safe direct pytest invocation") env = os.environ.copy() env["PYTHONPATH"] = "." + virtualenv_bin = project_dir.resolve() / ".venv" / "bin" + if virtualenv_bin.is_dir(): + inherited_path = env.get("PATH") + env["PATH"] = ( + os.pathsep.join((str(virtualenv_bin), inherited_path)) + if inherited_path + else str(virtualenv_bin) + ) completed = subprocess.run( list(argv), cwd=project_dir, diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 3de19fa2..66db9931 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -895,6 +895,8 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'ensure_tauri_frontend_dist "$manifest"' "opencode coverage evidence checks each Rust manifest for Tauri frontendDist requirements" assert_file_contains "$workflow_file" "rust_coverage_fail_under_lines()" "opencode coverage evidence reads repo-owned Rust coverage baselines" assert_file_contains "$workflow_file" "package.metadata.opencode.coverage.minimum_lines" "opencode coverage evidence documents the Rust coverage baseline metadata key" + assert_file_contains "$workflow_file" "workspace.metadata.opencode.coverage.minimum_lines" "opencode coverage evidence supports virtual-workspace Rust coverage baselines" + assert_file_contains "$workflow_file" "scripts/ci/rust_coverage_threshold.py" "opencode coverage evidence uses the tested trusted Rust threshold parser" assert_file_contains "$workflow_file" '--fail-under-lines "$threshold"' "opencode coverage evidence enforces the resolved Rust line coverage threshold" assert_file_contains "$workflow_file" "Python project dependencies (requirements.txt)" "opencode coverage evidence records repository Python dependency installation" assert_file_contains "$workflow_file" "uv run --no-project --with-requirements requirements.txt" "opencode coverage evidence resolves binary-only repository Python requirements before pytest" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 1ff1ff18..5a678111 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -330,6 +330,8 @@ def test_opencode_target_coverage_materializes_merge_tree_without_checkout_actio assert 'ensure_tauri_frontend_dist "$manifest"' in measure_step assert "rust_coverage_fail_under_lines()" in measure_step assert "package.metadata.opencode.coverage.minimum_lines" in measure_step + assert "workspace.metadata.opencode.coverage.minimum_lines" in measure_step + assert "scripts/ci/rust_coverage_threshold.py" in measure_step assert '--fail-under-lines "$threshold"' in measure_step assert "run_python_uv_lock_check()" in measure_step assert "Python uv lockfile consistency (${project_dir})" in measure_step diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 523dc2f9..b3fde646 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -4,6 +4,7 @@ import io import json +import os import runpy import subprocess import sys @@ -169,6 +170,8 @@ def test_safe_pytest_parser_rejects_shell_and_non_pytest_execution(command: str) def test_safe_pytest_executor_never_uses_a_shell(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """The realistic configured-command boundary executes validated argv with shell disabled.""" observed: dict[str, object] = {} + virtualenv_bin = tmp_path / ".venv" / "bin" + virtualenv_bin.mkdir(parents=True) def fake_run(argv, *, cwd, env, shell, check): observed.update(argv=argv, cwd=cwd, env=env, shell=shell, check=check) @@ -181,6 +184,7 @@ def fake_run(argv, *, cwd, env, shell, check): assert observed["shell"] is False assert observed["check"] is False assert observed["env"]["PYTHONPATH"] == "." + assert observed["env"]["PATH"].split(os.pathsep)[0] == str(virtualenv_bin) def test_configured_pytest_discovery_drops_injected_workflow_command(tmp_path: Path) -> None: diff --git a/tests/test_rust_coverage_threshold.py b/tests/test_rust_coverage_threshold.py new file mode 100644 index 00000000..c3ed47bd --- /dev/null +++ b/tests/test_rust_coverage_threshold.py @@ -0,0 +1,94 @@ +"""Tests for package and virtual-workspace Rust coverage baselines.""" + +import runpy +import sys +from pathlib import Path + +import pytest + +from scripts.ci import rust_coverage_threshold as threshold + + +def test_package_metadata_takes_precedence() -> None: + """A concrete package may override a workspace-wide default.""" + document = { + "package": {"metadata": {"opencode": {"coverage": {"minimum_lines": 92}}}}, + "workspace": {"metadata": {"opencode": {"coverage": {"minimum_lines": 88}}}}, + } + + assert threshold.resolve_minimum_lines(document) == 92.0 + + +def test_virtual_workspace_metadata_is_supported(tmp_path: Path) -> None: + """A root manifest without a package table can own the workspace baseline.""" + manifest = tmp_path / "Cargo.toml" + manifest.write_text( + '[workspace]\nmembers = ["crates/core"]\n\n' + "[workspace.metadata.opencode.coverage]\nminimum_lines = 90\n", + encoding="utf-8", + ) + + assert threshold.read_minimum_lines(manifest) == 90.0 + + +@pytest.mark.parametrize("value", [True, "90", -1, 101]) +def test_invalid_thresholds_fail_closed(value: object) -> None: + """Non-numeric and out-of-range baselines cannot weaken the coverage gate.""" + document = { + "workspace": {"metadata": {"opencode": {"coverage": {"minimum_lines": value}}}} + } + + with pytest.raises(ValueError, match="workspace.metadata.opencode.coverage.minimum_lines"): + threshold.resolve_minimum_lines(document) + + +def test_missing_metadata_keeps_central_default() -> None: + """No declaration remains distinguishable from an explicit zero threshold.""" + assert threshold.resolve_minimum_lines({"workspace": {}}) is None + assert threshold.resolve_minimum_lines( + {"workspace": {"metadata": {"opencode": {"coverage": {"minimum_lines": 0}}}}} + ) == 0.0 + + +def test_cli_prints_normalized_workspace_threshold( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """The trusted workflow CLI emits exactly the value passed to cargo llvm-cov.""" + manifest = tmp_path / "Cargo.toml" + manifest.write_text( + "[workspace.metadata.opencode.coverage]\nminimum_lines = 90.5\n", + encoding="utf-8", + ) + monkeypatch.setattr(sys, "argv", ["rust_coverage_threshold.py", str(manifest)]) + + assert threshold.main() == 0 + assert capsys.readouterr().out == "90.5\n" + + +def test_cli_reports_invalid_metadata( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Malformed repository metadata is an actionable nonzero CLI error.""" + manifest = tmp_path / "Cargo.toml" + manifest.write_text( + '[workspace.metadata.opencode.coverage]\nminimum_lines = "high"\n', + encoding="utf-8", + ) + monkeypatch.setattr(sys, "argv", ["rust_coverage_threshold.py", str(manifest)]) + + with pytest.raises(SystemExit, match="2"): + threshold.main() + assert "must be a number from 0 to 100" in capsys.readouterr().err + + +def test_script_entrypoint_exits_cleanly( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """The executable workflow entrypoint delegates to main and returns success.""" + manifest = tmp_path / "Cargo.toml" + manifest.write_text("[workspace]\nmembers = []\n", encoding="utf-8") + script = Path(threshold.__file__) + monkeypatch.setattr(sys, "argv", [str(script), str(manifest)]) + + with pytest.raises(SystemExit, match="0"): + runpy.run_path(str(script), run_name="__main__")