diff --git a/.github/workflows/context-graph-contracts-ci.yml b/.github/workflows/context-graph-contracts-ci.yml new file mode 100644 index 0000000000..8995d81153 --- /dev/null +++ b/.github/workflows/context-graph-contracts-ci.yml @@ -0,0 +1,1256 @@ +name: CGC central validation +on: + workflow_call: null +permissions: + contents: read +defaults: + run: + shell: bash +jobs: + test: + runs-on: + group: CWL CI isolated + labels: + - self-hosted + - Linux + - X64 + - cwlab-ci-isolated + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: + - '3.11' + - '3.12' + - '3.13' + - '3.14' + steps: + - name: Admit candidate before checkout + env: + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + if [[ "$REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" || ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "CGC repository or candidate SHA is not admitted" >&2 + exit 1 + fi + case "$EVENT_NAME" in + push) ;; + pull_request) + if [[ "$HEAD_REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" ]]; then + echo "fork or missing head repository is not admitted" >&2 + exit 1 + fi ;; + *) echo "unsupported candidate event" >&2; exit 1 ;; + esac + - name: Remove job-owned stale outputs before checkout + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + - name: Initialize private job state + id: private + run: | + set -euo pipefail + umask 077 + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_root="$(mktemp -d "$runner_root/cgc-ci.XXXXXXXX")" + chmod 700 "$private_root" + mkdir -m 700 "$private_root/home" "$private_root/config" + printf 'PRIVATE_ROOT=%s\nHOME=%s/home\nXDG_CONFIG_HOME=%s/config\nUV_CACHE_DIR=%s/cache\nUV_PROJECT_ENVIRONMENT=%s/venv\nTMPDIR=%s\nXDG_CACHE_HOME=%s/cache\nPYTHONPATH=\nPYTHONHOME=\nPYTHONOPTIMIZE=\nPYTHONNOUSERSITE=1\n' \ + "$private_root" "$private_root" "$private_root" "$private_root" \ + "$private_root" "$private_root" "$private_root" >> "$GITHUB_ENV" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - name: Verify exact source checkout + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + version: 0.11.32 + enable-cache: 'false' + - run: uv lock --check + - run: uv sync --frozen --extra dev --python "${{ matrix.python-version }}" + - run: uv run --frozen --extra dev --python "${{ matrix.python-version }}" python -m compileall -q src + - run: uv run --frozen --extra dev --python "${{ matrix.python-version }}" python -m ruff check . --output-format=github + - run: uv run --frozen --extra dev --python "${{ matrix.python-version }}" python -m coverage run -m pytest + -q tests .github/tests + - run: uv run --frozen --extra dev --python "${{ matrix.python-version }}" python -m coverage report --fail-under=100 + - name: Clean private job outputs + if: ${{ always() && steps.private.outcome == 'success' }} + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_name="${PRIVATE_ROOT##*/}" + if [[ ! "$private_name" =~ ^cgc-ci\.[A-Za-z0-9]{8}$ || + "$PRIVATE_ROOT" != "$runner_root/$private_name" || + -L "$PRIVATE_ROOT" || ! -d "$PRIVATE_ROOT" ]]; then + echo "private cleanup root is not an owned direct directory" >&2 + exit 1 + fi + rm -rf -- "$PRIVATE_ROOT" + package: + runs-on: + group: CWL CI isolated + labels: + - self-hosted + - Linux + - X64 + - cwlab-ci-isolated + timeout-minutes: 10 + steps: + - name: Admit candidate before checkout + env: + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + if [[ "$REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" || ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "CGC repository or candidate SHA is not admitted" >&2 + exit 1 + fi + case "$EVENT_NAME" in + push) ;; + pull_request) + if [[ "$HEAD_REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" ]]; then + echo "fork or missing head repository is not admitted" >&2 + exit 1 + fi ;; + *) echo "unsupported candidate event" >&2; exit 1 ;; + esac + - name: Remove job-owned stale outputs before checkout + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + - name: Initialize private job state + id: private + run: | + set -euo pipefail + umask 077 + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_root="$(mktemp -d "$runner_root/cgc-ci.XXXXXXXX")" + chmod 700 "$private_root" + mkdir -m 700 "$private_root/home" "$private_root/config" + printf 'PRIVATE_ROOT=%s\nHOME=%s/home\nXDG_CONFIG_HOME=%s/config\nUV_CACHE_DIR=%s/cache\nUV_PROJECT_ENVIRONMENT=%s/venv\nTMPDIR=%s\nXDG_CACHE_HOME=%s/cache\nPYTHONPATH=\nPYTHONHOME=\nPYTHONOPTIMIZE=\nPYTHONNOUSERSITE=1\n' \ + "$private_root" "$private_root" "$private_root" "$private_root" \ + "$private_root" "$private_root" "$private_root" >> "$GITHUB_ENV" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - name: Verify exact source checkout + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.14' + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + version: 0.11.32 + enable-cache: 'false' + - run: uv lock --check + - name: Capture dependency lock for checked-out commit + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: uv-lock-${{ github.event.pull_request.head.sha || github.sha }} + path: uv.lock + if-no-files-found: error + retention-days: '1' + if: ${{ success() }} + - run: uv sync --frozen --extra dev --python 3.14 + - run: uv build --wheel --sdist --out-dir dist + - name: Inspect wheel and source distribution contract resources + run: | + python - <<'PY' + from pathlib import Path + import tarfile + import zipfile + + wheel_path = next(Path("dist").glob("*.whl")) + sdist_path = next(Path("dist").glob("*.tar.gz")) + required_suffixes = { + "cwl_context_contracts/py.typed", + "cwl_context_contracts/contracts/context-fabric.asyncapi.json", + "cwl_context_contracts/conformance/cwl-timestamp-profile.v1.json", + "cwl_context_contracts/conformance/context-assertion-semantics.v1.json", + "cwl_context_contracts/conformance/context-assertion-event-semantics.v1.json", + "cwl_context_contracts/conformance/context-assertion-message-admission.v1.json", + "cwl_context_contracts/conformance/cloudevent-semantics.v1.json", + "cwl_context_contracts/conformance/cwl-json-interoperability.v1.json", + "cwl_context_contracts/conformance/data-management-assessment-semantics.v1.json", + "cwl_context_contracts/conformance/external-metadata-observation-semantics.v1.json", + "cwl_context_contracts/schemas/canonical-authority-uri.schema.json", + "cwl_context_contracts/schemas/canonical-asset-uri.schema.json", + "cwl_context_contracts/schemas/truth-status.schema.json", + "cwl_context_contracts/schemas/bitemporal-interval.schema.json", + "cwl_context_contracts/schemas/provenance-reference.schema.json", + "cwl_context_contracts/schemas/cloudevent-envelope.schema.json", + "cwl_context_contracts/schemas/context-membership.schema.json", + "cwl_context_contracts/schemas/context-assertion.schema.json", + "cwl_context_contracts/schemas/data-management-framework.schema.json", + "cwl_context_contracts/schemas/data-management-assessment.schema.json", + "cwl_context_contracts/schemas/external-metadata-source.schema.json", + "cwl_context_contracts/schemas/external-entity-reference.schema.json", + "cwl_context_contracts/schemas/metadata-observation-envelope.schema.json", + "cwl_context_contracts/schemas/metadata-projection-receipt.schema.json", + "cwl_context_contracts/fixtures/valid-event.json", + "cwl_context_contracts/fixtures/invalid-event.json", + "cwl_context_contracts/fixtures/valid-assertion.json", + "cwl_context_contracts/fixtures/invalid-assertion.json", + "cwl_context_contracts/fixtures/data-management-contract.valid.json", + "cwl_context_contracts/fixtures/data-management-assessment.valid.json", + "cwl_context_contracts/fixtures/metadata-observation-envelope.valid.json", + "cwl_context_contracts/fixtures/metadata-observation-envelope.invalid.json", + "cwl_context_contracts/fixtures/metadata-projection-receipt.valid.json", + } + + with zipfile.ZipFile(wheel_path) as wheel_file: + wheel_names = set(wheel_file.namelist()) + for suffix in required_suffixes: + assert any(name.endswith(suffix) for name in wheel_names), suffix + assert any(name.endswith("/licenses/LICENSE") for name in wheel_names) + + with tarfile.open(sdist_path, "r:gz") as sdist_file: + sdist_names = set(sdist_file.getnames()) + for suffix in required_suffixes: + assert any(name.endswith(suffix) for name in sdist_names), suffix + assert any(name.endswith("/LICENSE") for name in sdist_names) + PY + - run: uv venv .package-smoke --python 3.14 + - run: uv pip install --python .package-smoke/bin/python --no-index --find-links dist cwl-context-contracts + - name: Smoke-test installed contracts, schemas, and conformance fixtures + run: | + .package-smoke/bin/python -I - <<'PY' + from cwl_context_contracts import ( + CloudEventEnvelope, + ContextAssertion, + TruthStatus, + available_conformance_profile_names, + available_contract_names, + available_fixture_names, + available_schema_names, + load_conformance_profile, + load_contract, + load_fixture, + load_schema, + parse_cwl_timestamp, + admit_metadata_observation, + validate_data_management_assessment_semantics, + ) + + assert TruthStatus.OBSERVED.value == "observed" + for schema_name in available_schema_names(): + schema = load_schema(schema_name) + assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert schema["$id"].startswith("https://schemas.contextualwisdomlab.org/") + + assert available_contract_names() == ("context-fabric.asyncapi.json",) + asyncapi = load_contract("context-fabric.asyncapi.json") + assert asyncapi["asyncapi"] == "3.1.0" + assert "servers" not in asyncapi + assert "channels" not in asyncapi + assert "operations" not in asyncapi + + assert available_conformance_profile_names() == ( + "cwl-timestamp-profile.v1.json", + "context-assertion-semantics.v1.json", + "context-assertion-event-semantics.v1.json", + "context-assertion-message-admission.v1.json", + "cloudevent-semantics.v1.json", + "cwl-json-interoperability.v1.json", + "data-management-assessment-semantics.v1.json", + "external-metadata-observation-semantics.v1.json", + ) + timestamp_profile = load_conformance_profile( + "cwl-timestamp-profile.v1.json" + ) + for timestamp_value in timestamp_profile["valid_values"]: + parse_cwl_timestamp(timestamp_value) + for timestamp_value in timestamp_profile["invalid_values"]: + try: + parse_cwl_timestamp(timestamp_value) + except ValueError: + pass + else: + raise AssertionError( + f"negative timestamp vector unexpectedly parsed: {timestamp_value}" + ) + + assertion_profile = load_conformance_profile( + "context-assertion-semantics.v1.json" + ) + for vector in assertion_profile["invalid_vectors"]: + try: + ContextAssertion.from_mapping(vector["value"]) + except ValueError as exc: + assert vector["error_pattern"] in str(exc) + else: + raise AssertionError( + f"negative assertion vector unexpectedly parsed: {vector['case_id']}" + ) + + assertion_event_profile = load_conformance_profile( + "context-assertion-event-semantics.v1.json" + ) + for vector in assertion_event_profile["valid_vectors"]: + event = CloudEventEnvelope.from_mapping(vector["value"]) + assertion = ContextAssertion.from_event(event) + assert assertion.to_mapping() == vector["value"]["data"] + for vector in assertion_event_profile["invalid_vectors"]: + try: + event = CloudEventEnvelope.from_mapping(vector["value"]) + ContextAssertion.from_event(event) + except ValueError as exc: + assert vector["error_pattern"] in str(exc) + else: + raise AssertionError( + "negative assertion-event vector unexpectedly admitted: " + f"{vector['case_id']}" + ) + + event_profile = load_conformance_profile("cloudevent-semantics.v1.json") + for vector in event_profile["invalid_vectors"]: + try: + CloudEventEnvelope.from_mapping(vector["value"]) + except ValueError as exc: + assert vector["error_pattern"] in str(exc) + else: + raise AssertionError( + f"negative event vector unexpectedly parsed: {vector['case_id']}" + ) + for vector in event_profile["valid_vectors"]: + assert CloudEventEnvelope.from_mapping(vector["value"]).to_mapping() == ( + vector["value"] + ) + + assessment_profile = load_conformance_profile( + "data-management-assessment-semantics.v1.json" + ) + for vector in assessment_profile["valid_vectors"]: + validate_data_management_assessment_semantics(vector["value"]) + for vector in assessment_profile["invalid_vectors"]: + try: + validate_data_management_assessment_semantics(vector["value"]) + except ValueError as exc: + assert vector["error_pattern"] in str(exc) + else: + raise AssertionError( + "negative assessment vector unexpectedly validated: " + f"{vector['case_id']}" + ) + + metadata_profile = load_conformance_profile( + "external-metadata-observation-semantics.v1.json" + ) + for vector in metadata_profile["valid_vectors"]: + receipt = admit_metadata_observation(vector["value"]) + assert receipt.result.value == "admitted", vector["case_id"] + assert receipt.reason_code == vector["expected_reason_code"] + for vector in metadata_profile["rejected_vectors"]: + receipt = admit_metadata_observation(vector["value"]) + assert receipt.result.value == "rejected", vector["case_id"] + assert receipt.reason_code == vector["expected_reason_code"] + assert admit_metadata_observation( + load_fixture("metadata-observation-envelope.valid.json") + ).to_mapping() == load_fixture("metadata-projection-receipt.valid.json") + + assert available_fixture_names() == ( + "valid-event.json", + "invalid-event.json", + "valid-assertion.json", + "invalid-assertion.json", + "data-management-contract.valid.json", + "data-management-assessment.valid.json", + "metadata-observation-envelope.valid.json", + "metadata-observation-envelope.invalid.json", + "metadata-projection-receipt.valid.json", + ) + valid_event = load_fixture("valid-event.json") + invalid_event = load_fixture("invalid-event.json") + assert CloudEventEnvelope.from_mapping(valid_event).to_mapping() == valid_event + try: + CloudEventEnvelope.from_mapping(invalid_event) + except ValueError as exc: + assert "UUIDv7" in str(exc) or "RFC 9562" in str(exc) + else: + raise AssertionError("negative conformance fixture unexpectedly parsed") + + json_profile = load_conformance_profile( + "cwl-json-interoperability.v1.json" + ) + for integer_value in json_profile["valid_integer_values"]: + event_mapping = { + **valid_event, + "data": {"nested": [{"count": integer_value}]}, + } + assert ( + CloudEventEnvelope.from_mapping(event_mapping) + .to_mapping()["data"]["nested"][0]["count"] + == integer_value + ) + for integer_value in json_profile["invalid_integer_values"]: + event_mapping = { + **valid_event, + "data": {"nested": [{"count": integer_value}]}, + } + try: + CloudEventEnvelope.from_mapping(event_mapping) + except ValueError as exc: + assert "exact interoperable range" in str(exc) + else: + raise AssertionError( + f"out-of-range integer unexpectedly parsed: {integer_value}" + ) + + valid_assertion = load_fixture("valid-assertion.json") + invalid_assertion = load_fixture("invalid-assertion.json") + assert ContextAssertion.from_mapping(valid_assertion).to_mapping() == ( + valid_assertion + ) + try: + ContextAssertion.from_mapping(invalid_assertion) + except ValueError as exc: + assert "provenance" in str(exc) + else: + raise AssertionError("negative assertion fixture unexpectedly parsed") + PY + - name: Smoke-test installed conformance evidence commands + run: | + set -euo pipefail + umask 077 + export SMOKE_ROOT + SMOKE_ROOT="$(mktemp -d "${RUNNER_TEMP:?}/contracts-smoke.XXXXXX")" + trap 'rm -rf -- "$SMOKE_ROOT"' EXIT + .package-smoke/bin/python -I .package-smoke/bin/cwl-context-conformance > "$SMOKE_ROOT/conformance-report.json" + .package-smoke/bin/python -I .package-smoke/bin/cwl-context-conformance-manifest > "$SMOKE_ROOT/approved-manifest.json" + .package-smoke/bin/python -I .package-smoke/bin/cwl-context-conformance-verify \ + "$SMOKE_ROOT/approved-manifest.json" > "$SMOKE_ROOT/verification-report.json" + .package-smoke/bin/python -I .package-smoke/bin/cwl-context-conformance-admit \ + "$SMOKE_ROOT/approved-manifest.json" > "$SMOKE_ROOT/admission-report.json" + .package-smoke/bin/python -I - <<'PY' + import json + import os + from pathlib import Path + + conformance = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "conformance-report.json").read_text(encoding="utf-8") + ) + verification = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "verification-report.json").read_text(encoding="utf-8") + ) + admission = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "admission-report.json").read_text(encoding="utf-8") + ) + assert conformance["status"] == "pass" + assert conformance["failures"] == [] + assert verification["verified"] is True + assert verification["mismatches"] == [] + assert verification["next_action"] == ( + "accept the installed conformance evidence" + ) + assert admission["admitted"] is True + assert admission["semantic_conformance"]["status"] == "pass" + assert admission["manifest_verification"]["verified"] is True + assert admission["next_action"] == ( + "verify artifact provenance and runtime authorization before enabling " + "the integration" + ) + PY + - name: Clean private job outputs + if: ${{ always() && steps.private.outcome == 'success' }} + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_name="${PRIVATE_ROOT##*/}" + if [[ ! "$private_name" =~ ^cgc-ci\.[A-Za-z0-9]{8}$ || + "$PRIVATE_ROOT" != "$runner_root/$private_name" || + -L "$PRIVATE_ROOT" || ! -d "$PRIVATE_ROOT" ]]; then + echo "private cleanup root is not an owned direct directory" >&2 + exit 1 + fi + rm -rf -- "$PRIVATE_ROOT" + release-package-reproducibility: + runs-on: + group: CWL CI isolated + labels: + - self-hosted + - Linux + - X64 + - cwlab-ci-isolated + timeout-minutes: 10 + steps: + - name: Admit candidate before checkout + env: + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + if [[ "$REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" || ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "CGC repository or candidate SHA is not admitted" >&2 + exit 1 + fi + case "$EVENT_NAME" in + push) ;; + pull_request) + if [[ "$HEAD_REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" ]]; then + echo "fork or missing head repository is not admitted" >&2 + exit 1 + fi ;; + *) echo "unsupported candidate event" >&2; exit 1 ;; + esac + - name: Remove job-owned stale outputs before checkout + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + - name: Initialize private job state + id: private + run: | + set -euo pipefail + umask 077 + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_root="$(mktemp -d "$runner_root/cgc-ci.XXXXXXXX")" + chmod 700 "$private_root" + mkdir -m 700 "$private_root/home" "$private_root/config" + printf 'PRIVATE_ROOT=%s\nHOME=%s/home\nXDG_CONFIG_HOME=%s/config\nUV_CACHE_DIR=%s/cache\nUV_PROJECT_ENVIRONMENT=%s/venv\nTMPDIR=%s\nXDG_CACHE_HOME=%s/cache\nPYTHONPATH=\nPYTHONHOME=\nPYTHONOPTIMIZE=\nPYTHONNOUSERSITE=1\n' \ + "$private_root" "$private_root" "$private_root" "$private_root" \ + "$private_root" "$private_root" "$private_root" >> "$GITHUB_ENV" + - name: Checkout exact candidate into first clean tree + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + path: source-a + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - name: Checkout exact candidate into second clean tree + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + path: source-b + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.14' + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + version: 0.11.32 + enable-cache: 'false' + - name: Bind both clean trees to the exact source commit + id: source + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + first_sha="$(git -C source-a rev-parse HEAD)" + second_sha="$(git -C source-b rev-parse HEAD)" + if [[ "$first_sha" != "$EXPECTED_SHA" || "$second_sha" != "$EXPECTED_SHA" ]]; then + echo "clean build trees do not match the exact source SHA" >&2 + exit 1 + fi + source_date_epoch="$(git -C source-a show -s --format=%ct HEAD)" + if [[ ! "$source_date_epoch" =~ ^[0-9]+$ ]]; then + echo "source commit timestamp is not an integer epoch" >&2 + exit 1 + fi + echo "source_date_epoch=$source_date_epoch" >> "$GITHUB_OUTPUT" + - name: Verify both dependency locks + run: | + (cd source-a && uv lock --check) + (cd source-b && uv lock --check) + - name: Initialize fresh reproducibility output directories + run: | + cd "${GITHUB_WORKSPACE:?}" + rm -rf -- build-a build-b reproducibility-evidence + mkdir -- build-a build-b reproducibility-evidence + - name: Build release packages from first clean tree + env: + SOURCE_DATE_EPOCH: ${{ steps.source.outputs.source_date_epoch }} + run: | + (cd source-a && uv build --wheel --sdist --out-dir ../build-a) + - name: Build release packages from second clean tree + env: + SOURCE_DATE_EPOCH: ${{ steps.source.outputs.source_date_epoch }} + run: | + (cd source-b && uv build --wheel --sdist --out-dir ../build-b) + - name: Verify byte-reproducible release packages + run: | + python source-a/.github/scripts/verify_reproducible_package_builds.py \ + build-a build-b \ + | tee reproducibility-evidence/verification.txt + ( + cd build-a + sha256sum -- *.whl *.tar.gz + ) > reproducibility-evidence/SHA256SUMS + - name: Retain exact-candidate reproducibility evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: reproducibility-${{ github.event.pull_request.head.sha || github.sha }} + path: reproducibility-evidence + if-no-files-found: error + retention-days: '7' + if: ${{ success() }} + - name: Clean private job outputs + if: ${{ always() && steps.private.outcome == 'success' }} + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_name="${PRIVATE_ROOT##*/}" + if [[ ! "$private_name" =~ ^cgc-ci\.[A-Za-z0-9]{8}$ || + "$PRIVATE_ROOT" != "$runner_root/$private_name" || + -L "$PRIVATE_ROOT" || ! -d "$PRIVATE_ROOT" ]]; then + echo "private cleanup root is not an owned direct directory" >&2 + exit 1 + fi + rm -rf -- "$PRIVATE_ROOT" + package-evidence: + outputs: + package-snapshot: ${{ steps.package-evidence-verification.outputs.package_snapshot }} + runs-on: + group: CWL CI isolated + labels: + - self-hosted + - Linux + - X64 + - cwlab-ci-isolated + timeout-minutes: 10 + steps: + - name: Admit candidate before checkout + env: + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + if [[ "$REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" || ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "CGC repository or candidate SHA is not admitted" >&2 + exit 1 + fi + case "$EVENT_NAME" in + push) ;; + pull_request) + if [[ "$HEAD_REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" ]]; then + echo "fork or missing head repository is not admitted" >&2 + exit 1 + fi ;; + *) echo "unsupported candidate event" >&2; exit 1 ;; + esac + - name: Remove job-owned stale outputs before checkout + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + - name: Initialize private job state + id: private + run: | + set -euo pipefail + umask 077 + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_root="$(mktemp -d "$runner_root/cgc-ci.XXXXXXXX")" + chmod 700 "$private_root" + mkdir -m 700 "$private_root/home" "$private_root/config" + printf 'PRIVATE_ROOT=%s\nHOME=%s/home\nXDG_CONFIG_HOME=%s/config\nUV_CACHE_DIR=%s/cache\nUV_PROJECT_ENVIRONMENT=%s/venv\nTMPDIR=%s\nXDG_CACHE_HOME=%s/cache\nPYTHONPATH=\nPYTHONHOME=\nPYTHONOPTIMIZE=\nPYTHONNOUSERSITE=1\n' \ + "$private_root" "$private_root" "$private_root" "$private_root" \ + "$private_root" "$private_root" "$private_root" >> "$GITHUB_ENV" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - name: Verify exact candidate checkout + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.14' + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + version: 0.11.32 + enable-cache: 'false' + - run: uv lock --check + - name: Bind release builds to the exact source commit + id: source + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + source_sha="$(git rev-parse HEAD)" + if [[ "$source_sha" != "$EXPECTED_SHA" ]]; then + echo "package build tree does not match the exact source SHA" >&2 + exit 1 + fi + source_date_epoch="$(git show -s --format=%ct HEAD)" + if [[ ! "$source_date_epoch" =~ ^[0-9]+$ ]]; then + echo "source commit timestamp is not an integer epoch" >&2 + exit 1 + fi + echo "source_date_epoch=$source_date_epoch" >> "$GITHUB_OUTPUT" + - name: Build release packages for package evidence + env: + SOURCE_DATE_EPOCH: ${{ steps.source.outputs.source_date_epoch }} + run: uv build --wheel --sdist --out-dir dist + - name: Checkout exact candidate for reproducibility witness + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + path: reproducibility-source + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - name: Verify witness checkout and dependency lock + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + witness_sha="$(git -C reproducibility-source rev-parse HEAD)" + if [[ "$witness_sha" != "$EXPECTED_SHA" ]]; then + echo "reproducibility witness does not match the exact source SHA" >&2 + exit 1 + fi + (cd reproducibility-source && uv lock --check) + - name: Build independent reproducibility witness + env: + SOURCE_DATE_EPOCH: ${{ steps.source.outputs.source_date_epoch }} + run: | + (cd reproducibility-source && uv build --wheel --sdist --out-dir ../reproducibility-build) + - name: Bind package-evidence bytes to reproducibility proof + run: | + mkdir package-reproducibility-evidence + python .github/scripts/verify_reproducible_package_builds.py dist reproducibility-build \ + | tee package-reproducibility-evidence/verification.txt + ( + cd dist + sha256sum -- *.whl *.tar.gz + ) > package-reproducibility-evidence/package-evidence-SHA256SUMS + ( + cd reproducibility-build + sha256sum -- *.whl *.tar.gz + ) > package-reproducibility-evidence/witness-SHA256SUMS + - name: Retain package-evidence reproducibility proof + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: package-reproducibility-${{ github.event.pull_request.head.sha || github.sha }} + path: package-reproducibility-evidence + if-no-files-found: error + retention-days: '7' + if: ${{ success() }} + - name: Install the built wheel into an isolated SBOM root + id: sbom-root + run: | + uv venv .sbom-venv --python 3.14 + uv pip install --python .sbom-venv/bin/python --no-deps dist/*.whl + echo "path=$(.sbom-venv/bin/python - <<'PY' + import sysconfig + print(sysconfig.get_paths()["purelib"]) + PY + )" >> "$GITHUB_OUTPUT" + - name: Generate SPDX 3 JSON SBOM from installed package metadata + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 + with: + path: ${{ steps.sbom-root.outputs.path }} + format: spdx-json@3.0 + output-file: dist/cwl-context-contracts.spdx.json + upload-artifact: 'false' + upload-release-assets: 'false' + dependency-snapshot: 'false' + syft-version: v1.51.0 + - name: Validate SPDX 3.0.1 SBOM and artifact checksums + run: | + python - <<'PY' + import json + from pathlib import Path + + sbom_path = Path("dist/cwl-context-contracts.spdx.json") + sbom = json.loads(sbom_path.read_text(encoding="utf-8")) + assert sbom.get("@context") == "https://spdx.org/rdf/3.0.1/spdx-context.jsonld" + graph = sbom.get("@graph") + assert isinstance(graph, list) and graph + assert any( + item.get("type") == "CreationInfo" + and item.get("specVersion") == "3.0.1" + for item in graph + if isinstance(item, dict) + ) + assert any( + item.get("type") == "software_Package" + and item.get("name") == "cwl-context-contracts" + for item in graph + if isinstance(item, dict) + ) + wheels = list(Path("dist").glob("cwl_context_contracts-*.whl")) + sdists = list(Path("dist").glob("cwl_context_contracts-*.tar.gz")) + assert len(wheels) == 1 + assert len(sdists) == 1 + PY + ( + cd dist + sha256sum -- *.whl *.tar.gz cwl-context-contracts.spdx.json > SHA256SUMS + sha256sum -c SHA256SUMS + ) + - name: Verify package evidence with the release tool + id: package-evidence-verification + run: | + uv run --frozen cwl-context-package-evidence-verify dist \ + > dist/package-evidence-verification.json + package_snapshot="$(python - <<'PY' + import json + from pathlib import Path + + report = json.loads( + Path("dist/package-evidence-verification.json").read_text(encoding="utf-8") + ) + print(json.dumps(report, sort_keys=True, separators=(",", ":"))) + PY + )" + echo "package_snapshot=$package_snapshot" >> "$GITHUB_OUTPUT" + - name: Upload checked-out commit package evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: package-evidence-${{ github.event.pull_request.head.sha || github.sha }} + path: | + dist/*.whl + dist/*.tar.gz + dist/cwl-context-contracts.spdx.json + dist/SHA256SUMS + dist/package-evidence-verification.json + if-no-files-found: error + retention-days: '7' + if: ${{ success() }} + - name: Clean private job outputs + if: ${{ always() && steps.private.outcome == 'success' }} + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_name="${PRIVATE_ROOT##*/}" + if [[ ! "$private_name" =~ ^cgc-ci\.[A-Za-z0-9]{8}$ || + "$PRIVATE_ROOT" != "$runner_root/$private_name" || + -L "$PRIVATE_ROOT" || ! -d "$PRIVATE_ROOT" ]]; then + echo "private cleanup root is not an owned direct directory" >&2 + exit 1 + fi + rm -rf -- "$PRIVATE_ROOT" + installed-wheel: + runs-on: + group: CWL CI isolated + labels: + - self-hosted + - Linux + - X64 + - cwlab-ci-isolated + timeout-minutes: 10 + steps: + - name: Admit candidate before checkout + env: + EVENT_NAME: ${{ github.event_name }} + REPOSITORY: ${{ github.repository }} + HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + if [[ "$REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" || ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "CGC repository or candidate SHA is not admitted" >&2 + exit 1 + fi + case "$EVENT_NAME" in + push) ;; + pull_request) + if [[ "$HEAD_REPOSITORY" != "ContextualWisdomLab/context-graph-contracts" ]]; then + echo "fork or missing head repository is not admitted" >&2 + exit 1 + fi ;; + *) echo "unsupported candidate event" >&2; exit 1 ;; + esac + - name: Remove job-owned stale outputs before checkout + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + - name: Initialize private job state + id: private + run: | + set -euo pipefail + umask 077 + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_root="$(mktemp -d "$runner_root/cgc-ci.XXXXXXXX")" + chmod 700 "$private_root" + mkdir -m 700 "$private_root/home" "$private_root/config" + printf 'PRIVATE_ROOT=%s\nHOME=%s/home\nXDG_CONFIG_HOME=%s/config\nUV_CACHE_DIR=%s/cache\nUV_PROJECT_ENVIRONMENT=%s/venv\nTMPDIR=%s\nXDG_CACHE_HOME=%s/cache\nPYTHONPATH=\nPYTHONHOME=\nPYTHONOPTIMIZE=\nPYTHONNOUSERSITE=1\n' \ + "$private_root" "$private_root" "$private_root" "$private_root" \ + "$private_root" "$private_root" "$private_root" >> "$GITHUB_ENV" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: 'false' + repository: ContextualWisdomLab/context-graph-contracts + - name: Verify exact candidate checkout + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - name: Verify exact source checkout + env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$EXPECTED_SHA" + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.14' + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b + with: + version: 0.11.32 + enable-cache: 'false' + - run: uv lock --check + - run: uv sync --frozen --extra dev --python 3.14 + - run: uv build --wheel --sdist --out-dir dist + - run: uv venv .receipt-smoke --python 3.14 + - run: uv pip install --python .receipt-smoke/bin/python --no-index --find-links dist cwl-context-contracts + - name: Exercise Context Assertion admission SDK from installed wheel + run: | + .receipt-smoke/bin/python -I - <<'PY' + from cwl_context_contracts import ( + CONTEXT_ASSERTION_STRUCTURED_MEDIA_TYPE, + ContextAssertionAdmission, + admit_context_assertion_message, + load_conformance_profile, + ) + + profile = load_conformance_profile( + "context-assertion-event-semantics.v1.json" + ) + event_mapping = profile["valid_vectors"][0]["value"] + admission = admit_context_assertion_message( + CONTEXT_ASSERTION_STRUCTURED_MEDIA_TYPE, + event_mapping, + ) + assert type(admission) is ContextAssertionAdmission + assert admission.envelope.to_mapping() == event_mapping + assert admission.assertion.to_mapping() == event_mapping["data"] + assert admission.schema_version == 1 + assert admission.profile_id == ( + "urn:cwl:context-contracts:context-assertion-event-semantics:v1" + ) + assert admission.profile_version == 1 + assert admission.message_profile_id == ( + "urn:cwl:context-contracts:context-assertion-message-admission:v1" + ) + assert admission.message_profile_version == 1 + assert admission.admission_version == 1 + + try: + ContextAssertionAdmission( + envelope=admission.envelope, + assertion=admission.assertion, + ) + except ValueError as error: + assert str(error) == ( + "Context Assertion receipt must come from structured-message admission" + ) + else: + raise AssertionError( + "Context Assertion receipt was minted without transport admission" + ) + + hostile_media_types = ( + "\r\napplication/cloudevents+json", + "application/cloudevents+json\r\n", + "\vapplication/cloudevents+json", + "application/cloudevents+json\f", + ) + for media_type in hostile_media_types: + try: + admit_context_assertion_message(media_type, event_mapping) + except ValueError: + pass + else: + raise AssertionError( + "hostile structured media type unexpectedly admitted: " + f"{media_type!r}" + ) + + oversized_media_type = CONTEXT_ASSERTION_STRUCTURED_MEDIA_TYPE + (" " * 257) + try: + admit_context_assertion_message(oversized_media_type, event_mapping) + except ValueError as error: + assert str(error) == ( + "Context Assertion media type must not exceed 256 characters" + ) + else: + raise AssertionError("oversized structured media type unexpectedly admitted") + PY + - name: Exercise release evidence from installed wheel + run: | + set -euo pipefail + umask 077 + export SMOKE_ROOT + SMOKE_ROOT="$(mktemp -d "${RUNNER_TEMP:?}/contracts-smoke.XXXXXX")" + trap 'rm -rf -- "$SMOKE_ROOT"' EXIT + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-conformance-manifest \ + > "$SMOKE_ROOT/approved-manifest.json" + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-conformance-receipt \ + "$SMOKE_ROOT/approved-manifest.json" > "$SMOKE_ROOT/admission-receipt.json" + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-bundle-manifest \ + > "$SMOKE_ROOT/contract-bundle-manifest.json" + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-bundle-verify \ + "$SMOKE_ROOT/contract-bundle-manifest.json" \ + > "$SMOKE_ROOT/contract-bundle-verification.json" + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-release-admit \ + "$SMOKE_ROOT/approved-manifest.json" \ + "$SMOKE_ROOT/contract-bundle-manifest.json" \ + > "$SMOKE_ROOT/contract-release-admission.json" + mkdir -p "$SMOKE_ROOT/package-evidence" + cp dist/*.whl dist/*.tar.gz "$SMOKE_ROOT/package-evidence/" + package_version="$( + .receipt-smoke/bin/python -I -c \ + 'from importlib.metadata import version; print(version("cwl-context-contracts"))' + )" + cat > "$SMOKE_ROOT/package-evidence/cwl-context-contracts.spdx.json" < SHA256SUMS + ) + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-package-evidence-verify \ + "$SMOKE_ROOT/package-evidence" \ + > "$SMOKE_ROOT/package-evidence-verification.json" + .receipt-smoke/bin/python -I .receipt-smoke/bin/cwl-context-release-evidence-admit \ + "$SMOKE_ROOT/package-evidence" \ + "$SMOKE_ROOT/approved-manifest.json" \ + "$SMOKE_ROOT/contract-bundle-manifest.json" \ + > "$SMOKE_ROOT/complete-release-evidence-admission.json" + .receipt-smoke/bin/python -I - <<'PY' + import json + import os + from pathlib import Path + + receipt = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "admission-receipt.json").read_text(encoding="utf-8") + ) + bundle_manifest = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "contract-bundle-manifest.json").read_text(encoding="utf-8") + ) + bundle_verification = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "contract-bundle-verification.json").read_text( + encoding="utf-8" + ) + ) + release_admission = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "contract-release-admission.json").read_text( + encoding="utf-8" + ) + ) + package_evidence = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "package-evidence-verification.json").read_text( + encoding="utf-8" + ) + ) + complete_release_evidence = json.loads( + (Path(os.environ["SMOKE_ROOT"]) / "complete-release-evidence-admission.json").read_text( + encoding="utf-8" + ) + ) + assert receipt["receipt_format"] == ( + "cwl-context-conformance-admission-receipt/v1" + ) + assert receipt["canonicalization"] == "RFC8785" + assert receipt["manifest_normalization"] == "profile_name_ascending" + assert receipt["digest_algorithm"] == "sha256" + assert receipt["admitted"] is True + assert receipt["installed_distribution_name"] == "cwl-context-contracts" + assert len(receipt["approved_manifest_canonical_sha256"]) == 64 + assert len(receipt["admission_evidence_sha256"]) == 64 + assert receipt["next_action"] == ( + "verify artifact provenance and runtime authorization before enabling " + "the integration" + ) + assert bundle_manifest["manifest_format"] == ( + "cwl-context-bundle-manifest/v1" + ) + assert bundle_manifest["distribution_name"] == "cwl-context-contracts" + assert bundle_manifest["algorithm"] == "sha256" + assert bundle_manifest["resource_count"] == len(bundle_manifest["resources"]) + resource_paths = { + item["resource_path"] for item in bundle_manifest["resources"] + } + assert { + "schemas/data-management-framework.schema.json", + "schemas/data-management-assessment.schema.json", + "fixtures/data-management-contract.valid.json", + "fixtures/data-management-assessment.valid.json", + "conformance/data-management-assessment-semantics.v1.json", + "schemas/metadata-observation-envelope.schema.json", + "schemas/metadata-projection-receipt.schema.json", + "fixtures/metadata-observation-envelope.valid.json", + "conformance/external-metadata-observation-semantics.v1.json", + } <= resource_paths + assert bundle_manifest["resources"] == sorted( + bundle_manifest["resources"], + key=lambda item: item["resource_path"], + ) + assert all(len(item["sha256"]) == 64 for item in bundle_manifest["resources"]) + assert bundle_manifest["next_action"] == ( + "store this manifest with approved release evidence and verify semantic " + "conformance, package provenance, and runtime authorization before " + "enabling the integration" + ) + assert bundle_verification == { + "verification_format": "cwl-context-bundle-verification/v1", + "verified": True, + "installed_distribution_name": "cwl-context-contracts", + "installed_distribution_version": bundle_manifest[ + "distribution_version" + ], + "mismatches": [], + "next_action": "accept the installed contract bundle evidence", + } + assert release_admission["admission_format"] == ( + "cwl-context-contract-release-admission/v1" + ) + assert release_admission["admitted"] is True + assert release_admission["conformance_admission"]["admitted"] is True + assert release_admission["bundle_verification"]["verified"] is True + assert release_admission["next_action"] == ( + "obtain qualifying independent approval and verify artifact provenance, " + "protected-release evidence, and runtime authorization before enabling " + "the integration" + ) + assert package_evidence["verification_format"] == ( + "cwl-context-package-evidence-verification/v1" + ) + assert package_evidence["verified"] is True + assert package_evidence["mismatches"] == [] + assert len(package_evidence["artifacts"]) == 3 + assert package_evidence["next_action"] == ( + "verify artifact attestations bind these exact package bytes to the " + "intended protected main source commit before release" + ) + assert complete_release_evidence["admission_format"] == ( + "cwl-context-complete-release-evidence-admission/v1" + ) + assert complete_release_evidence["admitted"] is True + assert complete_release_evidence["installed_distribution_version"] == ( + bundle_manifest["distribution_version"] + ) + assert complete_release_evidence["package_distribution_version"] == ( + bundle_manifest["distribution_version"] + ) + assert complete_release_evidence["release_mismatches"] == [] + assert complete_release_evidence["contract_release_admission"][ + "admitted" + ] is True + assert complete_release_evidence["package_evidence_verification"][ + "verified" + ] is True + assert complete_release_evidence["next_action"] == ( + "verify artifact attestations bind these exact package bytes to the " + "intended protected main source commit, generate and independently verify " + "the attested release-source manifest, then satisfy independent review " + "and release authorization before publication" + ) + PY + - name: Clean private job outputs + if: ${{ always() && steps.private.outcome == 'success' }} + run: | + set -euo pipefail + cd "${GITHUB_WORKSPACE:?}" + # Delete only job-owned outputs, never a runner parent or another job's state. + rm -rf -- dist .package-smoke .receipt-smoke .sbom-venv source-a source-b \ + build-a build-b reproducibility-evidence reproducibility-source \ + reproducibility-build package-reproducibility-evidence + runner_root="$(cd -- "${RUNNER_TEMP:?}" && pwd -P)" + private_name="${PRIVATE_ROOT##*/}" + if [[ ! "$private_name" =~ ^cgc-ci\.[A-Za-z0-9]{8}$ || + "$PRIVATE_ROOT" != "$runner_root/$private_name" || + -L "$PRIVATE_ROOT" || ! -d "$PRIVATE_ROOT" ]]; then + echo "private cleanup root is not an owned direct directory" >&2 + exit 1 + fi + rm -rf -- "$PRIVATE_ROOT" + + required: + name: CGC required validation + if: ${{ always() }} + needs: [test, package, installed-wheel, release-package-reproducibility, package-evidence] + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + timeout-minutes: 2 + steps: + - name: Require every producer gate to succeed + env: + TEST_RESULT: ${{ needs.test.result }} + PACKAGE_RESULT: ${{ needs.package.result }} + INSTALLED_WHEEL_RESULT: ${{ needs.installed-wheel.result }} + RELEASE_PACKAGE_REPRODUCIBILITY_RESULT: ${{ needs.release-package-reproducibility.result }} + PACKAGE_EVIDENCE_RESULT: ${{ needs.package-evidence.result }} + run: | + set -euo pipefail + for result in "$TEST_RESULT" "$PACKAGE_RESULT" "$INSTALLED_WHEEL_RESULT" \ + "$RELEASE_PACKAGE_REPRODUCIBILITY_RESULT" "$PACKAGE_EVIDENCE_RESULT"; do + if [[ "$result" != "success" ]]; then + echo "a required CGC producer gate did not succeed" >&2 + exit 1 + fi + done diff --git a/docs/doctoring/context-graph-contracts-central-ci.md b/docs/doctoring/context-graph-contracts-central-ci.md new file mode 100644 index 0000000000..6f8dd710cc --- /dev/null +++ b/docs/doctoring/context-graph-contracts-central-ci.md @@ -0,0 +1,33 @@ +# CGC central CI producer — source and activation boundary + +The reusable workflow is `.github/workflows/context-graph-contracts-ci.yml` in the organization `.github` repository. It is **only** `workflow_call`; this source staging neither calls it from ContextualWisdomLab/context-graph-contracts nor enables an organization required-workflow ruleset, provisions a runner, or proves a hosted run. A separate, reviewed caller/activation must select this reusable workflow. No `workflow_dispatch`, caller inputs, `secrets: inherit`, executable caller path, or hosted-runner fallback is exposed. Its isolated runner group is `CWL CI isolated` with labels `self-hosted`, `Linux`, `X64`, `cwlab-ci-isolated`; unavailable runners leave the checks queued instead of silently substituting a hosted runner. + +Before **each** checkout, an executable shell admission step requires exact `ContextualWisdomLab/context-graph-contracts`, an event of `push` or same-repository `pull_request`, and a lowercase 40-hex candidate SHA. Any fork, missing head repository, wrong repo, other event, or malformed SHA fails before candidate checkout. Checkouts explicitly bind repository and `github.event.pull_request.head.sha || github.sha` with `persist-credentials: false`; jobs verify the checked-out head (both heads for the reproducibility job). Scope is caller-repository read-only permissions. This does **not** prove that a push SHA belongs to a particular protected ref; it is validation, not release authorization. + +The producer carries the product source's five non-signing jobs (product source `c2f9802e47c1722773f038a4c68be8d59bda6538`): + +| Central job | Source job / gate | +|---|---| +| `test` | `ci.yml`: Python 3.11–3.14, pinned setup-python and uv 0.11.32, lock/sync, compileall, Ruff, tests and coverage; coverage report explicitly enforces 100% | +| `package` | `ci.yml`: wheel/sdist resource assertions, offline installed-wheel schema/conformance/fixture checks and installed conformance commands | +| `installed-wheel` | `receipt-package-smoke.yml`: installed Context Assertion admission, bundle and complete release-evidence smoke | +| `release-package-reproducibility` | `reproducibility.yml`: two exact-source clean checkouts, fixed source epoch, byte comparison and checksum artifact | +| `package-evidence` | `supply-chain.yml`: independent reproducibility witness, installed-wheel SPDX 3.0.1 SBOM, checksum and package-evidence verifier | + +Actions are pinned to the product's exact revision. Uploads retain source lock and verified evidence artifacts only after successful preceding steps (`if-no-files-found: error`); fresh diagnostic artifacts may remain if a later step fails. Overall validation eligibility requires the fail-closed `required` aggregate, not artifact existence. Workspace-owned outputs are removed after admission, before checkout, and in a final step gated by successful private initialization. Pre-checkout cleanup never deletes an inherited `PRIVATE_ROOT`. Per-job uv/TMP/cache state and a fresh HOME/config directory are mode 0700 under canonical `RUNNER_TEMP`; inherited `PYTHONPATH`, `PYTHONHOME`, and `PYTHONOPTIMIZE` are cleared and user site imports disabled. Final private cleanup accepts only a direct, nonsymlink `cgc-ci.` directory with the initializer's eight-character suffix; traversal and nested paths fail closed. These guards do not provide protection against a concurrent hostile process changing filesystem names or candidate code rewriting Actions environment files. + +Installed package/receipt smoke invokes both Python and console entrypoints through the installed venv interpreter with `-I`, preserving all original semantic assertions while excluding cwd/PYTHONPATH source leakage. The result-only `required` job (`CGC required validation`) runs with `always()` and requires literal `success` from all five producers; failure, skipped, cancelled, or missing results cannot pass. It performs no checkout or candidate execution. Concurrency remains caller-only. + +The product's protected-main `attest-protected-main` signing job is intentionally **not** moved: this producer grants no `id-token`, attestation, release-environment, or write permissions. Existing product CI and its protected release authorization are not replaced or disabled by this file alone. + +## Offline verification and limitations + +From the central repository root, run `env -u PYTHONPATH python3 -m pytest tests/test_context_graph_contracts_ci_contract.py -q` to execute parsed admission, private initialization/cleanup, source-poisoning and aggregate result regressions. Run actionlint without ignores using the actual custom label catalog: + +```sh +actionlint -config-file /Users/seonghobae/.hermes/cache/scratch/cgc-ai-review-auto-20261008/actionlint-labels.yaml .github/workflows/context-graph-contracts-ci.yml +``` + +Local macOS replay against an archive of product `c2f9802` exercised the actual package and installed-wheel shell blocks, including wheel/sdist resource inspection, offline installation, installed module-origin checks, conformance, bundle and complete release-evidence semantics. The product test lane passed 794 tests with 2,168 statements and 660 branches at 100% on Python 3.14.7 using uv 0.11.32 and the frozen lock. This does not establish Python 3.11–3.13 or Linux acceptance, real Anchore SBOM generation, artifact uploads, runner registration/isolation, checkout action lifecycle, release signing, or hosted CI execution. + +The local full central suite returned 9 failures, 5,190 passes, 4 skips and 40 passing subtests before the additional runner-alias regression. All nine failures reproduced in the same three modules from the exact central baseline archive: two date-sensitive CodeQL coverage checks, two local OpenCode gateway integration checks with no request observed, and five isolation-path checks rejecting scratch paths under the host HOME. No unrelated repairs or criteria reductions were made. Full central coverage has not been cleared. Independent exact-byte review and a formal App approval remain separate from this repair; no approval, activation or merge is inferred from local passes. diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..5afeb920ad 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -35,9 +35,11 @@ def default_setup_scanning_nothing(name: str) -> dict: } -def covered_by_recent_analysis(name: str, *, days_ago: int = 1) -> dict: +def covered_by_recent_analysis( + name: str, *, days_ago: int = 1, now: datetime = NOW +) -> dict: """Return a repository payload covered by a recent, successful CodeQL analysis.""" - created_at = (NOW - timedelta(days=days_ago)).isoformat().replace("+00:00", "Z") + created_at = (now - timedelta(days=days_ago)).isoformat().replace("+00:00", "Z") return { "name": name, "archived": False, @@ -263,7 +265,11 @@ def test_null_latest_analysis_is_not_coverage() -> None: def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: - repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] + repositories = [ + covered_by_recent_analysis( + "DefaultNowRepo", days_ago=0, now=datetime.now(timezone.utc) + ) + ] assert audit.audit_codeql_coverage(repositories) == [] @@ -302,7 +308,14 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: payload_path = tmp_path / "repositories.json" payload_path.write_text( - json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), + json.dumps( + [ + covered_by_default_setup("ELUNVERA"), + covered_by_recent_analysis( + "Orgmetra", now=datetime.now(timezone.utc) + ), + ] + ), encoding="utf-8", ) diff --git a/tests/test_central_complex_existing_coverage.py b/tests/test_central_complex_existing_coverage.py new file mode 100644 index 0000000000..145e3c6fb5 --- /dev/null +++ b/tests/test_central_complex_existing_coverage.py @@ -0,0 +1,308 @@ +"""Exercise existing central fail-closed boundaries without commands or network.""" + +from __future__ import annotations + +import copy +import hashlib +import json +from urllib.error import HTTPError, URLError +from io import BytesIO + +import pytest + +from scripts.ci import noema_review_gate as noema +from scripts.ci import release_dependency_gate as gate +from scripts.ci import prescreen_release_runtime_archives as prescreen +from scripts.ci import pingora_edge_policy as policy +from tests.test_release_dependency_gate import build_capture +from tests.test_release_dependency_fanout_plan import _allowed, CONTROL +from tests.test_verify_release_scope_evidence_set import _prescreen_case, _scope_with_variants + + +def test_content_envelope_malformed_json_is_refused(monkeypatch): + """Malformed API JSON never reaches content decoding.""" + 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("kind", ["url", "timeout", "http"]) +def test_raw_blob_transport_errors_are_sanitized(monkeypatch, kind): + """Transport details are not leaked and the original cause is retained.""" + body = BytesIO(b"private response") + if kind == "http": + error = HTTPError("private url", 500, "private reason", {}, body) + elif kind == "url": + error = URLError("private dns") + else: + error = TimeoutError("private timeout") + def fail(request, timeout): + assert timeout == 30 + assert request.full_url.endswith("/git/blobs/" + "a" * 40) + raise error + monkeypatch.setattr(policy.github_opener, "open", fail) + try: + with pytest.raises(policy.PolicyError) as caught: + policy._github_open_raw_bytes("https://api.github.com/repos/a/b/git/blobs/" + "a" * 40, "token", 4) + assert str(caught.value) == f"GitHub raw blob request failed: {type(error).__name__}" + assert caught.value.__cause__ is error + finally: + error.close() if isinstance(error, HTTPError) else body.close() + + +def test_inline_content_requires_string_payload(): + """An otherwise regular base64 envelope cannot carry a numeric payload.""" + payload = {"type": "file", "encoding": "base64", "size": 1, "content": 1} + with pytest.raises(policy.PolicyError, match="malformed size or content field"): + policy._load_raw_file_bytes("https://api.github.com", "a/b", "a.md", "head", "token", lambda *args: payload) + + +def test_inline_encoding_is_rechecked_at_decode_boundary(): + """A synthetic changing API field cannot bypass the second encoding guard.""" + class ChangingEncoding(str): + def __ne__(self, other): + return other == "base64" + payload = {"type": "file", "encoding": ChangingEncoding("base64"), "size": 1, "content": "eA=="} + with pytest.raises(policy.PolicyError, match="not a regular base64 file"): + policy._load_raw_file_bytes("https://api.github.com", "a/b", "a.md", "head", "token", lambda *args: payload) + payload["encoding"] = "base64" + assert policy._load_raw_file_bytes("https://api.github.com", "a/b", "a.md", "head", "token", lambda *args: payload) == b"x" + + +@pytest.mark.parametrize("mutation", ["oversized", "changed", "architecture", "json"]) +def test_runtime_receipt_prescreen_refuses_untrusted_bytes(tmp_path, mutation): + """Receipt size, digest, JSON and interpreter identity are independent gates.""" + root, rows = _prescreen_case(tmp_path) + scope = _scope_with_variants(rows, root) + row = rows[0] + path = root / row["artifact_name"] / f"{row['leg']}.runtime.json" + if mutation == "oversized": + path.write_bytes(b"x" * (1024 * 1024 + 1)) + message, code = "runtime receipt is oversized", gate.SCOPE_UNVERIFIABLE + elif mutation == "changed": + path.write_bytes(path.read_bytes() + b" ") + message, code = "runtime receipt changed after transport", gate.SOURCE_HASH_MISMATCH + else: + runtime = json.loads(path.read_bytes()) + runtime["machine"] = "foreign" + path.write_bytes(b"{bad-json" if mutation == "json" else json.dumps(runtime).encode()) + row["members"][path.name] = hashlib.sha256(path.read_bytes()).hexdigest() + message, code = ("JSON" if mutation == "json" else "runtime interpreter differs"), gate.SCOPE_UNVERIFIABLE + with pytest.raises(gate.GateError, match=message) as caught: + prescreen.prescreen(scope, root) + assert caught.value.code == code + + +def test_macos_build_receipt_requires_known_interpreter_architecture(tmp_path): + """A hash-bound receipt still needs a recognized macOS interpreter suffix.""" + root, rows = _prescreen_case(tmp_path) + row = next(row for row in rows if row["leg"].startswith("universal2")) + folder = root / row["artifact_name"] + path = folder / f"{row['leg']}.build-first.json" + receipt = json.loads(path.read_bytes()) + receipt["build_env"] = "runner:macOS/UNKNOWN" + path.write_text(json.dumps(receipt)) + with pytest.raises(gate.GateError, match="build interpreter architecture is missing") as caught: + prescreen._build_packages(row, folder) + assert caught.value.code == gate.SCOPE_UNVERIFIABLE + + +def test_runtime_prescreen_requires_sdist_after_complete_iteration(tmp_path, monkeypatch): + """Thirteen valid runtime legs cannot replace the mandatory source leg.""" + root, rows = _prescreen_case(tmp_path) + scope = _scope_with_variants(rows, root) + sdist = rows[-1] + replacement = copy.deepcopy(rows[0]) + replacement["leg"] = "x86_64-unknown-linux-gnu-py3.15" + replacement["artifact_name"] = "repro-digest-" + replacement["leg"] + folder = root / replacement["artifact_name"] + folder.mkdir() + original = root / rows[0]["artifact_name"] + for archive in replacement["archives"]: + (folder / archive["file"]).write_bytes((original / archive["file"]).read_bytes()) + runtime_name = replacement["leg"] + ".runtime.json" + raw = b"{}" + (folder / runtime_name).write_bytes(raw) + replacement["members"][runtime_name] = hashlib.sha256(raw).hexdigest() + rows[-1] = replacement + monkeypatch.setattr(prescreen, "_runtime_target_architecture", lambda *args, **kwargs: "x86_64") + monkeypatch.setattr(prescreen, "_build_packages", lambda *args: []) + monkeypatch.setattr(prescreen, "_maturin_tool", lambda item, folder: { + "key": "tool", "legs": [item["leg"]], "build_envs": {item["leg"]: "fixture"}}) + with pytest.raises(gate.GateError, match="runtime archive coverage is incomplete") as caught: + prescreen.prescreen(scope, root) + assert caught.value.code == gate.SCOPE_UNVERIFIABLE + assert sdist["leg"] == "sdist" + + +def test_rust_build_hook_namespace_positive_and_negative(): + """Ordinary Rust build code is allowed but networking is 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"] + + +@pytest.mark.parametrize("mutation", ["invalid-sha", "absent", "symlink", "raced", "valid"]) +def test_selection_capture_binds_exact_regular_bounded_git_blob(tmp_path, monkeypatch, mutation): + """Capture checks immutable metadata and actual payload before publication.""" + capture = tmp_path / "capture" + commands = [] + def git(command, **kwargs): + commands.append(command) + assert kwargs["cwd"] == tmp_path + if command[1] == "ls-tree": + return "" if mutation == "absent" else f"{'120000' if mutation == 'symlink' else '100644'} blob oid\tdocs/release-license-selections.json" + if command[2] == "-s": + return b"2\n" + assert command[2:] == ["blob", "oid"] + return b"x" * (gate._MAX_METADATA_BYTES + 1) if mutation == "raced" else b"[]" + monkeypatch.setattr(gate.subprocess, "check_output", git) + sha = "short" if mutation == "invalid-sha" else "a" * 40 + if mutation in {"invalid-sha", "symlink", "raced"}: + message = {"invalid-sha": "exact commit SHA", "symlink": "regular Git blob", "raced": "exceeds bounded size"}[mutation] + with pytest.raises(gate.GateError, match=message): + gate.capture_license_selections(tmp_path, sha, capture) + assert not capture.exists() + else: + assert gate.main(["capture-license-selections", "--source", str(tmp_path), "--source-sha", sha, "--capture", str(capture)]) == 0 + if mutation == "valid": + assert (capture / "license-selections.json").read_bytes() == b"[]" + else: + assert not capture.exists() + if mutation == "invalid-sha": + assert commands == [] + + +@pytest.mark.parametrize("mutation", ["nonexact", "foreign-workspace", "workspace-version", "dev-match", "dev-mismatch"]) +def test_cargo_checkout_and_development_graph_binding(tmp_path, monkeypatch, mutation): + """Synthetic Git boundaries retain real source files, TOML and graph checking.""" + capture = build_capture(tmp_path / "capture") + source = tmp_path / "source" + 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"\nversion="1.0.0"\n') + (wheel / "Cargo.toml").write_text('[package]\nname="fast-mlsirm"\nversion="0.11.5"\n') + 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 == "workspace-version": + core.write_text('[package]\nname="local-core"\nversion.workspace=true\n') + (wheel / "Cargo.toml").write_text((wheel / "Cargo.toml").read_text() + '[workspace.package]\nversion="1.0.0"\n') + metadata_path = capture / "cargo/metadata.json" + metadata = json.loads(metadata_path.read_text()) + metadata["workspace_root"] = str(tmp_path / "foreign" if mutation == "foreign-workspace" else 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"}]}) + metadata_path.write_text(json.dumps(metadata)) + if mutation.startswith("dev-"): + (source / "Cargo.lock").write_bytes(lock.read_bytes() + b"# development\n") + dev = capture / "cargo-dev" + dev.mkdir() + (dev / "Cargo.lock").write_bytes((source / "Cargo.lock").read_bytes() if mutation == "dev-match" else b"different") + dev_metadata = copy.deepcopy(metadata) + dev_metadata["workspace_root"] = str(source) + (dev / "metadata.json").write_text(json.dumps(dev_metadata)) + committed = {str(p.relative_to(source)): p.read_bytes() for p in source.rglob("*") if p.is_file()} + def git(command, **kwargs): + assert command[:3] == ["git", "-C", str(source)] + if command[3:] == ["rev-parse", "--show-toplevel"]: + return str(source) + "\n" + if command[3:] == ["rev-parse", "HEAD"]: + return "a" * 40 + "\n" + if command[3] == "ls-tree": + return b"100644 blob oid\tCargo.lock\n" if "Cargo.lock" in committed else b"" + assert command[3] == "show" + return committed[command[4].split(":", 1)[1]] + monkeypatch.setattr(gate.subprocess, "check_output", git) + if mutation in {"nonexact", "foreign-workspace"}: + with pytest.raises(gate.GateError, match="Cargo source checkout cannot be bound"): + gate._enumerate_cargo(capture, source_root=source, source_sha="short" if mutation == "nonexact" else "a" * 40) + elif mutation == "dev-mismatch": + with pytest.raises(gate.GateError, match="development Cargo lock differs from source root"): + gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source) + elif mutation == "dev-match": + assert gate.gate(capture, stage=gate.LICENSE_STAGE, source_root=source).passed + else: + dependencies, failures, expected = gate._enumerate_cargo(capture, source_root=source, source_sha="a" * 40) + assert not failures + assert {item.key for item in dependencies} == expected == {"cargo/greencrate@0.1.0"} + + +@pytest.mark.parametrize("mutation, message", [ + ("valid", None), ("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_immutable_grant_boundaries(tmp_path, monkeypatch, mutation, message): + """Owner-compatible synthetic grants check complete digest and per-grant binding.""" + subject, archive_sha, source_sha = "cargo/example@1.0.0", "a" * 64, "b" * 40 + first, second = b"MIT", b"BSD" + content = (b"BAD" if mutation == "grant" else first) + (b"xx" if mutation == "separator" else b"\n\n") + second + digest = hashlib.sha256(content).hexdigest() + grants = (("LICENSE-MIT", 3, hashlib.sha256(first).hexdigest()), ("LICENSE-BSD", 3, hashlib.sha256(second).hexdigest())) + monkeypatch.setitem(gate._REVIEWED_SOURCE_NOTICES, subject, (archive_sha, "owner/repo", "c" * 40, digest, {"MIT"}, grants)) + upstream = [{"url": f"https://raw.githubusercontent.com/owner/repo/{'c' * 40}/{name}", "sha256": sha} for name, _, sha in grants] + selection = {"chosen": "MIT", "rationale": "Reviewed immutable grant bytes."} + choice = {"ecosystem": "cargo", "name": "example", "version": "1.0.0", **selection, "archive_sha256": archive_sha, "upstream_licenses": upstream, + "bundled_notice": {"path": "python/fast_mlsirm/_licenses/example.txt", "sha256": digest}} + choices = b"{}" if mutation == "not-list" else json.dumps([] if mutation == "missing-choice" else [choice]).encode() + def git(command, **kwargs): + assert command[:3] == ["git", "-C", str(tmp_path)] + if command[3] == "ls-tree": + return f"100644 blob {'selection' if command[-1].endswith('.json') else 'notice'}\t{command[-1]}\n" + if command[4] == "-s": + return str(gate._MAX_METADATA_BYTES + 1 if mutation == "oversized" else 1).encode() + assert command[4] == "blob" + return choices if command[5] == "selection" else content + monkeypatch.setattr(gate.subprocess, "check_output", git) + args = (tmp_path, "short" if mutation == "nonexact" else source_sha, subject, {"ecosystem": "cargo", "source_sha256": archive_sha}, selection) + if message: + with pytest.raises(gate.GateError, match=message): + gate._source_license_notice(*args) + else: + texts, bound = gate._source_license_notice(*args) + assert list(texts.values()) == ["MIT", "BSD"] + assert bound["source_sha"] == source_sha and bound["sha256"] == digest + + +def test_runtime_fixtures_cannot_push_fanout_past_512_jobs(tmp_path): + """The post-runtime merge limit applies even to fully authenticated fixtures.""" + capture, report = _allowed(tmp_path / "capture") + archives = [] + for index in range(513): + sha = hashlib.sha256(str(index).encode()).hexdigest() + key = f"pypi/example@1/sha256/{sha}" + fixture = gate.build_fixture(gate.Dependency("pypi", "example", "1"), {"source_sha256": sha, "archive_members": [], "install_hook_sources": {}, "parsed_inputs": [], "native_libraries": [], "known_vulnerabilities": []}) + fixture["id"] = key + archives.append({"key": key, "package_key": "pypi/example@1", "name": "example", "version": "1", "source_sha256": sha, "license": "MIT", "fixture": fixture, "fixture_sha256": gate.fixture_digest(fixture)}) + tool = archives[-1] + tool["key"] = f"github-release/example@1/sha256/{tool['source_sha256']}" + tool["package_key"] = "github-release/example@1" + tool["fixture"]["id"] = tool["key"] + tool["fixture"]["dependency"]["ecosystem"] = "github-release" + tool["fixture_sha256"] = gate.fixture_digest(tool["fixture"]) + path = tmp_path / "runtime.json" + path.write_text(json.dumps({"schema": "cwl.release-runtime-archive-licenses/3", "archives": archives[:-2], "build_packages": [archives[-2]], "build_tools": [tool]})) + with pytest.raises(gate.GateError, match="dependency plan exceeds 512 jobs") as caught: + gate.strix_fanout_plan(capture, report, CONTROL, 42, 2, path) + assert caught.value.code == gate.SCOPE_UNVERIFIABLE + + +@pytest.mark.parametrize("name,target,leg,member,kind", [ + ("libc.so.6", "x86_64-unknown-linux-gnu", "linux-py3.14", "module.so", "system-runtime"), + ("@rpath/fast_mlsirm._core.so", "universal2-apple-darwin", "mac-py3.14", "fast_mlsirm/_core.so", "self-install-name"), + ("python314.dll", "x86_64-pc-windows-msvc", "windows-py3.14", "module.pyd", "interpreter-runtime"), + ("foreign.dll", "x86_64-pc-windows-msvc", "windows-py3.14", "module.pyd", None), + ("foreign", "unknown-target", "unknown-py3.14", "module", None), +]) +def test_platform_link_classification_controls(name, target, leg, member, kind): + """Complete affected tests retain platform-specific positive and unknown controls.""" + result = gate.classify_platform_link(name, target, leg, member) + if kind is None: + assert result is None + else: + assert result["name"] == name and result["kind"] == kind diff --git a/tests/test_context_graph_contracts_ci_contract.py b/tests/test_context_graph_contracts_ci_contract.py new file mode 100644 index 0000000000..682e635ff1 --- /dev/null +++ b/tests/test_context_graph_contracts_ci_contract.py @@ -0,0 +1,439 @@ +"""Central CGC admission is executable, fixed-repository, and fail closed.""" + +import os +from pathlib import Path +import subprocess + +import pytest +import yaml + +WORKFLOW = ( + Path(__file__).resolve().parents[1] + / ".github/workflows/context-graph-contracts-ci.yml" +) +REPOSITORY = "ContextualWisdomLab/context-graph-contracts" + + +def workflow(): + """Load Actions YAML without YAML 1.1's boolean interpretation of on.""" + assert WORKFLOW.exists(), "central CGC workflow is missing" + return yaml.load(WORKFLOW.read_text(), Loader=yaml.BaseLoader) + + +def producer_jobs(): + """Return the five source-executing gates, excluding the result-only aggregate.""" + return {name: job for name, job in workflow()["jobs"].items() if name != "required"} + + +@pytest.mark.parametrize( + "event,repository,head,sha,expected", + [ + ("push", REPOSITORY, "", "a" * 40, 0), + ("pull_request", REPOSITORY, REPOSITORY, "b" * 40, 0), + ("push", "ContextualWisdomLab/other", "", "a" * 40, 1), + ("pull_request", REPOSITORY, "attacker/context-graph-contracts", "a" * 40, 1), + ("pull_request", REPOSITORY, "", "a" * 40, 1), + ("pull_request_target", REPOSITORY, REPOSITORY, "a" * 40, 1), + ("workflow_dispatch", REPOSITORY, REPOSITORY, "a" * 40, 1), + ("repository_dispatch", REPOSITORY, REPOSITORY, "a" * 40, 1), + ("schedule", REPOSITORY, REPOSITORY, "a" * 40, 1), + ("push", REPOSITORY, "", "main", 1), + ("pull_request", REPOSITORY, REPOSITORY, "", 1), + ("push", REPOSITORY.lower(), "", "a" * 40, 1), + ], +) +def test_parsed_admission_shell(event, repository, head, sha, expected): + """Execute each actual job admission script, not a test-side reimplementation.""" + for job in producer_jobs().values(): + step = job["steps"][0] + assert step["name"] == "Admit candidate before checkout" + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", step["run"]], + env={ + **os.environ, + "EVENT_NAME": event, + "REPOSITORY": repository, + "HEAD_REPOSITORY": head, + "EXPECTED_SHA": sha, + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == expected, result.stderr + + +def test_private_state_and_success_only_uploads(): + """Fresh private tool state is cleaned even when candidate execution fails.""" + for job in producer_jobs().values(): + steps = job["steps"] + private = next((step for step in steps if step.get("id") == "private"), None) + assert private is not None, "private job state must be initialized" + assert "umask 077" in private["run"] + assert "mktemp -d" in private["run"] + assert "UV_CACHE_DIR" in private["run"] + assert "UV_PROJECT_ENVIRONMENT" in private["run"] + assert ( + steps[-1]["if"] == "${{ always() && steps.private.outcome == 'success' }}" + ) + assert "PRIVATE_ROOT" in steps[-1]["run"] + for step in steps: + if step.get("uses", "").startswith("actions/upload-artifact@"): + assert step.get("if") == "${{ success() }}" + assert step["with"]["if-no-files-found"] == "error" + + +def test_all_source_gates_retained_without_release_signing(): + """The producer keeps five non-signing gates and no privileged signer.""" + data = workflow() + assert set(producer_jobs()) == { + "test", + "package", + "installed-wheel", + "release-package-reproducibility", + "package-evidence", + } + runs = { + name: "\n".join(step.get("run", "") for step in job["steps"]) + for name, job in producer_jobs().items() + } + test = runs["test"] + for gate in ( + "uv lock --check", + "uv sync --frozen --extra dev", + "compileall -q src", + "ruff check .", + "coverage run -m pytest", + "coverage report", + ): + assert gate in test + assert "coverage report --fail-under=100" in test + assert data["jobs"]["test"]["strategy"]["matrix"]["python-version"] == [ + "3.11", + "3.12", + "3.13", + "3.14", + ] + for job in producer_jobs().values(): + assert any( + step.get("with", {}).get("version") == "0.11.32" for step in job["steps"] + ) + package = runs["package"] + for gate in ( + "uv build --wheel --sdist", + "required_suffixes", + "--no-index", + "cwl-context-conformance-verify", + "cwl-context-conformance-admit", + ): + assert gate in package + assert ( + "verify_reproducible_package_builds.py" + in runs["release-package-reproducibility"] + ) + assert "verify_reproducible_package_builds.py" in runs["package-evidence"] + assert "cwl-context-package-evidence-verify" in runs["package-evidence"] + assert "cwl-context-release-evidence-admit" in runs["installed-wheel"] + for job in producer_jobs().values(): + assert not any( + "actions/attest@" in step.get("uses", "") for step in job["steps"] + ) + assert job.get("permissions", {}).get("id-token") is None + + +def test_private_shell_creates_and_removes_mode_0700_state(tmp_path): + """Execute initialization/cleanup with seeded stale outputs in a scratch workspace.""" + for job in producer_jobs().values(): + workspace = tmp_path / "workspace" + workspace.mkdir(exist_ok=True) + runner = tmp_path / "runner" + runner.mkdir(exist_ok=True) + env_file = tmp_path / "env" + env_file.write_text("") + stale = workspace / "dist" + stale.mkdir(exist_ok=True) + (stale / "stale.whl").write_text("stale fixture") + env = { + **os.environ, + "GITHUB_WORKSPACE": str(workspace), + "RUNNER_TEMP": str(runner), + "GITHUB_ENV": str(env_file), + } + for step in job["steps"]: + if step.get("id") == "private": + subprocess.run( + ["bash", "-euo", "pipefail", "-c", step["run"]], env=env, check=True + ) + values = dict(line.split("=", 1) for line in env_file.read_text().splitlines()) + private = Path(values["PRIVATE_ROOT"]) + assert private.stat().st_mode & 0o777 == 0o700 + subprocess.run( + ["bash", "-euo", "pipefail", "-c", job["steps"][-1]["run"]], + env={**env, **values}, + check=True, + ) + assert not private.exists() + assert not stale.exists() + + +def test_minimal_call_interface(): + """No caller-selected execution, credentials, or hosted fallback is exposed.""" + data = workflow() + assert data["on"] == {"workflow_call": "null"} + assert data["permissions"] == {"contents": "read"} + assert "concurrency" not in data, "concurrency belongs only to the caller" + for job in producer_jobs().values(): + assert job["runs-on"] == { + "group": "CWL CI isolated", + "labels": ["self-hosted", "Linux", "X64", "cwlab-ci-isolated"], + } + for step in job["steps"]: + if "actions/checkout@" in step.get("uses", ""): + assert step["with"]["repository"] == REPOSITORY + assert step["with"]["persist-credentials"] == "false" + assert ( + step["with"]["ref"] + == "${{ github.event.pull_request.head.sha || github.sha }}" + ) + + +def test_cleanup_requires_successful_private_initialization(): + """Rejected admission must never clean another candidate's workspace.""" + for job in producer_jobs().values(): + assert ( + job["steps"][-1]["if"] + == "${{ always() && steps.private.outcome == 'success' }}" + ) + + +def test_private_environment_records_have_no_indentation(tmp_path): + """GitHub environment keys must remain exact after YAML block parsing.""" + runner = tmp_path / "runner" + runner.mkdir() + for job in producer_jobs().values(): + env_file = tmp_path / "environment" + env_file.write_text("") + step = next(step for step in job["steps"] if step.get("id") == "private") + subprocess.run( + ["bash", "-euo", "pipefail", "-c", step["run"]], + env={**os.environ, "RUNNER_TEMP": str(runner), "GITHUB_ENV": str(env_file)}, + check=True, + ) + records = dict(line.split("=", 1) for line in env_file.read_text().splitlines()) + assert set(records) >= { + "PRIVATE_ROOT", + "UV_CACHE_DIR", + "UV_PROJECT_ENVIRONMENT", + "TMPDIR", + "XDG_CACHE_HOME", + } + + +def test_private_state_overrides_inherited_home_and_python_paths(tmp_path): + """Persistent runner home/config and import paths cannot reach tool commands.""" + for job in producer_jobs().values(): + env_file = tmp_path / "environment" + env_file.write_text("") + step = next(step for step in job["steps"] if step.get("id") == "private") + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", step["run"]], + env={ + **os.environ, + "RUNNER_TEMP": str(tmp_path), + "GITHUB_ENV": str(env_file), + "HOME": "/inherited/home", + "PYTHONPATH": "/inherited/source", + "PYTHONHOME": "/inherited/python", + "PYTHONOPTIMIZE": "2", + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + records = dict(line.split("=", 1) for line in env_file.read_text().splitlines()) + assert records["HOME"] == records["PRIVATE_ROOT"] + "/home" + assert Path(records["HOME"]).is_dir() + for key in ("PYTHONPATH", "PYTHONHOME", "PYTHONOPTIMIZE"): + assert records[key] == "" + assert records["PYTHONNOUSERSITE"] == "1" + + +@pytest.mark.parametrize("phase", ["before", "after"]) +@pytest.mark.parametrize("escape", ["traversal", "symlink", "nested"]) +def test_cleanup_never_follows_private_root_escape(tmp_path, phase, escape): + """Actual cleanup refuses glob-matching traversal and linked private parents.""" + for index, job in enumerate(producer_jobs().values()): + base = tmp_path / str(index) + workspace = base / "workspace" + runner = base / "runner" + victim = base / "victim" + for path in (workspace, runner, victim): + path.mkdir(parents=True) + marker = victim / "keep" + marker.write_text("unrelated state") + if escape == "traversal": + (runner / "cgc-ci.12345678").mkdir() + private = str(runner / "cgc-ci.12345678/../../victim") + elif escape == "symlink": + (runner / "cgc-ci.12345678").symlink_to(base, target_is_directory=True) + private = str(runner / "cgc-ci.12345678/victim") + else: + (runner / "cgc-ci.12345678").mkdir() + (runner / "cgc-ci.12345678/victim").symlink_to( + victim, target_is_directory=True + ) + private = str(runner / "cgc-ci.12345678/victim") + "/" + step = job["steps"][1] if phase == "before" else job["steps"][-1] + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", step["run"]], + env={ + **os.environ, + "GITHUB_WORKSPACE": str(workspace), + "RUNNER_TEMP": str(runner), + "PRIVATE_ROOT": private, + }, + capture_output=True, + text=True, + check=False, + ) + assert marker.exists(), (result.returncode, result.stderr, private) + if phase == "after": + assert result.returncode != 0, "unsafe private cleanup must fail closed" + + +@pytest.mark.parametrize( + "job_name,step_name,venv_name", + [ + ( + "package", + "Smoke-test installed contracts, schemas, and conformance fixtures", + ".package-smoke", + ), + ( + "package", + "Smoke-test installed conformance evidence commands", + ".package-smoke", + ), + ( + "installed-wheel", + "Exercise Context Assertion admission SDK from installed wheel", + ".receipt-smoke", + ), + ( + "installed-wheel", + "Exercise release evidence from installed wheel", + ".receipt-smoke", + ), + ], +) +def test_installed_smoke_ignores_source_imports( + tmp_path, job_name, step_name, venv_name +): + """Actual smoke shell reaches installed modules despite poisoned source paths.""" + import sys + + venv = tmp_path / venv_name + subprocess.run( + [sys.executable, "-m", "venv", "--without-pip", str(venv)], check=True + ) + purelib = subprocess.run( + [ + str(venv / "bin/python"), + "-I", + "-c", + "import sysconfig; print(sysconfig.get_paths()['purelib'])", + ], + capture_output=True, + text=True, + check=True, + ).stdout.strip() + installed = Path(purelib) / "cwl_context_contracts" + installed.mkdir() + (installed / "__init__.py").write_text("raise RuntimeError('INSTALLED_WITNESS')\n") + source = tmp_path / "cwl_context_contracts" + source.mkdir() + (source / "__init__.py").write_text("raise RuntimeError('SOURCE_LEAK')\n") + for command in ("cwl-context-conformance", "cwl-context-conformance-manifest"): + script = venv / "bin" / command + script.write_text(f"#!{venv}/bin/python\nimport cwl_context_contracts\n") + script.chmod(0o700) + step = next( + step + for step in workflow()["jobs"][job_name]["steps"] + if step.get("name") == step_name + ) + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", step["run"]], + cwd=tmp_path, + env={ + **os.environ, + "RUNNER_TEMP": str(tmp_path), + "PRIVATE_ROOT": str(tmp_path), + "PYTHONPATH": str(tmp_path), + }, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode != 0 + assert "INSTALLED_WITNESS" in result.stderr, result.stderr + assert "SOURCE_LEAK" not in result.stderr + + +@pytest.mark.parametrize("result", ["success", "failure", "cancelled", "skipped", ""]) +def test_required_aggregate_refuses_any_non_success(result): + """Required result fails closed across every producer dependency result.""" + jobs = workflow()["jobs"] + required = jobs.get("required") + assert required is not None, "one stable required result must cover all five gates" + gates = set(jobs) - {"required"} + assert set(required["needs"]) == gates + assert required["if"] == "${{ always() }}" + for failed_gate in gates: + env = { + **os.environ, + **{name.upper().replace("-", "_") + "_RESULT": "success" for name in gates}, + } + env[failed_gate.upper().replace("-", "_") + "_RESULT"] = result + actual = subprocess.run( + ["bash", "-euo", "pipefail", "-c", required["steps"][0]["run"]], + env=env, + capture_output=True, + text=True, + check=False, + ) + assert actual.returncode == (0 if result == "success" else 1), actual.stderr + + +def test_private_initialization_accepts_symlinked_runner_parent(tmp_path): + """Platform aliases of RUNNER_TEMP are canonicalized before cleanup admission.""" + runner = tmp_path / "real" + runner.mkdir() + alias = tmp_path / "alias" + alias.symlink_to(runner, target_is_directory=True) + workspace = tmp_path / "workspace" + workspace.mkdir() + for job in producer_jobs().values(): + env_file = tmp_path / "environment" + env_file.write_text("") + env = { + **os.environ, + "RUNNER_TEMP": str(alias), + "GITHUB_ENV": str(env_file), + "GITHUB_WORKSPACE": str(workspace), + } + private = next(step for step in job["steps"] if step.get("id") == "private") + subprocess.run( + ["bash", "-euo", "pipefail", "-c", private["run"]], env=env, check=True + ) + records = dict(line.split("=", 1) for line in env_file.read_text().splitlines()) + result = subprocess.run( + ["bash", "-euo", "pipefail", "-c", job["steps"][-1]["run"]], + env={**env, **records}, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert not Path(records["PRIVATE_ROOT"]).exists() 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_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_opencode_gateway_route_integration.py b/tests/test_opencode_gateway_route_integration.py index 3893914c0e..313f25cc19 100644 --- a/tests/test_opencode_gateway_route_integration.py +++ b/tests/test_opencode_gateway_route_integration.py @@ -143,6 +143,7 @@ def _run_opencode( "HOME": str(tmp_path / "home"), "XDG_CONFIG_HOME": str(config_home), "NO_COLOR": "1", + "OPENCODE_DISABLE_PROJECT_CONFIG": "1", "CONTEXTUAL_ORCHESTRATOR_BASE_URL": gateway_origin, "CONTEXTUAL_ORCHESTRATOR_TOKEN": "stub-token", } @@ -183,6 +184,29 @@ def _run_opencode( ) +@pytest.mark.parametrize( + ("base_url", "expected_path"), + [ + ("{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}/v1", GATEWAY_SERVED_ROUTE), + ("{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}", "/chat/completions"), + ], +) +def test_route_probe_ignores_ancestor_project_config( + gateway_stub, tmp_path, base_url, expected_path +) -> None: + """Ancestor project config cannot change the probe's synthetic provider.""" + (tmp_path / "opencode.jsonc").write_text( + json.dumps({"agent": {"build": {"prompt": "{file:./missing-prompt.md}"}}}), + encoding="utf-8", + ) + probe_root = tmp_path / "probe" + probe_root.mkdir() + origin, requested_paths = gateway_stub + _run_opencode(probe_root, base_url, origin, requested_paths) + assert requested_paths, "the CLI issued no request to the gateway stub" + assert requested_paths[0] == expected_path + + def test_tracked_config_requests_the_gateway_served_route(gateway_stub, tmp_path) -> None: """The shipped baseURL must make the CLI ask for the served /v1 route.""" origin, requested_paths = gateway_stub 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_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_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_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 1b1cdf3722..e26657a526 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -671,6 +671,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): @@ -1571,7 +1572,21 @@ def test_isolated_command_rejects_executable_outside_bound_roots(monkeypatch, tm ) -def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(monkeypatch, tmp_path): +@pytest.fixture +def isolated_host_home(monkeypatch, tmp_path): + """Separate synthetic host home from scratch without relaxing production denial. + + TMPDIR may legitimately live under the caller's home on macOS. These + path-resolution fixtures model a workspace outside the denied host home; + the separate host-home rejection test continues to use the real home. + """ + home = tmp_path / "host-home" + home.mkdir() + monkeypatch.setattr(Path, "home", classmethod(lambda cls: home)) + return home + + +def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(monkeypatch, tmp_path, isolated_host_home): """A ./gradlew-style repository launcher resolves against the sandboxed cwd. ``shutil.which`` resolves any command string containing a path separator @@ -1602,7 +1617,7 @@ def test_isolated_command_resolves_repo_local_launcher_against_sandboxed_cwd(mon assert command.endswith("./gradlew build") -def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypatch, tmp_path): +def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypatch, tmp_path, isolated_host_home): """A bare command resolves when PATH itself has a relative entry. ``shutil.which`` joins a relative ``PATH`` entry with the *calling @@ -1635,7 +1650,7 @@ def test_isolated_command_resolves_bare_command_via_relative_path_entry(monkeypa assert command.endswith("tool") -def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_mount(monkeypatch, tmp_path): +def test_isolated_command_translates_absolute_workspace_launcher_to_sandbox_mount(monkeypatch, tmp_path, isolated_host_home): """An absolute copied-repo launcher path is rewritten to its /workspace equivalent. ``isolated_command`` binds ``sandbox_root`` at ``SANDBOX_MOUNT`` inside @@ -1681,6 +1696,23 @@ def test_which_relative_to_cwd_returns_none_for_empty_path(tmp_path): assert result is None +def test_isolated_command_rejects_explicit_host_home_even_inside_workspace(monkeypatch, tmp_path): + """An allowed scratch workspace cannot turn a host-home executable into code.""" + monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) + sandbox = tmp_path / "sandbox" + repo = sandbox / "repo" + repo.mkdir(parents=True) + launcher = repo / "launch.sh" + launcher.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + launcher.chmod(0o755) + monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) + with pytest.raises(RuntimeError, match="host home directory"): + sandboxed_web_e2e.isolated_command( + str(launcher), backend="/usr/bin/bwrap", cwd=repo, + sandbox_root=sandbox, env={"PATH": "/usr/bin"}, + ) + + def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypatch, tmp_path): """A relative PATH entry cannot be used to search the real host filesystem outside the copy. @@ -1704,7 +1736,7 @@ def test_isolated_command_rejects_relative_path_entry_escaping_sandbox(monkeypat ) -def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path): +def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_path, isolated_host_home): """A repo-local launcher path that lexically escapes the sandbox root is still rejected.""" monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" @@ -1724,7 +1756,7 @@ def test_isolated_command_rejects_repo_local_path_traversal(monkeypatch, tmp_pat ) -def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path): +def test_isolated_command_rejects_explicit_external_path(monkeypatch, tmp_path, isolated_host_home): """An explicit absolute path outside the workspace and bind roots is still rejected.""" monkeypatch.setattr(sandboxed_web_e2e.shutil, "which", lambda *_args, **_kwargs: None) sandbox = tmp_path / "sandbox" 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"