diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index 95642b55c..41b2185e8 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -6,24 +6,30 @@ on: paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/ensure_opencode_asyncio_toolchain.sh" - "tests/conftest.py" - "tests/test_materialize*.py" - "tests/test_trusted_uv*.py" - "tests/test_uv*.py" - "tests/test_repository_branch_coverage_*.py" + - "tests/test_opencode_asyncio_toolchain_contract.py" - "requirements-opencode-review-ci-hashes.txt" + - "requirements-opencode-review-ci.txt" - "pyproject.toml" push: branches: [main] paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/ensure_opencode_asyncio_toolchain.sh" - "tests/conftest.py" - "tests/test_materialize*.py" - "tests/test_trusted_uv*.py" - "tests/test_uv*.py" - "tests/test_repository_branch_coverage_*.py" + - "tests/test_opencode_asyncio_toolchain_contract.py" - "requirements-opencode-review-ci-hashes.txt" + - "requirements-opencode-review-ci.txt" - "pyproject.toml" concurrency: @@ -109,6 +115,9 @@ jobs: - name: Install hash-locked quality tooling run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt + - name: Verify asyncio coverage toolchain imports + run: bash scripts/ci/ensure_opencode_asyncio_toolchain.sh + - name: Run trusted uv tests with complete branch coverage run: | cat >"${RUNNER_TEMP}/trusted-uv-coveragerc" <<'EOF' @@ -159,6 +168,7 @@ jobs: tests/test_uv_redirect_boundary.py \ tests/test_uv_workspace_fail_closed.py \ tests/test_trusted_uv_materializer_quality_workflow_contract.py \ + tests/test_opencode_asyncio_toolchain_contract.py \ tests/test_repository_branch_coverage_javascript_and_noema.py \ tests/test_repository_branch_coverage_review_schedulers.py \ tests/test_repository_branch_coverage_execution_sandboxes.py \ diff --git a/AGENTS.md b/AGENTS.md index 16f0981c0..e597f2e27 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,3 +5,4 @@ Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/hourly-nvidia-nim-autofix.md`](docs/doctoring/hourly-nvidia-nim-autofix.md). Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. +Coverage sandbox asyncio execution is pinned through `requirements-opencode-review-ci.txt` and `scripts/ci/ensure_opencode_asyncio_toolchain.sh`; quality CI must run that helper after the hash-lock install. Do not rewrite hashed `opencode-review-dispatch.yml` for that import. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6fe6621b6..c18ad6983 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -25,6 +25,27 @@ flowchart LR Products -->|"standalone or as module"| Buyer ``` +## Coverage asyncio toolchain + +The hash-pinned OpenCode review lock includes `pytest-asyncio==1.4.0`. +`scripts/ci/ensure_opencode_asyncio_toolchain.sh` fail-closed imports that +plugin after the quality job installs the lock. The independent +review-dispatch workflow remains the hashed review-agent key blob. + +```mermaid +flowchart LR + Lock["requirements-opencode-review-ci-hashes.txt"] + Quality["trusted-uv-materializer-quality-ci"] + Helper["ensure_opencode_asyncio_toolchain.sh"] + Image["Coverage image on merge"] + Buyer["Downstream marked asyncio suite"] + + Lock --> Quality + Quality --> Helper + Lock --> Image + Image --> Buyer +``` + ## Hourly NVIDIA NIM repair gate ```mermaid diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de9130a5..41dd64c7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Semantic Versioning where the repository publishes a release. ### Added +- Added hash-pinned `pytest-asyncio==1.4.0` and `typing-extensions==4.16.0` plus `scripts/ci/ensure_opencode_asyncio_toolchain.sh` so the coverage sandbox can execute marked asyncio tests without rewriting the hashed review-dispatch workflow. +- Execute that asyncio helper on the hash-locked quality job and collect a marked coroutine the same way a downstream buyer suite does, so an unread helper cannot claim the runtime is present. - Added a trusted pull-request comment router for `@cwl-noema-review` and review-only `@opencode-agent` dispatches, with an organization sweep, exact-head receipts, repository allowlisting, fixed runners, immutable checkout pins, and a permanent 100% statement/branch/docstring quality gate. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. - Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, NVIDIA NIM model boundary, credential isolation, and fail-closed unattended-agent permissions. diff --git a/CLAUDE.md b/CLAUDE.md index 7127d3c1c..818fbbef3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,6 +114,9 @@ repeatable compile command. without running the test suite will break CI. - **100% coverage and 100% docstrings on `scripts/ci/`** are hard gates, not aspirations. New helper code needs matching tests and docstrings. +- **Asyncio coverage plugin** is pinned in `requirements-opencode-review-ci.txt` and checked by + `scripts/ci/ensure_opencode_asyncio_toolchain.sh` after the quality job installs the + hash lock. Do not edit `opencode-review-dispatch.yml` to carry that import. - **`pull_request_target` trust boundary.** The required review workflows run the *base branch's* trusted scripts. A PR that edits the trusted review workflows can fail its own checks until the base branch catches up; a same-head manual `workflow_dispatch` Strix run may supply review evidence diff --git a/docs/doctoring/opencode-asyncio-test-runtime.md b/docs/doctoring/opencode-asyncio-test-runtime.md new file mode 100644 index 000000000..3f12a7e34 --- /dev/null +++ b/docs/doctoring/opencode-asyncio-test-runtime.md @@ -0,0 +1,69 @@ +# OpenCode coverage sandbox asyncio test runtime + +## Decision + +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include; a lone `--require-hashes` line is not lock evidence. + +The trusted OpenCode coverage image must execute repository tests that use +`pytest.mark.asyncio`. The pinned review toolchain therefore includes +`pytest-asyncio==1.4.0` and the Python 3.12 audit runtime's +`typing-extensions==4.16.0`. `scripts/ci/ensure_opencode_asyncio_toolchain.sh` +imports `pytest_asyncio` alongside `coverage`, `interrogate`, `pytest`, and +`pytest_cov`. The hashed `opencode-review-dispatch.yml` review-agent blob is +not rewritten to carry that import. + +A missing plugin is a coverage-evidence failure. It is not permission to skip +async tests and still claim 100% execution of the repository suite. + +The helper is not documentation. After the quality job installs the hash lock, +`trusted-uv-materializer-quality-ci.yml` executes the helper and the contract +suite collects a marked coroutine the same way a downstream buyer suite does. +pytest-asyncio registers through setuptools entry points, so installing the +pin is what makes `pytest.mark.asyncio` collect (Krekel et al., 2026; +Tvrtković, 2026). Keep that execution on the quality path instead of editing +the independent review-agent dispatch blob. + +## Why the pin is required + +PEP 492 defines native coroutines as first-class Python syntax (Selivanov, +2015). pytest does not run those tests unless an asyncio plugin is installed +in the same isolated image that records coverage. NIST SP 800-218 PW.4.1 +requires third-party software to come from expected, trusted sources with +integrity verification (Souppaya et al., 2022). ISO/IEC 25010 treats +functional completeness and testability as product quality characteristics; +an unread helper does not satisfy either (International Organization for +Standardization, 2023). The hash-pinned lock is that source; an untrusted +head cannot replace or omit the plugin. + +## Rollback + +Rollback requires an independently reviewed change that still executes marked +asyncio tests inside the same isolated coverage image. Removing +`pytest-asyncio` without a replacement plugin reintroduces silent skips. + +## Next action + +After this lands on protected `main`, rerun coverage on an affected async +consumer such as `ContextualWisdomLab/pg-erd-cloud` and keep the issue open +until that consumer's marked coroutine suite collects under the merged lock. + +## References + +International Organization for Standardization. (2023). *Systems and software +engineering — Systems and software Quality Requirements and Evaluation +(SQuaRE) — Product quality model* (ISO/IEC 25010:2023). +https://www.iso.org/standard/78176.html + +Krekel, H., & pytest-dev team. (2026). *pytest documentation*. pytest-dev. +https://docs.pytest.org/en/stable/ + +Selivanov, Y. (2015). *Coroutines with async and await syntax* (PEP 492). +Python Software Foundation. https://peps.python.org/pep-0492/ + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development +Framework (SSDF) version 1.1: Recommendations for mitigating the risk of +software vulnerabilities* (NIST Special Publication 800-218). National +Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 + +Tvrtković, T. (2026). *pytest-asyncio 1.4.0*. pytest-dev. +https://pypi.org/project/pytest-asyncio/1.4.0/ diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 367ac6285..1a78b9799 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -107,69 +107,88 @@ coverage==7.14.3 \ # via # -r requirements-opencode-review-ci.txt # pytest-cov -hypothesis==6.163.0 \ - --hash=sha256:002a9709345892279fb0e81b5a05b72d08cfe81f937339827be0d588607ca9b0 \ - --hash=sha256:00d3091b28de83c5116e0ccd9a4bcb28ef61d2aace5df91093bb22434fd2350c \ - --hash=sha256:0a0c396244c13805edcb73ff467c4c8178ccefc41c4ef5ed00a68e612fd773e9 \ - --hash=sha256:0a933aca9ebf9daf951d07cf01200c94c321b6ee0b42cc7b67675c9686d914c2 \ - --hash=sha256:0cba5202f74e7e4cdb676d86f26e8cc1b4fdc88f7f58ba73c8ac45b6b22f3070 \ - --hash=sha256:213527755f0fc2b1f3721e73fd60023e2752a48f914e3e2df8d35111956ae5c8 \ - --hash=sha256:21e72e8d5818e5ef8cd6a2191c386e3fd1a6d9e3739cf97289b4d9b5dbc8e38d \ - --hash=sha256:2849c23b2e0fe2eef4c1ec336b01eac7ad7397c49fca43c264f59ec1e6046eac \ - --hash=sha256:28a6cc1c25a6cc9b6ec079eaabd32ac769994831ecddd57123ce43c9056dcf34 \ - --hash=sha256:31dc46c48aa53c3ec92d03120978ca7f19b9cf96d195ed3fc93503f1433c94a6 \ - --hash=sha256:320b076bf6436f971f1c73ee651e60001226d1b4e341f2c4a1ca87248261ca03 \ - --hash=sha256:331906cb029b6b360b8ebac3ec00c3cfa720037fe2efb294a503a1979c9a9a8f \ - --hash=sha256:34fc895691a2420595506eb17f3a104f2fa9039f013c0770a6cc2743ccaf6fed \ - --hash=sha256:3b6cee2afe6c67b31a4a64b63a876e0b020befdc61daabea80f7a0e14f19203a \ - --hash=sha256:3f3cceb4720a39127622fbf3bcebe1775b894372c53b5edddfdef10bbdeef9ec \ - --hash=sha256:40dfab6fe6a02a80abef81aebf88e53cd529e3f2f6ba3486b674a67b1f4a3512 \ - --hash=sha256:4159a1c2560e10de51b1c14956e277eb1b37526c9abef9e87c1e531760486448 \ - --hash=sha256:487ab8ec2f01a225d6a1e2ceadc5290cde2c691952bd2e7f76199cf82e06fb25 \ - --hash=sha256:4ab0dadc09c537d4ac57e564039dfe7daf09c98375306d54bfc0fd6c218efcca \ - --hash=sha256:50073f8e63c1e7d3403899755657a990d8bba7b5b5bff66b1c56796d4969bb28 \ - --hash=sha256:520480d4bd3a17557616c25923640953e360332c89d012fffcebd69857e674a9 \ - --hash=sha256:52f16840add2eb02c2416f3b83cec4f527b6c19699f2d31eff4859233c715526 \ - --hash=sha256:56ed585baab75cb98462c57ca88bbdc6a9d935a14118dd572fb476c3ecec2a06 \ - --hash=sha256:58be45d1737bf8c2e10cf29505c0f10f8a23d61bc82e4339182a6c8251cbc2d9 \ - --hash=sha256:59f5fdb8addb44c17520a60d50542d9db6ceba577bbf54efefa9c10ee20be140 \ - --hash=sha256:5a3ac6c62d49f7fe518dfe7fa924fa03aac839993702207802b0e45f9e1b0dab \ - --hash=sha256:67d1593941ede41052b4a35ec25b50d0e280358c7674ef7812d520010e7e8bdf \ - --hash=sha256:6ae63dec6d1d467b7f4737455f81a7a82f14a41c14510937fcfbc726a085b5f8 \ - --hash=sha256:7a3db868a943c814cc557104712d43bf609adfe5ea9f708f38377d366b4855f8 \ - --hash=sha256:7ca7b20bf38d51e15f7808b0239791c4792b1709ce0c63093acaff56a09c31e6 \ - --hash=sha256:7cb3d927360fe73f9a06d646e6082237142ee39c24679c7133d22bf06dd03b45 \ - --hash=sha256:7ef8954e37c80e0c46e6161eef1c72c71059b95250e620a77bd646f6c7a52a2d \ - --hash=sha256:8aac96db8a6c7ee43aba2ee0d3c43893da1fb7c38ed54790c1be2b6d8fd87b96 \ - --hash=sha256:8c5d1e6bad47edf6fb1d7406cf6d67314ac08325c63a49550d782a4596ea302b \ - --hash=sha256:9105c66ea8dbc108adc42058bb7b65bd953f53ee178bf63bf9ebb0cded6c8c96 \ - --hash=sha256:9be37b7ddf0af9e3f9112cd133afc34e78a56da1f96db5f2b4fc289fe1c4d1c3 \ - --hash=sha256:9c084749c115ea7918cf7efa144682783da17eec70d1276689182b871126e715 \ - --hash=sha256:9d23f0f3a14bb6e6f99c793d340196dba4af95ba25bfcab624d1794f540f5e27 \ - --hash=sha256:a16ebce774755a7a652bd44c62101dc914372ed1a98935969624848c9627b4a4 \ - --hash=sha256:a2a20e9835d3c4b293a709ee6ef769bcb18c6ed4ef337a9e251c1a9496d5e8be \ - --hash=sha256:a57352efa938889ea9992667a5014c0fc870d03945de71918574d1cf28276378 \ - --hash=sha256:ab34c61d9249f1a8129cb4276062c04e3e47b5be8de6446e7c7fe11362d6fe43 \ - --hash=sha256:b123b4995a7612f1130e2b2362c9a5d0568df887bf7e7bdb45c23af8cd5423c9 \ - --hash=sha256:b268211e625cd550e361fc387bf1db5deb1e9cae0ce4041116f0a0aafeef7c06 \ - --hash=sha256:b2ddcdaf6691101e06dc4a5add7b8c8fdf1e68daba599255a281f3f3550d3331 \ - --hash=sha256:b4ad2134405d5345434c22dea96bbc12c85abcfc3c253a8063dbc9ff01164555 \ - --hash=sha256:b839dfd1342bb50570cb0c66b80322307cdb468abf14faf5df4dab022bc1b9ce \ - --hash=sha256:b8f22fb8218ba6a452bf9000fc656e1ed57625d17cc8a3871a0fcea3b1b69ebf \ - --hash=sha256:bd312b15044b1c1a0920a5827a830559b2d1fa380851cedf509f8b835309c5b9 \ - --hash=sha256:c0ec3b709508ccd835d8ded1db025b7800618f2289a22a6bfd4927da5f4eb33c \ - --hash=sha256:c4f5be1482189c7b0a1dcac269fffe97a7d18cc04ac9a9a4d6613212dd87f38b \ - --hash=sha256:ca1b48bde68c528a79dec2a2859e05035802e5b1c9c3579f388c9de6ed6d0148 \ - --hash=sha256:d0838a28e9943d5b834ebae59b02adda76e2cd1e65caa808104c72102052057d \ - --hash=sha256:e165f6cc2075059b7c95dac1612bfb25494f72d90f56880e84c288b089f8a896 \ - --hash=sha256:e568a3d766b7ba8df00e0c33efc4c6530cde14fbc72daabe4824eed211ed7596 \ - --hash=sha256:ee47c2cb1be03a052ebd3549dad07f636a98b3ccfd7acbe5e17b3b7da0ab9e37 \ - --hash=sha256:f1fe222f50a1898e87a1e7323ab35f9e956278efabe4dd55a1342808206d05ad \ - --hash=sha256:f28ad27193c1fbcfb52ef2ee63d2b721563525089e80962b4268b306dac45507 \ - --hash=sha256:f2f1b67a48da86d3e41c9445367b49a49f7efdb60fc8b5e3593f05e6afb2efbe \ - --hash=sha256:f7f706df6839dcc53f20833f2933cbcd126fd2fdee7c312e053de49df4b64e44 \ - --hash=sha256:fae7305ae20fddeea09df317b920c45d3e20bfedbdb041f4db6ca5267c458189 \ - --hash=sha256:ffdda3006a383a48f71a23b4f2b3fae3fe1b09af67925d885985f7ec34d66bcb +hypothesis==6.165.9 \ + --hash=sha256:01a2a4106fa90396ffa1c402968a8bd25af056e1fdf7974a369c4057cc4035f1 \ + --hash=sha256:0a4a32dc8e8728417aa13055903dbf742dd0b5cedda7c2031b4b2a0bcc1d463d \ + --hash=sha256:0bd589a011b3d0c4b98d78f467e5a5a6b96bf5be5d15b39ef47e65c225e8d790 \ + --hash=sha256:0c7f0507d8224e5b44c2651d7ef8df15ac6d38eb56c98e7f451c3090804ce109 \ + --hash=sha256:0d4a613404ba216b6f698ce9df6f9c22d04d8e8a571c42525e83905cf1b1cd10 \ + --hash=sha256:0df8f8736d176d3a5092890b6fb9030f9c726d272b52af0e1ce887d62fc21699 \ + --hash=sha256:1000afaf05be59c88c13b458fd74d123e3361ca3d14fe127814a1231cb9cb7df \ + --hash=sha256:10e826a708456c965e125f15dbf5a6f463e41dcf230a930b7a3e0ce7036f5176 \ + --hash=sha256:11bcdfb14b52d41384cc14ecd3f6d6152735bc2f7af0b9f3fa8685d630a1e98d \ + --hash=sha256:126e7bf873892649cc5f697c3b3a6548f077a56ae3601b22fddef2ea1ba6f64d \ + --hash=sha256:1551df11534d192616dc1b4539b6e526c4704d08b22d2ffb6ea3479ce6b87d94 \ + --hash=sha256:1aaba05b943345d2a448b8c1c795bb191adf842b450bfc7207bdd1bf139a9b58 \ + --hash=sha256:20b177ef8aae2a2817a2223d103353d120118e686a152bcd01832a27ddc9388e \ + --hash=sha256:22dd9e264f2a79a1b63a5fae13351e3cf0088f584fb05bb0e23f21b8fba9df7c \ + --hash=sha256:2b0a64a05255caa9a5ad90e1ab57eb26d64123b005234e22c80e2f723832e047 \ + --hash=sha256:2f255b389364f71e6e9305ba99bf13733354cb0dc4a97d07db8aef76278a345e \ + --hash=sha256:306c1efc0f01f3d5c29c45996e055437971337aa24b6c6735b0514cfb5df60ce \ + --hash=sha256:3295f35299c6d4f7bc12dc583bdca776a5e41cf22a185e5209800885950bb163 \ + --hash=sha256:34a4591db1dcf6aa544b4f884520ff0f1bc02ac3d11c76f78c73535b6b672272 \ + --hash=sha256:41082aff3e438ac1eeb86002c269b9b5d86bab563e911614c2c8d34f405e02db \ + --hash=sha256:431ab894062666c93157234f9949a4db3549fbf3c220777abae3482e1b43f9bc \ + --hash=sha256:488b88b6856e430763a7ac5a5fc6e20a798766dca8df8f94576de2281ee60afd \ + --hash=sha256:4a2081699e0b16b35c7b34cf0ff01997e8fc6038b520b9f29c106764158c411a \ + --hash=sha256:4d7bbb89c256d02406b7875dbe4058ab79fba6d8e6b56dd1d771e0c9800a02d4 \ + --hash=sha256:59f29ef2965caf1ae367ee0598ac0d293551b95744a5d49888737a3b3d9dd944 \ + --hash=sha256:5d40a92c6d025725da7827431a71b7657ce2e3923ff8f1773d6b15d717f77ea0 \ + --hash=sha256:5d755b56098801a28a419172e57beb536b8c32929e7a37bb5ef12f2c00abd284 \ + --hash=sha256:5f6b4ae650480368e2477e8f875cdbfe8533a7b2e09f049fe206c48b20174622 \ + --hash=sha256:6564fb9bbe1760719070a6fd37335fbf43696fe2911c386ba55fb3cbaaafb82a \ + --hash=sha256:658c33d8a54cead6081f73fcc4e2c0fbe43d251d3f4170630748d8e5dcbd542f \ + --hash=sha256:676a3bd04261b7bdd35374a3f4dabd82025fcead110a067a76328799f394a4d0 \ + --hash=sha256:67a0c0346ac0e8e106fad0224269a87c665768cc8d45de6e367e9b260ef36d5a \ + --hash=sha256:70fe20939f63eb661227fd2b05193eba1e5ea6e547a2fbe158839f2acc519f4f \ + --hash=sha256:7186be7b5ce1aee61614ede14fc6f3f072d94ab3c3549613903cd1c9a37848ea \ + --hash=sha256:740074d1b6ae4a5335b7a17f44f776c3952d6c7f4a6c84f23ae93e1907834692 \ + --hash=sha256:7812f1ce9e2bfc0958f2a96975f16a1e1f2400f9d8b7043723ff5811647fe0f6 \ + --hash=sha256:7d3297fe95a253cea81b48e581e456105016ebe782d823dfb57ec09c3854301f \ + --hash=sha256:7fc9f46d88732fc74539ca7748aedc736c5e111431b06bd8ef2534440379aff2 \ + --hash=sha256:8401f55aba2d51f20b02c42e9e08b7eff2018639bdb1af16a4c279ba7e129c81 \ + --hash=sha256:84a93a152ffaf92fa6b3f94d366f4c866e26f3f1b643fcc6da4ee2ddfa038aa9 \ + --hash=sha256:8795161db0a7c270c4f3e0eb754bd0ed96b17133d6f851c7444f9733c4c9d37f \ + --hash=sha256:88bde76aaab7f8abcd9412d135eaa49e8f7d89d068188fae2768a7803c80df89 \ + --hash=sha256:8c3d7d54802641c2f8b49f478f153567770d299d639ee38fc0a0fa3d95ee07cb \ + --hash=sha256:8c7dcb0a21dab077e65bcb082ee321b7e31402cf980da648b4369c71086ad53c \ + --hash=sha256:92b264bef7860bf37b5f22b5949083d26f113351da8b99e93adb0554d3e2c110 \ + --hash=sha256:9bc2d40dd2a71ba3971b902d31c8b1b0c40d3d465591fd71bb8ce6f541add530 \ + --hash=sha256:a5d1fa312020b499c517e1217ef4440c56a9b52e20b47b420b48a583a81c412b \ + --hash=sha256:a7f51c2297d116c2c025643762269eed0767589b6fc858d8de69d9f80cf33a23 \ + --hash=sha256:a8b2d7415b1f7ceec6e689c992b919af41e339eafb35d6367a3abbb5b6db9753 \ + --hash=sha256:a9995cbf91e0b22cb8d9aaaf6b10994b2ac1ebc3b01b4a91d3e82761a2737043 \ + --hash=sha256:a9a1e17200a2b369a38c19c2184efbc041a40b784675ba72ac6800d8c1260a54 \ + --hash=sha256:ae34d13d8438ea7f3ee18292290b9f608de6070309f3c5826644a88d197c319d \ + --hash=sha256:af42ad4c24b69d4482e4813e148f458fc090911c0cf9055225c62fbd1f0b102e \ + --hash=sha256:afd99a069df90c73c15d53e275d11a782fd5f17f5423002868ce9d2438ab567f \ + --hash=sha256:b80ffe65bed7f42681b845bdfde445c3da46a84eb2c4fdbc928521786b551c65 \ + --hash=sha256:b8f5b63a8fba74d19a12ad4abaa95afb7466d66d1adc90ba3026e40b7924452c \ + --hash=sha256:bab95b4ef980cbc2b4632e49057588766cda930ffbd727d70edfea289da98dd3 \ + --hash=sha256:be54673f76785ff954b511745d4a7a18c7d7fe1d893cb81a8917cd28878fbd55 \ + --hash=sha256:c01b77160328d8fce22bb5121fe1f5fee36b434a1220603500605b88ab854d7a \ + --hash=sha256:c3432a48e5895cba6b36c3e5abb915163d5169dc755685b4cd4720b710d77e3c \ + --hash=sha256:c7edcc3dcec42b413c900f8e660f3741c21f0055c53bde21d728f72a21713247 \ + --hash=sha256:c908edd69e4308c3cb194f0853aa7dfaa15e905242e81646b9f12881cd2683d3 \ + --hash=sha256:cc98fa897f48127fdf2e7d1b2428f839656cd52a6344095e501371d222b9c19d \ + --hash=sha256:d0fb16358e29f3090a9ab529e84df52923ddcd8505785c019bf48cdcd45e094a \ + --hash=sha256:d49496271282bf424ddd728108f49b92e1d32066a3d0ba18990d5b01c40c3175 \ + --hash=sha256:d69ab6500e47d7c21310360d1ee5cb6441660a10408e856d3738f07536732ed8 \ + --hash=sha256:d7871212e7a1e9d317b2cc11934c6f28aaaa48b594231165a0963d3b3f5159b2 \ + --hash=sha256:d8c3e8c45ca5e2e4f519021836e5d86fb2229915c3f92fb50db7bb7364dfb2c1 \ + --hash=sha256:e1d9492193b52087df4d264edd77618869d447b5c7f6a022c7bb11b88d577cb8 \ + --hash=sha256:e500b97f899e375e47b407a48f3450cb1dd44bca757529c0490cb4533d878b30 \ + --hash=sha256:e50c687c0e86bb5898f36b845ab84439f44ef2369f0f9bbcbf6aa6860640324d \ + --hash=sha256:e547823eca16eab9c9569f292e5ce8f95d8a59f1d67e504e97a841d17eb0b562 \ + --hash=sha256:ed61ad45184997f97c3baf255f34aaaf4edff63f1a61aad87919034b4ab9f64e \ + --hash=sha256:f52fd4f1b95f614c3fec53840d603f0a3b19e68eaa91c8d609222999c3ee5506 \ + --hash=sha256:f67414675f2263948e5a5977b636b5cb1cc5f33b82a9be34f8a6b9a8293d4707 \ + --hash=sha256:f6caa4194f37a02232ecb964600d17facc23a6aa5b0295008989f31e329eb837 \ + --hash=sha256:f82aed6f0631b5073bd87789aa56609e7c83bd762384fd24c84f3435816b2569 \ + --hash=sha256:f94e4456f7b0dd04f4f80f26a2c499370f6048f393ff61436de4e192e278e2df \ + --hash=sha256:f96f36c3d3677f22ff990d7aab4235576a8429e72dab5382c45bf489630972f5 \ + --hash=sha256:fba20d9c60d6b8aec76f32cd22109e1b1f19a9985317255e2b43940d27996097 \ + --hash=sha256:fd9e242fabb05715ec2d3b9b96dcddf683fc3891a04b6685b79e400e35ccbe18 # via -r requirements-opencode-review-ci.txt iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ @@ -179,9 +198,9 @@ interrogate==1.7.0 \ --hash=sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0 \ --hash=sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 # via -r requirements-opencode-review-ci.txt -packaging==26.2 \ - --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ - --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via pytest pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ @@ -202,7 +221,12 @@ pytest==9.1.1 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c # via # -r requirements-opencode-review-ci.txt + # pytest-asyncio # pytest-cov +pytest-asyncio==1.4.0 \ + --hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 \ + --hash=sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42 + # via -r requirements-opencode-review-ci.txt pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 @@ -215,6 +239,10 @@ tabulate==0.10.0 \ --hash=sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d \ --hash=sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3 # via interrogate +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via -r requirements-opencode-review-ci.txt uv==0.11.25 \ --hash=sha256:2c1cfe97dce56c997dfa3214bdb8955b7b34cceea7505520185e22ad99c0eb6b \ --hash=sha256:3febca65ec5bc336ddaf7e4f724704f2c894c16839723df14865ee00b4acf38d \ diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index 0b585231f..317ed628a 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -5,5 +5,7 @@ coverage==7.14.3 hypothesis>=6.100 interrogate==1.7.0 pytest==9.1.1 +pytest-asyncio==1.4.0 pytest-cov==7.1.0 +typing-extensions==4.16.0 uv==0.11.25 diff --git a/scripts/ci/ensure_opencode_asyncio_toolchain.sh b/scripts/ci/ensure_opencode_asyncio_toolchain.sh new file mode 100755 index 000000000..33671c7c8 --- /dev/null +++ b/scripts/ci/ensure_opencode_asyncio_toolchain.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Fail closed unless the reviewed coverage toolchain can import pytest-asyncio. +set -euo pipefail + +python3 -I -c 'import coverage, interrogate, pytest, pytest_asyncio, pytest_cov; print("trusted offline Python test toolchain imports passed")' diff --git a/tests/test_opencode_asyncio_toolchain_contract.py b/tests/test_opencode_asyncio_toolchain_contract.py new file mode 100644 index 000000000..4bccc2036 --- /dev/null +++ b/tests/test_opencode_asyncio_toolchain_contract.py @@ -0,0 +1,116 @@ +"""Permanent contract for the trusted asyncio coverage toolchain.""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_HELPER_PATH = _REPOSITORY_ROOT / "scripts/ci/ensure_opencode_asyncio_toolchain.sh" +_REVIEW_REQUIREMENTS_PATH = _REPOSITORY_ROOT / "requirements-opencode-review-ci.txt" +_REVIEW_HASH_LOCK_PATH = _REPOSITORY_ROOT / "requirements-opencode-review-ci-hashes.txt" +_QUALITY_WORKFLOW_PATH = ( + _REPOSITORY_ROOT / ".github/workflows/trusted-uv-materializer-quality-ci.yml" +) +_IMPORT_LINE = ( + "import coverage, interrogate, pytest, pytest_asyncio, pytest_cov" +) +_PYTEST_ASYNCIO_HASHES = ( + "933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", + "c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", +) +_TYPING_EXTENSIONS_HASHES = ( + "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", + "dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", +) + + +def test_opencode_coverage_toolchain_supports_repository_asyncio_tests() -> None: + """Keep the trusted coverage sandbox able to execute marked asyncio tests.""" + + requirements = _REVIEW_REQUIREMENTS_PATH.read_text(encoding="utf-8") + hash_lock = _REVIEW_HASH_LOCK_PATH.read_text(encoding="utf-8") + helper = _HELPER_PATH.read_text(encoding="utf-8") + + assert "pytest-asyncio==1.4.0" in requirements + assert "pytest-asyncio==1.4.0" in hash_lock + assert _IMPORT_LINE in helper + for digest in _PYTEST_ASYNCIO_HASHES: + assert digest in hash_lock + + +def test_opencode_hash_lock_is_installable_by_python_security_runtime() -> None: + """Keep the Python 3.12 audit runtime's conditional dependency pinned.""" + + requirements = _REVIEW_REQUIREMENTS_PATH.read_text(encoding="utf-8") + hash_lock = _REVIEW_HASH_LOCK_PATH.read_text(encoding="utf-8") + + assert "typing-extensions==4.16.0" in requirements + assert "typing-extensions==4.16.0" in hash_lock + for digest in _TYPING_EXTENSIONS_HASHES: + assert digest in hash_lock + + +def test_asyncio_helper_is_valid_fail_closed_bash() -> None: + """Reject a helper that cannot be parsed before quality CI executes it.""" + + bash = shutil.which("bash") + if bash is None: + pytest.skip("bash is required to syntax-check the asyncio helper") + + result = subprocess.run( + [bash, "-n", str(_HELPER_PATH)], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + +def test_quality_ci_executes_asyncio_helper_after_hash_lock_install() -> None: + """Keep the helper on the installed lock path, not as unread documentation.""" + + workflow = _QUALITY_WORKFLOW_PATH.read_text(encoding="utf-8") + install_index = workflow.index( + "python -m pip install --disable-pip-version-check --require-hashes " + "-r requirements-opencode-review-ci-hashes.txt" + ) + helper_index = workflow.index( + "bash scripts/ci/ensure_opencode_asyncio_toolchain.sh" + ) + assert helper_index > install_index + assert workflow.count('"scripts/ci/ensure_opencode_asyncio_toolchain.sh"') == 2 + assert workflow.count('"tests/test_opencode_asyncio_toolchain_contract.py"') == 2 + assert workflow.count('"requirements-opencode-review-ci.txt"') == 2 + assert "tests/test_opencode_asyncio_toolchain_contract.py" in workflow + + +def test_pinned_plugin_collects_a_marked_coroutine_like_a_buyer_suite( + tmp_path: Path, +) -> None: + """Reproduce the downstream failure: marked async tests must collect and pass.""" + + pytest.importorskip("pytest_asyncio") + sample = tmp_path / "test_buyer_async_suite.py" + sample.write_text( + "import pytest\n" + "\n" + "@pytest.mark.asyncio\n" + "async def test_marked_coroutine_reaches_an_assertion() -> None:\n" + " assert True\n", + encoding="utf-8", + ) + result = subprocess.run( + [sys.executable, "-m", "pytest", str(sample), "-q"], + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "async def functions are not natively supported" not in result.stdout + assert "async def functions are not natively supported" not in result.stderr diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py index 23a849bd8..3d52a7620 100644 --- a/tests/test_trusted_uv_materializer_quality_workflow_contract.py +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -20,12 +20,15 @@ def test_quality_workflow_runs_for_every_materializer_surface() -> None: required_paths = ( '".github/workflows/trusted-uv-materializer-quality-ci.yml"', '"scripts/ci/materialize_base_python_requirements.py"', + '"scripts/ci/ensure_opencode_asyncio_toolchain.sh"', '"tests/conftest.py"', '"tests/test_materialize*.py"', '"tests/test_trusted_uv*.py"', '"tests/test_uv*.py"', '"tests/test_repository_branch_coverage_*.py"', + '"tests/test_opencode_asyncio_toolchain_contract.py"', '"requirements-opencode-review-ci-hashes.txt"', + '"requirements-opencode-review-ci.txt"', '"pyproject.toml"', ) for required_path in required_paths: @@ -80,6 +83,8 @@ def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> assert "unset COVERAGE_RCFILE" in workflow assert "python -m interrogate --fail-under 100" in workflow assert "python -m compileall -q" in workflow + assert "bash scripts/ci/ensure_opencode_asyncio_toolchain.sh" in workflow + assert "tests/test_opencode_asyncio_toolchain_contract.py" in workflow required_tests = ( "tests/test_materialize_base_python_requirements.py",