diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 763604415..e8d89f175 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -535,15 +535,22 @@ jobs: coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" coverage_build_dir="${RUNNER_TEMP}/opencode-coverage-tool-build" trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt" + trusted_base_python_installer="${GITHUB_WORKSPACE}/scripts/ci/install_base_python_locks.py" if [ ! -f "$trusted_ci_requirements" ] || [ -L "$trusted_ci_requirements" ]; then echo "::error::Trusted coverage requirements must be a regular non-symlink file." exit 1 fi + if [ ! -f "$trusted_base_python_installer" ] || [ -L "$trusted_base_python_installer" ]; then + echo "::error::Trusted base Python lock installer must be a regular non-symlink file." + exit 1 + fi sudo rm -rf "$coverage_build_dir" mkdir -p "$coverage_build_dir" chmod 0700 "$coverage_build_dir" install -m 0644 "$trusted_ci_requirements" \ "$coverage_build_dir/requirements-opencode-review-ci-hashes.txt" + install -m 0755 "$trusted_base_python_installer" \ + "$coverage_build_dir/install-base-python-locks.py" python3 -I "$GITHUB_WORKSPACE/scripts/ci/materialize_base_python_requirements.py" \ --repo-root "$COVERAGE_SOURCE_WORKDIR" \ --base-sha "$PR_BASE_SHA" \ @@ -625,18 +632,12 @@ jobs: --only-binary=:all: \ -r /tmp/requirements-opencode-review-ci-hashes.txt \ && rm -f /tmp/requirements-opencode-review-ci-hashes.txt + COPY install-base-python-locks.py /usr/local/libexec/install-base-python-locks.py COPY base-python-requirements /tmp/base-python-requirements - RUN set -eu; \ - while IFS= read -r requirements_file; do \ - [ -n "$requirements_file" ] || continue; \ - python3 -m pip install \ - --break-system-packages \ - --disable-pip-version-check \ - --require-hashes \ - --only-binary=:all: \ - -r "/tmp/base-python-requirements/${requirements_file}"; \ - done str: + """Return the source directory used for supplement recovery groups.""" + parent = str(pathlib.PurePosixPath(self.source).parent) + return "" if parent == "." else parent + + +def _manifest_entries( + requirements_root: pathlib.Path, +) -> list[LockCandidate]: + """Load and validate trusted materializer output.""" + root = requirements_root.resolve() + manifest_path = root / "manifest.json" + if not manifest_path.is_file() or manifest_path.is_symlink(): + raise ValueError("base Python lock manifest must be a regular non-symlink file") + try: + manifest: Any = json.loads(manifest_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise ValueError(f"base Python lock manifest is invalid: {exc}") from exc + if not isinstance(manifest, list): + raise ValueError("base Python lock manifest must be a JSON array") + + entries: list[LockCandidate] = [] + seen_files: set[str] = set() + for entry in manifest: + if not isinstance(entry, dict): + raise ValueError("base Python lock manifest entries must be objects") + generated_file = entry.get("file") + source = entry.get("source") + if not isinstance(generated_file, str) or not GENERATED_LOCK_RE.fullmatch( + generated_file + ): + raise ValueError("base Python lock manifest contains an unsafe file name") + source_path = pathlib.PurePosixPath(source) if isinstance(source, str) else None + if ( + source_path is None + or source_path.is_absolute() + or not source_path.parts + or ".." in source_path.parts + ): + raise ValueError("base Python lock manifest contains an unsafe source path") + if generated_file in seen_files: + raise ValueError("base Python lock manifest contains duplicate file names") + seen_files.add(generated_file) + + candidate = root / generated_file + if not candidate.is_file() or candidate.is_symlink(): + raise ValueError( + f"materialized base Python lock {generated_file} must be a regular file" + ) + entries.append( + LockCandidate( + generated_file=generated_file, + source=str(source_path), + path=candidate, + ) + ) + return entries + + +def _pip_command(requirements: Sequence[pathlib.Path], *, preflight: bool) -> list[str]: + """Build a hash-enforced pip command for one candidate or recovery group.""" + command = [ + sys.executable, + "-m", + "pip", + "install", + "--break-system-packages", + "--disable-pip-version-check", + "--require-hashes", + "--only-binary=:all:", + ] + if preflight: + command.extend(["--dry-run", "--ignore-installed"]) + for requirements_file in requirements: + command.extend(["-r", str(requirements_file)]) + return command + + +def _bounded_failure_output(output: str, *, maximum_lines: int = 120) -> str: + """Keep the dependency root cause visible without flooding Actions logs.""" + lines = output.rstrip().splitlines() + if len(lines) <= maximum_lines: + return "\n".join(lines) + leading_lines = 40 + trailing_lines = maximum_lines - leading_lines + omitted = len(lines) - maximum_lines + return "\n".join( + [ + *lines[:leading_lines], + f"... {omitted} dependency-resolution log lines omitted ...", + *lines[-trailing_lines:], + ] + ) + + +def _is_deferable_preflight_failure(output: str) -> bool: + """Return whether a failed candidate may be grouped or safely skipped. + + A hash-bearing supplement can fail pip's independent-closure check because a + transitive pin/hash lives in a sibling lock, and a base lock can explicitly + reject the pinned coverage-image interpreter. Those states are safe to + recover through a same-directory group or defer to the later networkless + coverage run. Hash mismatches, resolver crashes, empty diagnostics, and + registry/network failures remain fatal so a broken trusted build cannot be + mistaken for an optional lock. + """ + return bool(output.strip()) and any( + pattern.search(output) for pattern in DEFERABLE_PREFLIGHT_FAILURES + ) + + +def _report_fatal_preflight_failure( + entry_label: str, + output: str, + *, + stderr: TextIO, +) -> None: + """Publish one bounded, source-aware fatal preflight failure.""" + print( + "::error::Trusted base Python lock preflight failed for " + f"{entry_label}; only incomplete hash closures or explicit Python " + "interpreter incompatibility may be deferred.", + file=stderr, + ) + failure_output = _bounded_failure_output(output) + if failure_output: + print(failure_output, file=stderr) + + +def install_materialized_locks( + requirements_root: pathlib.Path, + *, + runner: Runner = subprocess.run, + stdout: TextIO = sys.stdout, + stderr: TextIO = sys.stderr, +) -> int: + """Preflight and install independent base lock closures.""" + try: + entries = _manifest_entries(requirements_root) + except (OSError, ValueError) as exc: + print(f"::error::Could not validate base Python locks: {exc}", file=stderr) + return 2 + + installed = 0 + skipped = 0 + preflight_results: dict[str, subprocess.CompletedProcess[str]] = {} + independently_valid: set[str] = set() + for entry in entries: + print( + f"Preflighting trusted base Python lock candidate {entry.source} " + f"({entry.generated_file}).", + file=stdout, + flush=True, + ) + preflight = runner( + _pip_command([entry.path], preflight=True), + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + preflight_results[entry.generated_file] = preflight + if preflight.returncode == 0: + independently_valid.add(entry.generated_file) + elif not _is_deferable_preflight_failure(preflight.stdout or ""): + _report_fatal_preflight_failure( + entry.source, + preflight.stdout or "", + stderr=stderr, + ) + return preflight.returncode or 1 + + by_source_directory: dict[str, list[LockCandidate]] = defaultdict(list) + for entry in entries: + by_source_directory[entry.source_directory].append(entry) + + install_plans: list[list[LockCandidate]] = [] + covered_files: set[str] = set() + for source_directory, directory_entries in by_source_directory.items(): + invalid_entries = [ + entry + for entry in directory_entries + if entry.generated_file not in independently_valid + ] + if not invalid_entries or len(directory_entries) < 2: + continue + print( + "Preflighting same-directory trusted base Python lock group " + f"{source_directory or '.'}: " + + ", ".join(entry.source for entry in directory_entries), + file=stdout, + flush=True, + ) + group_preflight = runner( + _pip_command([entry.path for entry in directory_entries], preflight=True), + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + if group_preflight.returncode != 0: + if not _is_deferable_preflight_failure(group_preflight.stdout or ""): + _report_fatal_preflight_failure( + ", ".join(entry.source for entry in directory_entries), + group_preflight.stdout or "", + stderr=stderr, + ) + return group_preflight.returncode or 1 + continue + install_plans.append(directory_entries) + covered_files.update(entry.generated_file for entry in directory_entries) + print( + "Recovered trusted base Python supplement(s) through a complete " + f"same-directory hash closure: {source_directory or '.'}.", + file=stdout, + flush=True, + ) + + for entry in entries: + if entry.generated_file in covered_files: + continue + if entry.generated_file in independently_valid: + install_plans.append([entry]) + covered_files.add(entry.generated_file) + continue + + skipped += 1 + print( + "::warning::Skipping trusted base Python requirement candidate " + f"{entry.source}: hash-bearing content is not an independently " + "installable dependency closure and no same-directory lock group " + "completed it.", + file=stderr, + ) + failure_output = _bounded_failure_output( + preflight_results[entry.generated_file].stdout or "" + ) + print(failure_output, file=stderr) + + for plan in install_plans: + plan_sources = ", ".join(entry.source for entry in plan) + print( + f"Installing validated trusted base Python lock closure: {plan_sources}.", + file=stdout, + flush=True, + ) + installation = runner( + _pip_command([entry.path for entry in plan], preflight=False), + check=False, + ) + if installation.returncode != 0: + print( + "::error::A preflight-valid trusted base Python lock closure failed " + f"during installation: {plan_sources}.", + file=stderr, + ) + return installation.returncode or 1 + installed += len(plan) + + print( + "Trusted base Python lock installation summary: " + f"candidates={len(entries)} installed={installed} skipped={skipped}.", + file=stdout, + ) + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + """Install materialized lock candidates supplied by the trusted workflow.""" + parser = argparse.ArgumentParser() + parser.add_argument("--requirements-root", required=True, type=pathlib.Path) + args = parser.parse_args(argv) + return install_materialized_locks(args.requirements_root) + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index fafe680ce..28ce5364f 100644 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -43,14 +43,16 @@ def _requirement_lines(content: bytes) -> list[str]: def _is_hash_pinned(content: bytes) -> bool: - """Return whether lock content is fully hash-pinned and safe to materialize. + """Return whether content carries hash pins and is safe to preflight. Discovery is content-based rather than name-based so hash-pinned locks in any location (a service subdirectory, ``requirements-dev.txt``, - ``requirements-test.txt``) are installed for offline coverage, while an + ``requirements-test.txt``) can be considered for offline coverage, while an unpinned or PR-mutable requirements file is still excluded from the networked - build context. An empty file carries no installable dependency and is not - materialized. + build context. Hash syntax cannot prove that a file includes every transitive + dependency, so the trusted image installer separately preflights every + candidate as an independent ``--require-hashes`` closure. An empty file + carries no installable dependency and is not materialized. """ lines = _requirement_lines(content) if not lines: @@ -165,7 +167,7 @@ def main(argv: list[str] | None = None) -> int: ) else: print( - "No tracked hash-pinned Python requirement locks exist at the validated base SHA." + "No tracked hash-bearing Python requirement candidates exist at the validated base SHA." ) return 0 diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index b6b9b12ed..75e18c860 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -118,7 +118,11 @@ DEFAULT_STALE_OPENCODE_MINUTES = 90 DEFAULT_UPDATE_BRANCH_HEAD_POLL_ATTEMPTS = 6 DEFAULT_UPDATE_BRANCH_HEAD_POLL_SECONDS = 5.0 -OPENCODE_WORKFLOW_NAMES = {"OpenCode Review", "Required OpenCode Review"} +OPENCODE_WORKFLOW_NAMES = { + "OpenCode Review", + "Required OpenCode Review", + "OpenCode Review Dispatch", +} RUNNING_CHECK_STATES = {"PENDING", "EXPECTED", "QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED"} FAILED_CHECK_CONCLUSIONS = {"FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "STARTUP_FAILURE"} ACTION_REQUIRED_CONCLUSIONS = {"ACTION_REQUIRED"} @@ -1906,7 +1910,10 @@ def active_review_run_refs( ) head = str(pr.get("headRefOid") or "").lower() number = int(pr["number"]) - dispatch_title_prefix = f"{run_title} {target_repo}#{number}@" + dispatch_title_prefixes = tuple( + f"{title} {target_repo}#{number}@" + for title in sorted({run_title, *workflow_aliases}, key=len, reverse=True) + ) current: list[tuple[str, str]] = [] stale: list[tuple[str, str]] = [] @@ -1924,10 +1931,15 @@ def active_review_run_refs( continue run_ref = (run_repo, str(run_id)) display_title = str(run_data.get("display_title") or "") - if ( - run_data.get("event") == "repository_dispatch" - and display_title.startswith(dispatch_title_prefix) - ): + dispatch_title_prefix = next( + ( + prefix + for prefix in dispatch_title_prefixes + if display_title.startswith(prefix) + ), + None, + ) + if run_data.get("event") == "repository_dispatch" and dispatch_title_prefix: dispatched_head = display_title.removeprefix(dispatch_title_prefix).lower() if not GIT_SHA_RE.fullmatch(dispatched_head): continue diff --git a/tests/test_install_base_python_locks.py b/tests/test_install_base_python_locks.py new file mode 100644 index 000000000..4f1feebe6 --- /dev/null +++ b/tests/test_install_base_python_locks.py @@ -0,0 +1,398 @@ +"""Regression tests for trusted base Python lock installation.""" + +from __future__ import annotations + +import io +import json +import pathlib +import subprocess + +import pytest + +from scripts.ci import install_base_python_locks as installer + + +def write_candidate( + root: pathlib.Path, + *, + generated_file: str, + source: str, + content: str = "demo==1 --hash=sha256:" + ("a" * 64) + "\n", +) -> None: + """Append one manifest entry and write its materialized lock.""" + manifest_path = root / "manifest.json" + manifest = ( + json.loads(manifest_path.read_text(encoding="utf-8")) + if manifest_path.exists() + else [] + ) + manifest.append({"file": generated_file, "source": source}) + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + (root / generated_file).write_text(content, encoding="utf-8") + + +def test_recovers_partial_supplement_with_same_directory_lock(tmp_path) -> None: + """An optional supplement can join its sibling lock without widening scope.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="backend/requirements-agent.txt", + ) + write_candidate( + tmp_path, + generated_file="requirements-001.txt", + source="backend/requirements-hashes.txt", + ) + commands: list[list[str]] = [] + + def fake_runner(command: list[str], **kwargs): + commands.append(command) + requirements = [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "-r" + ] + if ( + "--dry-run" in command + and len(requirements) == 1 + and requirements[0].endswith("requirements-000.txt") + ): + return subprocess.CompletedProcess( + command, + 1, + stdout=( + "ERROR: In --require-hashes mode, all requirements must have " + "their versions pinned with ==: httpx>=0.27" + ), + ) + return subprocess.CompletedProcess(command, 0, stdout="") + + stdout = io.StringIO() + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stdout=stdout, + stderr=stderr, + ) + + assert result == 0 + assert len(commands) == 4 + assert "--dry-run" in commands[0] + assert "--ignore-installed" in commands[0] + assert "--dry-run" in commands[1] + assert "--ignore-installed" in commands[1] + assert "--dry-run" in commands[2] + assert commands[2].count("-r") == 2 + assert "--dry-run" not in commands[3] + assert commands[3].count("-r") == 2 + assert stderr.getvalue() == "" + assert "Recovered trusted base Python supplement" in stdout.getvalue() + assert "candidates=2 installed=2 skipped=0" in stdout.getvalue() + + +def test_skips_partial_candidate_without_completing_sibling(tmp_path) -> None: + """An unrecoverable hash-bearing supplement remains visible and non-fatal.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="backend/requirements-agent.txt", + ) + + def fake_runner(command: list[str], **kwargs): + return subprocess.CompletedProcess( + command, + 1, + stdout=( + "ERROR: In --require-hashes mode, all requirements must have " + "their versions pinned with ==: httpx>=0.27" + ), + ) + + stdout = io.StringIO() + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stdout=stdout, + stderr=stderr, + ) + + assert result == 0 + assert "requirements-agent.txt" in stderr.getvalue() + assert "httpx>=0.27" in stderr.getvalue() + assert "candidates=1 installed=0 skipped=1" in stdout.getvalue() + + +def test_failed_same_directory_group_still_skips_partial_candidates(tmp_path) -> None: + """A sibling group that remains incomplete cannot become an install plan.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="backend/requirements-agent.txt", + ) + write_candidate( + tmp_path, + generated_file="requirements-001.txt", + source="backend/requirements-extra.txt", + ) + commands: list[list[str]] = [] + + def fake_runner(command: list[str], **kwargs): + commands.append(command) + return subprocess.CompletedProcess( + command, + 1, + stdout=( + "ERROR: In --require-hashes mode, all requirements must have " + "their versions pinned with ==: httpx>=0.27" + ), + ) + + stdout = io.StringIO() + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stdout=stdout, + stderr=stderr, + ) + + assert result == 0 + assert len(commands) == 3 + assert commands[-1].count("-r") == 2 + assert "installed=0 skipped=2" in stdout.getvalue() + assert stderr.getvalue().count("httpx>=0.27") == 2 + + +@pytest.mark.parametrize( + "failure_output", + [ + "", + ("ERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE"), + "WARNING: Retrying after connection broken by ConnectionError", + "ERROR: Could not fetch URL https://pypi.org/simple/demo/", + "pip resolver crashed without a classified dependency error", + ], +) +def test_unclassified_preflight_failure_is_fatal(tmp_path, failure_output: str) -> None: + """Hash, network, empty, and unknown preflight failures fail closed.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="requirements-hashes.txt", + ) + + def fake_runner(command: list[str], **kwargs): + return subprocess.CompletedProcess(command, 23, stdout=failure_output) + + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stderr=stderr, + ) + + assert result == 23 + assert "only incomplete hash closures" in stderr.getvalue() + assert "requirements-hashes.txt" in stderr.getvalue() + if failure_output: + assert failure_output in stderr.getvalue() + + +def test_explicit_python_incompatibility_is_visible_and_nonfatal(tmp_path) -> None: + """A base lock for another interpreter may defer to coverage execution.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="requirements-hashes.txt", + ) + + def fake_runner(command: list[str], **kwargs): + return subprocess.CompletedProcess( + command, + 1, + stdout=( + "ERROR: Package 'demo' requires a different Python: " + "3.14.0 not in '<3.14,>=3.10'" + ), + ) + + stdout = io.StringIO() + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stdout=stdout, + stderr=stderr, + ) + + assert result == 0 + assert "requires a different Python" in stderr.getvalue() + assert "candidates=1 installed=0 skipped=1" in stdout.getvalue() + + +def test_fatal_same_directory_group_failure_aborts(tmp_path) -> None: + """A group cannot turn a registry or integrity failure into a skip.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="backend/requirements-agent.txt", + ) + write_candidate( + tmp_path, + generated_file="requirements-001.txt", + source="backend/requirements-hashes.txt", + ) + call_count = 0 + + def fake_runner(command: list[str], **kwargs): + nonlocal call_count + call_count += 1 + if call_count <= 2: + return subprocess.CompletedProcess( + command, + 1, + stdout=( + "ERROR: In --require-hashes mode, all requirements must have " + "their versions pinned with ==: httpx>=0.27" + ), + ) + return subprocess.CompletedProcess( + command, + 29, + stdout="ERROR: Could not fetch URL https://pypi.org/simple/httpx/", + ) + + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stderr=stderr, + ) + + assert result == 29 + assert call_count == 3 + assert "Could not fetch URL" in stderr.getvalue() + + +@pytest.mark.parametrize( + ("manifest_text", "candidate_files", "error"), + [ + (None, (), "regular non-symlink"), + ("{not-json", (), "manifest is invalid"), + ("{}", (), "must be a JSON array"), + ("[1]", (), "entries must be objects"), + ( + '[{"file":"requirements-000.txt","source":"/absolute.txt"}]', + ("requirements-000.txt",), + "unsafe source path", + ), + ( + ( + '[{"file":"requirements-000.txt","source":"one.txt"},' + '{"file":"requirements-000.txt","source":"two.txt"}]' + ), + ("requirements-000.txt",), + "duplicate file names", + ), + ( + '[{"file":"requirements-000.txt","source":"missing.txt"}]', + (), + "must be a regular file", + ), + ], +) +def test_manifest_validation_failures( + tmp_path, + manifest_text: str | None, + candidate_files: tuple[str, ...], + error: str, +) -> None: + """Malformed trusted materializer output fails before any pip command.""" + if manifest_text is not None: + (tmp_path / "manifest.json").write_text(manifest_text, encoding="utf-8") + for candidate_file in candidate_files: + (tmp_path / candidate_file).write_text("lock", encoding="utf-8") + + with pytest.raises(ValueError, match=error): + installer._manifest_entries(tmp_path) + + +def test_bounded_failure_output_preserves_root_and_tail() -> None: + """Long resolver logs retain their leading context and final root cause.""" + output = "\n".join(f"line-{index}" for index in range(150)) + + bounded = installer._bounded_failure_output(output) + + assert bounded.splitlines()[0] == "line-0" + assert "30 dependency-resolution log lines omitted" in bounded + assert bounded.splitlines()[-1] == "line-149" + + +def test_rejects_unsafe_manifest_before_running_pip(tmp_path) -> None: + """Generated and source paths must remain inside trusted materializer output.""" + (tmp_path / "manifest.json").write_text( + json.dumps([{"file": "../escape.txt", "source": "/absolute/lock.txt"}]), + encoding="utf-8", + ) + called = False + + def fake_runner(command: list[str], **kwargs): + nonlocal called + called = True + return subprocess.CompletedProcess(command, 0, stdout="") + + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stderr=stderr, + ) + + assert result == 2 + assert not called + assert "unsafe file name" in stderr.getvalue() + + +def test_install_failure_after_successful_preflight_is_fatal(tmp_path) -> None: + """A registry or hash race after preflight must fail the image build.""" + write_candidate( + tmp_path, + generated_file="requirements-000.txt", + source="requirements-hashes.txt", + ) + call_count = 0 + + def fake_runner(command: list[str], **kwargs): + nonlocal call_count + call_count += 1 + return subprocess.CompletedProcess( + command, + 0 if call_count == 1 else 19, + stdout="", + ) + + stderr = io.StringIO() + result = installer.install_materialized_locks( + tmp_path, + runner=fake_runner, + stderr=stderr, + ) + + assert result == 19 + assert "failed during installation" in stderr.getvalue() + + +def test_main_forwards_requirements_root(monkeypatch, tmp_path) -> None: + """The CLI delegates the exact requirements root to the installer.""" + seen: list[pathlib.Path] = [] + + def fake_install(root: pathlib.Path) -> int: + seen.append(root) + return 7 + + monkeypatch.setattr(installer, "install_materialized_locks", fake_install) + + assert installer.main(["--requirements-root", str(tmp_path)]) == 7 + assert seen == [tmp_path] diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index ff66279b0..21b984f4d 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -264,7 +264,7 @@ def test_main_reports_when_no_locks_exist( == 0 ) assert ( - "No tracked hash-pinned Python requirement locks exist" + "No tracked hash-bearing Python requirement candidates exist" in capsys.readouterr().out ) diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 9d9b90803..f834cfe77 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -453,6 +453,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' in measure_step assert "The networked build context contains only this" in measure_step assert 'install -m 0644 "$trusted_ci_requirements"' in measure_step + assert 'install -m 0755 "$trusted_base_python_installer"' in measure_step + assert "COPY install-base-python-locks.py" in measure_step + assert "python3 -I /usr/local/libexec/install-base-python-locks.py" in measure_step assert "docker build --pull --no-cache --network=default" in measure_step assert '"$coverage_build_dir"' in measure_step assert measure_step.index("docker build --pull --no-cache") < measure_step.index( @@ -530,11 +533,17 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): trusted_requirements = Path( "requirements-opencode-review-ci-hashes.txt" ).read_text(encoding="utf-8") + base_python_installer = Path( + "scripts/ci/install_base_python_locks.py" + ).read_text(encoding="utf-8") compile_script = Path( "scripts/ci/compile_opencode_review_lock.sh" ).read_text(encoding="utf-8") normalized_compile_script = " ".join(compile_script.replace("\\\n", " ").split()) assert "pytest-cov==7.1.0" in trusted_requirements + assert '"--dry-run"' in base_python_installer + assert '"--ignore-installed"' in base_python_installer + assert "not an independently" in base_python_installer assert ( "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" in trusted_requirements @@ -1751,7 +1760,8 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert "Trusted offline Python test toolchain" in measure assert "python3 -m coverage run -m pytest tests" in measure assert "materialize_base_python_requirements.py" in measure - assert "base-python-requirements/manifest.txt" in measure + assert "install_base_python_locks.py" in measure + assert "base-python-requirements" in measure assert "read directly from the live-validated base SHA" in measure assert 'chmod 0444 "$implementation_changed_files"' in measure assert "npm ci --ignore-scripts" in coverage_job diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 5c787e51c..3e421e903 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2151,15 +2151,27 @@ def fake_run(args, stdin=None): assert not any(call[:3] == ["gh", "workflow", "run"] for call in calls) -def test_dispatch_opencode_review_deduplicates_current_head_repository_dispatch(monkeypatch, capsys): +@pytest.mark.parametrize( + ("workflow_name", "run_title"), + [ + ("OpenCode Review Dispatch", "OpenCode Review Dispatch"), + ("Required OpenCode Review", "Required OpenCode Review"), + ], +) +def test_dispatch_opencode_review_deduplicates_current_head_repository_dispatch( + monkeypatch, + capsys, + workflow_name, + run_title, +): calls = [] head_sha = "a" * 40 current_dispatch = { "id": 9100, - "name": "Required OpenCode Review", + "name": workflow_name, "event": "repository_dispatch", "head_sha": "default-branch-sha", - "display_title": f"Required OpenCode Review owner/repo#1@{head_sha}", + "display_title": f"{run_title} owner/repo#1@{head_sha}", "pull_requests": [], }