diff --git a/.Jules/palette.md b/.Jules/palette.md index 159662d06c..9864e9e356 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,5 @@ -## 2024-05-20 - Repository without UI Codebase -**Learning:** This repository is a GitHub organization profile consisting entirely of Markdown documentation and static assets, and does not contain an active UI or frontend application codebase. -**Action:** Since there is no UI, no UX enhancements can be applied. Aborting UX enhancements and PR creation as per instructions. +## 2026-09-29 - Product UI boundary confirmed + +**Learning:** This repository owns the ContextualWisdomLab GitHub Actions control plane, including production CI scripts, tests, workflows, and documentation. It does not own an interactive product UI or frontend application. + +**Action:** Do not invent product UI work in this repository. Improve the control-plane artifacts here and route reusable product UI work to its canonical product owner. diff --git a/.github/workflows/agent-mention-router-quality-ci.yml b/.github/workflows/agent-mention-router-quality-ci.yml index 9c36a89119..6e95b322a3 100644 --- a/.github/workflows/agent-mention-router-quality-ci.yml +++ b/.github/workflows/agent-mention-router-quality-ci.yml @@ -13,7 +13,10 @@ on: - "scripts/ci/agent_mention_sweep.py" - "tests/test_agent_mention_*.py" - "tests/test_pr_review_fix_scheduler_coverage.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" push: branches: [main] paths: @@ -26,7 +29,10 @@ on: - "scripts/ci/agent_mention_sweep.py" - "tests/test_agent_mention_*.py" - "tests/test_pr_review_fix_scheduler_coverage.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" concurrency: group: agent-mention-router-quality-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index dc3baea170..2c41358cf1 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -63,6 +63,8 @@ on: - "scripts/ci/contextual_orchestrator_review_sidecar.sh" - "tests/test_zdr_policy.py" - "tests/test_contextual_orchestrator_review_policy.py" + - "tests/test_contextual_orchestrator_review_launcher.py" + - "tests/test_review_preflight_concurrency.py" - "tests/test_contextual_orchestrator_review_sidecar_contract.py" - "tests/test_sidecar_disk_free_guard.py" - "tests/test_hourly_review_repair_callers.py" @@ -116,9 +118,11 @@ on: - "docs/doctoring/exact-artifact-sbom-attestation.md" - "docs/doctoring/exact-artifact-sbom-quality-runner-consolidation-20260903.md" - "CHANGELOG.d/20260903-exact-artifact-quality-runner-consolidation.md" + - "requirements-opencode-review-ci.txt" - "requirements-opencode-review-ci-hashes.txt" - "requirements-noema-document-ci.txt" - "requirements-noema-document-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" # PR validation only: a new head cancels only an older run of this workflow # for the same repository and pull request. @@ -237,11 +241,13 @@ jobs: tests/test_strix_quality_timeout_fixture_budget.py) strix_suite=true ;; - requirements-opencode-review-ci-hashes.txt) + requirements-opencode-review-ci.txt|\ + requirements-opencode-review-ci-hashes.txt|\ + requirements-noema-document-ci.txt|\ + scripts/ci/compile_opencode_review_lock.sh) noema_suite=true opencode_suite=true ;; - requirements-noema-document-ci.txt|\ requirements-noema-document-ci-hashes.txt) noema_suite=true ;; @@ -267,6 +273,8 @@ jobs: scripts/ci/contextual_orchestrator_review_sidecar.sh|\ tests/test_zdr_policy.py|\ tests/test_contextual_orchestrator_review_policy.py|\ + tests/test_contextual_orchestrator_review_launcher.py|\ + tests/test_review_preflight_concurrency.py|\ tests/test_contextual_orchestrator_review_sidecar_contract.py|\ tests/test_sidecar_disk_free_guard.py|\ tests/test_hourly_review_repair_callers.py|\ @@ -444,11 +452,14 @@ jobs: --cov=scripts.ci.pr_review_autofix_context \ --cov=scripts.ci.zdr_policy \ --cov=scripts.ci.contextual_orchestrator_review_policy \ + --cov=scripts.ci.contextual_orchestrator_review_launcher \ --cov-branch \ --cov-fail-under=100 \ tests/test_pr_review_conflict_scope.py \ tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ + tests/test_contextual_orchestrator_review_launcher.py \ + tests/test_review_preflight_concurrency.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ tests/test_sidecar_disk_free_guard.py \ tests/test_hourly_review_repair_callers.py \ @@ -488,6 +499,8 @@ jobs: scripts/ci/contextual_orchestrator_review_launcher.py \ tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ + tests/test_contextual_orchestrator_review_launcher.py \ + tests/test_review_preflight_concurrency.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ tests/test_sidecar_disk_free_guard.py \ tests/test_hourly_review_repair_callers.py \ diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 08ea600538..e743a3e23d 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -493,6 +493,10 @@ jobs: trusted_archive="${RUNNER_TEMP}/trusted-noema-source.tar.gz" api_url="${GITHUB_API_URL:-https://api.github.com}" curl -fsSL \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 1 \ + --retry-max-time 30 \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "$trusted_archive" \ diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 1194d7eea6..bbe692b417 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -152,6 +152,10 @@ jobs: api_url="${GITHUB_API_URL:-https://api.github.com}" mkdir -p "$trusted_source_dir" curl -fsSL \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 1 \ + --retry-max-time 30 \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "$trusted_archive" \ diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index a26fd4857e..7777f02bed 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -354,6 +354,10 @@ jobs: trusted_archive="${RUNNER_TEMP}/trusted-scheduler-source.tar.gz" api_url="${GITHUB_API_URL:-https://api.github.com}" curl -fsSL \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 1 \ + --retry-max-time 30 \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "$trusted_archive" \ diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index e05a8b155a..2cba51bbc6 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -18,6 +18,10 @@ on: - "tests/test_repository_label_convergence.py" - "tests/test_repository_label_identity.py" - "tests/test_repository_label_live_verification.py" + - "requirements-opencode-review-ci.txt" + - "requirements-opencode-review-ci-hashes.txt" + - "requirements-noema-document-ci.txt" + - "scripts/ci/compile_opencode_review_lock.sh" - ".github/workflows/repository-metadata-reconcile.yml" schedule: - cron: "23 * * * *" @@ -42,6 +46,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 persist-credentials: false - name: Verify exact revision shell: bash diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index db70ec324c..ea78c30cc7 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -2,28 +2,37 @@ name: Trusted uv Materializer Quality CI on: pull_request: - branches: [main] paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/verify_release_maturin_tool_assets.py" - "tests/conftest.py" - "tests/test_materialize*.py" - "tests/test_trusted_uv*.py" - "tests/test_uv*.py" - "tests/test_repository_branch_coverage_*.py" + - "tests/test_verify_release_maturin_tool_assets.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" - "pyproject.toml" push: branches: [main] paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/verify_release_maturin_tool_assets.py" - "tests/conftest.py" - "tests/test_materialize*.py" - "tests/test_trusted_uv*.py" - "tests/test_uv*.py" - "tests/test_repository_branch_coverage_*.py" + - "tests/test_verify_release_maturin_tool_assets.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" - "pyproject.toml" concurrency: @@ -98,6 +107,7 @@ jobs: with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Set up current stable Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/CHANGELOG.d/20260930-github-api-http-error-close.md b/CHANGELOG.d/20260930-github-api-http-error-close.md new file mode 100644 index 0000000000..df2feb4a78 --- /dev/null +++ b/CHANGELOG.d/20260930-github-api-http-error-close.md @@ -0,0 +1,7 @@ +## Fixed + +- Close file-like `HTTPError` responses in the central CodeQL, Strix, Noema, + Pingora, review-preflight, Pages, and sandbox-readiness clients after bounded + status/telemetry extraction, preventing Python 3.14 resource leaks without + permitting redirects, suppressing warnings, or weakening bearer-token + authority checks; cap CodeQL diagnostic error-body reads at 400 bytes. diff --git a/CHANGELOG.d/20261001-gap-baseline-source-restoration.md b/CHANGELOG.d/20261001-gap-baseline-source-restoration.md new file mode 100644 index 0000000000..a2d7d53a8f --- /dev/null +++ b/CHANGELOG.d/20261001-gap-baseline-source-restoration.md @@ -0,0 +1,5 @@ +## Fixed + +- Restore the complete product and technical gap baseline after a connector + display truncation replaced the source artifact, and add a regression guard + that rejects future truncation banners. diff --git a/CHANGELOG.d/20261001-noema-document-reader-dependencies.md b/CHANGELOG.d/20261001-noema-document-reader-dependencies.md new file mode 100644 index 0000000000..486643700c --- /dev/null +++ b/CHANGELOG.d/20261001-noema-document-reader-dependencies.md @@ -0,0 +1,5 @@ +### Noema document reader closes new transitive advisories + +- The generated npm lock now selects `fast-uri` 3.1.8 and `ip-address` 10.7.1, + removing the three medium-severity findings published against the previous + transitive versions while leaving the direct runtime manifest unchanged. diff --git a/CHANGELOG.d/20261001-trusted-review-archive-retry.md b/CHANGELOG.d/20261001-trusted-review-archive-retry.md new file mode 100644 index 0000000000..783b243562 --- /dev/null +++ b/CHANGELOG.d/20261001-trusted-review-archive-retry.md @@ -0,0 +1,6 @@ +### Central review source survives transient GitHub archive failures + +- Required Noema, Required OpenCode, and the PR review merge scheduler now use + bounded native `curl` retries when materializing the immutable trusted + `.github` source archive. Authentication, exact-SHA binding, extraction, and + fail-closed behavior are unchanged. diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d7903f11..b8e8dfc577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +### Maturin download failures close every transport response + +- Refactor the bounded Maturin asset downloader so successful and rejected + responses share one unconditional close path while `HTTPError` keeps its own + explicit close path. A new regression exercises a non-200 response and an + opener-raised HTTP error. This removes an impossible optional-response branch + without changing hosts, redirects, byte limits, hashes, or fail-closed error + mapping; the focused suite is 17 passed with 100% statement and branch + coverage. The trusted full-suite workflow now also tracks the verifier source + and its focused test, so a future lifecycle change cannot omit the repository + coverage gate that detected this regression. The pull-request trigger admits + stacked canonical-owner bases as well as `main`; the protected-branch push + trigger remains restricted to `main`. + ### Shared Strix lock advances beyond the PyJWT recursion DoS - Advance the explicit Strix source pin and generated hash lock from PyJWT @@ -17,6 +31,40 @@ CVE-2026-97687 and CVE-2026-97689 found by exact-head Python Security while preserving hash checking and the existing Strix cryptography override. +### Full-suite parser locks and honest branch coverage converge + +- Consolidated the complete valid ancestry of `.github#2521` into `.github#2530` + with ordinary two-parent merges so the parser-lock and repository-coverage + gates no longer wait on one another. Behavior-level tests exercise the final + branches in the OpenCode queue, Strix dependency classifier, release runtime + prescreener, and release dependency gate without exclusions, pragmas, + threshold reductions, or sample shrinking. Direct consumers of the common + generated lock now also track both source requirements and the canonical + compiler. `.github#2532`'s warning-fatal HTTP response-lifecycle repair is + carried in the same successor so the complete suite can regenerate one + exact-head receipt. Protected hosted Checks and qualifying independent review + remain mandatory before ordinary merge; predecessors remain open until + merged-tree equivalence is proven. The combined Python 3.14 warnings-fatal + suite passes 5,291 tests with 5 optional skips and 40 subtests; all 18,729 + production statements and 7,642 branches are covered, and production + Docstring coverage is 100%. The durable review-repair owner also triggers, + executes, compiles, and measures both launcher runtime suites at 100%. + Exact Git blobs, Cargo development locks, runtime receipts, final fanout caps, + and the Python 3.10 TOML fallback are covered as explicit trust boundaries; + one unreachable postcondition was removed only after prior fail-closed + validation made that state mechanically impossible. + +### Full-suite quality environments install their collection parsers + +- The common OpenCode quality input now owns the existing hash-pinned + `defusedxml` document parser and `PyYAML` workflow parser used during complete + repository test collection. Its Python 3.14/Linux lock was regenerated by + the repository compiler without manual hash edits. Local verification + reproduced the lock byte-for-byte, installed the common and Noema locks + together, imported both parsers, and passed 73 focused contracts with two + optional skips. Hosted exact-head Checks and qualifying independent review + remain required before protected merge. + ### Shared security fixtures use patched PyJWT and PyO3 releases - The Strix hash lock now takes PyJWT `2.14.0` as an explicit source input, diff --git a/docs/doctoring/central-coverage-owner-stack-2521.md b/docs/doctoring/central-coverage-owner-stack-2521.md new file mode 100644 index 0000000000..412899d94e --- /dev/null +++ b/docs/doctoring/central-coverage-owner-stack-2521.md @@ -0,0 +1,46 @@ +# Central coverage owner stack RCA + +## Incident and ownership + +`ContextualWisdomLab/.github#2521` owns the repository-wide Python statement +and branch coverage repair. Concurrent PRs carried valid prerequisites rather +than competing implementations: #2530 owns the hash-pinned parser inputs used +by complete test collection, and #2532 owns explicit closure of file-like +GitHub `HTTPError` responses. Both were preserved as ordinary merge parents; +neither failure was treated as a reason to discard a PR. + +## Root causes + +The first warning-fatal integrated run separated three causes: + +1. the predecessor tree lacked parser dependencies required during collection; +2. synthetic GitHub redirect/error paths leaked response objects under Python + 3.14 and therefore failed when `ResourceWarning` was promoted to an error; +3. four central scripts retained executable paths that their tests did not + reach, while two tests invoked a live `gh` boundary or asserted nothing. + +The remaining coverage work was test-first and behavior-bound. It exercises +queue admission/cancellation, bounded repository scans, exact Git blobs, +Cargo workspace and development-lock identity, runtime receipt architecture, +release fanout limits, and Python 3.10 TOML parser fallback. The runtime archive +prescreener's final repeated count check was proven unreachable because its +earlier exact-cardinality, uniqueness, and per-archive validation already +reject every false case; only that redundant branch was removed. + +## Evidence and acceptance + +- RED integrated evidence: 5,196 passed, 6 skipped; 90 statements missed and + 29 partial branches across the four remaining owner modules. +- Focused release dependency evidence: 950 passed with warnings treated as + errors. +- GREEN combined-successor evidence after re-fetching and ordinarily merging + current #2530 and #2521 heads: 5,291 passed, 5 skipped, 40 subtests passed; + 18,729/18,729 production statements and 7,642/7,642 branches covered, with + production Docstring coverage at 100%. +- No warning, security result, or fail-closed input validation was suppressed. + +This local result is not merge authority. Acceptance requires publication to +the re-fetched #2530 successor branch without force, fresh hosted Checks bound +to that exact commit, qualifying independent review, and ordinary protected +merge. The #2521 predecessor remains open until that protected merge and +complete tree carryover are verified. diff --git a/docs/doctoring/full-suite-parser-locks-20260930.md b/docs/doctoring/full-suite-parser-locks-20260930.md new file mode 100644 index 0000000000..8454481059 --- /dev/null +++ b/docs/doctoring/full-suite-parser-locks-20260930.md @@ -0,0 +1,81 @@ +# Full-suite parser dependency admission + +## Incident + +At protected `main@37b10243cec3d160ecc9c1be75c71428b160a703`, the common +OpenCode quality lock did not install the parsers imported by repository test +collection. Agent Mention Router Quality run +[36443475158](https://github.com/ContextualWisdomLab/.github/actions/runs/36443475158) +and source-repair run +[36443475290](https://github.com/ContextualWisdomLab/.github/actions/runs/36443475290) +reported `ModuleNotFoundError: No module named 'defusedxml'` and 13 collection +errors. The newer main additionally has four workflow tests importing `yaml`. +These are environment failures before test execution, not product findings or +passing coverage measurements. + +## Repair boundary + +The shared tooling input reuses `requirements-noema-document-ci.txt` for the +existing `defusedxml==0.7.1` pin and adds the existing security-tooling +`PyYAML==6.0.3` pin. The generated lock is rebuilt only with +`./scripts/ci/compile_opencode_review_lock.sh` (Python 3.14, Linux target, +`--upgrade`, hashes). The required upgrade also resolves Hypothesis 6.168.3 +instead of 6.168.0. No hashes are edited by hand. + +Installing the entire Bandit lock alongside this lock is not the repair: their +Pygments pins differ. The common quality environment instead resolves its own +complete, consistent dependency closure. Hash checking remains mandatory. +PyYAML's repository reports MIT. GitHub reports NOASSERTION for defusedxml, +so its installed hash-pinned 0.7.1 wheel's LICENSE was read directly and confirms +Python Software Foundation License Version 2; a missing SPDX detection is not +interpreted as missing permission. + +A contract regression requires both parser packages in the installed lock. +RED: one failing test because both packages were absent. GREEN: 36 tests passed +and two optional document-format tests skipped after hash-locked installation. +The full repository suite is a separate verification step, not implied by that +focused result. On code head `41e95af9dd1ac95dea716d53c44df1eacbd81d9d`, the final +isolated run finished with 5,165 passed, five skipped and 40 subtests passed in +356.08 seconds. It used only the common quality lock plus the project-pinned +pip seed, disabled incidental pytest plugin autoload and cleared live event/token +environment variables. An inherited ResourceWarning from a synthetic HTTPError +fixture remains visible; it is not suppressed or treated as a product failure. + +## Verification environment + +The first YAML-present full run on source-repair head `deb47f8db` finished with +5,232 passed, seven failed, five skipped and 40 subtests passed. Two failures +were a locally unseeded virtualenv lacking pip; five used pytest temporary +paths beneath the host home, which the sandbox correctly refuses. Installing +project-pinned pip 26.2.1 and using an isolated `/tmp` basetemp made all seven +reproductions pass. These local environment corrections do not weaken sandbox +validation and are not changes to production code. + +An initial clean-lock run had one further environment failure: macOS inherited +`/tmp` group 0, which this user does not belong to, and silently cleared the +setgid bit when the test requested mode 2600. A direct mode probe observed 600, +while a staff-owned directory retained 2600. The final run used a unique +staff-owned parent outside the host home; the security check itself was not +changed. Both the targeted permission test and the complete suite passed there. + +Source-repair's own two scripts separately measured 100% statement/branch +coverage and 100% docstrings. Neither those measurements nor the parser repair +constitute hosted exact-head approval or merge authorization. + +## Consumers and limits + +Quality workflows install `requirements-opencode-review-ci-hashes.txt` directly. +The central OpenCode coverage image also consumes it in +`.github/workflows/opencode-review-dispatch.yml`, together with the existing +Noema document lock. No exact-head fast-mlsirm Noema HTTP 400 or Strix report +scope failure is claimed resolved by this dependency change. Those incidents +require their own current-head transport/report evidence and independent review. + +## References + +Python Packaging Authority. (n.d.). *Secure installs*. Retrieved September 30, +2026, from https://pip.pypa.io/en/stable/topics/secure-installs/ + +The pip documentation requires every dependency in hash-checking mode to be +pinned and hashed. The repair preserves that contract rather than adding an +unhashed installation fallback. diff --git a/docs/doctoring/github-api-http-error-response-lifecycle.md b/docs/doctoring/github-api-http-error-response-lifecycle.md new file mode 100644 index 0000000000..e26110c0b8 --- /dev/null +++ b/docs/doctoring/github-api-http-error-response-lifecycle.md @@ -0,0 +1,86 @@ +# GitHub API HTTP error response lifecycle + +Status: Proposed source repair; exact-head hosted security Checks and independent review remain mandatory. + +## Incident and exact owner + +Protected `ContextualWisdomLab/.github` `main` at +`37b10243cec3d160ecc9c1be75c71428b160a703` reproduced eight failures in +`tests/test_github_api_url_boundary.py` under Python 3.14.7 with warnings promoted +to errors. Both authenticated GitHub REST clients correctly refused a synthetic +302 without sending a second request, but the resulting `HTTPError` response was +left open and Python reported `ResourceWarning: Implicitly cleaning up +`. + +The exact RED command was: + +```console +GITHUB_ACTIONS=true python -m pytest tests/test_github_api_url_boundary.py -q -W error +``` + +Result on the protected revision: `8 failed, 26 passed`. The same result in a +clean protected-revision worktree proves this is a pre-existing central supplier +defect rather than a consumer or `.github#2040` branch-only failure. + +## Root cause and repair + +Python documents `urllib.error.HTTPError` as both an exception and the same kind +of file-like response returned by `urlopen()`, with a readable error-body file +pointer. The clients converted the exception into their domain error but did not +close that response object. Success responses already used context managers; the +HTTP error path violated the same lifecycle boundary. + +The repair keeps the existing fail-closed redirect policy and error mapping. +The first exact-tree verification also exposed the same lifecycle defect in +other central HTTP boundaries, so the owner repair now covers every observed +path rather than leaving warning-fatal failures for a successor: + +- `codeql_ghas_configuration_identity._request_json` reads at most 400 bytes of + diagnostic body and closes the `HTTPError` in `finally`, including decode/read + failures; +- `strix_evidence_binding.default_github_opener` snapshots the status code, + closes the response, and then raises `EvidenceBindingError`; +- `noema_review_gate.call_llm` extracts only bounded allowlisted gateway + telemetry and then closes the response in `finally`; +- the Pingora artifact client closes JSON and raw-blob HTTP errors after + preserving its existing 404/domain-error mapping; +- the review preflight closes provider HTTP errors after recording only the + safe status and retry fields; +- Pages publication and sandbox readiness probes close rejected redirects + before returning their existing fail-closed result; +- the production-opener regression now asserts that the single synthetic 302 + response is closed as well as proving no redirected bearer request occurs, + and each newly discovered caller has an equivalent close contract. + +The 400-byte intake bound matches the pre-existing 400-character public +diagnostic limit without reading an arbitrarily large untrusted response first. +No warning filter, security suppression, redirect allowance, timeout, or check +threshold changed. + +## Acceptance + +1. The protected-revision RED becomes GREEN under Python 3.14.7 and `-W error`. +2. CodeQL identity, Strix evidence, Noema, Pingora, review-preflight, Pages, + sandbox readiness, and URL-authority suites remain GREEN with warnings + fatal. +3. Changed production behavior retains contract coverage and public docs. +4. Hosted Semgrep, Bandit, CodeQL, dependency, and independent review Checks pass + on the exact PR head before ordinary protected merge. +5. `.github#2040` then merges the protected repair normally and regenerates its + own exact-head evidence; stale predecessor failures are not rerun as proof. + +Local repair evidence on Python 3.14.7 is `35 passed` for the URL-authority +suite after adding the bounded-intake regression (`1 failed` before the repair), +`220 passed` for the original eight-file CodeQL/Strix impact suite, `69 passed` +for the sandbox E2E suite, and `5161 passed, 10 skipped, 40 subtests passed` +for the complete warning-fatal test tree. The original two production modules +each report 100% statement/branch coverage, while the repository-wide report +remains at its pre-existing 99% because unrelated production files outside this +delta retain uncovered branches. Hosted exact-head Checks, rather than this +local evidence, remain the merge authority. + +## Primary reference + +Python Software Foundation. (2026). *urllib.error — Exception classes raised by +urllib.request* (Python 3.14.7 documentation). +https://docs.python.org/3.14/library/urllib.error.html diff --git a/docs/doctoring/maturin-download-response-lifecycle-20261001.md b/docs/doctoring/maturin-download-response-lifecycle-20261001.md new file mode 100644 index 0000000000..b6765daae8 --- /dev/null +++ b/docs/doctoring/maturin-download-response-lifecycle-20261001.md @@ -0,0 +1,44 @@ +# Maturin download response-lifecycle RCA + +## Incident + +Trusted uv Materializer run `36811202519`, job `110206427182`, checked out +`.github#1653@5cd141ec2c33b631d164af936cd1c9de70e4c9a4`. All 5,314 tests passed, +but the complete repository gate reported 99% coverage. The only incomplete +owner was `scripts/ci/verify_release_maturin_tool_assets.py`: five statements +and two branches in its non-200 and `HTTPError` response paths. + +## Root cause and boundary + +The bounded downloader belongs to the central `.github#2530` successor, not +the #1653 label-taxonomy delta. Its nullable `response` finalizer encoded a +false branch that cannot fall through: when `opener.open()` raises, control +re-raises from the `HTTPError` handler before the code following the finalizer. +That made honest 100% branch evidence impossible even though successful +responses were closed. The Trusted uv workflow path filter also omitted this +verifier and its tests, so fixing the canonical owner would not itself request +the complete gate that originally exposed the defect downstream. After the +path repair, owner head `8cf2ea5f73976d47b2267fb52ac28284323404b7` +still produced no gate because the workflow admitted only PRs whose base was +`main`, while #2530 is correctly stacked on canonical owner #2531. + +## Repair + +The downloader now closes every returned response in one unconditional nested +`finally` block. An opener-raised `HTTPError` remains independently closed by +its handler. Tests assert closure for both an HTTP 503 response and an HTTP 502 +exception. A workflow contract now requires both verifier paths in pull-request +and protected-branch triggers, and a separate contract admits stacked PR bases +while the push trigger remains restricted to protected `main`. The repair does +not change admitted hosts, the one-hop redirect +contract, credentials, request timeout, byte bounds, digests, or error mapping. + +## Evidence and remaining gates + +- Hosted RED: 5,314 passed, 5 skipped, 40 subtests; 18,775 statements with 5 + missing, 7,652 branches with 2 partial; total 99%. +- Local owner GREEN: 17 focused tests; 107/107 statements and 34/34 branches; + `git diff --check` clean. +- Required before acceptance: complete exact-head hosted suite, security and + CodeQL verdicts, qualifying independent approval, ordinary owner integration, + then ordinary merge-forward into #1653 and fresh consumer Checks. diff --git a/docs/doctoring/noema-document-reader-dependency-baseline-20261001.md b/docs/doctoring/noema-document-reader-dependency-baseline-20261001.md new file mode 100644 index 0000000000..4e723f2040 --- /dev/null +++ b/docs/doctoring/noema-document-reader-dependency-baseline-20261001.md @@ -0,0 +1,33 @@ +# Noema document-reader dependency baseline + +## Incident + +Security Scan run +[`36773087489`](https://github.com/ContextualWisdomLab/.github/actions/runs/36773087489), +Trivy job `110084194330`, found three medium-severity vulnerabilities in the +generated Noema document-reader lock at exact PR head +`a99784219305d1b6e14cf76f0acea30c5ee45e21`: + +- `fast-uri` 3.1.7: CVE-2026-86472 / GHSA-hrr3-gc8f-f4qj; +- `ip-address` 10.7.0: CVE-2026-101911 / GHSA-h3mg-xc3c-68pw; and +- `ip-address` 10.7.0: CVE-2026-101912 / GHSA-j6r3-76f7-8jcv. + +The upstream fast-uri advisory fixes the 3.x line in 3.1.8. The ip-address +advisories fix both findings in 10.7.1; this lock selects the first patched 10.7.1 +release within the existing `^10.2.0` transitive range. + +## Ownership and decision + +The vulnerable file is owned by the central `.github` Noema document-reader +runtime, so the repair stays in that owner instead of patching consumer +repositories. The direct manifest is unchanged. `npm update` regenerated only +the two transitive lock entries, including registry URL and integrity digest. + +## Verification and remaining gate + +`tests/test_noema_document_reader_runtime_dependencies.py` first failed on +3.1.7 and 10.7.0, then passed with exactly one safe entry for each package. +The focused Python context suite passed 14 tests with 2 skipped, `npm ci +--ignore-scripts` reproduced the lock, and `npm audit --omit=dev +--audit-level=moderate` reported zero vulnerabilities. Hosted Security Scan, +CodeQL, independent approval, and ordinary protected merge remain required. diff --git a/docs/doctoring/product-gap-baseline-source-integrity-20261001.md b/docs/doctoring/product-gap-baseline-source-integrity-20261001.md new file mode 100644 index 0000000000..a009f92bbb --- /dev/null +++ b/docs/doctoring/product-gap-baseline-source-integrity-20261001.md @@ -0,0 +1,28 @@ +# Product gap baseline source-integrity repair + +## Incident + +Repository Metadata Reconcile run `36777030872`, job `110097412873`, ran +against `.github#2530@61a3f2ebbf6adf9c237a647552ccffeb52b00f47`. +Five tests failed because `docs/product-technical-gap-baseline.md` began with +a connector display warning and contained only 433 lines. The previous complete +artifact contained 3,703 lines and 361,021 bytes. + +## Root cause + +Commit `bd3cfe8645844ec7b140a1b73d2b339f8e4283dc` attempted to change one +dependency-floor row, but persisted a truncated connector rendering instead of +the Git blob. This removed G-17 publication evidence, the 107-row live +inventory, APA 7th references, and buyer-facing PRD/TRD/UML/Gap evidence. + +## RED to GREEN + +- RED `c8d41f52962814d080a1c4947acbff2f3f9bb81e` rejects the connector warning + and output-line banner at the artifact boundary. +- GREEN restores the complete parent blob and reapplies only the intended + `ip-address` 10.7.1 dependency-floor row. +- The focused integrity, gap-baseline, and published-lineage tests must pass + before the full warnings-fatal repository suite is accepted. + +Hosted exact-head Metadata, authenticated agent verdicts, and independent +approval remain required; this record is not merge authorization. diff --git a/docs/doctoring/trusted-review-archive-transient-retry.md b/docs/doctoring/trusted-review-archive-transient-retry.md new file mode 100644 index 0000000000..341f79323d --- /dev/null +++ b/docs/doctoring/trusted-review-archive-transient-retry.md @@ -0,0 +1,37 @@ +# Trusted review archive transient retry + +## Incident + +Bounded Noema continuation run +[`36760921156`](https://github.com/ContextualWisdomLab/.github/actions/runs/36760921156), +job `110043067331`, resolved the trusted workflow source to protected +`main@37b10243cec3d160ecc9c1be75c71428b160a703` and then failed before model +setup. The GitHub archive API returned HTTP 502 while the workflow executed a +single `curl -fsSL` request. The source revision, target pull request, and model +route were not the cause. + +## Decision + +The three central workflows that materialize the same immutable trusted archive +use `curl --retry 3 --retry-all-errors --retry-delay 1 --retry-max-time 30`. +This is a bounded transport repair at the canonical `.github` owner. It does +not select a mutable ref, bypass archive extraction checks, change credentials, +or convert a terminal failure into success. + +## Alternatives + +- Manual rerun was rejected because it would not repair the repeated control + path. +- A custom retry helper was rejected because native `curl` already provides the + bounded behavior and another abstraction would enlarge the trusted surface. +- An unbounded loop was rejected because it could occupy review capacity without + a terminal result. + +## Verification and remaining gate + +`tests/test_trusted_archive_retry_behavior.py` runs the production command from +each workflow against a local server that returns one 502 followed by a 200. +The test was RED on the predecessor and GREEN after the bounded retry options. +Hosted exact-head Checks, unresolved-thread review, qualifying independent +approval, and ordinary protected merge remain required. A local GREEN result is +not merge authorization. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index ac32ad2877..f839e18b4e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -6,18 +6,42 @@ |---|---|---|---| | The transitive lock security regression converted dot-separated version parts with `int()`, so an npm prerelease such as `3.1.8-beta.1` raised `ValueError` before the security assertion and could not preserve prerelease ordering against the final fixed release | Independent `.github#2531` review `PRR_kwDOS_C14s8AAAABQb3fbw`; RED commit `b4a24d2585e0b076c51552c43f3c29a466fdd6dd` reproduces the exception in `test_prerelease_does_not_satisfy_final_security_minimum`; GREEN commit `e169a9d3defe06f427ca53071bc8ff3a20483b4a` passes the focused file and the 5,176-test repository suite | Parse every matching hoisted or nested lock version with the already pinned `packaging.version.Version`, compare it with semantic final-release minima, and retain the prerelease regression case | **Proposed / focused 4 tests and full 5,176 tests GREEN; exact-head hosted Checks and qualifying approval required** | +## 2026-10-01 Maturin response-lifecycle coverage closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#1653@5cd141ec2c33b631d164af936cd1c9de70e4c9a4` passed all 5,314 tests but failed the complete branch gate because the canonical Maturin downloader left five error-path statements and two branches unexecuted; the owner workflow omitted both verifier paths and stacked PR bases | Trusted uv Materializer run `36811202519`, job `110206427182`; `verify_release_maturin_tool_assets.py` 95%, missing lines 104 and 106-112 plus branch 114→116; no owner run at #2530 predecessor `8cf2ea5f73976d47b2267fb52ac28284323404b7` because its base was #2531 rather than `main` | Repair canonical successor `.github#2530`: exercise non-200 and opener-raised `HTTPError` closure, replace the impossible nullable-response finalizer with one unconditional response-owned close scope, add source/test and stacked-PR trigger contracts to the complete gate while retaining protected-main push scope, preserve all network and fail-closed boundaries, then ordinary-merge the accepted owner head into #1653 | **Proposed / hosted RED reproduced; focused verifier coverage GREEN locally; path and stacked-admission contracts RED→GREEN; exact-head hosted full-suite, security, CodeQL, and independent approval required** | + ## 2026-10-01 bounded Maturin release downloader SAST closure | Gap | Exact evidence | Action | Status | |---|---|---|---| | The canonical Maturin asset verifier replaced a suppressed dynamic `urlopen` call with direct `HTTPSConnection`, but Semgrep's low-confidence certificate-validation audit still rejects every use of that low-level API | `.github#2531@e33d97d022e1c1a26b35d51f9fa4b695fe969547`; SAST run `36802256836`; job `110178968784`; rule `python.lang.security.audit.httpsconnection-detected.httpsconnection-detected` | At source-repair head `eede925e71e0f1526560a305f5219a13c6631227`, replace the low-level connection with a standard-library opener whose redirect handler admits one credential-free HTTPS hop only from the fixed GitHub release path to the exact release-assets host; preserve bounded reads and terminal response closure; prohibit scanner suppressions | **Proposed / RED scanner-contract reproduction and 31 tests plus 4 subtests GREEN locally; source-repair exact-head Security, Python Security, SAST, and runtime-quality GREEN; terminal CodeQL and independent approval required** | +## 2026-10-01 gap baseline source integrity + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| A connector display truncation was committed as the baseline source, removing 3,270 lines of PRD/TRD/UML, Gap-register, APA 7th, and live-inventory evidence | Repository Metadata Reconcile run `36777030872`; job `110097412873`; exact broken head `.github#2530@61a3f2ebbf6adf9c237a647552ccffeb52b00f47`; RED `c8d41f52962814d080a1c4947acbff2f3f9bb81e` | Restore the last complete 3,703-line source blob, reapply only the intended first-patched dependency-floor row, and reject future connector truncation banners in the repository quality gate | **Proposed / local RED→GREEN complete; exact-head hosted Metadata and independent approval required** | + +## 2026-10-01 Noema document-reader transitive security baseline + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The generated Noema document-reader lock selected `fast-uri` 3.1.7 and `ip-address` 10.7.0 after CVE-2026-86472, CVE-2026-101911, and CVE-2026-101912 were published | Security Scan run `36773087489`; Trivy job `110084194330`; exact predecessor `.github#2530@a99784219305d1b6e14cf76f0acea30c5ee45e21` | At the central `.github` owner, regenerate only the two transitive entries to `fast-uri` 3.1.8 and the first patched `ip-address` 10.7.1 release, scan every hoisted or nested lock entry in a regression contract, reproduce with `npm ci`, and require a zero-vulnerability npm audit | **Proposed / local RED→GREEN and audit complete; exact-head hosted security and independent approval required** | + ## 2026-10-01 shared Strix LiteLLM credential-exfiltration closure | Gap | Exact evidence | Action | Status | |---|---|---|---| | The shared Strix hash lock selected LiteLLM 1.94.1, which is affected by CVE-2026-84377 / GHSA-3cv6-jpf6-8222 and can expose configured provider credentials through authenticated routing overrides | Failing predecessor `.github#2531@516471fbe7d4e93a50c7bbba20402447f06f8d8b`, Python Security run `36799069276`, job `110169140365`; repaired exact head `fe879f7b7f48f729f757e03851bf61149470ccb5`, Python Security run `36800615364`, Security Scan run `36800615435`, SAST run `36800615456`, and runtime-quality run `36800615444` | Preserve stacked #2545's Noema document-reader transitive repair, add a direct `litellm==1.94.3` source floor, regenerate the complete hash lock, bind source and lock with a RED-to-GREEN regression, and require authenticated CodeQL verdict evidence plus independent approval | **Proposed / dependency, security, SAST, and runtime-quality Checks GREEN; CodeQL run `36800615319` fail-closed pending authenticated verdicts; independent approval required** | +## 2026-10-01 trusted review archive transient transport + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| Required Noema resolved the correct immutable trusted source but a single GitHub archive API HTTP 502 exhausted materialization before model setup | `.github` run `36760921156`, job `110043067331`; protected source `37b10243cec3d160ecc9c1be75c71428b160a703`; combined successor predecessor `.github#2530@5b3a76ea71d7ae2fa9b1719f4f82df8d52e98082` | At the central `.github` owner, add bounded native transport retries to Noema, OpenCode, and merge-scheduler trusted archive downloads; prove the actual commands against a deterministic 502→200 server without changing exact-SHA, credential, extraction, or fail-closed contracts | **Proposed / local RED→GREEN complete; exact-head hosted Checks and independent approval required** | + ## 2026-10-01 PyJWT recursion denial-of-service closure | Gap | Exact evidence | Action | Status | @@ -37,6 +61,19 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-09-30 central coverage owner stack delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-CENTRAL-COVERAGE-OWNER-01 | **Proposed — complete local integration GREEN; hosted exact-head acceptance pending** | Current coverage owner `.github#2521@61fb469a…`, parser/security/response integration successor `.github#2530@2510618f…`, GitHub API response-lifecycle owner `.github#2532@9d3ec75d…`를 ordinary two-parent merge로 보존했다. 첫 integrated warning-fatal run은 `5196 passed, 6 skipped` 뒤 queue/Strix/release prescreen/release dependency의 실제 미실행 분기 90개와 partial branch 29개를 드러냈다. Dummy/live-CLI tests를 bounded behavior contracts로 교체하고, exact Git blob·Cargo development lock·runtime receipt·final fanout cap·Python 3.10 TOML fallback을 검증했으며, 앞선 필수조건 때문에 도달 불가능했던 prescreener postcondition만 제거했다. 두 live head를 재수집·일반 병합한 combined successor는 `5291 passed, 5 skipped, 40 subtests passed`, owned production `18729/18729` statements 및 `7642/7642` branches, Docstring 100%, warning 0이다. | Canonical owner는 중앙 `ContextualWisdomLab/.github`이며 source delta는 ordinary merge ancestry로만 통합한다. #2530과 #2521의 live head 이동을 재수집해 force 없이 merge했고 새 integrated tree 전체를 재검증했다. 게시된 #2530 exact head의 hosted security/quality Checks 및 qualifying independent approval을 새로 확인한다. queued/skipped/pending을 성공으로 간주하지 않고 #2521/#2530/#2532를 단순 Close하지 않는다. [RCA와 검증 근거](doctoring/central-coverage-owner-stack-2521.md). | + +### 2026-09-30 full-suite parser-lock incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-QUALITY-FULL-SUITE-PARSER-LOCK-01 | **Proposed — `.github#2530` combined successor preserves live parser, security, response-lifecycle, and coverage heads; hosted acceptance pending** | Protected `main@37b10243…`의 common quality lock만 설치하는 전체 suite가 `defusedxml`을 찾지 못해 collection error 13건으로 중단됐고, 같은 suite의 신규 workflow 계약은 `yaml`을 import한다. Concurrent-head 재검증 뒤 live #2530 `2510618f…`와 live #2521 `61fb469a…`를 Force Push·rebase 없이 ordinary merge했다. 이 ancestry는 security prerequisite #2531 `d1aa3659…`, response-lifecycle prerequisite #2532 `9d3ec75d…`, parser lock, coverage 수리를 함께 보존한다. 잠금을 직접 설치하는 모든 workflow는 생성 lock, 두 source input, compiler 변경을 추적하며 선택형 runtime quality도 실제 consumer suite를 실행한다. Review-repair owner는 launcher runtime 두 suite를 직접 실행·계측한다. Combined local evidence는 Python 3.14 warnings-fatal 5,291 passed, 5 skipped, 40 subtests, production 18,729/18,729 statements·7,642/7,642 branches, Docstring 100%다. 이 행은 live Project #1 상태나 merge authorization을 주장하지 않는다. | Canonical owner는 중앙 `.github`의 source requirement, 생성 hash lock, compiler, 직접 소비 quality workflows다. 새 combined exact head의 hosted Checks, 미해결 thread 0, qualifying independent approval을 다시 수집해야 ordinary protected merge할 수 있다. #2531/#2532/#2521은 protected successor merge와 complete carryover를 확인하기 전 닫지 않는다. | +| CONTROL-REPOSITORY-BRANCH-COVERAGE-01 | **Proposed — `.github#2521`의 전체 유효 delta를 `.github#2530` combined successor가 ordinary merge로 승계; exact-head hosted acceptance pending** | `.github#2521`는 production exclusion을 제거해 전역 100% 주장을 정직하게 RED로 되돌렸다. 네 잔여 소유자인 `opencode_queue_priority`, `strix_unverified_dependency`, `prescreen_release_runtime_archives`, `release_dependency_gate`의 실제 분기를 test-first로 모두 실행했고, launcher production omission도 제거했다. `.github#2530`의 parser lock 없이는 전체 suite collection이 실패하고, parser-lock PR은 이 coverage gap 때문에 전역 gate가 실패하는 순환 선행조건이었다. predecessor의 current head `61fb469a…`까지 successor ancestry에 보존하며 predecessor는 successor의 보호 병합과 tree 동등성을 확인하기 전 닫지 않는다. | Canonical owner는 중앙 `.github`의 production CI modules와 complete repository gate다. coverage 제외·pragma·threshold 하향·샘플 축소는 허용하지 않는다. refreshed combined successor의 complete warnings-fatal suite, 100% statement/branch report, hosted Checks, independent approval, ordinary protected merge를 새 exact head에서 완료해야 한다. | + ### 2026-09-30 공유 보안 기준 exact-head delta 이 delta는 아래 2026-08-26 인벤토리를 덮어쓰지 않는다. 2026-09-30 재수집한 @@ -47,6 +84,12 @@ |---|---|---|---| | CONTROL-SHARED-SECURITY-LOCK-01 | **Source repair in progress — release HOLD** | `.github#1026@6f645a73502e159d5a229805afa34868ad9bb851`의 Security Scan run `36495499815`는 공통 Rust fixture의 PyO3 `0.22.6`에서 GHSA-36hh-v3qg-5jq4와 GHSA-chgr-c6px-7xpp를 검출했고, Python Security run `36495499871`은 공통 Strix hash lock의 PyJWT `2.13.0`에서 CVE-2026-102274를 검출했다. 두 파일은 #1026 변경 범위 밖이며 보호 `main`에도 동일하게 남아 있었다. RED commit `cd84d887`는 PyO3 `0.29.2`와 PyJWT `2.14.0` source/lock parity를 요구한다. | 중앙 `.github`가 공통 fixture와 Strix lock을 소유한다. [RCA와 검증 계약](doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md)에 따라 owner PR의 exact-head Checks와 독립 승인, ordinary protected merge, immutable consumer source pin 갱신, 그리고 #1026의 비강제 main merge-forward가 순서대로 필요하다. 어떤 실패도 #1026 전용 패치나 bypass로 처리하지 않는다. | +### 2026-09-30 GitHub API response lifecycle incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-GITHUB-API-HTTP-ERROR-CLOSE-01 | **Proposed — protected-main RED reproduced; source repair under hosted exact-head verification** | 보호된 `.github/main@37b10243cec3d160ecc9c1be75c71428b160a703`의 Python 3.14.7 `tests/test_github_api_url_boundary.py -W error`가 실제 CodeQL/Strix opener의 synthetic 302 여덟 경우에서 `ResourceWarning: Implicitly cleaning up `로 `8 failed, 26 passed`였다. 첫 repair의 warning-fatal full suite가 동일 defect를 Noema/Pingora/preflight/Pages/sandbox readiness에서 추가로 드러냈다. | Canonical owner는 중앙 `.github`이다. 각 caller가 기존 bounded status/telemetry와 fail-closed mapping을 보존한 뒤 file-like error response를 명시적으로 닫는다. `5161 passed, 10 skipped, 40 subtests passed`로 complete warning-fatal local tree가 GREEN이다. [RCA와 acceptance](doctoring/github-api-http-error-response-lifecycle.md)를 따라 exact-head hosted security, independent review, ordinary protected merge를 완료한 뒤 `.github#2040`과 review-transport stack이 새 protected head를 정상 병합해 downstream 증거를 재생성해야 한다. | + ### 2026-09-19 exact-head incident delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | diff --git a/pyproject.toml b/pyproject.toml index ff6353c164..cebf24066b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,16 +19,8 @@ pythonpath = ["."] [tool.coverage.run] branch = true source = ["scripts/ci"] -# contextual_orchestrator_review_launcher.py imports the vendored -# contextual-orchestrator library, which is installed only inside the review -# sidecar's runtime (scripts/ci/contextual_orchestrator_review_sidecar.sh). -# It cannot be imported by this repository's test suite, so it is omitted from -# coverage exactly as contextual-orchestrator omits its own runtime-only -# __main__.py and server.py. The policy and ZDR decision logic it consumes are -# fully tested offline. omit = [ "tests/*", - "scripts/ci/contextual_orchestrator_review_launcher.py", ] [tool.coverage.report] diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 116009874b..f58dc7f4d3 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -137,88 +137,96 @@ coverage==7.15.4 \ # via # -r requirements-opencode-review-ci.txt # pytest-cov -hypothesis==6.168.0 \ - --hash=sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669 \ - --hash=sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9 \ - --hash=sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a \ - --hash=sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4 \ - --hash=sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc \ - --hash=sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76 \ - --hash=sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1 \ - --hash=sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d \ - --hash=sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099 \ - --hash=sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f \ - --hash=sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29 \ - --hash=sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c \ - --hash=sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01 \ - --hash=sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377 \ - --hash=sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7 \ - --hash=sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f \ - --hash=sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c \ - --hash=sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0 \ - --hash=sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d \ - --hash=sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517 \ - --hash=sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131 \ - --hash=sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507 \ - --hash=sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09 \ - --hash=sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9 \ - --hash=sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2 \ - --hash=sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67 \ - --hash=sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a \ - --hash=sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3 \ - --hash=sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063 \ - --hash=sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab \ - --hash=sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4 \ - --hash=sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b \ - --hash=sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6 \ - --hash=sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda \ - --hash=sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc \ - --hash=sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795 \ - --hash=sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475 \ - --hash=sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2 \ - --hash=sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e \ - --hash=sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f \ - --hash=sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4 \ - --hash=sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128 \ - --hash=sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7 \ - --hash=sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28 \ - --hash=sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236 \ - --hash=sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782 \ - --hash=sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8 \ - --hash=sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79 \ - --hash=sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e \ - --hash=sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780 \ - --hash=sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20 \ - --hash=sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27 \ - --hash=sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349 \ - --hash=sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef \ - --hash=sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9 \ - --hash=sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8 \ - --hash=sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93 \ - --hash=sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63 \ - --hash=sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8 \ - --hash=sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b \ - --hash=sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04 \ - --hash=sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887 \ - --hash=sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c \ - --hash=sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149 \ - --hash=sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0 \ - --hash=sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5 \ - --hash=sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1 \ - --hash=sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f \ - --hash=sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd \ - --hash=sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193 \ - --hash=sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7 \ - --hash=sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc \ - --hash=sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc \ - --hash=sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571 \ - --hash=sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d \ - --hash=sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213 \ - --hash=sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68 \ - --hash=sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441 \ - --hash=sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad \ - --hash=sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d \ - --hash=sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843 +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via -r requirements-noema-document-ci.txt +hypothesis==6.168.3 \ + --hash=sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408 \ + --hash=sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89 \ + --hash=sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62 \ + --hash=sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897 \ + --hash=sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef \ + --hash=sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b \ + --hash=sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102 \ + --hash=sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575 \ + --hash=sha256:140fe3cc7ba98a4ffeeb6492c318b43c326110a4d7b4f56bb759d5c264e2dc1c \ + --hash=sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa \ + --hash=sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7 \ + --hash=sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595 \ + --hash=sha256:26084ef31653108da9e5eb171278a1d930e90d8631604391f828219f5c1f6cb2 \ + --hash=sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef \ + --hash=sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f \ + --hash=sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7 \ + --hash=sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16 \ + --hash=sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2 \ + --hash=sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96 \ + --hash=sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074 \ + --hash=sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90 \ + --hash=sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4 \ + --hash=sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a \ + --hash=sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766 \ + --hash=sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462 \ + --hash=sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca \ + --hash=sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5 \ + --hash=sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd \ + --hash=sha256:558625ff28e415f2f770cad618c2c9e05a045a6586949397f178d537bec2f083 \ + --hash=sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef \ + --hash=sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc \ + --hash=sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026 \ + --hash=sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f \ + --hash=sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd \ + --hash=sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5 \ + --hash=sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c \ + --hash=sha256:7311d8ff296806ea41513d52edc832fcf43a1dc8d5c5f308cc386fc31b253a05 \ + --hash=sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa \ + --hash=sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e \ + --hash=sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d \ + --hash=sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa \ + --hash=sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b \ + --hash=sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a \ + --hash=sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d \ + --hash=sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018 \ + --hash=sha256:836eda971f25ddfce274107ed113297e8acede8d9b267775742b8ba8a404d0f0 \ + --hash=sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b \ + --hash=sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24 \ + --hash=sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5 \ + --hash=sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696 \ + --hash=sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b \ + --hash=sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b \ + --hash=sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75 \ + --hash=sha256:aac5889ea7b0b86cf2bf5709250b7576c44859dd249d1be23c009a7d6fb9f742 \ + --hash=sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2 \ + --hash=sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11 \ + --hash=sha256:b1872d2dc3f3758dfbf17cc39838b8e6b6a5c39a37811c067245e87b12d50f74 \ + --hash=sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba \ + --hash=sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a \ + --hash=sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128 \ + --hash=sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed \ + --hash=sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95 \ + --hash=sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068 \ + --hash=sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96 \ + --hash=sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7 \ + --hash=sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb \ + --hash=sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957 \ + --hash=sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38 \ + --hash=sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86 \ + --hash=sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499 \ + --hash=sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4 \ + --hash=sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248 \ + --hash=sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2 \ + --hash=sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd \ + --hash=sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c \ + --hash=sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483 \ + --hash=sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0 \ + --hash=sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580 \ + --hash=sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe \ + --hash=sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675 \ + --hash=sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6 \ + --hash=sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09 \ + --hash=sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab \ + --hash=sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e \ + --hash=sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837 # via -r requirements-opencode-review-ci.txt iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ @@ -272,6 +280,81 @@ pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 # via -r requirements-opencode-review-ci.txt +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via -r requirements-opencode-review-ci.txt sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index bf2112ed68..452ce2f074 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -12,4 +12,7 @@ interrogate==1.7.0 maturin==1.15.0 pytest==9.1.1 pytest-cov==7.1.0 +# Full-suite collection imports the Noema document parser and workflow YAML. +-r requirements-noema-document-ci.txt +PyYAML==6.0.3 uv==0.12.7 diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py index 53e00c41c6..295ecf544e 100644 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ b/scripts/ci/codeql_ghas_configuration_identity.py @@ -29,6 +29,7 @@ DEFAULT_SETUP_ANALYSIS_KEY = "dynamic/github-code-scanning/codeql:analyze" CODEQL_TOOL_NAME = "CodeQL" GITHUB_API_AUTHORITY = "api.github.com" +MAX_HTTP_ERROR_DIAGNOSTIC_BYTES = 400 class ConfigurationIdentityError(RuntimeError): @@ -199,7 +200,12 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: with _GITHUB_API_OPENER.open(request, timeout=timeout_seconds) as response: payload = response.read().decode("utf-8") except urllib.error.HTTPError as exc: - body = exc.read().decode("utf-8", errors="replace")[-400:] + try: + body = exc.read(MAX_HTTP_ERROR_DIAGNOSTIC_BYTES).decode( + "utf-8", errors="replace" + ) + finally: + exc.close() raise ConfigurationIdentityError( f"GitHub API GET failed with HTTP {exc.code}: {body}" ) from exc diff --git a/scripts/ci/collect_release_strix_bindings.py b/scripts/ci/collect_release_strix_bindings.py index c4478fdeff..8162ba36c1 100644 --- a/scripts/ci/collect_release_strix_bindings.py +++ b/scripts/ci/collect_release_strix_bindings.py @@ -337,6 +337,8 @@ def collect_bindings( def main() -> None: + """Collect and print the full release dependency review from CLI inputs.""" + parser = argparse.ArgumentParser() for name in ( "capture", "license-report", "plan", "metadata", "attempt", "repository", diff --git a/scripts/ci/contextual_orchestrator_review_launcher.py b/scripts/ci/contextual_orchestrator_review_launcher.py index 811dc7d3f8..8d5fe6c100 100644 --- a/scripts/ci/contextual_orchestrator_review_launcher.py +++ b/scripts/ci/contextual_orchestrator_review_launcher.py @@ -33,6 +33,7 @@ import sys from pathlib import Path from typing import Any, Callable +from urllib.error import HTTPError from scripts.ci.contextual_orchestrator_review_policy import ( FREE_POOL_CREDENTIAL_NAMES, @@ -329,10 +330,15 @@ def _safe_retry_after_seconds(exc: Exception) -> int | None: # runs inside the probe walk's exception handler, so a ValueError here # would escape ``_preflight_review_agents`` -- whose callers catch only # ``ReviewPreflightError`` -- and kill the boot before any evidence file - # is written. Every ``isdecimal`` string is accepted by ``int``. - if not isinstance(raw, str) or not raw.strip().isdecimal(): + # is written. Reject more than five decimal characters before conversion: + # the accepted ceiling is 86400, and Python deliberately rejects very + # long integer strings before ``int`` can return a value. + if not isinstance(raw, str): return None - seconds = int(raw.strip()) + normalized = raw.strip() + if len(normalized) > 5 or not normalized.isdecimal(): + return None + seconds = int(normalized) return seconds if 0 <= seconds <= 86400 else None @@ -389,19 +395,25 @@ def _record_provider_exception(row: dict[str, object], exc: Exception) -> None: row: The in-progress per-route evidence row to update. exc: The exception a probe attempt raised. """ - row["status"] = "rejected" - error_type = type(exc).__name__ - row["error_type"] = ( - error_type if error_type.isidentifier() and len(error_type) <= 64 else "provider_error" - ) - http_status = _safe_http_status(exc) - if http_status is not None: - row["http_status"] = http_status - retry_after = _safe_retry_after_seconds(exc) - if retry_after is not None: - row["retry_after_s"] = retry_after - row.pop("finish_reason", None) - row.pop("reasoning_without_content", None) + try: + row["status"] = "rejected" + error_type = type(exc).__name__ + row["error_type"] = ( + error_type + if error_type.isidentifier() and len(error_type) <= 64 + else "provider_error" + ) + http_status = _safe_http_status(exc) + if http_status is not None: + row["http_status"] = http_status + retry_after = _safe_retry_after_seconds(exc) + if retry_after is not None: + row["retry_after_s"] = retry_after + row.pop("finish_reason", None) + row.pop("reasoning_without_content", None) + finally: + if isinstance(exc, HTTPError): + exc.close() def _demote_agent(agent: object, penalty: int) -> object: @@ -1242,7 +1254,7 @@ def main(argv: list[str] | None = None) -> int: try: discovered, discovery_errors = discover_all_models() - except Exception as exc: # pragma: no cover - provider/networking failure is runtime-only + except Exception as exc: raise SystemExit(f"review sidecar discovery failed: {exc}") from exc _log_discovery_errors(discovery_errors) routable_discovered = _routable_discovered_models(discovered) @@ -1388,5 +1400,5 @@ def main(argv: list[str] | None = None) -> int: return 0 -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": raise SystemExit(main()) diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 3ddc126604..99d228d5e1 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -22,6 +22,7 @@ import urllib.parse import urllib.request from typing import Any +from urllib.error import HTTPError try: import tomllib @@ -376,8 +377,11 @@ def _download_trusted_uv_archive() -> bytes: break payload.extend(chunk) except OSError as exc: + error_type = type(exc).__name__ + if isinstance(exc, HTTPError): + exc.close() raise RuntimeError( - f"trusted uv archive download failed: {type(exc).__name__}" + f"trusted uv archive download failed: {error_type}" ) from exc if len(payload) > TRUSTED_UV_DOWNLOAD_MAX_BYTES: diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 380ee22675..c08df174a3 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -1719,12 +1719,15 @@ def call_llm( http_status: int | None = None retry_after_seconds: int | None = None if isinstance(exc, urllib.error.HTTPError): - active_phase = "response_error" - http_status = exc.code if type(exc.code) is int else None - retry_after_seconds = parse_http_retry_after_seconds(exc.headers) - gateway_telemetry = _extract_http_error_telemetry(exc) - model_value = gateway_telemetry.get("served_model") - served_model = model_value if isinstance(model_value, str) else None + try: + active_phase = "response_error" + http_status = exc.code if type(exc.code) is int else None + retry_after_seconds = parse_http_retry_after_seconds(exc.headers) + gateway_telemetry = _extract_http_error_telemetry(exc) + model_value = gateway_telemetry.get("served_model") + served_model = model_value if isinstance(model_value, str) else None + finally: + exc.close() elapsed = time.monotonic() - attempt_started current_failure = _stable_failure_diagnostic(exc) model_note = served_model or "unknown" diff --git a/scripts/ci/opencode_queue_priority.py b/scripts/ci/opencode_queue_priority.py index 7024099b62..75ec11c58b 100755 --- a/scripts/ci/opencode_queue_priority.py +++ b/scripts/ci/opencode_queue_priority.py @@ -38,6 +38,8 @@ @dataclass(frozen=True) class QueuedRun: + """Identify one queued OpenCode review run.""" + run_id: int created_at: datetime repo: str @@ -47,6 +49,8 @@ class QueuedRun: @dataclass(frozen=True) class PrState: + """Describe the live pull request state used to classify a queued run.""" + state: str head: str priority: bool @@ -54,6 +58,8 @@ class PrState: @dataclass(frozen=True) class Plan: + """Partition ordered queued runs by priority and validity.""" + keep: tuple[QueuedRun, ...] cancel_current: tuple[QueuedRun, ...] cancel_stale: tuple[QueuedRun, ...] @@ -94,10 +100,14 @@ def metrics(p: Plan, *, now: datetime) -> dict: def _gh(*args: str, stdin: str | None = None) -> str: + """Run a GitHub CLI command and return its standard output.""" + return subprocess.run(["gh", *args], input=stdin, capture_output=True, text=True, check=True).stdout def fetch_queued() -> list[QueuedRun]: + """Return queued dispatch runs with parseable pull request identities.""" + out = _gh("api", "--paginate", f"repos/{CENTRAL}/actions/workflows/{WORKFLOW}/runs?status=queued&per_page=100", "--jq", ".workflow_runs[]|[.id,.created_at,.display_title]|@json") runs = [] @@ -111,6 +121,8 @@ def fetch_queued() -> list[QueuedRun]: def fetch_live(keys: set[tuple[str, int]], label: str) -> dict[tuple[str, int], PrState]: + """Fetch live pull request states and trusted priority labels for keys.""" + live: dict[tuple[str, int], PrState] = {} perms: dict[tuple[str, str], str] = {} keys_sorted = sorted(keys) @@ -144,6 +156,8 @@ def fetch_live(keys: set[tuple[str, int]], label: str) -> dict[tuple[str, int], def main(argv: list[str] | None = None) -> int: + """Print queue metrics and optionally record and cancel selected runs.""" + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--label", default="review-priority") ap.add_argument("--include-current", action="store_true", help="also cancel non-priority current-head runs") diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 5c1e39d9e3..4c9697f1d7 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -467,11 +467,17 @@ def _github_open_json(url: str, token: str) -> object: with github_opener.open(request, timeout=30) as response: payload = response.read(MAX_RESPONSE_BYTES + 1) except (HTTPError, URLError, TimeoutError) as exc: - if isinstance(exc, HTTPError) and exc.code == 404: - raise ArtifactDeclarationNotFoundError( - f"GitHub API reported no resource for policy evidence at {url}" + try: + if isinstance(exc, HTTPError) and exc.code == 404: + raise ArtifactDeclarationNotFoundError( + f"GitHub API reported no resource for policy evidence at {url}" + ) from exc + raise PolicyError( + f"GitHub API request failed for policy evidence: {type(exc).__name__}" ) from exc - raise PolicyError(f"GitHub API request failed for policy evidence: {type(exc).__name__}") from exc + finally: + if isinstance(exc, HTTPError): + exc.close() if len(payload) > MAX_RESPONSE_BYTES: raise PolicyError("GitHub API policy response exceeded the bounded response size") try: @@ -497,7 +503,13 @@ def _github_open_raw_bytes(url: str, token: str, max_bytes: int) -> bytes: with github_opener.open(request, timeout=30) as response: raw = response.read(max_bytes + 1) except (HTTPError, URLError, TimeoutError) as exc: - raise PolicyError(f"GitHub raw blob request failed: {type(exc).__name__}") from exc + try: + raise PolicyError( + f"GitHub raw blob request failed: {type(exc).__name__}" + ) from exc + finally: + if isinstance(exc, HTTPError): + exc.close() if len(raw) > max_bytes: raise PolicyError("GitHub raw blob exceeded the bounded response size") return raw @@ -614,8 +626,6 @@ def _load_raw_file_bytes( return raw if encoding == "none": raise PolicyError(f"GitHub content evidence for {path} has no inline content and no verifiable oversized size") - if encoding != "base64": - raise PolicyError(f"GitHub content evidence for {path} is not a regular base64 file") encoded = payload.get("content") if not isinstance(encoded, str): raise PolicyError(f"GitHub content evidence for {path} has a malformed size or content field") diff --git a/scripts/ci/place_maturin_extension.py b/scripts/ci/place_maturin_extension.py index 45ff27aba5..c6fd690216 100755 --- a/scripts/ci/place_maturin_extension.py +++ b/scripts/ci/place_maturin_extension.py @@ -19,10 +19,14 @@ def _inside(path: pathlib.Path, root: pathlib.Path) -> bool: + """Return whether path is root or is contained by root.""" + return path == root or root in path.parents def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]: + """Copy wheel extensions into existing project package directories.""" + project = project_dir.resolve() pyproject = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8")) python_source = pyproject.get("tool", {}).get("maturin", {}).get("python-source", ".") @@ -48,6 +52,8 @@ def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]: def main(argv: list[str]) -> int: + """Run extension placement and return its process status.""" + if len(argv) != 2: print("usage: place_maturin_extension.py WHEEL PROJECT_DIR", file=sys.stderr) return 2 diff --git a/scripts/ci/prescreen_release_runtime_archives.py b/scripts/ci/prescreen_release_runtime_archives.py index 7631a38a06..7211b2acf8 100644 --- a/scripts/ci/prescreen_release_runtime_archives.py +++ b/scripts/ci/prescreen_release_runtime_archives.py @@ -109,6 +109,8 @@ def _build_packages(item: Mapping[str, Any], folder: Path) -> list[dict[str, Any raise gate.GateError(gate.CAPTURE_INCOMPLETE, f"{leg}: {name} metadata is ambiguous") metadata_root = PurePosixPath(metadata[0]).parent def read_file(path: str) -> bytes: + """Read one bounded package file from the captured build snapshot.""" + entry = members.get(f"{name}/{path}") if entry is None or entry.file_size > 4 * 1024 * 1024: raise gate.GateError(gate.CAPTURE_INCOMPLETE, f"{leg}: {name} text file is missing or oversized") @@ -391,17 +393,14 @@ def prescreen(scope: Any, root: Path) -> dict[str, list[dict[str, Any]]]: "native_properties": native_properties, "fixture": fixture, "fixture_sha256": gate.fixture_digest(fixture), "legs": [leg]} - if (len(seen_legs) != 13 or "sdist" not in seen_legs - or seen_variants != {f"universal2-apple-darwin-py{version}" - for version in ("3.12", "3.13", "3.14")} - or not rows): - raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "runtime archive coverage is incomplete") return {"archives": sorted(rows.values(), key=lambda row: (row["key"], row["source_sha256"])), "build_packages": sorted(build_rows.values(), key=lambda row: row["key"]), "build_tools": sorted(tool_rows.values(), key=lambda row: row["key"])} def main() -> None: + """Write the prescreened runtime archive license report.""" + parser = argparse.ArgumentParser() parser.add_argument("--verified-scope", required=True) parser.add_argument("--scope-root", required=True) diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 1570455818..a78bd09725 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -16,7 +16,7 @@ import sys from pathlib import Path from typing import Any -from urllib.error import URLError +from urllib.error import HTTPError, URLError from urllib.request import HTTPRedirectHandler, Request, build_opener @@ -38,8 +38,6 @@ class _NoPagesRedirects(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): """Raise an HTTPError instead of following the redirect.""" - from urllib.error import HTTPError - raise HTTPError(req.full_url, code, msg, headers, fp) @@ -247,7 +245,13 @@ def _pages_publication_ready(repository: str, current: dict[str, Any]) -> None: if not response.read(1): raise RuntimeError(f"GitHub Pages returned empty content for {repository}") except (URLError, TimeoutError, OSError) as exc: - raise RuntimeError(f"GitHub Pages is not reachable for {repository}") from exc + try: + raise RuntimeError( + f"GitHub Pages is not reachable for {repository}" + ) from exc + finally: + if isinstance(exc, HTTPError): + exc.close() def _repository_file_exists(repository: str, default_branch: str, path: str) -> bool: diff --git a/scripts/ci/release_dependency_gate.py b/scripts/ci/release_dependency_gate.py index 96347278f6..f11178f107 100644 --- a/scripts/ci/release_dependency_gate.py +++ b/scripts/ci/release_dependency_gate.py @@ -48,6 +48,7 @@ import ast import email.parser import hashlib +import importlib import io import json import os @@ -63,10 +64,15 @@ from pathlib import Path, PurePosixPath from typing import Any, Iterable, Mapping, Sequence -try: - import tomllib -except ModuleNotFoundError: # Python 3.10; already declared in the dev group. - import tomli as tomllib +def _import_toml_parser(): + """Return the stdlib TOML parser, or the declared Python 3.10 backport.""" + try: + return importlib.import_module("tomllib") + except ModuleNotFoundError: # Python 3.10; already declared in the dev group. + return importlib.import_module("tomli") + + +tomllib = _import_toml_parser() try: from scripts.ci.spdx_license_policy import ( @@ -1835,6 +1841,8 @@ def _enumerate_cargo(capture: Path, *, source_root: Path | None = None, raise GateError(CAPTURE_INCOMPLETE, "Cargo source checkout cannot be bound") from error def source_blob(path: Path) -> bytes: + """Read and verify one regular source file from the selected commit.""" + try: relative = path.relative_to(bound_root) if any((bound_root / parent).is_symlink() for parent in (relative, *relative.parents)): @@ -2126,6 +2134,8 @@ def _source_license_notice(source: Path | None, source_sha: str, subject: str, raise GateError(CAPTURE_INCOMPLETE, "source notice needs an exact release commit") def blob(path: str) -> bytes: + """Read one bounded regular blob from the selected release commit.""" + entry = subprocess.check_output( ["git", "-C", str(source), "ls-tree", source_sha, "--", path], text=True) if not entry.startswith("100644 blob "): diff --git a/scripts/ci/resolve_base_rust_toolchain.py b/scripts/ci/resolve_base_rust_toolchain.py index 6dcd1642c2..7861bc336a 100755 --- a/scripts/ci/resolve_base_rust_toolchain.py +++ b/scripts/ci/resolve_base_rust_toolchain.py @@ -23,6 +23,8 @@ def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None: + """Read path from the base commit, returning None when absent.""" + completed = subprocess.run( ["git", "-C", str(repo_root), "show", f"{base_sha}:{path}"], check=False, @@ -34,6 +36,8 @@ def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None: def _channel(content: str) -> str | None: + """Return the Rust channel declared by TOML or a legacy bare file.""" + try: toolchain = tomllib.loads(content).get("toolchain") except tomllib.TOMLDecodeError: @@ -47,6 +51,8 @@ def _channel(content: str) -> str | None: def resolve(repo_root: pathlib.Path, base_sha: str) -> str: + """Resolve an exact base Rust release, its fallback, or no override.""" + if not SHA_RE.fullmatch(base_sha): raise ValueError("base SHA must be a full 40-character commit id") for path in PIN_FILES: @@ -66,6 +72,8 @@ def resolve(repo_root: pathlib.Path, base_sha: str) -> str: def main(argv: list[str] | None = None) -> int: + """Print the resolved base Rust toolchain and return a process status.""" + parser = argparse.ArgumentParser() parser.add_argument("--repo-root", required=True, type=pathlib.Path) parser.add_argument("--base-sha", required=True) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index b0376c0822..fb5702f0c2 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -584,6 +584,9 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: if 200 <= response.status < 500: return True time.sleep(1) + except urllib.error.HTTPError as exc: + exc.close() + time.sleep(1) except (urllib.error.URLError, TimeoutError): time.sleep(1) return False diff --git a/scripts/ci/scan_release_native_links.py b/scripts/ci/scan_release_native_links.py index 92af3d3c98..ad302d7894 100644 --- a/scripts/ci/scan_release_native_links.py +++ b/scripts/ci/scan_release_native_links.py @@ -69,6 +69,8 @@ def _reader() -> dict[str, str]: def _links(binary: bytes, target: str, reader: str, *, allow_subset: bool = False) -> list[dict]: + """Inspect a native binary and return validated dynamic-link rows.""" + if len(binary) > 128 * 1024 * 1024: raise ValueError("release native extension exceeds inspection limit") with tempfile.NamedTemporaryFile() as temporary: @@ -101,6 +103,8 @@ def _links(binary: bytes, target: str, reader: str, *, allow_subset: bool = Fals def scan(verified: dict, root: Path, source_sha: str, reader: dict[str, str]) -> dict: + """Build a native-link report from the exact verified distributions.""" + rows = verified.get("verified_distributions") if isinstance(verified, dict) else None if (not re.fullmatch(r"[0-9a-f]{40}", source_sha) or not isinstance(rows, list) or len(rows) != 13 @@ -168,6 +172,8 @@ def scan(verified: dict, root: Path, source_sha: str, reader: dict[str, str]) -> def main() -> None: + """Write the verified release native-link report.""" + parser = argparse.ArgumentParser() parser.add_argument("--verified-distributions", required=True) parser.add_argument("--distribution-root", required=True) diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py index 7319040df2..aed54ded1a 100644 --- a/scripts/ci/strix_evidence_binding.py +++ b/scripts/ci/strix_evidence_binding.py @@ -305,8 +305,10 @@ def default_github_opener(url: str, token: str) -> Any: with _GITHUB_API_OPENER.open(request, timeout=30) as response: payload = response.read() except HTTPError as exc: + status_code = exc.code + exc.close() raise EvidenceBindingError( - f"GitHub changed-file request failed with HTTP {exc.code}" + f"GitHub changed-file request failed with HTTP {status_code}" ) from exc except URLError as exc: raise EvidenceBindingError( diff --git a/scripts/ci/strix_report_scope.py b/scripts/ci/strix_report_scope.py index cc374c5852..158a570ea2 100644 --- a/scripts/ci/strix_report_scope.py +++ b/scripts/ci/strix_report_scope.py @@ -23,6 +23,8 @@ def _names_scoped_ancestor(report: str, changed_paths: list[str]) -> bool: def validate(output: Path, changed_paths: list[str]) -> None: + """Validate that a completed Strix report covers changed source scope.""" + if not output.is_dir() or output.is_symlink(): raise ValueError("scan output directory is missing") runs = [path for path in output.iterdir() if path.is_dir() and not path.is_symlink()] diff --git a/scripts/ci/strix_unverified_dependency.py b/scripts/ci/strix_unverified_dependency.py index bc4c781708..29563d2045 100755 --- a/scripts/ci/strix_unverified_dependency.py +++ b/scripts/ci/strix_unverified_dependency.py @@ -51,10 +51,14 @@ def named_packages(report: str) -> set[str]: def _is_requirements(name: str) -> bool: + """Return whether a filename is a supported requirements manifest.""" + return name.startswith("requirements") and name.endswith((".txt", ".in")) def _manifest_text(repo_root: Path) -> str: + """Return lower-cased text from bounded dependency manifests.""" + chunks = [] for path in repo_root.rglob("*"): if any(part in SKIP_DIRS for part in path.relative_to(repo_root).parts[:-1]): @@ -82,6 +86,8 @@ def unverified_dependency_finding(report: str, repo_root: Path) -> bool: def main(argv: list[str]) -> int: + """Classify command-line report paths and return a process status.""" + if len(argv) != 3: print(__doc__, file=sys.stderr) return 2 diff --git a/scripts/ci/verify_release_distribution_set.py b/scripts/ci/verify_release_distribution_set.py index 1779d5dfde..21e685b891 100644 --- a/scripts/ci/verify_release_distribution_set.py +++ b/scripts/ci/verify_release_distribution_set.py @@ -40,6 +40,7 @@ class DistributionSetError(ValueError): def _strict_object(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]: + """Build a JSON object while rejecting duplicate keys.""" result: dict[str, Any] = {} for key, value in pairs: if key in result: @@ -49,10 +50,12 @@ def _strict_object(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]: def _reject_constant(value: str) -> Any: + """Reject a non-finite JSON constant.""" raise DistributionSetError(f"non-finite JSON value: {value}") def _json_bytes(data: bytes) -> Any: + """Parse bounded UTF-8 JSON with strict object and number handling.""" if len(data) > MAX_CONTROL_BYTES: raise DistributionSetError("control JSON is too large") try: @@ -63,6 +66,7 @@ def _json_bytes(data: bytes) -> Any: def _timestamp(value: Any) -> datetime: + """Parse a canonical UTC timestamp.""" if not isinstance(value, str) or not value.endswith("Z"): raise DistributionSetError("missing canonical UTC timestamp") try: @@ -75,6 +79,7 @@ def _timestamp(value: Any) -> datetime: def _digest(value: Any) -> str: + """Validate and return a canonical SHA-256 artifact digest.""" if not isinstance(value, str) or DIGEST_RE.fullmatch(value) is None: raise DistributionSetError("missing canonical artifact digest") return value @@ -82,6 +87,7 @@ def _digest(value: Any) -> str: def _artifact(artifacts: Mapping[str, Mapping[str, Any]], name: str, artifact_id: int, digest: str, run_id: int, control_sha: str, started: datetime) -> None: + """Require an artifact to match the expected workflow-run identity.""" item = artifacts.get(name) if item is None or type(artifact_id) is not int or artifact_id <= 0: raise DistributionSetError(f"{name}: missing immutable artifact identity") @@ -99,6 +105,7 @@ def _artifact(artifacts: Mapping[str, Mapping[str, Any]], name: str, artifact_id @contextmanager def _archive(repository: str, artifact_id: int, digest: str, fetch: Callable[[str, int, BinaryIO], None]): + """Yield an artifact ZIP after verifying its bounded bytes and digest.""" with tempfile.TemporaryFile() as archive: fetch(repository, artifact_id, archive) if archive.tell() > MAX_ARCHIVE_BYTES: @@ -113,6 +120,7 @@ def _archive(repository: str, artifact_id: int, digest: str, def _members(archive: zipfile.ZipFile, expected: set[str]) -> dict[str, zipfile.ZipInfo]: + """Return the exact safe archive members required by the caller.""" entries = archive.infolist() if len(entries) != len(expected) or {entry.filename for entry in entries} != expected: raise DistributionSetError("artifact ZIP members differ from the expected set") @@ -126,6 +134,7 @@ def _members(archive: zipfile.ZipFile, expected: set[str]) -> dict[str, zipfile. def _record_rows(data: bytes, source_sha: str) -> dict[str, tuple[str, str]]: + """Parse source-bound reproducibility rows keyed by release leg.""" if len(data) > MAX_CONTROL_BYTES: raise DistributionSetError("reproducibility record is too large") try: @@ -269,6 +278,7 @@ def fetch_artifact(repository: str, artifact_id: int, output: BinaryIO) -> None: def main() -> None: + """Verify the CLI-supplied release distribution set.""" parser = argparse.ArgumentParser() for option in ("repository", "source-sha", "control-sha", "run-id", "run-attempt", "record-artifact-id", "record-artifact-digest", "wheel-filename", diff --git a/scripts/ci/verify_release_maturin_tool_assets.py b/scripts/ci/verify_release_maturin_tool_assets.py index b91e1b7469..8e2ee595c0 100644 --- a/scripts/ci/verify_release_maturin_tool_assets.py +++ b/scripts/ci/verify_release_maturin_tool_assets.py @@ -97,12 +97,16 @@ def _download(filename: str) -> bytes: opener = urllib.request.build_opener( urllib.request.ProxyHandler({}), _ExactReleaseRedirect() ) - response = None try: response = opener.open(request, timeout=60) - if response.status != 200: - raise ValueError(f"maturin release download returned HTTP {response.status}") - raw = response.read(MAX_ASSET_BYTES + 1) + try: + if response.status != 200: + raise ValueError( + f"maturin release download returned HTTP {response.status}" + ) + raw = response.read(MAX_ASSET_BYTES + 1) + finally: + response.close() except urllib.error.HTTPError as error: try: raise ValueError( @@ -110,15 +114,13 @@ def _download(filename: str) -> bytes: ) from error finally: error.close() - finally: - if response is not None: - response.close() if len(raw) > MAX_ASSET_BYTES: raise ValueError("maturin release asset exceeds inspection limit") return raw def _binary(raw: bytes, filename: str) -> bytes: + """Extract the single bounded executable from an approved asset archive.""" if filename.endswith(".zip"): with zipfile.ZipFile(io.BytesIO(raw)) as archive: members = archive.infolist() @@ -167,6 +169,7 @@ def verify_assets(evidence: dict, reader: str, fetch=_download) -> None: def main() -> None: + """Verify fixed Maturin release assets from command-line inputs.""" parser = argparse.ArgumentParser() parser.add_argument("--asset-root", type=Path) args = parser.parse_args() diff --git a/scripts/ci/verify_release_scope_evidence_set.py b/scripts/ci/verify_release_scope_evidence_set.py index 094ecabcb2..7af41ec4cc 100644 --- a/scripts/ci/verify_release_scope_evidence_set.py +++ b/scripts/ci/verify_release_scope_evidence_set.py @@ -55,6 +55,7 @@ def _build_python_snapshot(folder: Path, leg: str, source_sha: str, members: Mapping[str, str]) -> None: + """Verify repeated build receipts and their exact Python snapshot.""" first = _json_bytes((folder / f"{leg}.build-first.json").read_bytes()) second = _json_bytes((folder / f"{leg}.build-second.json").read_bytes()) snapshot = folder / f"{leg}.build-python.zip" @@ -119,6 +120,7 @@ def _build_python_snapshot(folder: Path, leg: str, source_sha: str, def _wheel_identity(path: Path) -> tuple[str, str]: + """Read a runtime wheel's normalized project name and version.""" with zipfile.ZipFile(path) as archive: metadata = [item for item in archive.infolist() if item.filename.endswith(".dist-info/METADATA") @@ -224,6 +226,7 @@ def _runtime_target_architecture(runtime: Any, leg: str, *, intel: bool = False) def _runtime_archives(folder: Path, leg: str, source_sha: str, distribution: Mapping[str, Any], members: Mapping[str, str], *, intel: bool = False) -> list[dict[str, Any]]: + """Verify runtime dependency archives and their lock receipt.""" runtime = _json_bytes((folder / f"{leg}.runtime.json").read_bytes()) if (not isinstance(runtime, Mapping) or runtime.get("source_sha") != source_sha or runtime.get("leg") != leg @@ -492,6 +495,7 @@ def verify_scope_evidence_set( def main() -> None: + """Verify primary and Intel macOS scope evidence from CLI inputs.""" parser = argparse.ArgumentParser() for option in ("repository", "source-sha", "control-sha", "run-id", "run-attempt", "record-artifact-id", "record-artifact-digest", "verified-distributions", diff --git a/tests/test_agent_mention_workflow_contract.py b/tests/test_agent_mention_workflow_contract.py index e640121ad1..1cce19993e 100644 --- a/tests/test_agent_mention_workflow_contract.py +++ b/tests/test_agent_mention_workflow_contract.py @@ -52,6 +52,19 @@ def test_workflow_uses_local_event_and_central_sweep_with_job_scoped_writes() -> assert "agent_mention_sweep.py" in sweep +def test_full_suite_tooling_lock_includes_collection_dependencies() -> None: + """A quality install must provide both parsers imported during suite collection.""" + lock = (ROOT / "requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) + requirements = { + line.split("==", 1)[0].casefold() + for line in lock.splitlines() + if line and not line.startswith(("#", " ", "-")) and "==" in line + } + assert {"defusedxml", "pyyaml"} <= requirements + + def test_quality_workflow_measures_exact_files_without_module_name_warnings() -> None: """Coverage includes the two script paths instead of treating paths as modules.""" diff --git a/tests/test_agent_review_runtime_quality_consolidation.py b/tests/test_agent_review_runtime_quality_consolidation.py index 2b0e83312b..47884f9068 100644 --- a/tests/test_agent_review_runtime_quality_consolidation.py +++ b/tests/test_agent_review_runtime_quality_consolidation.py @@ -154,6 +154,49 @@ def test_review_repair_suite_is_selected_and_conditionally_executed() -> None: assert workflow.count("runs-on:") == 1 +def test_review_launcher_runtime_is_owned_by_the_review_repair_suite() -> None: + """Trigger, execute, compile, and measure the launcher's runtime contracts.""" + + workflow = _workflow_text() + + assert workflow.count("tests/test_contextual_orchestrator_review_launcher.py") == 4 + assert workflow.count("tests/test_review_preflight_concurrency.py") == 4 + assert "--cov=scripts.ci.contextual_orchestrator_review_launcher" in workflow + + +@pytest.mark.parametrize( + "changed_path", + ( + "requirements-opencode-review-ci.txt", + "requirements-noema-document-ci.txt", + "scripts/ci/compile_opencode_review_lock.sh", + ), +) +def test_common_lock_sources_select_every_python_consumer_suite( + changed_path: str, +) -> None: + """A common-lock source change must execute both Python consumer suites.""" + + workflow = _workflow_text() + selector = workflow.split(' case "$changed_path" in\n', 1)[1].split( + " esac", 1 + )[0] + result = subprocess.run( + [ + "bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", + 'IFS= read -r changed_path\nnoema_suite=false\nopencode_suite=false\n' + 'case "$changed_path" in\n' + selector + + 'esac\nprintf "%s,%s" "$noema_suite" "$opencode_suite"\n', + ], + input=changed_path + "\n", + text=True, + capture_output=True, + check=True, + ) + assert result.stdout == "true,true" + assert result.stderr == "" + + @pytest.mark.parametrize( ("changed_path", "starts_runner", "review_repair", "queue"), ( diff --git a/tests/test_codeql_ghas_configuration_identity.py b/tests/test_codeql_ghas_configuration_identity.py index 817cd56497..84a04a4de9 100644 --- a/tests/test_codeql_ghas_configuration_identity.py +++ b/tests/test_codeql_ghas_configuration_identity.py @@ -430,8 +430,8 @@ def test_request_json_maps_http_and_transport_failures(monkeypatch): """HTTP and transport failures become ConfigurationIdentityError.""" class _HTTPError(identity.urllib.error.HTTPError): - def read(self) -> bytes: - return b"denied" + def read(self, size: int = -1) -> bytes: + return b"denied"[:size] def raise_http(request, timeout=30): del request, timeout diff --git a/tests/test_contextual_orchestrator_review_launcher.py b/tests/test_contextual_orchestrator_review_launcher.py new file mode 100644 index 0000000000..50d484da9c --- /dev/null +++ b/tests/test_contextual_orchestrator_review_launcher.py @@ -0,0 +1,549 @@ +"""Behavior coverage for the contextual-orchestrator review launcher.""" + +from __future__ import annotations + +import json +import logging +import runpy +import sys +import time +from pathlib import Path +from types import ModuleType, SimpleNamespace +from urllib.error import HTTPError + +import pytest + +from scripts.ci import contextual_orchestrator_review_launcher as launcher +from scripts.ci import contextual_orchestrator_review_policy as policy + + +LAUNCHER_PATH = Path(launcher.__file__) + + +def _discovered_model( + *, + provider_name: str = "bytez", + model_id: str = "test/free", + agent_id: str = "bytez_free", + credential_name: str = "BYTEZ_API_KEY", + prompt_price: float = 0.0, + completion_price: float = 0.0, + output_modalities: tuple[str, ...] = ("text",), +) -> SimpleNamespace: + """Return one complete discovery row shaped like the owner runtime.""" + provider_settings = { + "bytez": ("https://api.bytez.com/models/v2/openai/v1", "raw-token"), + "openai": ("https://api.openai.com/v1", "bearer"), + } + base_url, auth_scheme = provider_settings[provider_name] + return SimpleNamespace( + provider_name=provider_name, + model_id=model_id, + agent_id=agent_id, + chat_base_url=base_url, + credential_name=credential_name, + auth_scheme=auth_scheme, + output_modalities=output_modalities, + prompt_price_per_1k=prompt_price, + completion_price_per_1k=completion_price, + currency_code="USD", + ) + + +def _install_owner_runtime( + monkeypatch: pytest.MonkeyPatch, + *, + discovered_models: list[object] | None = None, + registered_credentials: list[str] | None = None, + auth_token: str | None = "gateway-token", + discovery_error: Exception | None = None, + failing_agent_ids: frozenset[str] = frozenset(), +) -> SimpleNamespace: + """Install controlled owner-package modules without copying owner source.""" + runtime_state = SimpleNamespace( + discovered_models=discovered_models or [_discovered_model()], + registered_credentials=( + ["BYTEZ_API_KEY"] + if registered_credentials is None + else registered_credentials + ), + auth_token=auth_token, + discovery_error=discovery_error, + failing_agent_ids=failing_agent_ids, + configured_levels=[], + model_clients=[], + served_requests=[], + ) + + owner_package = ModuleType("contextual_orchestrator") + owner_package.__path__ = [] + + credentials_module = ModuleType("contextual_orchestrator.credentials") + credentials_module.get_credential = lambda credential_name: runtime_state.auth_token + + capability_module = ModuleType("contextual_orchestrator.chat_capability") + capability_module.is_general_chat_agent_model_id = ( + lambda model_id: not str(model_id).startswith("embedding-") + ) + + discovery_module = ModuleType("contextual_orchestrator.model_discovery") + + def discover_all_models() -> tuple[list[object], list[object]]: + if runtime_state.discovery_error is not None: + raise runtime_state.discovery_error + return runtime_state.discovered_models, [] + + discovery_module.discover_all_models = discover_all_models + discovery_module.free_discovered_models = lambda models: [ + model + for model in models + if getattr(model, "prompt_price_per_1k", None) == 0.0 + and getattr(model, "completion_price_per_1k", None) == 0.0 + ] + + orchestrator_module = ModuleType("contextual_orchestrator.orchestrator") + + class ModelClient: + """Return explicit text or a bounded provider rejection by agent id.""" + + def __init__(self, **settings: object) -> None: + self.settings = settings + runtime_state.model_clients.append(self) + + def proxy_send_once( + self, agent: object, endpoint: str, payload: dict[str, object] + ) -> dict[str, object]: + if str(getattr(agent, "id", "")) in runtime_state.failing_agent_ids: + raise HTTPError( + "https://provider.invalid", 401, "private", {}, None + ) + return { + "choices": [ + {"finish_reason": "stop", "message": {"content": "OK"}} + ] + } + + class TaskOrchestrator: + """Capture the validated serving pool.""" + + def __init__(self, agents: list[object], *, client: object) -> None: + self.agents = agents + self.client = client + + def load_agents(catalog_path: str) -> list[SimpleNamespace]: + catalog = json.loads(Path(catalog_path).read_text(encoding="utf-8")) + return [SimpleNamespace(**agent) for agent in catalog["agents"]] + + orchestrator_module.ModelClient = ModelClient + orchestrator_module.TaskOrchestrator = TaskOrchestrator + orchestrator_module.load_agents = load_agents + + gateway_module = ModuleType("contextual_orchestrator.review_gateway") + gateway_module.REVIEW_AUTH_CREDENTIAL_NAME = "REVIEW_GATEWAY_TOKEN" + gateway_module.register_review_credentials = ( + lambda environment: runtime_state.registered_credentials + ) + + server_module = ModuleType("contextual_orchestrator.server") + + class SecurityConfig: + """Retain the authentication boundary passed to the server.""" + + def __init__(self, *, auth_token: str, max_body_bytes: int) -> None: + self.auth_token = auth_token + self.max_body_bytes = max_body_bytes + + def serve( + orchestrator: object, + *, + host: str, + port: int, + security: object, + ) -> None: + runtime_state.served_requests.append( + SimpleNamespace( + orchestrator=orchestrator, + host=host, + port=port, + security=security, + ) + ) + + server_module.SecurityConfig = SecurityConfig + server_module.serve = serve + + logging_module = ModuleType("contextual_orchestrator.debug_logging") + logging_module.configure_logging = runtime_state.configured_levels.append + + owner_modules = { + "contextual_orchestrator": owner_package, + "contextual_orchestrator.credentials": credentials_module, + "contextual_orchestrator.chat_capability": capability_module, + "contextual_orchestrator.model_discovery": discovery_module, + "contextual_orchestrator.orchestrator": orchestrator_module, + "contextual_orchestrator.review_gateway": gateway_module, + "contextual_orchestrator.server": server_module, + "contextual_orchestrator.debug_logging": logging_module, + } + for module_name, module_value in owner_modules.items(): + monkeypatch.setitem(sys.modules, module_name, module_value) + return runtime_state + + +def _launcher_arguments(temporary_path: Path, *extra_arguments: str) -> list[str]: + """Return complete launcher arguments rooted in one temporary directory.""" + return [ + "--discovery-out", + str(temporary_path / "discovery.json"), + "--catalog-out", + str(temporary_path / "catalog.json"), + "--report-out", + str(temporary_path / "report.json"), + "--preflight-out", + str(temporary_path / "preflight.json"), + *extra_arguments, + ] + + +def test_main_serves_a_discovered_free_route( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """A free text route reaches the authenticated loopback server.""" + runtime_state = _install_owner_runtime( + monkeypatch, + discovered_models=[ + _discovered_model(), + _discovered_model( + provider_name="openai", + model_id="gpt-priced", + agent_id="openai_priced", + credential_name="OPENAI_API_KEY", + prompt_price=1.0, + completion_price=2.0, + ), + ], + registered_credentials=["BYTEZ_API_KEY", "OPENAI_API_KEY"], + ) + + assert launcher.main(_launcher_arguments(tmp_path)) == 0 + + assert runtime_state.configured_levels == [launcher.DEFAULT_SIDECAR_LOG_LEVEL] + assert len(runtime_state.served_requests) == 1 + served_request = runtime_state.served_requests[0] + assert served_request.host == "127.0.0.1" + assert served_request.port == 18080 + assert served_request.security.auth_token == "gateway-token" + assert served_request.security.max_body_bytes == launcher.REVIEW_MAX_BODY_BYTES + assert [agent.id for agent in served_request.orchestrator.agents] == [ + "bytez_free" + ] + assert json.loads((tmp_path / "preflight.json").read_text(encoding="utf-8"))[ + "ready_count" + ] == 1 + + +def test_main_uses_the_priced_fallback_only_after_free_routes_fail( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Auto mode replaces a rejected free catalog with its priced successor.""" + free_model = _discovered_model() + priced_model = _discovered_model( + provider_name="openai", + model_id="gpt-priced", + agent_id="openai_priced", + credential_name="OPENAI_API_KEY", + prompt_price=1.0, + completion_price=2.0, + ) + runtime_state = _install_owner_runtime( + monkeypatch, + discovered_models=[free_model, priced_model], + registered_credentials=["BYTEZ_API_KEY", "OPENAI_API_KEY"], + failing_agent_ids=frozenset({"bytez_free"}), + ) + + assert launcher.main(_launcher_arguments(tmp_path, "--pool", "auto")) == 0 + + catalog = json.loads((tmp_path / "catalog.json").read_text(encoding="utf-8")) + report = json.loads((tmp_path / "report.json").read_text(encoding="utf-8")) + assert [agent["id"] for agent in catalog["agents"]] == ["openai_priced"] + assert report["fallback_reason"] == "primary_routes_unavailable" + assert report["primary_selected_count"] == 1 + assert not (tmp_path / "catalog.json.priced").exists() + assert [agent.id for agent in runtime_state.served_requests[0].orchestrator.agents] == [ + "openai_priced" + ] + + +def test_main_keeps_the_free_catalog_when_priced_policy_rejects_fallback( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """An unavailable optional priced catalog cannot replace a valid free pool.""" + free_model = _discovered_model() + priced_model = _discovered_model( + provider_name="openai", + model_id="gpt-priced", + agent_id="openai_priced", + credential_name="OPENAI_API_KEY", + prompt_price=1.0, + completion_price=2.0, + ) + _install_owner_runtime( + monkeypatch, + discovered_models=[free_model, priced_model], + registered_credentials=["BYTEZ_API_KEY", "OPENAI_API_KEY"], + ) + real_builder = policy.build_zdr_prioritized_catalog + + def reject_priced_rows(rows: list[dict[str, object]], **settings: object) -> object: + if rows and all(row.get("cost_evidence") == "priced" for row in rows): + raise policy.PolicyError("priced fallback unavailable") + return real_builder(rows, **settings) + + monkeypatch.setattr(policy, "build_zdr_prioritized_catalog", reject_priced_rows) + + assert launcher.main(_launcher_arguments(tmp_path, "--pool", "auto")) == 0 + catalog = json.loads((tmp_path / "catalog.json").read_text(encoding="utf-8")) + assert [agent["id"] for agent in catalog["agents"]] == ["bytez_free"] + + +@pytest.mark.parametrize( + ("runtime_settings", "expected_message"), + ( + ({"auth_token": None}, "requires an explicit --auth-token"), + ({"registered_credentials": []}, "requires at least one provider credential"), + ( + {"discovery_error": RuntimeError("discovery unavailable")}, + "review sidecar discovery failed: discovery unavailable", + ), + ( + { + "discovered_models": [ + _discovered_model(output_modalities=("image",)) + ] + }, + "discovered no eligible models", + ), + ), +) +def test_main_fails_closed_before_serving_invalid_runtime_state( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + runtime_settings: dict[str, object], + expected_message: str, +) -> None: + """Missing trust prerequisites terminate before a server can be exposed.""" + runtime_state = _install_owner_runtime(monkeypatch, **runtime_settings) + + with pytest.raises(SystemExit, match=expected_message): + launcher.main(_launcher_arguments(tmp_path)) + + assert runtime_state.served_requests == [] + + +def test_main_persists_sanitized_preflight_failure( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + """A provider rejection writes bounded evidence and never starts serving.""" + runtime_state = _install_owner_runtime( + monkeypatch, failing_agent_ids=frozenset({"bytez_free"}) + ) + + with pytest.raises(SystemExit, match="review sidecar preflight failed"): + launcher.main(_launcher_arguments(tmp_path)) + + preflight_report = json.loads( + (tmp_path / "preflight.json").read_text(encoding="utf-8") + ) + assert preflight_report["routes"][0]["http_status"] == 401 + assert "private" not in json.dumps(preflight_report) + assert "preflight_route_rejected provider=bytez" in capsys.readouterr().err + assert runtime_state.served_requests == [] + + +def test_script_entrypoint_exits_with_main_result( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Direct script execution propagates the successful launcher exit code.""" + runtime_state = _install_owner_runtime(monkeypatch) + monkeypatch.setattr( + sys, "argv", [str(LAUNCHER_PATH), *_launcher_arguments(tmp_path)] + ) + + with pytest.raises(SystemExit) as exit_result: + runpy.run_path(str(LAUNCHER_PATH), run_name="__main__") + + assert exit_result.value.code == 0 + assert len(runtime_state.served_requests) == 1 + + +@pytest.mark.parametrize( + ("response_value", "expected_text", "expected_reasoning"), + ( + (object(), False, False), + ({"choices": [object()]}, False, False), + ({"choices": [{"message": object()}]}, False, False), + ( + {"choices": [{"finish_reason": "bad-value!", "message": {}}]}, + False, + False, + ), + ), +) +def test_response_parsers_reject_malformed_provider_shapes( + response_value: object, expected_text: bool, expected_reasoning: bool +) -> None: + """Malformed response layers remain unusable without raising.""" + assert launcher._chat_response_has_text(response_value) is expected_text + assert ( + launcher._response_has_reasoning_without_content(response_value) + is expected_reasoning + ) + finish_reason = launcher._response_finish_reason(response_value) + if isinstance(response_value, dict) and response_value.get("choices"): + first_choice = response_value["choices"][0] + if isinstance(first_choice, dict) and first_choice.get("finish_reason"): + assert finish_reason == "unknown" + return + assert finish_reason is None + + +def test_report_rows_skip_missing_identity_and_use_policy_defaults() -> None: + """Only complete route identities become policy rows.""" + missing_identity = SimpleNamespace(provider_name="", model_id="") + defaulted_model = SimpleNamespace( + provider_name="bytez", + model_id="test/free", + agent_id="bytez_free", + output_modalities=("text",), + ) + + assert launcher._route_identity(SimpleNamespace()) == ("", "") + assert launcher._report_rows([missing_identity], frozenset()) == [] + row = launcher._report_rows( + [defaulted_model], frozenset({("bytez", "test/free")}) + )[0] + assert row["base_url"] == "https://api.bytez.com/models/v2/openai/v1" + assert row["credential_key"] == "BYTEZ_API_KEY" + assert row["auth_scheme"] == "Key" + + +@pytest.mark.parametrize( + ("function_call", "expected_message"), + ( + ( + lambda: launcher._bounded_primary_catalog_limit( + 0, pool="free", has_free_rows=True + ), + "must be positive", + ), + ( + lambda: launcher._bounded_fallback_catalog_limit(0, primary_count=0), + "must be positive", + ), + ( + lambda: launcher._bounded_fallback_catalog_limit(4, primary_count=5), + "exceeds the preflight budget", + ), + ), +) +def test_catalog_limits_reject_impossible_budgets( + function_call: object, expected_message: str +) -> None: + """Invalid catalog budgets fail before route selection.""" + with pytest.raises(ValueError, match=expected_message): + function_call() + + +def test_preflight_without_fallback_preserves_the_primary_failure() -> None: + """A failed primary stage cannot be disguised when no successor exists.""" + primary_report = {"routes": [], "escalations_used": 0} + + def reject_primary(agents: list[object], **settings: object) -> object: + raise launcher.ReviewPreflightError("primary unavailable", primary_report) + + with pytest.raises(launcher.ReviewPreflightError) as error_result: + launcher._preflight_with_fallback( + [object()], [], client=object(), preflight=reject_primary + ) + + assert error_result.value.report is primary_report + + +def test_concurrent_preflight_waits_for_an_outstanding_route() -> None: + """Exhausting the iterator waits for its live probe instead of failing early.""" + agent = SimpleNamespace( + id="slow-ready", provider_name="bytez", model="test/free", priority=0 + ) + + class SlowClient: + """Delay long enough for the scheduler to enter its blocking receive.""" + + def proxy_send_once( + self, selected_agent: object, endpoint: str, payload: dict[str, object] + ) -> dict[str, object]: + time.sleep(0.02) + return { + "choices": [ + {"finish_reason": "stop", "message": {"content": "OK"}} + ] + } + + viable_agents, preflight_report = launcher._preflight_review_agents_concurrently( + [agent], client=SlowClient() + ) + + assert viable_agents == [agent] + assert preflight_report["ready_count"] == 1 + + +def test_postponed_concurrent_probe_cannot_spend_an_escalation( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A postponed candidate reports the first-pass escalation reservation.""" + agents = [ + SimpleNamespace( + id=str(index), provider_name="bytez", model=f"test/{index}", priority=0 + ) + for index in range(3) + ] + probe_calls = 0 + + def deterministic_probe( + selected_agents: list[object], **settings: object + ) -> tuple[list[object], dict[str, object]]: + nonlocal probe_calls + probe_calls += 1 + selected_agent = selected_agents[0] + if probe_calls <= 2: + route = { + "agent_id": selected_agent.id, + "provider": "bytez", + "model": selected_agent.model, + "status": "rejected", + "error_type": "HTTPError", + "http_status": 429, + } + else: + route = { + "agent_id": selected_agent.id, + "provider": "bytez", + "model": selected_agent.model, + "status": "rejected", + "error_type": "escalation_budget_exhausted", + } + report = { + "routes": [route], + "escalations_used": launcher.REVIEW_PREFLIGHT_MAX_ESCALATIONS, + } + raise launcher.ReviewPreflightError("unavailable", report) + + monkeypatch.setattr(launcher, "_preflight_review_agents", deterministic_probe) + + with pytest.raises(launcher.ReviewPreflightError) as error_result: + launcher._preflight_review_agents_concurrently(agents, client=object()) + + postponed_route = error_result.value.report["routes"][2] + assert postponed_route["error_type"] == "escalation_reserved_for_first_pass" diff --git a/tests/test_coverage_configuration.py b/tests/test_coverage_configuration.py new file mode 100644 index 0000000000..168dbc56d3 --- /dev/null +++ b/tests/test_coverage_configuration.py @@ -0,0 +1,30 @@ +"""Contracts for the repository-wide production coverage boundary.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +if sys.version_info >= (3, 11): + import tomllib +else: # pragma: no cover - exercised by the Python 3.10 CI lane + import tomli as tomllib + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +ALLOWED_COVERAGE_OMISSIONS = { + "tests/*", +} + + +def test_production_coverage_omissions_are_explicitly_allowlisted() -> None: + """Reject production exclusions that manufacture a 100% coverage result.""" + + with (REPOSITORY_ROOT / "pyproject.toml").open("rb") as config_file: + project_configuration = tomllib.load(config_file) + + configured_omissions = set( + project_configuration["tool"]["coverage"]["run"]["omit"] + ) + + assert configured_omissions == ALLOWED_COVERAGE_OMISSIONS diff --git a/tests/test_github_api_url_boundary.py b/tests/test_github_api_url_boundary.py index a9050584fd..1c856c3bfa 100644 --- a/tests/test_github_api_url_boundary.py +++ b/tests/test_github_api_url_boundary.py @@ -8,6 +8,7 @@ import re import subprocess from typing import Any +from urllib.error import HTTPError from urllib.request import Request from urllib.response import addinfourl @@ -44,6 +45,7 @@ def __init__(self, target: str) -> None: """Store the redirect target and initialize the observed request ledger.""" self.target = target self.calls: list[tuple[str, str | None]] = [] + self.responses: list[Any] = [] def https_open(self, request: Request) -> Any: """Return a synthetic redirect response without contacting a network target.""" @@ -52,6 +54,7 @@ def https_open(self, request: Request) -> Any: headers["Location"] = self.target response = addinfourl(BytesIO(b""), headers, request.full_url, code=302) response.msg = "Found" + self.responses.append(response) return response @@ -71,6 +74,20 @@ def read(self) -> bytes: return b"[]" +class _ReadSizeRecordingBody(BytesIO): + """Record the requested byte limit for one synthetic HTTP error body.""" + + def __init__(self, payload: bytes) -> None: + """Store the payload and initialize the read-size ledger.""" + super().__init__(payload) + self.read_sizes: list[int] = [] + + def read(self, size: int = -1) -> bytes: + """Record the caller's bound before returning response bytes.""" + self.read_sizes.append(size) + return super().read(size) + + def _unexpected_open(*_args: Any, **_kwargs: Any) -> Any: """Fail if a rejected authority reaches the network/file opener boundary.""" pytest.fail("rejected GitHub API authority reached opener") @@ -165,6 +182,8 @@ def test_production_openers_reject_redirect_without_forwarding_bearer( assert transport.calls == [ (CANONICAL_GITHUB_API_URL, "Bearer test-token"), ] + assert len(transport.responses) == 1 + assert transport.responses[0].closed @pytest.mark.parametrize("target", REDIRECT_TARGETS) @@ -231,6 +250,37 @@ def strix_open(request: Any, **_kwargs: Any) -> _JsonResponse: assert strix_calls == [CANONICAL_GITHUB_API_URL] +def test_codeql_identity_client_bounds_http_error_diagnostic_read( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A hostile GitHub error body cannot force an unbounded diagnostic read.""" + body = _ReadSizeRecordingBody(b"x" * 8_192) + error = HTTPError( + CANONICAL_GITHUB_API_URL, + 502, + "Bad Gateway", + Message(), + body, + ) + + def raise_http_error(*_args: Any, **_kwargs: Any) -> Any: + """Raise the synthetic response at the authenticated opener boundary.""" + raise error + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", raise_http_error) + + with pytest.raises(identity.ConfigurationIdentityError, match="HTTP 502"): + identity._request_json( + CANONICAL_GITHUB_API_URL, + token="test-token", + timeout_seconds=1, + ) + + assert body.read_sizes + assert all(0 < size <= 400 for size in body.read_sizes) + assert body.closed + + def test_documented_opener_lineage_references_published_commits() -> None: """Owner evidence must name the published commits that carry each repair.""" doctoring = Path( diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index a2da04ae25..06b66953ac 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -9,6 +9,7 @@ import tarfile import zipfile from pathlib import Path +from urllib.error import HTTPError import pytest @@ -811,6 +812,30 @@ def test_download_trusted_uv_archive_rejects_network_and_size_failures( materializer._download_trusted_uv_archive() +def test_download_trusted_uv_archive_closes_transformed_http_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The downloader owns and closes an HTTP response once it transforms the error.""" + error_body = io.BytesIO(b"private body") + provider_error = HTTPError( + materializer.TRUSTED_UV_ARCHIVE_URL, + 503, + "private body", + {}, + error_body, + ) + monkeypatch.setattr( + materializer.urllib.request, + "urlopen", + lambda *_a, **_k: (_ for _ in ()).throw(provider_error), + ) + + with pytest.raises(RuntimeError, match="download failed: HTTPError"): + materializer._download_trusted_uv_archive() + + assert error_body.closed + + def test_verified_uv_binary_accepts_exact_archive( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/test_materialize_base_rust_dependencies.py b/tests/test_materialize_base_rust_dependencies.py index d8b409d342..e7364897e6 100644 --- a/tests/test_materialize_base_rust_dependencies.py +++ b/tests/test_materialize_base_rust_dependencies.py @@ -489,6 +489,30 @@ def test_run_cargo_vendor_propagates_missing_binary(tmp_path: Path) -> None: materializer.materialize(repo, base_sha, tmp_path / "out") +def test_materialize_surfaces_cargo_vendor_timeout( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A stalled cargo process fails closed with its stable exception class.""" + repo = tmp_path / "repo" + _init_repo(repo) + _write_single_crate_workspace(repo, generate_lock=False) + base_sha = _commit_all(repo) + + monkeypatch.setattr( + materializer, + "_run_cargo_vendor", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + subprocess.TimeoutExpired(["cargo", "vendor"], 900) + ), + ) + + with pytest.raises( + RuntimeError, + match="could not run trusted cargo vendor for base manifest Cargo.lock: TimeoutExpired", + ): + materializer.materialize(repo, base_sha, tmp_path / "out") + + def test_materialize_surfaces_cargo_vendor_failure_detail( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_noema_document_reader_runtime_dependencies.py b/tests/test_noema_document_reader_runtime_dependencies.py new file mode 100644 index 0000000000..6d2441cf02 --- /dev/null +++ b/tests/test_noema_document_reader_runtime_dependencies.py @@ -0,0 +1,37 @@ +"""Security contracts for Noema document-reader runtime dependencies.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + + +LOCK_PATH = ( + Path(__file__).resolve().parents[1] + / "scripts" + / "ci" + / "noema-document-reader" + / "package-lock.json" +) + + +@pytest.mark.parametrize( + ("package_name", "safe_version"), + (("fast-uri", "3.1.8"), ("ip-address", "10.7.1")), +) +def test_noema_document_reader_uses_exclusive_safe_runtime_dependency( + package_name: str, safe_version: str +) -> None: + """The lock must contain one exact non-vulnerable transitive package.""" + lock = json.loads(LOCK_PATH.read_text(encoding="utf-8")) + package_path = f"node_modules/{package_name}" + matching_packages = { + path: metadata + for path, metadata in lock["packages"].items() + if path == package_path or path.endswith(f"/{package_path}") + } + + assert list(matching_packages) == [package_path] + assert matching_packages[package_path]["version"] == safe_version diff --git a/tests/test_noema_preflight_capacity.py b/tests/test_noema_preflight_capacity.py index a6ed03db3c..d6beb753ce 100644 --- a/tests/test_noema_preflight_capacity.py +++ b/tests/test_noema_preflight_capacity.py @@ -218,6 +218,19 @@ def test_directory_report_path_keeps_plain_failure(tmp_path, monkeypatch): assert outputs["transport_capacity_unavailable"] == "false" +def test_symlinked_report_parent_is_rejected(tmp_path): + """A symlinked evidence directory cannot redirect the trusted report read.""" + real_parent = tmp_path / "real-parent" + real_parent.mkdir() + report = _all_429() + (real_parent / "report.json").write_text(json.dumps(report), encoding="utf-8") + symlink_parent = tmp_path / "report-parent" + symlink_parent.symlink_to(real_parent, target_is_directory=True) + + assert capacity.load_preflight_report(real_parent / "report.json") == report + assert capacity.load_preflight_report(symlink_parent / "report.json") is None + + def test_oversized_report_keeps_plain_failure(tmp_path, monkeypatch): """The classifier reads a bounded prefix and rejects anything larger.""" padded = _all_429() diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 5b3ef6c703..5d3fb66605 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1760,11 +1760,12 @@ def test_call_llm_http_400_is_transport_but_not_capacity(monkeypatch, capsys): """A non-transient 400 stays typed transport without authorizing re-dispatch.""" monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") + error_body = io.BytesIO(b"{}") class Opener: def open(self, request): raise noema.urllib.error.HTTPError( - request.full_url, 400, "Bad Request", {}, io.BytesIO(b"{}") + request.full_url, 400, "Bad Request", {}, error_body ) monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *_args: Opener()) @@ -1774,6 +1775,7 @@ def open(self, request): assert exc_info.value.capacity_unavailable is False assert exc_info.value.http_status == 400 + assert error_body.closed assert "outcome=provider_capacity_unavailable" not in capsys.readouterr().out @@ -1964,7 +1966,7 @@ def test_noema_redirect_handler_rejects_redirects(): handler = noema.NoRedirectHandler() request = noema.urllib.request.Request("https://llm.example.test/chat") - with pytest.raises(noema.urllib.error.HTTPError): + with pytest.raises(noema.urllib.error.HTTPError) as exc_info: handler.redirect_request( request, fp=None, @@ -1973,6 +1975,7 @@ def test_noema_redirect_handler_rejects_redirects(): headers={}, newurl="http://169.254.169.254/latest/meta-data/", ) + exc_info.value.close() def test_call_llm_rejects_control_character_scheme_evasion(monkeypatch): @@ -2879,6 +2882,14 @@ def test_fetch_file_content_at_ref_refuses_malformed_base64(monkeypatch): noema.fetch_file_content_at_ref("owner/repo", "docs/a.md", "deadbeef") +def test_fetch_file_content_at_ref_refuses_malformed_json(monkeypatch): + """A malformed GitHub API envelope fails closed before metadata inspection.""" + monkeypatch.setattr(noema, "run", lambda *args, **kwargs: "{not-json") + + with pytest.raises(RuntimeError, match="GitHub content response was malformed"): + noema.fetch_file_content_at_ref("owner/repo", "docs/a.md", "deadbeef") + + @pytest.mark.parametrize("payload,reason", [ ({"content": "", "encoding": "none", "size": 1048577}, "API omitted"), ({"content": "", "encoding": "base64", "size": 1}, "nonempty file"), diff --git a/tests/test_opencode_queue_priority.py b/tests/test_opencode_queue_priority.py index b1d5aa5c17..c7820939cd 100644 --- a/tests/test_opencode_queue_priority.py +++ b/tests/test_opencode_queue_priority.py @@ -2,7 +2,16 @@ from __future__ import annotations +import json +import runpy +import subprocess +import sys from datetime import datetime, timezone +from types import SimpleNamespace + +import pytest + +from scripts.ci import opencode_queue_priority as queue from scripts.ci.opencode_queue_priority import ( PrState, @@ -63,3 +72,229 @@ def test_metrics_surface_deferred_backlog_and_priority_position() -> None: assert m["deferred"] == 2 assert m["oldest_deferred_hours"] == 9.0 assert m["priority_positions"] == {"o/a#1": 2} + +def test_metrics_report_zero_age_when_no_current_run_is_deferred() -> None: + """An empty current-head queue must not invent a deferred age.""" + assert metrics(plan([], {}), now=NOW) == { + "queued": 0, + "deferred": 0, + "stale": 0, + "oldest_deferred_hours": 0.0, + "priority_positions": {}, + } + + +def test_fetch_queued_parses_only_bound_dispatch_titles(monkeypatch: pytest.MonkeyPatch) -> None: + """Unrelated queued workflows must not become cancellation candidates.""" + response = "\n".join( + json.dumps(item) + for item in [ + [101, "2026-09-29T06:30:00Z", "Review ContextualWisdomLab/naruon#974@abcdef0"], + [102, "2026-09-29T06:31:00Z", "Review another-org/naruon#974@abcdef0"], + [103, "2026-09-29T06:32:00Z", "Review ContextualWisdomLab/naruon#974@ABCDEF0"], + ] + ) + monkeypatch.setattr(queue, "_gh", lambda *_args, **_kwargs: response) + + assert queue.fetch_queued() == [ + QueuedRun( + 101, + datetime(2026, 9, 29, 6, 30, tzinfo=timezone.utc), + "ContextualWisdomLab/naruon", + 974, + "abcdef0", + ) + ] + + +def _pull_request_node(*, labels: list[str], events: list[dict]) -> dict: + return { + "state": "OPEN", + "headRefOid": HEAD, + "labels": {"nodes": [{"name": name} for name in labels]}, + "timelineItems": {"nodes": events}, + } + + +def test_fetch_live_accepts_only_a_writer_applied_priority_label( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A label without a trusted applying actor must not gain queue priority.""" + nodes = { + "p0": { + "pullRequest": _pull_request_node(labels=["other"], events=[]), + }, + "p1": { + "pullRequest": _pull_request_node( + labels=["review-priority"], + events=[ + {"label": {"name": "other"}, "actor": {"login": "ignored"}}, + {"label": {"name": "review-priority"}, "actor": None}, + {"label": {"name": "review-priority"}, "actor": {"login": "alice"}}, + {"label": {"name": "review-priority"}, "actor": {"login": "alice"}}, + ], + ), + }, + "p2": { + "pullRequest": _pull_request_node( + labels=["review-priority"], + events=[ + {"label": {"name": "review-priority"}, "actor": {"login": "bob"}}, + ], + ), + }, + "p3": None, + } + permission_requests: list[str] = [] + + def fake_gh(*args: str, stdin: str | None = None) -> str: + del stdin + if args[:2] == ("api", "graphql"): + return json.dumps({"data": nodes}) + endpoint = args[1] + permission_requests.append(endpoint) + if endpoint.endswith("/alice/permission"): + return "write\n" + raise subprocess.CalledProcessError(1, ["gh", *args]) + + monkeypatch.setattr(queue, "_gh", fake_gh) + live = queue.fetch_live( + { + ("ContextualWisdomLab/a", 1), + ("ContextualWisdomLab/b", 2), + ("ContextualWisdomLab/c", 3), + ("ContextualWisdomLab/d", 4), + }, + "review-priority", + ) + + assert live == { + ("ContextualWisdomLab/a", 1): PrState("OPEN", HEAD, False), + ("ContextualWisdomLab/b", 2): PrState("OPEN", HEAD, True), + ("ContextualWisdomLab/c", 3): PrState("OPEN", HEAD, False), + } + assert permission_requests == [ + "repos/ContextualWisdomLab/b/collaborators/alice/permission", + "repos/ContextualWisdomLab/c/collaborators/bob/permission", + ] + + +def test_fetch_live_treats_null_graphql_data_as_no_live_pr( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A null GraphQL payload must fail closed instead of fabricating PR state.""" + monkeypatch.setattr(queue, "_gh", lambda *_args, **_kwargs: '{"data": null}') + assert queue.fetch_live({("ContextualWisdomLab/a", 1)}, "review-priority") == {} + + +def test_apply_requires_recording_the_cancel_list_first(capsys: pytest.CaptureFixture[str]) -> None: + """The CLI must reject an apply request that has no audit issue.""" + with pytest.raises(SystemExit) as error: + queue.main(["--apply"]) + + assert error.value.code == 2 + assert "--apply requires --post-issue" in capsys.readouterr().err + + +def test_main_default_report_does_not_target_a_current_head_run( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Dry-run defaults must report, but not list, a non-priority current head.""" + current = run(2, "ContextualWisdomLab/a", 1) + monkeypatch.setattr(queue, "fetch_queued", lambda: [current]) + monkeypatch.setattr( + queue, + "fetch_live", + lambda _keys, _label: {(current.repo, current.pr): PrState("OPEN", HEAD, False)}, + ) + + assert queue.main([]) == 0 + + output = capsys.readouterr().out + assert '"deferred": 1' in output + assert output.endswith("run_id\tcreated_at\trepository\tpr\thead_sha\treason\n") + + +def test_main_records_targets_before_cancelling_only_still_queued_runs( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Apply must record both reasons first and cancel only a queued target.""" + stale = run(1, "ContextualWisdomLab/a", 1, hour=3) + current = run(2, "ContextualWisdomLab/b", 2, hour=4) + priority = run(3, "ContextualWisdomLab/c", 3, hour=5) + monkeypatch.setattr(queue, "fetch_queued", lambda: [stale, current, priority]) + monkeypatch.setattr( + queue, + "fetch_live", + lambda _keys, _label: { + (stale.repo, stale.pr): PrState("CLOSED", HEAD, False), + (current.repo, current.pr): PrState("OPEN", HEAD, False), + (priority.repo, priority.pr): PrState("OPEN", HEAD, True), + }, + ) + calls: list[tuple[tuple[str, ...], str | None]] = [] + + def fake_gh(*args: str, stdin: str | None = None) -> str: + calls.append((args, stdin)) + if f"/runs/{stale.run_id}" in args[1] and args[-2:] == ("--jq", ".status"): + return "queued\n" + if f"/runs/{current.run_id}" in args[1] and args[-2:] == ("--jq", ".status"): + return "completed\n" + return "" + + monkeypatch.setattr(queue, "_gh", fake_gh) + + assert queue.main( + [ + "--label", + "urgent", + "--include-current", + "--post-issue", + "ContextualWisdomLab/control#9", + "--apply", + ] + ) == 0 + + output = capsys.readouterr().out + assert f"{stale.run_id}\t" in output and "\tstale\n" in output + assert f"{current.run_id}\t" in output and "\tdeferred\n" in output + assert "cancelled=1" in output + assert calls[0][0] == ( + "issue", + "comment", + "9", + "-R", + "ContextualWisdomLab/control", + "--body-file", + "-", + ) + assert "Label `urgent` (maintainer-applied) kept: 1. To cancel: 2." in (calls[0][1] or "") + assert [args for args, _stdin in calls if args[-1].endswith("/cancel")] == [ + ( + "api", + "-X", + "POST", + f"repos/{queue.CENTRAL}/actions/runs/{stale.run_id}/cancel", + ) + ] + + +def test_script_entrypoint_reports_an_empty_queue( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Direct execution must invoke the CLI rather than silently doing nothing.""" + monkeypatch.setattr(sys, "argv", [queue.__file__]) + monkeypatch.setattr( + subprocess, + "run", + lambda *_args, **_kwargs: SimpleNamespace(stdout=""), + ) + + with pytest.raises(SystemExit) as error: + runpy.run_path(queue.__file__, run_name="__main__") + + assert error.value.code == 0 + assert '"queued": 0' in capsys.readouterr().out diff --git a/tests/test_pingora_edge_policy.py b/tests/test_pingora_edge_policy.py index aba34f6346..93fbc58c15 100644 --- a/tests/test_pingora_edge_policy.py +++ b/tests/test_pingora_edge_policy.py @@ -766,12 +766,16 @@ def test_declared_prefix_for_path_matches_by_path_segment() -> None: def test_github_open_json_maps_not_found_to_artifact_declaration_error(monkeypatch: pytest.MonkeyPatch) -> None: """A 404 from the GitHub API is distinguished from every other transport failure.""" + error_body = BytesIO() monkeypatch.setattr( policy.github_opener, "open", - lambda _request, timeout: (_ for _ in ()).throw(HTTPError("x", 404, "not found", {}, BytesIO())), + lambda _request, timeout: (_ for _ in ()).throw( + HTTPError("x", 404, "not found", {}, error_body) + ), ) with pytest.raises(policy.ArtifactDeclarationNotFoundError): policy._github_open_json("https://api.github.com/repos/a/b", "token") + assert error_body.closed def test_png_structure_validation_fails_closed_on_malformed_chunks() -> None: @@ -1062,6 +1066,7 @@ def test_changed_file_pagination_bound_is_provably_unreachable() -> None: ({"type": "file", "encoding": "none", "size": 1}, "no inline content"), ({"type": "file", "encoding": "none", "size": "not-an-int"}, "malformed size"), ({"type": "file", "encoding": "utf-8", "size": 1, "content": "x"}, "invalid encoding"), + ({"type": "file", "encoding": "base64", "size": 1, "content": 1}, "malformed size"), ({"type": "file", "encoding": "base64", "size": 1, "content": "!"}, "invalid base64"), ({"type": "file", "encoding": "base64", "size": 2, "content": base64.b64encode(b"x").decode()}, "size mismatch"), ({"type": "file", "encoding": "base64", "size": 1, "content": base64.b64encode(b"\xff").decode()}, "not valid UTF-8"), @@ -1247,6 +1252,23 @@ def open_response(request: object, timeout: int) -> FakeResponse: policy._github_open_raw_bytes(url, "token", 3) +@pytest.mark.parametrize("exc", [URLError("dns"), TimeoutError(), HTTPError("x", 500, "bad", {}, BytesIO())]) +def test_github_open_raw_bytes_sanitizes_transport_failures( + monkeypatch: pytest.MonkeyPatch, exc: Exception, +) -> None: + """Raw-blob failures preserve only their stable class, never response text.""" + + def fail(_request: object, timeout: int) -> object: + assert timeout == 30 + raise exc + + monkeypatch.setattr(policy.github_opener, "open", fail) + url = "https://api.github.com/repos/a/b/git/blobs/" + "a" * 40 + with pytest.raises(policy.PolicyError) as raised: + policy._github_open_raw_bytes(url, "token", 4) + assert str(raised.value) == f"GitHub raw blob request failed: {type(exc).__name__}" + + @pytest.mark.parametrize( "url", [ @@ -1268,8 +1290,9 @@ def test_github_open_json_rejects_nonapproved_origins(url: str) -> None: def test_github_opener_never_constructs_redirect_requests() -> None: """The policy opener refuses redirects rather than changing API origins.""" - with pytest.raises(HTTPError): + with pytest.raises(HTTPError) as exc_info: policy.NoRedirectHandler().redirect_request(policy.Request("https://example.com"), None, 302, "Found", {}, "https://evil.example") + exc_info.value.close() def test_annotation_escapes_workflow_command_fields() -> None: diff --git a/tests/test_place_maturin_extension.py b/tests/test_place_maturin_extension.py index a1d6f7757a..2b7460e440 100644 --- a/tests/test_place_maturin_extension.py +++ b/tests/test_place_maturin_extension.py @@ -2,6 +2,8 @@ from __future__ import annotations +import runpy +import sys import zipfile from pathlib import Path @@ -65,6 +67,17 @@ def test_python_source_outside_the_project_is_rejected(tmp_path: Path) -> None: placer.place(wheel, project) +def test_non_string_python_source_is_rejected(tmp_path: Path) -> None: + """A malformed maturin source root cannot be coerced into a filesystem path.""" + + project = _project(tmp_path, "python") + (project / "pyproject.toml").write_text( + "[tool.maturin]\npython-source = 1\n", encoding="utf-8" + ) + with pytest.raises(ValueError, match="must be a string"): + placer.place(tmp_path / "unused.whl", project) + + def test_traversing_wheel_member_is_rejected(tmp_path: Path) -> None: project = _project(tmp_path, "python") wheel = _wheel(tmp_path, {f"../../{_SO}": b"ELF"}) @@ -89,3 +102,34 @@ def test_offline_maturin_build_places_the_extension_for_pytest() -> None: build = workflow.split("build_maturin_extension_if_needed() {", 1)[1].split("\n }", 1)[0] assert 'python3 "$2" "$dist_dir"/*.whl .' in build assert '"${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py"' in build + + +def test_cli_rejects_wrong_arity_and_reports_placement_failure( + tmp_path: Path, capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI distinguishes invocation errors from invalid build artifacts.""" + + assert placer.main([]) == 2 + assert "usage:" in capsys.readouterr().err + + project = _project(tmp_path, "python") + assert placer.main([str(tmp_path / "missing.whl"), str(project)]) == 1 + assert "Could not place" in capsys.readouterr().err + + +def test_script_entrypoint_places_and_reports_extension( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + """The executable script exits successfully after placing a real wheel member.""" + + project = _project(tmp_path, "python") + wheel = _wheel(tmp_path, {f"pkg/{_SO}": b"ELF"}) + script = Path(placer.__file__) + monkeypatch.setattr(sys, "argv", [str(script), str(wheel), str(project)]) + + with pytest.raises(SystemExit) as exited: + runpy.run_path(str(script), run_name="__main__") + + assert exited.value.code == 0 + assert "Placed built extension" in capsys.readouterr().out + assert (project / "python" / "pkg" / _SO).read_bytes() == b"ELF" diff --git a/tests/test_pr_review_fix_scheduler.py b/tests/test_pr_review_fix_scheduler.py index 6b9bd91e0c..51168c3d50 100644 --- a/tests/test_pr_review_fix_scheduler.py +++ b/tests/test_pr_review_fix_scheduler.py @@ -1452,6 +1452,24 @@ def test_fix_inspect_skip_wait_and_error_paths(monkeypatch): assert '"action": "error"' in payload_lines[-1] +def test_inspect_pr_reuses_prefetched_comments(monkeypatch): + """A queue snapshot decides marker freshness without a duplicate API fetch.""" + args = fix.parse_args(["--repo", "owner/repo", "--base-branch", "main"]) + marker = { + "body": ( + f"{fix.FIX_MARKER} head_sha={'a' * 40} " + f"epoch={int(time.time())} -->" + ) + } + monkeypatch.setattr(fix, "needs_autofix", lambda pr: (True, ("reason",))) + monkeypatch.setattr(fix, "issue_comments", lambda repo, number: []) + + assert fix.inspect_pr("owner/repo", make_pr(), args, comments=[marker]) == ( + "wait", + ("recent autofix marker exists for this head",), + ) + + def test_fix_parse_args_and_self_test(monkeypatch): """Fix scheduler CLI validates inputs and exposes self-test.""" assert fix.main(["--self-test"]) == 0 diff --git a/tests/test_product_technical_gap_baseline.py b/tests/test_product_technical_gap_baseline.py index d44ffdb8e6..a91e1ce5ba 100644 --- a/tests/test_product_technical_gap_baseline.py +++ b/tests/test_product_technical_gap_baseline.py @@ -9,6 +9,14 @@ DOCTORING = Path("docs/doctoring/product-technical-gap-baseline.md") +def test_baseline_is_not_a_truncated_connector_rendering() -> None: + """The committed baseline must be the source artifact, not UI display output.""" + source = BASELINE.read_text(encoding="utf-8") + + assert not source.startswith("Warning: truncated output") + assert "\nTotal output lines: " not in source[:200] + + def test_baseline_binds_current_governance_sources_and_buyer_contract() -> None: """The shipped baseline must point agents to product, governance, and evidence.""" source = BASELINE.read_text(encoding="utf-8") diff --git a/tests/test_release_dependency_fanout_plan.py b/tests/test_release_dependency_fanout_plan.py index af996a0428..77283ebce0 100644 --- a/tests/test_release_dependency_fanout_plan.py +++ b/tests/test_release_dependency_fanout_plan.py @@ -96,6 +96,56 @@ def test_fanout_adds_distinct_exact_archive_fixtures(tmp_path: Path) -> None: gate.strix_fanout_plan(capture, report_path, CONTROL, 42, 2, archive_report) +def test_archive_rows_cannot_expand_a_bounded_base_plan_past_the_limit( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """A bounded licence plan still fails if runtime rows push the final matrix over its cap.""" + capture, report_path = _allowed(tmp_path) + dependency_count = len(json.loads(report_path.read_text())["dependencies"]) + monkeypatch.setattr(gate, "STRIX_PLAN_LIMIT", dependency_count) + + def archive_row(ecosystem: str, name: str, digest_character: str) -> dict: + source_digest = digest_character * 64 + package_key = f"{ecosystem}/{name}@1" + key = f"{package_key}/sha256/{source_digest}" + fixture = gate.build_fixture( + gate.Dependency(ecosystem, name, "1"), + { + "source_sha256": source_digest, + "archive_members": [], + "install_hook_sources": {}, + "parsed_inputs": [], + "native_libraries": [], + "known_vulnerabilities": [], + }, + ) + fixture["id"] = key + return { + "key": key, + "package_key": package_key, + "name": name, + "version": "1", + "source_sha256": source_digest, + "license": "MIT", + "fixture": fixture, + "fixture_sha256": gate.fixture_digest(fixture), + } + + runtime = archive_row("pypi", "runtime-package", "1") + build = archive_row("pypi", "build-package", "2") + tool = archive_row("github-release", "maturin", "3") + archive_report = tmp_path / "archive-report.json" + archive_report.write_text(json.dumps({ + "schema": "cwl.release-runtime-archive-licenses/3", + "archives": [runtime], + "build_packages": [build], + "build_tools": [tool], + })) + + with pytest.raises(gate.GateError, match="dependency plan exceeds"): + gate.strix_fanout_plan(capture, report_path, CONTROL, 42, 2, archive_report) + + def test_plan_refuses_denied_missing_extra_and_duplicate_scope(tmp_path: Path) -> None: mutators = { "denied": lambda capture, report: report.__setitem__("result", "FAIL"), diff --git a/tests/test_release_dependency_gate.py b/tests/test_release_dependency_gate.py index 62054fff5c..1700d374c2 100644 --- a/tests/test_release_dependency_gate.py +++ b/tests/test_release_dependency_gate.py @@ -11,6 +11,7 @@ import hashlib import io import json +import subprocess import tarfile import zipfile from pathlib import Path @@ -738,6 +739,14 @@ def test_benign_cmdclass_without_lifecycle_override_passes(tmp_path: Path) -> No assert gate.gate(capture).failures == [] +def test_rust_hook_without_process_or_network_namespace_is_benign() -> None: + """Ordinary Rust build code stays allowed while network namespaces are detected.""" + assert gate.detect_install_hooks({"build.rs": "fn main() { println!(\"cargo:rerun\"); }"}) == [] + assert gate.detect_install_hooks({"build.rs": "use std::net; fn main() {}"}) == [ + "build.rs references a process/network namespace" + ] + + # --------------------------------------------------------------------------- # RED: Strix structured evidence # --------------------------------------------------------------------------- @@ -899,6 +908,31 @@ def test_selection_capture_reads_commit_and_rejects_duplicates(tmp_path: Path) - gate._load_selections(capture) +def test_capture_license_selection_cli_publishes_the_exact_commit_blob(tmp_path: Path) -> None: + """The CLI command delegates to the same exact-commit capture boundary.""" + source = tmp_path / "source" + source.mkdir() + path = source / "docs/release-license-selections.json" + path.parent.mkdir() + path.write_text("[]") + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "-qm", "selection"], check=True) + source_sha = subprocess.check_output( + ["git", "-C", str(source), "rev-parse", "HEAD"], text=True, + ).strip() + capture = tmp_path / "capture" + + assert gate.main([ + "capture-license-selections", + "--source", str(source), + "--source-sha", source_sha, + "--capture", str(capture), + ]) == 0 + assert (capture / "license-selections.json").read_text() == "[]" + + def test_selection_loader_refuses_dangling_link_and_nonstring_choice(tmp_path: Path) -> None: path = tmp_path / "license-selections.json" path.symlink_to(tmp_path / "missing") @@ -938,6 +972,58 @@ def metadata_only(command, **kwargs): assert not capture.exists() +def test_selection_capture_refuses_unbound_sources_and_payload_size_races( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + """Selection capture accepts only exact regular blobs whose bytes remain bounded.""" + source = tmp_path / "source" + source.mkdir() + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run([ + "git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "--allow-empty", "-qm", "empty", + ], check=True) + sha = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() + capture = tmp_path / "capture" + + with pytest.raises(gate.GateError, match="exact commit SHA"): + gate.capture_license_selections(source, "not-a-sha", capture) + gate.capture_license_selections(source, sha, capture) + assert not capture.exists() + + selection = source / "docs/release-license-selections.json" + selection.parent.mkdir() + selection.symlink_to(source / "missing") + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "-qm", "link"], check=True) + link_sha = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() + with pytest.raises(gate.GateError, match="regular Git blob"): + gate.capture_license_selections(source, link_sha, capture) + + selection.unlink() + selection.write_text("[]") + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Test", "-c", + "user.email=test@example.invalid", "commit", "-qm", "regular"], check=True) + regular_sha = subprocess.check_output( + ["git", "-C", str(source), "rev-parse", "HEAD"], text=True, + ).strip() + + original = gate.subprocess.check_output + + def raced_blob(command, **kwargs): + if command[1:3] == ["cat-file", "-s"]: + return "1\n" if kwargs.get("text") else b"1\n" + if command[1:3] == ["cat-file", "blob"]: + return b"x" * (gate._MAX_METADATA_BYTES + 1) + return original(command, **kwargs) + + monkeypatch.setattr(gate.subprocess, "check_output", raced_blob) + with pytest.raises(gate.GateError, match="exceeds bounded size"): + gate.capture_license_selections(source, regular_sha, capture) + + @pytest.mark.parametrize("expression", ["MIT/Apache-2.0", "Apache-2.0/MIT", "Apache-2.0 / MIT"]) def test_cargo_legacy_pair_keeps_choice_and_text_checks(expression: str) -> None: evidence = _cargo_evidence(license_expression=expression) @@ -1034,9 +1120,99 @@ def test_missing_full_text_is_independent_of_dual_license_choice(selection) -> N assert decision.allowed == (selection is not None) -@pytest.mark.parametrize("mutation", [None, "missing_source", "wrong_sha", "foreign_path", +@pytest.mark.parametrize( + ("mutation_name", "expected_message"), + [ + ("nonexact", "exact release commit"), + ("oversized", "blob exceeds bounded size"), + ("not-list", "must be a JSON array"), + ("missing-choice", "lacks one explicit notice selection"), + ("separator", "grant separator differs"), + ("grant", "grant bytes differ"), + ], +) +def test_reviewed_source_notice_refuses_unbound_git_evidence( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mutation_name: str, + expected_message: str, +) -> None: + """Reviewed supplemental grants remain exact-SHA, bounded, and byte separated.""" + subject = "cargo/example@1.0.0" + archive_sha = "a" * 64 + source_sha = "b" * 40 + source = tmp_path / "source" + source.mkdir() + first_grant = b"MIT" + second_grant = b"BSD" + content = first_grant + b"\n\n" + second_grant + if mutation_name == "separator": + content = first_grant + b"xx" + second_grant + elif mutation_name == "grant": + content = b"BAD\n\n" + second_grant + notice_digest = hashlib.sha256(content).hexdigest() + grants = ( + ("LICENSE-MIT", 3, hashlib.sha256(first_grant).hexdigest()), + ("LICENSE-BSD", 3, hashlib.sha256(second_grant).hexdigest()), + ) + monkeypatch.setitem( + gate._REVIEWED_SOURCE_NOTICES, + subject, + (archive_sha, "owner/repo", "c" * 40, notice_digest, {"MIT"}, grants), + ) + upstream = [ + { + "url": f"https://raw.githubusercontent.com/owner/repo/{'c' * 40}/{name}", + "sha256": digest, + } + for name, _, digest in grants + ] + choice = { + "ecosystem": "cargo", + "name": "example", + "version": "1.0.0", + "chosen": "MIT", + "rationale": "Reviewed both immutable grant bytes.", + "archive_sha256": archive_sha, + "upstream_licenses": upstream, + "bundled_notice": { + "path": "python/fast_mlsirm/_licenses/example.txt", + "sha256": notice_digest, + }, + } + choices = [] if mutation_name == "missing-choice" else [choice] + choices_payload = b"{}" if mutation_name == "not-list" else json.dumps(choices).encode() + + def git_blob(command, **kwargs): + operation = command[3] + if operation == "ls-tree": + path = command[-1] + oid = "selection-oid" if path == "docs/release-license-selections.json" else "notice-oid" + return f"100644 blob {oid}\t{path}\n" + if operation == "cat-file" and command[4] == "-s": + size = gate._MAX_METADATA_BYTES + 1 if mutation_name == "oversized" else 1 + return f"{size}\n" if kwargs.get("text") else f"{size}\n".encode() + if operation == "cat-file" and command[4] == "blob": + return choices_payload if command[5] == "selection-oid" else content + raise AssertionError(command) + + monkeypatch.setattr(gate.subprocess, "check_output", git_blob) + selected_sha = "short" if mutation_name == "nonexact" else source_sha + with pytest.raises(gate.GateError, match=expected_message): + gate._source_license_notice( + source, + selected_sha, + subject, + {"ecosystem": "cargo", "source_sha256": archive_sha}, + {"chosen": "MIT", "rationale": "Reviewed both immutable grant bytes."}, + ) + + +@pytest.mark.parametrize("mutation", [None, "missing_source", "wrong_sha", "nonexact_sha", + "foreign_workspace", "workspace_version", "foreign_path", "changed_manifest", "changed_lock", "captured_lock", - "symlink", "identity", "missing_dev"]) + "symlink", "identity", "missing_dev", "matching_dev_lock", + "changed_dev_lock"]) def test_nested_cargo_workspace_requires_immutable_release_source(tmp_path, mutation): import subprocess @@ -1051,8 +1227,14 @@ def test_nested_cargo_workspace_requires_immutable_release_source(tmp_path, muta lock = capture / "cargo/Cargo.lock" lock.write_text(lock.read_text() + '\n[[package]]\nname = "local-core"\nversion = "1.0.0"\n') (wheel / "Cargo.lock").write_bytes(lock.read_bytes()) - if mutation == "missing_dev": + if mutation in {"missing_dev", "matching_dev_lock", "changed_dev_lock"}: (source / "Cargo.lock").write_bytes(lock.read_bytes() + b"# separate development lock\n") + if mutation in {"matching_dev_lock", "changed_dev_lock"}: + dev = capture / "cargo-dev" + dev.mkdir() + dev_lock = ((source / "Cargo.lock").read_bytes() if mutation == "matching_dev_lock" + else b"different development lock\n") + (dev / "Cargo.lock").write_bytes(dev_lock) def git(*args): return subprocess.check_output(["git", "-C", str(source), *args], text=True).strip() git("init", "-q") @@ -1070,6 +1252,20 @@ def git(*args): metadata["resolve"]["nodes"].append({"id": "local-id", "deps": [{"pkg": "greencrate-id"}]}) if mutation == "wrong_sha": sha = "a" * 40 + elif mutation == "nonexact_sha": + sha = "short" + elif mutation == "foreign_workspace": + metadata["workspace_root"] = str(tmp_path / "foreign") + elif mutation == "workspace_version": + core.write_text('[package]\nname = "local-core"\nversion.workspace = true\n') + (wheel / "Cargo.toml").write_text( + '[package]\nname = "fast-mlsirm"\nversion = "0.11.5"\n' + '[workspace]\nmembers = ["../core"]\n[workspace.package]\nversion = "1.0.0"\n' + ) + subprocess.run(["git", "-C", str(source), "add", "."], check=True) + subprocess.run(["git", "-C", str(source), "-c", "user.name=Fixture", "-c", + "user.email=fixture@example.invalid", "commit", "-qm", "workspace version"], check=True) + sha = git("rev-parse", "HEAD") elif mutation == "foreign_path": metadata["packages"][-1]["manifest_path"] = str(tmp_path / "foreign/Cargo.toml") elif mutation == "changed_manifest": @@ -1087,14 +1283,22 @@ def git(*args): elif mutation == "identity": metadata["packages"][-1]["name"] = "foreign-core" _write(path, metadata) - if mutation == "missing_dev": + if mutation in {"missing_dev", "matching_dev_lock", "changed_dev_lock"}: release = json.loads((capture / "release.json").read_text()) release["source_sha"] = sha _write(capture / "release.json", release) - with pytest.raises(gate.GateError, match="development Cargo graph is missing"): + if mutation == "matching_dev_lock": + dev_metadata = json.loads((capture / "cargo/metadata.json").read_text()) + dev_metadata["workspace_root"] = str(source) + _write(capture / "cargo-dev/metadata.json", dev_metadata) + assert gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source).passed + return + expected = ("development Cargo graph is missing" if mutation == "missing_dev" + else "development Cargo lock differs from source root") + with pytest.raises(gate.GateError, match=expected): gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source) return - if mutation is not None: + if mutation not in {None, "workspace_version"}: with pytest.raises(gate.GateError, match=gate.CAPTURE_INCOMPLETE): gate._enumerate_cargo(capture, source_root=None if mutation == "missing_source" else source, source_sha=sha) diff --git a/tests/test_release_dependency_gate_capture_and_seal.py b/tests/test_release_dependency_gate_capture_and_seal.py index f3f5753d81..e80aa4c4eb 100644 --- a/tests/test_release_dependency_gate_capture_and_seal.py +++ b/tests/test_release_dependency_gate_capture_and_seal.py @@ -9,10 +9,8 @@ from __future__ import annotations -import builtins import hashlib import json -import runpy from pathlib import Path from typing import Any @@ -37,19 +35,25 @@ def test_gate_import_and_toml_parsing_without_stdlib_tomllib(monkeypatch): - """Exercise the complete module import with the Python 3.10 TOML parser.""" - tomli = pytest.importorskip("tomli") - original = builtins.__import__ + """Select the declared Python 3.10 TOML parser when stdlib lacks one.""" + class BackportParser: + @staticmethod + def loads(source_text): + assert source_text == '[package]\nlicense="MIT"' + return {"package": {"license": "MIT"}} + + tomli = BackportParser() + original = gate.importlib.import_module def import_without_tomllib(name, *args, **kwargs): if name == "tomllib": raise ModuleNotFoundError("No module named 'tomllib'", name="tomllib") - return original(name, *args, **kwargs) + return tomli if name == "tomli" else original(name, *args, **kwargs) - monkeypatch.setattr(builtins, "__import__", import_without_tomllib) - loaded = runpy.run_path(gate.__file__) - assert loaded["tomllib"] is tomli - assert loaded["tomllib"].loads('[package]\nlicense="MIT"')['package']['license'] == "MIT" + monkeypatch.setattr(gate.importlib, "import_module", import_without_tomllib) + parser = gate._import_toml_parser() + assert parser is tomli + assert parser.loads('[package]\nlicense="MIT"')['package']['license'] == "MIT" def test_binder_resolves_next_to_this_script() -> None: diff --git a/tests/test_release_dependency_gate_remaining_branches.py b/tests/test_release_dependency_gate_remaining_branches.py new file mode 100644 index 0000000000..db6b9c13c2 --- /dev/null +++ b/tests/test_release_dependency_gate_remaining_branches.py @@ -0,0 +1,443 @@ +"""Close the release dependency gate's remaining fail-closed branch gaps.""" + +from __future__ import annotations + +import hashlib +import importlib +import json +import runpy +import subprocess +import tomllib +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from scripts.ci import release_dependency_gate as gate +from tests.test_release_dependency_fanout_plan import CONTROL, _allowed +from tests.test_release_dependency_gate import CRATE_HASH, _write, build_capture + + +def _commit(source: Path, message: str = "fixture") -> str: + """Commit the complete fixture and return its exact Git identity.""" + + subprocess.run(["git", "init", "-q", str(source)], check=True) + subprocess.run(["git", "add", "."], cwd=source, check=True) + subprocess.run( + [ + "git", + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.invalid", + "-c", + "commit.gpgsign=false", + "commit", + "--allow-empty", + "-qm", + message, + ], + cwd=source, + check=True, + ) + return subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=source, text=True + ).strip() + + +def test_python_310_toml_fallback_loads_the_declared_compatibility_module( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A Python 3.10 runtime without ``tomllib`` must use the declared fallback.""" + + real_import_module = importlib.import_module + fallback = SimpleNamespace(loads=tomllib.loads) + + def import_without_tomllib(name: str, package: str | None = None) -> Any: + if name == "tomllib": + raise ModuleNotFoundError("simulated Python 3.10") + if name == "tomli": + return fallback + return real_import_module(name, package) + + monkeypatch.setattr(importlib, "import_module", import_without_tomllib) + namespace = runpy.run_path(str(Path(gate.__file__))) + assert namespace["tomllib"] is fallback + + +def test_benign_rust_build_script_has_no_process_or_network_finding() -> None: + """A Rust build script without a process/network namespace remains admissible.""" + + assert gate.detect_install_hooks({"build.rs": "fn main() { println!(\"cargo:rerun\"); }"}) == [] + + +@pytest.mark.parametrize( + ("name", "target", "leg", "member", "expected_kind"), + [ + ("libc.so.6", "manylinux_2_28_x86_64", "linux-py3.13", "pkg/core.so", "system-runtime"), + ("libunknown.so", "manylinux_2_28_x86_64", "linux-py3.13", "pkg/core.so", None), + ( + "@rpath/fast_mlsirm._core.cpython-313-darwin.so", + "macosx_11_0_x86_64-darwin", + "darwin-py3.13", + "fast_mlsirm/_core.cpython-313-darwin.so", + "self-install-name", + ), + ("PYTHON313.DLL", "win_amd64-windows", "windows-py3.13", "pkg/core.pyd", "interpreter-runtime"), + ("UNKNOWN.DLL", "win_amd64-windows", "windows-py3.13", "pkg/core.pyd", None), + ("libc.so.6", "freebsd_14_x86_64", "freebsd-py3.13", "pkg/core.so", None), + ], +) +def test_platform_link_classifier_distinguishes_reviewed_and_unknown_runtime_links( + name: str, + target: str, + leg: str, + member: str, + expected_kind: str | None, +) -> None: + """Only the target-specific runtime identities receive an allowlisted basis.""" + + result = gate.classify_platform_link(name, target, leg, member) + assert (result and result["kind"]) == expected_kind + + +def test_selection_capture_cli_accepts_an_exact_commit_without_a_selection_file( + tmp_path: Path, +) -> None: + """An absent optional selection file is a successful, empty capture via the CLI.""" + + source = tmp_path / "source" + source.mkdir() + sha = _commit(source) + capture = tmp_path / "capture" + assert gate.main( + [ + "capture-license-selections", + "--source", + str(source), + "--source-sha", + sha, + "--capture", + str(capture), + ] + ) == 0 + assert not capture.exists() + + +def test_selection_capture_requires_an_exact_commit_sha(tmp_path: Path) -> None: + """A branch-like selection identity is rejected before any Git object lookup.""" + + with pytest.raises(gate.GateError, match="exact commit SHA"): + gate.capture_license_selections(tmp_path, "main", tmp_path / "capture") + + +def test_selection_capture_rejects_a_symlink_git_object(tmp_path: Path) -> None: + """A committed symlink cannot stand in for the regular selection JSON blob.""" + + source = tmp_path / "source" + path = source / "docs" / "release-license-selections.json" + path.parent.mkdir(parents=True) + path.symlink_to("elsewhere.json") + sha = _commit(source) + with pytest.raises(gate.GateError, match="regular Git blob"): + gate.capture_license_selections(source, sha, tmp_path / "capture") + + +def test_selection_capture_rechecks_the_blob_size_after_read( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Git returning more bytes than its size metadata cannot bypass the read bound.""" + + source = tmp_path / "source" + path = source / "docs" / "release-license-selections.json" + path.parent.mkdir(parents=True) + path.write_text("[]", encoding="utf-8") + sha = _commit(source) + real_check_output = gate.subprocess.check_output + + def inconsistent_git(command: list[str], **kwargs: Any) -> Any: + if command[1:3] == ["cat-file", "-s"]: + return b"1\n" + if command[1:3] == ["cat-file", "blob"]: + return b"oversized" + return real_check_output(command, **kwargs) + + monkeypatch.setattr(gate.subprocess, "check_output", inconsistent_git) + monkeypatch.setattr(gate, "_MAX_METADATA_BYTES", 2) + with pytest.raises(gate.GateError, match="bounded size"): + gate.capture_license_selections(source, sha, tmp_path / "capture") + + +def _source_bound_cargo_capture( + tmp_path: Path, *, workspace_version: bool = False +) -> tuple[Path, Path, str]: + """Build a small source-bound Cargo capture backed by real Git objects.""" + + capture = build_capture(tmp_path / "capture") + source = (tmp_path / "source").resolve() + wheel = source / "crates" / "wheel" + core = source / "crates" / "core" / "Cargo.toml" + wheel.mkdir(parents=True) + core.parent.mkdir(parents=True) + core.write_text( + '[package]\nname = "local-core"\n' + + ('version.workspace = true\n' if workspace_version else 'version = "1.0.0"\n'), + encoding="utf-8", + ) + wheel_manifest = '[package]\nname = "fast-mlsirm"\nversion = "0.11.5"\n' + if workspace_version: + wheel_manifest += '\n[workspace.package]\nversion = "1.0.0"\n' + (wheel / "Cargo.toml").write_text(wheel_manifest, encoding="utf-8") + lock = capture / "cargo" / "Cargo.lock" + lock.write_text( + lock.read_text(encoding="utf-8") + + '\n[[package]]\nname = "local-core"\nversion = "1.0.0"\n', + encoding="utf-8", + ) + (wheel / "Cargo.lock").write_bytes(lock.read_bytes()) + sha = _commit(source) + + metadata_path = capture / "cargo" / "metadata.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + metadata["workspace_root"] = str(wheel) + metadata["packages"][0]["manifest_path"] = str(wheel / "Cargo.toml") + metadata["packages"].append( + { + "id": "local-id", + "name": "local-core", + "version": "1.0.0", + "source": None, + "manifest_path": str(core), + } + ) + metadata["resolve"]["nodes"][0]["deps"].append({"pkg": "local-id"}) + metadata["resolve"]["nodes"].append( + {"id": "local-id", "deps": [{"pkg": "greencrate-id"}]} + ) + _write(metadata_path, metadata) + return capture, source, sha + + +def test_cargo_source_binding_rejects_a_nonexact_commit(tmp_path: Path) -> None: + """Source-bound Cargo declarations require a full immutable commit identity.""" + + capture, source, _sha = _source_bound_cargo_capture(tmp_path) + with pytest.raises(gate.GateError, match="source checkout cannot be bound"): + gate._enumerate_cargo(capture, source_root=source, source_sha="main") + + +def test_cargo_workspace_must_be_inside_the_selected_source(tmp_path: Path) -> None: + """Absolute Cargo metadata still fails when its workspace escapes the checkout.""" + + capture, source, sha = _source_bound_cargo_capture(tmp_path) + metadata_path = capture / "cargo" / "metadata.json" + metadata = json.loads(metadata_path.read_text(encoding="utf-8")) + metadata["workspace_root"] = str((tmp_path / "outside").resolve()) + _write(metadata_path, metadata) + with pytest.raises(gate.GateError, match="source checkout cannot be bound"): + gate._enumerate_cargo(capture, source_root=source, source_sha=sha) + + +def test_cargo_workspace_inherited_version_is_bound_to_the_root_manifest( + tmp_path: Path, +) -> None: + """A path crate's inherited version must resolve from committed workspace bytes.""" + + capture, source, sha = _source_bound_cargo_capture(tmp_path, workspace_version=True) + dependencies, failures, expected = gate._enumerate_cargo( + capture, source_root=source, source_sha=sha + ) + assert failures == [] + assert {dependency.key for dependency in dependencies} == expected == { + "cargo/greencrate@0.1.0" + } + + +@pytest.mark.parametrize("matches_source", [False, True]) +def test_development_cargo_lock_must_match_the_committed_root_lock( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + matches_source: bool, +) -> None: + """A distinct development graph is admitted only with the exact root lock bytes.""" + + capture = build_capture(tmp_path / "capture") + source = tmp_path / "source" + source.mkdir() + root_lock = (capture / "cargo" / "Cargo.lock").read_bytes() + b"# development graph\n" + (source / "Cargo.lock").write_bytes(root_lock) + sha = _commit(source) + release = json.loads((capture / "release.json").read_text(encoding="utf-8")) + release["source_sha"] = sha + _write(capture / "release.json", release) + dev = capture / "cargo-dev" + dev.mkdir() + (dev / "Cargo.lock").write_bytes(root_lock if matches_source else b"other lock") + dependency = gate.Dependency( + "cargo", "greencrate", "0.1.0", frozenset({CRATE_HASH}) + ) + + def enumerate_cargo(*_args: Any, **_kwargs: Any) -> tuple[list[gate.Dependency], list[gate.Failure], set[str]]: + return [dependency], [], {dependency.key} + + monkeypatch.setattr(gate, "_enumerate_cargo", enumerate_cargo) + if not matches_source: + with pytest.raises(gate.GateError, match="development Cargo lock differs"): + gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source) + return + report = gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source) + assert report.passed + + +def _source_notice_fixture( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mutation: str, +) -> tuple[Path, str, str, dict[str, str], dict[str, Any]]: + """Create one reviewed supplemental notice declaration in a real Git tree.""" + + source = tmp_path / "source" + notice_path = "python/fast_mlsirm/_licenses/example-1.0.0.txt" + first, second = b"MIT", b"Apache" + content = first + (b"--" if mutation == "separator" else b"\n\n") + second + digest = hashlib.sha256(content).hexdigest() + first_digest = "0" * 64 if mutation == "grant" else hashlib.sha256(first).hexdigest() + grants = ( + ("LICENSE-MIT", len(first), first_digest), + ("LICENSE-APACHE", len(second), hashlib.sha256(second).hexdigest()), + ) + archive_sha = "a" * 64 + upstream_commit = "b" * 40 + repository = "example/project" + upstream = [ + { + "url": f"https://raw.githubusercontent.com/{repository}/{upstream_commit}/{name}", + "sha256": sha, + } + for name, _size, sha in grants + ] + selection = { + "chosen": "MIT AND Apache-2.0", + "rationale": "Retain both immutable grants.", + } + choice = { + "ecosystem": "cargo", + "name": "example", + "version": "1.0.0", + **selection, + "archive_sha256": archive_sha, + "bundled_notice": {"path": notice_path, "sha256": digest}, + "upstream_licenses": upstream, + } + choices: Any = [choice] + if mutation == "object": + choices = {"choice": choice} + elif mutation == "missing-choice": + choices = [] + _write(source / "docs" / "release-license-selections.json", choices) + notice = source / notice_path + notice.parent.mkdir(parents=True) + notice.write_bytes(content) + sha = _commit(source) + monkeypatch.setitem( + gate._REVIEWED_SOURCE_NOTICES, + "cargo/example@1.0.0", + ( + archive_sha, + repository, + upstream_commit, + digest, + {"MIT AND Apache-2.0"}, + grants, + ), + ) + evidence = {"ecosystem": "cargo", "source_sha256": archive_sha} + return source, sha, notice_path, selection, evidence + + +@pytest.mark.parametrize( + ("mutation", "message"), + [ + ("bad-sha", "exact release commit"), + ("oversized", "bounded size"), + ("object", "JSON array"), + ("missing-choice", "one explicit notice selection"), + ("separator", "separator differs"), + ("grant", "grant bytes differ"), + ], +) +def test_source_notice_refuses_each_unbound_git_or_grant_shape( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mutation: str, + message: str, +) -> None: + """Every reviewed source notice remains bound to exact Git and grant bytes.""" + + source, sha, _notice_path, selection, evidence = _source_notice_fixture( + tmp_path, monkeypatch, mutation + ) + if mutation == "bad-sha": + sha = "main" + elif mutation == "oversized": + monkeypatch.setattr(gate, "_MAX_METADATA_BYTES", 1) + with pytest.raises(gate.GateError, match=message): + gate._source_license_notice( + source, sha, "cargo/example@1.0.0", evidence, selection + ) + + +def _runtime_archive_row(ecosystem: str, name: str, digest_byte: str) -> dict[str, Any]: + """Build one internally consistent runtime archive verdict row.""" + + source_hash = digest_byte * 64 + key = f"{ecosystem}/{name}@1.0/sha256/{source_hash}" + evidence = { + "source_sha256": source_hash, + "archive_members": [], + "install_hook_sources": {}, + "parsed_inputs": [], + "native_libraries": [], + "known_vulnerabilities": [], + } + fixture = gate.build_fixture(gate.Dependency(ecosystem, name, "1.0"), evidence) + fixture["id"] = key + return { + "key": key, + "package_key": f"{ecosystem}/{name}@1.0", + "name": name, + "version": "1.0", + "source_sha256": source_hash, + "license": "MIT", + "fixture": fixture, + "fixture_sha256": gate.fixture_digest(fixture), + } + + +def test_runtime_archives_cannot_expand_an_in_limit_plan_past_the_job_cap( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Runtime archive rows are counted with base dependencies before fanout.""" + + capture, report_path = _allowed(tmp_path) + report = json.loads(report_path.read_text(encoding="utf-8")) + monkeypatch.setattr(gate, "STRIX_PLAN_LIMIT", len(report["dependencies"])) + archive_report = tmp_path / "runtime.json" + archive_report.write_text( + json.dumps( + { + "schema": "cwl.release-runtime-archive-licenses/3", + "archives": [_runtime_archive_row("pypi", "runtime", "a")], + "build_packages": [_runtime_archive_row("pypi", "builder", "b")], + "build_tools": [ + _runtime_archive_row("github-release", "maturin", "c") + ], + } + ), + encoding="utf-8", + ) + with pytest.raises(gate.GateError, match="plan exceeds"): + gate.strix_fanout_plan( + capture, report_path, CONTROL, 1, 1, archive_report + ) diff --git a/tests/test_repository_branch_coverage_reporting_edges.py b/tests/test_repository_branch_coverage_reporting_edges.py index f5dbf1dae0..0d84369e98 100644 --- a/tests/test_repository_branch_coverage_reporting_edges.py +++ b/tests/test_repository_branch_coverage_reporting_edges.py @@ -2,6 +2,8 @@ from __future__ import annotations +import runpy +import sys from pathlib import Path import pytest @@ -21,6 +23,25 @@ def test_sanitizer_without_trailing_newline_stays_without_one() -> None: assert sanitizer.sanitize_text("plain") == "plain" +def test_sanitizer_cli_writes_utf8_redacted_summary( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """The command-line boundary preserves UTF-8 evidence while redacting secrets.""" + + source = tmp_path / "coverage.md" + destination = tmp_path / "coverage-output.md" + source.write_text("결과: 통과\nAPI_KEY=비밀\n", encoding="utf-8") + monkeypatch.setattr(sys, "argv", ["sanitize", str(source), str(destination)]) + + with pytest.raises(SystemExit) as exc_info: + runpy.run_path( + "scripts/ci/sanitize_github_output_summary.py", run_name="__main__" + ) + + assert exc_info.value.code == 0 + assert destination.read_text(encoding="utf-8") == "결과: 통과\nAPI_KEY=\n" + + def test_sbom_defensive_relationship_and_license_shapes() -> None: """Malformed relationship and license entries fail closed to NOASSERTION.""" diff --git a/tests/test_repository_metadata_live_verification.py b/tests/test_repository_metadata_live_verification.py index 6ae83d8c47..4fecd171e8 100644 --- a/tests/test_repository_metadata_live_verification.py +++ b/tests/test_repository_metadata_live_verification.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import io import importlib.util import json from pathlib import Path @@ -145,10 +146,11 @@ def build_ok(handler): assert len(handlers) == 1 assert isinstance(handlers[0], RECONCILER._NoPagesRedirects) from urllib.error import HTTPError - with pytest.raises(HTTPError): + with pytest.raises(HTTPError) as exc_info: handlers[0].redirect_request( RECONCILER.Request("https://example.com"), None, 302, "redirect", {}, "http://127.0.0.1/" ) + exc_info.value.close() with pytest.raises(RuntimeError, match="not built"): RECONCILER._pages_publication_ready("Repo", {**ready, "status": "building"}) @@ -179,6 +181,28 @@ def build_ok(handler): RECONCILER._pages_publication_ready("Repo", ready) +def test_pages_publication_ready_closes_mapped_http_error(monkeypatch) -> None: + """Pages verification closes the response when it maps an HTTP failure.""" + ready = { + "status": "built", + "html_url": "https://contextualwisdomlab.github.io/Repo/", + } + error_body = io.BytesIO(b"private body") + provider_error = RECONCILER.HTTPError( + ready["html_url"], 503, "private body", {}, error_body + ) + monkeypatch.setattr( + RECONCILER, + "build_opener", + lambda *args: FakeOpener(error=provider_error), + ) + + with pytest.raises(RuntimeError, match="not reachable"): + RECONCILER._pages_publication_ready("Repo", ready) + + assert error_body.closed + + def test_verify_repository_accepts_converged_disabled_and_enabled_pages( monkeypatch, ) -> None: diff --git a/tests/test_repository_metadata_workflow_pages.py b/tests/test_repository_metadata_workflow_pages.py index 5c05dfbe3d..2823719040 100644 --- a/tests/test_repository_metadata_workflow_pages.py +++ b/tests/test_repository_metadata_workflow_pages.py @@ -50,6 +50,20 @@ def test_metadata_pr_validation_cancels_superseded_head_runs() -> None: assert "github.event.pull_request.head.sha" not in concurrency + +def test_metadata_validation_fetches_history_for_published_lineage_contracts() -> None: + """The complete suite must have ancestry for its published-commit checks.""" + + workflow = WORKFLOW.read_text(encoding="utf-8") + validate_checkout = workflow.split( + "- name: Check out exact revision", 1 + )[1].split("- name: Verify exact revision", 1)[0] + + assert re.search( + r"(?m)^[ \t]+fetch-depth:[ \t]+0[ \t]*$", + validate_checkout, + ) + def test_manifest_accepts_explicit_workflow_pages_mode() -> None: """Workflow-backed Pages intent is explicit without changing legacy records.""" diff --git a/tests/test_resolve_base_rust_toolchain.py b/tests/test_resolve_base_rust_toolchain.py index db47136c80..26e248a127 100644 --- a/tests/test_resolve_base_rust_toolchain.py +++ b/tests/test_resolve_base_rust_toolchain.py @@ -2,7 +2,9 @@ from __future__ import annotations +import runpy import subprocess +import sys from pathlib import Path import pytest @@ -81,3 +83,37 @@ def test_invalid_base_sha_is_rejected(tmp_path: Path) -> None: def test_central_release_is_itself_exact() -> None: assert resolver.EXACT_RELEASE_RE.fullmatch(resolver.CENTRAL_RUST_TOOLCHAIN) + + +def test_cli_prints_resolved_release_and_rejects_untrusted_sha( + tmp_path: Path, capsys: pytest.CaptureFixture[str], +) -> None: + """The CLI prints a trusted-base pin and reports malformed revision input.""" + + base = _commit(tmp_path, {"rust-toolchain": "1.97.1\n"}) + assert resolver.main(["--repo-root", str(tmp_path), "--base-sha", base]) == 0 + assert capsys.readouterr().out == "1.97.1\n" + + assert resolver.main(["--repo-root", str(tmp_path), "--base-sha", "HEAD"]) == 1 + assert "base SHA must be" in capsys.readouterr().err + + +def test_script_entrypoint_uses_the_trusted_base( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + """The executable script resolves and prints the exact base-commit toolchain.""" + + base = _commit(tmp_path, {"rust-toolchain.toml": '[toolchain]\nchannel = "1.97.1"\n'}) + _commit(tmp_path, {"rust-toolchain.toml": '[toolchain]\nchannel = "1.80.0"\n'}) + script = Path(resolver.__file__) + monkeypatch.setattr( + sys, + "argv", + [str(script), "--repo-root", str(tmp_path), "--base-sha", base], + ) + + with pytest.raises(SystemExit) as exited: + runpy.run_path(str(script), run_name="__main__") + + assert exited.value.code == 0 + assert capsys.readouterr().out == "1.97.1\n" diff --git a/tests/test_review_preflight_concurrency.py b/tests/test_review_preflight_concurrency.py index 641c726a0e..cf161029a0 100644 --- a/tests/test_review_preflight_concurrency.py +++ b/tests/test_review_preflight_concurrency.py @@ -2,6 +2,7 @@ import runpy import threading +from io import BytesIO from pathlib import Path from types import SimpleNamespace from urllib.error import HTTPError @@ -63,12 +64,17 @@ def test_unavailable_pool_fails_closed_within_the_probe_budget(): """Concurrent completion does not enlarge the committed probe budget.""" namespace = runpy.run_path(str(LAUNCHER)) calls = [] + provider_errors = [] class Client: """Return explicit provider rate-limit responses.""" def proxy_send_once(self, agent, endpoint, payload): calls.append(agent.id) - raise HTTPError('https://provider.invalid', 429, 'private body', {}, None) + provider_error = HTTPError( + 'https://provider.invalid', 429, 'private body', {}, None + ) + provider_errors.append(provider_error) + raise provider_error with pytest.raises(namespace['ReviewPreflightError']) as error: namespace['_preflight_review_agents_concurrently'](agents(24), client=Client()) @@ -77,6 +83,57 @@ def proxy_send_once(self, agent, endpoint, payload): assert report['ready_count'] == report['pending_count'] == 0 assert all(row['status'] == 'rejected' and row['http_status'] == 429 for row in report['routes']) assert 'private body' not in str(report) + assert all(provider_error.closed for provider_error in provider_errors) + + +def test_hostile_retry_after_is_rejected_and_http_error_is_closed(): + """Unbounded optional telemetry cannot abort preflight or leak its response.""" + + namespace = runpy.run_path(str(LAUNCHER)) + error_body = BytesIO(b"private body") + provider_error = HTTPError( + "https://provider.invalid", + 429, + "private body", + {"Retry-After": "9" * 5000}, + error_body, + ) + + class Client: + """Return one rate-limit response with hostile optional telemetry.""" + + def proxy_send_once(self, agent, endpoint, payload): + raise provider_error + + with pytest.raises(namespace["ReviewPreflightError"]) as error: + namespace["_preflight_review_agents_concurrently"](agents(1), client=Client()) + + route = error.value.report["routes"][0] + assert route["status"] == "rejected" + assert route["http_status"] == 429 + assert "retry_after_s" not in route + assert "private body" not in str(error.value.report) + assert error_body.closed + + +def test_http_error_is_closed_when_optional_telemetry_raises(): + """Cleanup remains mandatory if later telemetry classification regresses.""" + namespace = runpy.run_path(str(LAUNCHER)) + error_body = BytesIO(b"private body") + provider_error = HTTPError( + "https://provider.invalid", 503, "private body", {}, error_body + ) + + def broken_telemetry(exc): + raise ValueError("telemetry parser failed") + + record_exception = namespace["_record_provider_exception"] + record_exception.__globals__["_safe_retry_after_seconds"] = broken_telemetry + + with pytest.raises(ValueError, match="telemetry parser failed"): + record_exception({}, provider_error) + + assert error_body.closed def test_parallel_escalations_share_one_budget(): diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..2231f48ede 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -6,6 +6,7 @@ import socket import subprocess import sys +from io import BytesIO from pathlib import Path import pytest @@ -671,6 +672,7 @@ def test_no_redirect_handler_raises_httperror_without_following(): sandboxed_web_e2e.NoRedirectHandler().redirect_request(request, None, 302, "Found", {}, "http://127.0.0.1") assert exc_info.value.code == 302 + exc_info.value.close() def test_wait_for_url_returns_false_after_timeout(monkeypatch, tmp_path): @@ -695,6 +697,43 @@ def open(self, url, timeout): assert sandboxed_web_e2e.wait_for_url("http://127.0.0.1:8000/health", 1, service) is False +def test_wait_for_url_closes_redirect_response(monkeypatch, tmp_path): + """A rejected readiness redirect releases its file-like HTTP response.""" + + class RunningProcess: + def poll(self): + return None + + ticks = iter([0, 0, 2]) + error_body = BytesIO() + redirect_error = sandboxed_web_e2e.urllib.error.HTTPError( + "http://127.0.0.1:8000/health", 302, "Found", {}, error_body + ) + + class RedirectingOpener: + def open(self, url, timeout): + raise redirect_error + + monkeypatch.setattr(sandboxed_web_e2e.time, "monotonic", lambda: next(ticks)) + monkeypatch.setattr(sandboxed_web_e2e.time, "sleep", lambda seconds: None) + monkeypatch.setattr( + sandboxed_web_e2e.urllib.request, + "build_opener", + lambda *args: RedirectingOpener(), + ) + service = sandboxed_web_e2e.Service( + "web", "serve", RunningProcess(), tmp_path / "web.log" + ) + + assert ( + sandboxed_web_e2e.wait_for_url( + "http://127.0.0.1:8000/health", 1, service + ) + is False + ) + assert error_body.closed + + def test_main_runs_with_stubbed_services(monkeypatch, tmp_path, capsys): """Main records success evidence without requiring real POSIX services.""" repo = tmp_path / "repo" diff --git a/tests/test_spdx_license_policy.py b/tests/test_spdx_license_policy.py index 53bee3ec72..072d93f29d 100644 --- a/tests/test_spdx_license_policy.py +++ b/tests/test_spdx_license_policy.py @@ -131,6 +131,18 @@ def test_dual_license_selection_needs_a_rationale() -> None: assert (decision.allowed, decision.code) == (False, policy.LICENSE_SELECTION_INVALID) +def test_dual_license_selection_must_be_valid_spdx() -> None: + """A rationale cannot make a malformed SPDX selection valid.""" + decision = policy.evaluate_license_expression( + "MIT OR GPL-2.0-only", selection="MIT AND", rationale="commercial choice" + ) + + assert (decision.allowed, decision.code) == ( + False, + policy.LICENSE_SELECTION_INVALID, + ) + + def test_selection_must_name_an_operand_of_the_expression() -> None: """A selection naming a license the dependency never offered is rejected.""" decision = policy.evaluate_license_expression( diff --git a/tests/test_strix_report_scope.py b/tests/test_strix_report_scope.py index 284b34e8d5..955557e214 100644 --- a/tests/test_strix_report_scope.py +++ b/tests/test_strix_report_scope.py @@ -1,32 +1,19 @@ """A completed Strix report must name the PR source it assessed.""" +from __future__ import annotations + import json -import subprocess +import runpy import sys from pathlib import Path -SCRIPT = Path(__file__).resolve().parents[1] / "scripts/ci/strix_report_scope.py" - - -def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_path: Path) -> None: - run = tmp_path / "current-scan" - run.mkdir() - (run / "run.json").write_text( - json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), - encoding="utf-8", - ) - report = run / "penetration_test_report.md" - report.write_text("Python OpenSSH client RCE vulnerability.\n", encoding="utf-8") - command = [sys.executable, str(SCRIPT), str(tmp_path), "python/fast_mlsirm/report.py"] - assert subprocess.run(command, capture_output=True).returncode == 1 +import pytest - report.write_text("Assessed python/fast_mlsirm/report.py; no vulnerabilities found.\n", encoding="utf-8") - assert subprocess.run(command, capture_output=True).returncode == 0 - assert subprocess.run(command[:-1], capture_output=True).returncode == 1 +from scripts.ci import strix_report_scope as report_scope -# Scope section of the 0-finding fast-mlsirm#2246 report (run 36580588738, -# attempt 2): it names the PR-scope directories it audited, not a file. +SCRIPT = Path(__file__).resolve().parents[1] / "scripts/ci/strix_report_scope.py" +CHANGED_PATH = "python/fast_mlsirm/report.py" SCOPED_DIRECTORY_REPORT = """# Methodology **Scope:** @@ -38,38 +25,191 @@ def test_report_scope_rejects_unrelated_success_and_accepts_scoped_success(tmp_p """ -def _completed_run(tmp_path: Path, report: str) -> None: - run = tmp_path / "current-scan" - run.mkdir() - (run / "run.json").write_text( - json.dumps({"status": "completed", "scan_results": {"scan_completed": True, "success": True}}), - encoding="utf-8", +def _write_report( + output_dir: Path, + *, + metadata: object | None = None, + report_text: str = f"Assessed {CHANGED_PATH}; no vulnerabilities found.\n", +) -> Path: + """Create one ordinary scan report and return its run directory.""" + run_dir = output_dir / "current-scan" + run_dir.mkdir(parents=True, exist_ok=True) + if metadata is None: + metadata = { + "status": "completed", + "scan_results": {"scan_completed": True, "success": True}, + } + (run_dir / "run.json").write_text(json.dumps(metadata), encoding="utf-8") + (run_dir / "penetration_test_report.md").write_text( + report_text, encoding="utf-8" ) - (run / "penetration_test_report.md").write_text(report, encoding="utf-8") + return run_dir -def test_scan_scope_directory_containing_a_changed_file_identifies_the_scope(tmp_path: Path) -> None: - _completed_run(tmp_path, SCOPED_DIRECTORY_REPORT) - changed = ["crates/mlsirm-core/src/gpu_regression.rs", "python/fast_mlsirm/regression.py"] - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 0 +def test_validate_accepts_only_a_completed_scoped_report(tmp_path: Path) -> None: + """A completed report naming a changed source path is accepted.""" + _write_report(tmp_path) + report_scope.validate(tmp_path, [CHANGED_PATH]) -def test_scan_scope_directory_unrelated_to_changed_files_is_rejected(tmp_path: Path) -> None: - _completed_run(tmp_path, SCOPED_DIRECTORY_REPORT) - changed = ["docs/methods.md", "tests/test_regression.py"] - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 +def test_validate_rejects_missing_or_linked_output(tmp_path: Path) -> None: + """The trusted scan root must be a real directory.""" + with pytest.raises(ValueError, match="output directory is missing"): + report_scope.validate(tmp_path / "missing", [CHANGED_PATH]) + real_dir = tmp_path / "real" + real_dir.mkdir() + linked_dir = tmp_path / "linked" + linked_dir.symlink_to(real_dir, target_is_directory=True) + with pytest.raises(ValueError, match="output directory is missing"): + report_scope.validate(linked_dir, [CHANGED_PATH]) -def test_bare_repository_directory_or_scope_root_is_not_enough(tmp_path: Path) -> None: - changed = ["crates/mlsirm-core/src/gpu_regression.rs"] - _completed_run(tmp_path, "Audited crates/mlsirm-core; nothing found.\n") - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 - (tmp_path / "current-scan" / "penetration_test_report.md").write_text( - "Scope: `/workspace/strix-pr-scope.AoFHD6/`; nothing found.\n", encoding="utf-8" - ) - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 - # A repository-root equivalent under the scope root binds nothing either. - (tmp_path / "current-scan" / "penetration_test_report.md").write_text( - "Scope: `/workspace/strix-pr-scope.AoFHD6/.`; nothing found.\n", encoding="utf-8" + +def test_validate_requires_exactly_one_real_run_directory(tmp_path: Path) -> None: + """Files and linked directories never count as the single current run.""" + output_dir = tmp_path / "output" + output_dir.mkdir() + (output_dir / "noise.txt").write_text("ignored", encoding="utf-8") + linked_target = tmp_path / "linked-target" + linked_target.mkdir() + (output_dir / "linked-run").symlink_to( + linked_target, target_is_directory=True ) - assert subprocess.run([sys.executable, str(SCRIPT), str(tmp_path), *changed], capture_output=True).returncode == 1 + with pytest.raises(ValueError, match="exactly one current scan report"): + report_scope.validate(output_dir, [CHANGED_PATH]) + + _write_report(output_dir) + (output_dir / "second-run").mkdir() + with pytest.raises(ValueError, match="exactly one current scan report"): + report_scope.validate(output_dir, [CHANGED_PATH]) + + +@pytest.mark.parametrize("linked_name", ["run.json", "penetration_test_report.md"]) +def test_validate_rejects_missing_or_linked_report_files( + tmp_path: Path, linked_name: str +) -> None: + """Neither report input may be absent or redirected through a symlink.""" + run_dir = _write_report(tmp_path) + target = run_dir / linked_name + target.unlink() + with pytest.raises(ValueError, match="report files are missing or linked"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + outside = tmp_path / f"outside-{linked_name}" + outside.write_text("{}" if linked_name == "run.json" else CHANGED_PATH) + target.symlink_to(outside) + with pytest.raises(ValueError, match="report files are missing or linked"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +@pytest.mark.parametrize( + ("metadata", "expected_error"), + [ + ([], "metadata is not an object"), + ( + {"status": "completed", "scan_results": ["invalid"]}, + "results are not an object", + ), + ( + { + "status": "running", + "scan_results": {"scan_completed": True, "success": True}, + }, + "report is incomplete", + ), + ( + { + "status": "completed", + "scan_results": {"scan_completed": False, "success": True}, + }, + "report is incomplete", + ), + ( + { + "status": "completed", + "scan_results": {"scan_completed": True, "success": False}, + }, + "report is incomplete", + ), + ], +) +def test_validate_rejects_malformed_or_incomplete_metadata( + tmp_path: Path, metadata: object, expected_error: str +) -> None: + """Metadata shape and every completion signal fail closed.""" + _write_report(tmp_path, metadata=metadata) + with pytest.raises(ValueError, match=expected_error): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +def test_validate_rejects_malformed_json_and_unrelated_scope(tmp_path: Path) -> None: + """Unreadable metadata and a report unrelated to the diff never pass.""" + run_dir = _write_report(tmp_path) + (run_dir / "run.json").write_text("{not-json", encoding="utf-8") + with pytest.raises(json.JSONDecodeError): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + _write_report(tmp_path, report_text="Python OpenSSH client RCE vulnerability.\n") + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate(tmp_path, [CHANGED_PATH]) + + +def test_scan_scope_directory_containing_a_changed_file_is_accepted( + tmp_path: Path, +) -> None: + """A reviewed scope directory binds changed files beneath that directory.""" + _write_report(tmp_path, report_text=SCOPED_DIRECTORY_REPORT) + changed_paths = [ + "crates/mlsirm-core/src/gpu_regression.rs", + "python/fast_mlsirm/regression.py", + ] + report_scope.validate(tmp_path, changed_paths) + + +def test_scan_scope_directory_unrelated_to_changed_files_is_rejected( + tmp_path: Path, +) -> None: + """A directory scope cannot authorize unrelated changed paths.""" + _write_report(tmp_path, report_text=SCOPED_DIRECTORY_REPORT) + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate(tmp_path, ["docs/methods.md", "tests/test_regression.py"]) + + +@pytest.mark.parametrize( + "report_text", + [ + "Audited crates/mlsirm-core; nothing found.\n", + "Scope: `/workspace/strix-pr-scope.AoFHD6/`; nothing found.\n", + "Scope: `/workspace/strix-pr-scope.AoFHD6/.`; nothing found.\n", + ], +) +def test_bare_repository_directory_or_scope_root_is_rejected( + tmp_path: Path, report_text: str +) -> None: + """Bare directory prose and repository roots bind no changed file.""" + _write_report(tmp_path, report_text=report_text) + with pytest.raises(ValueError, match="does not identify a changed source file"): + report_scope.validate( + tmp_path, ["crates/mlsirm-core/src/gpu_regression.rs"] + ) + + +def test_cli_reports_validation_error_and_accepts_scoped_success( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """The command boundary maps validation failures to exit 1 and stderr.""" + monkeypatch.setattr(sys, "argv", [str(SCRIPT), str(tmp_path), CHANGED_PATH]) + with pytest.raises(SystemExit) as missing_report_exit: + runpy.run_path(str(SCRIPT), run_name="__main__") + assert missing_report_exit.value.code == 1 + assert "ERROR: Strix report scope: expected exactly one" in capsys.readouterr().err + + _write_report(tmp_path) + monkeypatch.setattr(sys, "argv", [str(SCRIPT), str(tmp_path)]) + with pytest.raises(SystemExit) as empty_scope_exit: + runpy.run_path(str(SCRIPT), run_name="__main__") + assert empty_scope_exit.value.code == 1 + assert "does not identify a changed source file" in capsys.readouterr().err + + monkeypatch.setattr(sys, "argv", [str(SCRIPT), str(tmp_path), CHANGED_PATH]) + runpy.run_path(str(SCRIPT), run_name="__main__") diff --git a/tests/test_strix_unverified_dependency.py b/tests/test_strix_unverified_dependency.py index a4d2ac8c87..cde612f6b0 100644 --- a/tests/test_strix_unverified_dependency.py +++ b/tests/test_strix_unverified_dependency.py @@ -8,11 +8,15 @@ from __future__ import annotations +import runpy import subprocess import sys from pathlib import Path +import pytest + from scripts.ci.strix_unverified_dependency import ( + main, named_packages, unverified_dependency_finding, ) @@ -89,6 +93,54 @@ def test_findings_without_a_package_name_are_never_dropped(tmp_path: Path) -> No assert not unverified_dependency_finding(report, repo) +def test_requirements_files_are_dependency_manifests(tmp_path: Path) -> None: + repo = _rust_python_repo(tmp_path / "repo") + (repo / "requirements-production.in").write_text("orjson==3.10.15\n") + report = "**Target:** orjson 3.10.15\n" + + assert not unverified_dependency_finding(report, repo) + + +def test_dependency_names_outside_regular_manifests_are_ignored(tmp_path: Path) -> None: + repo = _rust_python_repo(tmp_path / "repo") + (repo / "README.md").write_text("lodash\n") + git_dir = repo / ".git" + git_dir.mkdir() + (git_dir / "Cargo.lock").write_text('[[package]]\nname = "lodash"\n') + source = repo / "symlink-source.txt" + source.write_text("lodash\n") + (repo / "requirements-symlink.txt").symlink_to(source) + oversized = repo / "requirements-oversized.txt" + oversized.write_text("lodash\n") + with oversized.open("ab") as handle: + handle.truncate(9 * 1024 * 1024) + + assert unverified_dependency_finding(LODASH_REPORT, repo) + + +def test_main_returns_usage_error_without_both_paths(capsys: pytest.CaptureFixture[str]) -> None: + assert main([str(HELPER)]) == 2 + assert "Usage: strix_unverified_dependency.py REPORT REPO_ROOT" in capsys.readouterr().err + + +def test_main_classifies_report_and_emits_warning(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + repo = _rust_python_repo(tmp_path / "repo") + report = tmp_path / "vuln-0001.md" + report.write_text(LODASH_REPORT) + + assert main([str(HELPER), str(report), str(repo)]) == 0 + assert "lodash" in capsys.readouterr().err + (repo / "yarn.lock").write_text('lodash@^4.17.20:\n version "4.17.20"\n') + assert main([str(HELPER), str(report), str(repo)]) == 1 + + +def test_script_entrypoint_exits_with_main_status(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, "argv", [str(HELPER)]) + + with pytest.raises(SystemExit, match="2"): + runpy.run_path(str(HELPER), run_name="__main__") + + def test_cli_exit_status_and_message(tmp_path: Path) -> None: repo = _rust_python_repo(tmp_path / "repo") report = tmp_path / "vuln-0001.md" diff --git a/tests/test_trusted_archive_retry_behavior.py b/tests/test_trusted_archive_retry_behavior.py new file mode 100644 index 0000000000..a09e278fbd --- /dev/null +++ b/tests/test_trusted_archive_retry_behavior.py @@ -0,0 +1,98 @@ +"""Behavioral regression for transient trusted-archive download failures.""" + +from __future__ import annotations + +import os +import subprocess +import tempfile +import textwrap +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + +import pytest + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_ROOT = REPOSITORY_ROOT / ".github" / "workflows" +WORKFLOWS = ( + "noema-review.yml", + "opencode-review.yml", + "pr-review-merge-scheduler.yml", +) + + +class TransientArchiveHandler(BaseHTTPRequestHandler): + """Return one transient 502 response before a successful archive body.""" + + request_count = 0 + + def do_GET(self) -> None: # noqa: N802 - stdlib handler API + """Serve the deterministic failure-then-success sequence.""" + type(self).request_count += 1 + if type(self).request_count == 1: + self.send_response(502) + self.end_headers() + return + body = b"trusted-archive" + self.send_response(200) + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format: str, *args: object) -> None: + """Keep the regression output focused on assertion failures.""" + + +def archive_command(workflow_path: Path) -> str: + """Extract archive assignments plus the production curl command.""" + lines = workflow_path.read_text(encoding="utf-8").splitlines() + start = next( + index for index, line in enumerate(lines) if "trusted_archive=" in line + ) + stop = next( + index + for index in range(start, len(lines)) + if "TRUSTED_SOURCE_REF}" in lines[index] + ) + return textwrap.dedent("\n".join(lines[start : stop + 1])) + + +@pytest.mark.parametrize("workflow_name", WORKFLOWS) +def test_trusted_archive_download_retries_transient_502(workflow_name: str) -> None: + """The production curl command must survive one transient GitHub 502.""" + TransientArchiveHandler.request_count = 0 + server = ThreadingHTTPServer(("127.0.0.1", 0), TransientArchiveHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with tempfile.TemporaryDirectory() as temporary_directory: + temporary_path = Path(temporary_directory) + environment = os.environ | { + "GH_TOKEN": "test-token", + "GITHUB_API_URL": f"http://127.0.0.1:{server.server_port}", + "GITHUB_WORKSPACE": temporary_directory, + "RUNNER_TEMP": temporary_directory, + "TRUSTED_SOURCE_REF": "1" * 40, + } + result = subprocess.run( + [ + "bash", + "-euo", + "pipefail", + "-c", + archive_command(WORKFLOW_ROOT / workflow_name), + ], + env=environment, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert TransientArchiveHandler.request_count == 2 + archive_path = next(temporary_path.glob("trusted-*.tar.gz")) + assert archive_path.read_bytes() == b"trusted-archive" + finally: + server.shutdown() + server.server_close() + thread.join() diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py index 50a5ddb5fe..ed4ad55681 100644 --- a/tests/test_trusted_uv_materializer_quality_workflow_contract.py +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -4,6 +4,15 @@ WORKFLOW_PATH = Path(".github/workflows/trusted-uv-materializer-quality-ci.yml") +AGENT_MENTION_WORKFLOW_PATH = Path( + ".github/workflows/agent-mention-router-quality-ci.yml" +) +AGENT_RUNTIME_WORKFLOW_PATH = Path( + ".github/workflows/agent-review-runtime-quality-ci.yml" +) +REPOSITORY_METADATA_WORKFLOW_PATH = Path( + ".github/workflows/repository-metadata-reconcile.yml" +) def _workflow_text() -> str: @@ -20,18 +29,33 @@ def test_quality_workflow_runs_for_every_materializer_surface() -> None: required_paths = ( '".github/workflows/trusted-uv-materializer-quality-ci.yml"', '"scripts/ci/materialize_base_python_requirements.py"', + '"scripts/ci/verify_release_maturin_tool_assets.py"', '"tests/conftest.py"', '"tests/test_materialize*.py"', '"tests/test_trusted_uv*.py"', '"tests/test_uv*.py"', '"tests/test_repository_branch_coverage_*.py"', + '"tests/test_verify_release_maturin_tool_assets.py"', '"requirements-opencode-review-ci-hashes.txt"', + '"requirements-opencode-review-ci.txt"', + '"requirements-noema-document-ci.txt"', + '"scripts/ci/compile_opencode_review_lock.sh"', '"pyproject.toml"', ) for required_path in required_paths: assert workflow.count(required_path) == 2 +def test_quality_workflow_admits_stacked_pull_requests() -> None: + """A non-default canonical owner base must not suppress exact-head evidence.""" + + pull_request_trigger = _workflow_text().split(" pull_request:\n", 1)[1].split( + " push:\n", 1 + )[0] + + assert "branches:" not in pull_request_trigger + + def test_quality_workflow_pins_actions_and_uses_read_only_permissions() -> None: """Quality evidence executes from the exact PR head with least privilege.""" @@ -49,6 +73,50 @@ def test_quality_workflow_pins_actions_and_uses_read_only_permissions() -> None: ) == 2 assert workflow.count("persist-credentials: false") == 2 assert workflow.count("ref: ${{ github.event.pull_request.head.sha }}") == 2 + minimum_job, full_job = workflow.split(" full-quality-gate:\n", 1) + assert "fetch-depth: 0" not in minimum_job + assert full_job.count("fetch-depth: 0") == 1 + + +def test_common_lock_source_changes_trigger_every_direct_consumer() -> None: + """Every workflow installing the generated common lock tracks its sources.""" + + tracked_inputs = ( + '"requirements-opencode-review-ci.txt"', + '"requirements-noema-document-ci.txt"', + '"scripts/ci/compile_opencode_review_lock.sh"', + ) + consumer_paths = ( + (AGENT_MENTION_WORKFLOW_PATH, 2), + (AGENT_RUNTIME_WORKFLOW_PATH, 1), + (REPOSITORY_METADATA_WORKFLOW_PATH, 1), + ) + for workflow_path, expected_count in consumer_paths: + workflow = workflow_path.read_text(encoding="utf-8") + for tracked_input in tracked_inputs: + assert workflow.count(tracked_input) == expected_count + + +def test_metadata_consumer_tracks_the_generated_common_lock() -> None: + """The metadata workflow must run when its installed lock changes.""" + + workflow = REPOSITORY_METADATA_WORKFLOW_PATH.read_text(encoding="utf-8") + + assert workflow.count('"requirements-opencode-review-ci-hashes.txt"') == 1 + + +def test_common_lock_source_declares_the_transitive_parser_inputs() -> None: + """The reviewed source binds both parser requirements regenerated in the lock.""" + + source = Path("requirements-opencode-review-ci.txt").read_text(encoding="utf-8") + lock = Path("requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) + + assert "-r requirements-noema-document-ci.txt" in source + assert "PyYAML==6.0.3" in source + assert "defusedxml==0.7.1" in lock + assert "pyyaml==6.0.3" in lock def test_minimum_python_contract_exercises_the_tomli_fallback() -> None: diff --git a/tests/test_verify_release_maturin_tool_assets.py b/tests/test_verify_release_maturin_tool_assets.py index e9c80e91f0..6aa84a0827 100644 --- a/tests/test_verify_release_maturin_tool_assets.py +++ b/tests/test_verify_release_maturin_tool_assets.py @@ -6,6 +6,7 @@ import runpy import sys import tarfile +import urllib.error import urllib.request import zipfile from pathlib import Path @@ -176,6 +177,52 @@ def build_opener(proxy_handler, redirect_handler): verifier._download("maturin-x86_64-pc-windows-msvc.zip") +def test_maturin_download_closes_unsuccessful_responses_and_http_errors(monkeypatch): + """Every rejected transport response releases its underlying connection.""" + closed = [] + + class Response: + status = 503 + + def close(self): + closed.append("response") + + class ResponseOpener: + def open(self, _request, timeout): + assert timeout == 60 + return Response() + + monkeypatch.setattr( + verifier.urllib.request, + "build_opener", + lambda _proxy, _redirect: ResponseOpener(), + ) + with pytest.raises(ValueError, match="HTTP 503"): + verifier._download("maturin-x86_64-pc-windows-msvc.zip") + assert closed == ["response"] + + transport_error = urllib.error.HTTPError( + "https://github.com/asset", 502, "Bad Gateway", {}, io.BytesIO() + ) + monkeypatch.setattr( + transport_error, "close", lambda: closed.append("http-error") + ) + + class ErrorOpener: + def open(self, _request, timeout): + assert timeout == 60 + raise transport_error + + monkeypatch.setattr( + verifier.urllib.request, + "build_opener", + lambda _proxy, _redirect: ErrorOpener(), + ) + with pytest.raises(ValueError, match="HTTP 502"): + verifier._download("maturin-x86_64-pc-windows-msvc.zip") + assert closed == ["response", "http-error"] + + def test_maturin_download_rejects_unlisted_name_before_network(monkeypatch): """Caller-controlled paths and URLs never reach the network transport.""" monkeypatch.setattr( diff --git a/tests/test_verify_release_scope_evidence_set.py b/tests/test_verify_release_scope_evidence_set.py index 449754b2b3..384100d524 100644 --- a/tests/test_verify_release_scope_evidence_set.py +++ b/tests/test_verify_release_scope_evidence_set.py @@ -583,6 +583,22 @@ def test_build_snapshot_refuses_unlisted_native_file(tmp_path: Path, monkeypatch _build_packages(row, folder) +def test_build_snapshot_refuses_unknown_universal_interpreter_architecture( + tmp_path: Path, +) -> None: + """A universal build receipt must bind one recognized interpreter architecture.""" + scope_root, scope_rows = _prescreen_case(tmp_path) + scope_row = next(row for row in scope_rows if row["leg"].startswith("universal2-apple-darwin-")) + artifact_folder = scope_root / scope_row["artifact_name"] + receipt_path = artifact_folder / f"{scope_row['leg']}.build-first.json" + receipt = json.loads(receipt_path.read_text()) + receipt["build_env"] = "/opt/python/UNKNOWN" + receipt_path.write_text(json.dumps(receipt)) + + with pytest.raises(gate.GateError, match="build interpreter architecture is missing"): + _build_packages(scope_row, artifact_folder) + + def test_runtime_archive_refuses_unknown_native_link(tmp_path: Path, monkeypatch) -> None: """A runtime archive with an unlicensed dynamic target cannot pass prescreen.""" root, rows = _prescreen_case(tmp_path) @@ -761,6 +777,26 @@ def test_build_package_prescreen_refuses_incomplete_build_evidence( _build_packages(scope_row, artifact_folder) +def test_build_package_prescreen_refuses_unknown_macos_build_architecture( + tmp_path: Path, +) -> None: + """Universal2 build evidence must identify its ARM64 or X64 interpreter.""" + scope_root, scope_rows = _prescreen_case(tmp_path) + scope_row = next( + row for row in scope_rows + if row["leg"].startswith("universal2-apple-darwin-") + ) + build_leg = scope_row["leg"] + artifact_folder = scope_root / scope_row["artifact_name"] + receipt_path = artifact_folder / f"{build_leg}.build-first.json" + build_receipt = json.loads(receipt_path.read_text()) + build_receipt["build_env"] = "runner:macos/15/macOS/RISCV64" + receipt_path.write_text(json.dumps(build_receipt)) + + with pytest.raises(gate.GateError, match="build interpreter architecture is missing"): + _build_packages(scope_row, artifact_folder) + + @pytest.mark.parametrize("mutation_name", ["missing", "oversized"]) def test_build_package_prescreen_refuses_missing_or_oversized_text( tmp_path: Path, mutation_name: str, @@ -912,9 +948,15 @@ def test_prescreen_refuses_malformed_scope_and_runtime_rows(tmp_path: Path) -> N with pytest.raises(gate.GateError, match="verified scope evidence is incomplete"): prescreen({}, tmp_path) - for mutation_name in ("scope-row", "runtime-set", "archive-row"): + for mutation_name in ("scope-count", "duplicate-leg", "scope-row", "runtime-set", "archive-row"): scope_root, scope_rows = _prescreen_case(tmp_path / mutation_name) - if mutation_name == "scope-row": + if mutation_name == "scope-count": + scope_rows.pop() + expected_message = "verified scope evidence is incomplete" + elif mutation_name == "duplicate-leg": + scope_rows[1]["leg"] = scope_rows[0]["leg"] + expected_message = "scope evidence row is malformed" + elif mutation_name == "scope-row": scope_rows[0]["leg"] = ".." expected_message = "scope evidence row is malformed" elif mutation_name == "runtime-set": @@ -927,6 +969,36 @@ def test_prescreen_refuses_malformed_scope_and_runtime_rows(tmp_path: Path) -> N prescreen(_scope_with_variants(scope_rows, scope_root), scope_root) +@pytest.mark.parametrize(("mutation_name", "expected_message"), [ + ("oversized", "runtime receipt is oversized"), + ("changed", "runtime receipt changed after transport"), + ("wrong-architecture", "runtime interpreter differs from required target architecture"), +]) +def test_prescreen_refuses_untrusted_runtime_receipt( + tmp_path: Path, mutation_name: str, expected_message: str, +) -> None: + """Runtime identity must stay bounded, transported-byte-bound, and target-correct.""" + scope_root, scope_rows = _prescreen_case(tmp_path) + scope = _scope_with_variants(scope_rows, scope_root) + scope_row = scope_rows[0] + runtime_name = f"{scope_row['leg']}.runtime.json" + runtime_path = scope_root / scope_row["artifact_name"] / runtime_name + if mutation_name == "oversized": + runtime_path.write_bytes(b"x" * (1024 * 1024 + 1)) + elif mutation_name == "changed": + scope_row["members"][runtime_name] = "0" * 64 + else: + runtime = json.loads(runtime_path.read_text()) + runtime["machine"] = "foreign" + runtime_path.write_text(json.dumps(runtime)) + scope_row["members"][runtime_name] = hashlib.sha256( + runtime_path.read_bytes() + ).hexdigest() + + with pytest.raises(gate.GateError, match=expected_message): + prescreen(scope, scope_root) + + def test_prescreen_coalesces_duplicate_archive_identity(tmp_path: Path) -> None: scope_root, scope_rows = _prescreen_case(tmp_path) first_row, second_row = scope_rows[:2] @@ -956,28 +1028,79 @@ def changed_license_evidence(archive_bytes: bytes, ecosystem_name: str) -> dict: prescreen(_scope_with_variants(scope_rows, scope_root), scope_root) +@pytest.mark.parametrize(("replacement_leg", "expected_message"), [ + ("extra-py3.12", "runtime archive coverage is incomplete"), + ( + "x86_64-unknown-linux-gnu-py3.15", + "runtime interpreter differs from required target architecture", + ), +]) def test_prescreen_refuses_complete_rows_without_sdist( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + replacement_leg: str, + expected_message: str, ) -> None: scope_root, scope_rows = _prescreen_case(tmp_path) sdist_row = scope_rows[-1] first_archive = dict(scope_rows[0]["archives"][0]) first_path = scope_root / scope_rows[0]["artifact_name"] / first_archive["file"] - sdist_row["leg"] = "extra-py3.12" - sdist_row["artifact_name"] = "repro-digest-extra-py3.12" + sdist_row["leg"] = replacement_leg + sdist_row["artifact_name"] = f"repro-digest-{replacement_leg}" sdist_row["archives"] = [first_archive] extra_folder = scope_root / sdist_row["artifact_name"] extra_folder.mkdir() (extra_folder / first_archive["file"]).write_bytes(first_path.read_bytes()) + if replacement_leg.endswith("py3.15"): + runtime_name = f"{sdist_row['leg']}.runtime.json" + runtime = json.loads( + (scope_root / scope_rows[0]["artifact_name"] + / f"{scope_rows[0]['leg']}.runtime.json").read_text() + ) + runtime.update(leg=sdist_row["leg"], python_version="3.15") + runtime_path = extra_folder / runtime_name + runtime_path.write_text(json.dumps(runtime)) + sdist_row["members"][runtime_name] = hashlib.sha256( + runtime_path.read_bytes() + ).hexdigest() monkeypatch.setattr(prescreen_module, "_build_packages", lambda item, folder: []) monkeypatch.setattr(prescreen_module, "_maturin_tool", lambda item, folder: { "key": "github-release/maturin@1.15.0/sha256/" + "a" * 64, "legs": [item["leg"]], "build_envs": {item["leg"]: "fixture"}, }) - with pytest.raises(gate.GateError, match="runtime archive coverage is incomplete"): + with pytest.raises(gate.GateError, match=expected_message): prescreen(_scope_with_variants(scope_rows, scope_root), scope_root) +@pytest.mark.parametrize("mutation_name", ["oversized", "digest", "architecture"]) +def test_prescreen_refuses_untrusted_runtime_receipts( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation_name: str, +) -> None: + """Runtime receipts are bounded, hash-bound, and structurally validated.""" + scope_root, scope_rows = _prescreen_case(tmp_path) + scope = _scope_with_variants(scope_rows, scope_root) + row = next(item for item in scope_rows if item["leg"] != "sdist") + runtime_name = f"{row['leg']}.runtime.json" + runtime_path = scope_root / row["artifact_name"] / runtime_name + if mutation_name == "oversized": + runtime_path.write_bytes(b"x" * (1024 * 1024 + 1)) + elif mutation_name == "digest": + row["members"][runtime_name] = "0" * 64 + else: + def invalid_architecture(*args, **kwargs): + del args, kwargs + raise DistributionSetError("untrusted architecture") + + monkeypatch.setattr(prescreen_module, "_runtime_target_architecture", invalid_architecture) + + with pytest.raises(gate.GateError, match={ + "oversized": "runtime receipt is oversized", + "digest": "runtime receipt changed after transport", + "architecture": "untrusted architecture", + }[mutation_name]): + prescreen(scope, scope_root) + + def test_prescreen_cli_writes_once_and_refuses_existing_output( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: