diff --git a/.github/workflows/capsule-customer-execution.yml b/.github/workflows/capsule-customer-execution.yml index 38334214..b3582378 100644 --- a/.github/workflows/capsule-customer-execution.yml +++ b/.github/workflows/capsule-customer-execution.yml @@ -47,6 +47,8 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version: ${{ matrix.python }} + - name: Install declared external corpus native requirements + run: sudo apt-get update && sudo apt-get install -y libodbc2 bubblewrap - name: Prepare fresh customer-owned installation shell: bash run: | @@ -107,6 +109,11 @@ jobs: set -euo pipefail # Administrator prerequisite only: all installation and analysis runs as the runner user. # Keep Ubuntu's global AppArmor namespace restriction enabled. + sudo install -d -o root -g root -m 0755 /specfact-corpus-offline + sudo install -o root -g root -m 0555 /usr/bin/bwrap /specfact-corpus-offline/bwrap + sha256sum /usr/bin/bwrap | cut -d ' ' -f 1 | sudo tee /specfact-corpus-offline/bwrap.sha256 > /dev/null + sudo chmod 0444 /specfact-corpus-offline/bwrap.sha256 + echo "SPECFACT_CORPUS_OFFLINE_LAUNCHER=/specfact-corpus-offline/bwrap" >> "$GITHUB_ENV" profile="$CUSTOMER_ROOT/capsule-userns.apparmor" cat > "$profile" <, @@ -114,6 +121,9 @@ jobs: profile specfact-customer-capsule "$CUSTOMER_ROOT/cache/**/opt/specfact/bin/bwrap-static" flags=(unconfined) { userns, } + profile specfact-customer-offline-network "/specfact-corpus-offline/bwrap" flags=(unconfined) { + userns, + } profile specfact-customer-capsule-umask "$CUSTOMER_ROOT/cache-077/**/opt/specfact/bin/bwrap-static" flags=(unconfined) { userns, } @@ -127,6 +137,12 @@ jobs: python scripts/capsule_customer_gate.py \ --repository "$GITHUB_WORKSPACE" \ --evidence "$CUSTOMER_ROOT/evidence" --mode "$MODE" + - name: Validate pinned external Python corpus with automatic runtimes + shell: bash + run: | + set -euo pipefail + unset GITHUB_TOKEN GH_TOKEN PYTHONPATH + python scripts/external_capsule_corpus.py --workspace "$CUSTOMER_ROOT/external" - name: Upload reports and exact installation evidence if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -134,6 +150,7 @@ jobs: name: capsule-${{ env.MODE }}-cp${{ matrix.python }}-${{ github.run_attempt }} path: | ${{ env.CUSTOMER_ROOT }}/evidence/ + ${{ env.CUSTOMER_ROOT }}/external/evidence/ ${{ env.CUSTOMER_ROOT }}/denied-evidence/ ${{ env.CUSTOMER_ROOT }}/installation-evidence/ ${{ env.CUSTOMER_ROOT }}/installed-*.txt diff --git a/.github/workflows/pr-orchestrator.yml b/.github/workflows/pr-orchestrator.yml index 6b55f6ac..cbb7cde8 100644 --- a/.github/workflows/pr-orchestrator.yml +++ b/.github/workflows/pr-orchestrator.yml @@ -43,6 +43,8 @@ jobs: - "packages/specfact-code-review/**" - "registry/**" - "scripts/capsule_customer_gate.py" + - "scripts/external_capsule_corpus.py" + - "tests/fixtures/portable-runtime/**" - "scripts/link_dev_module.py" - "publish_bundle_selection.py" - "tests/unit/test_publish_bundle_selection.py" diff --git a/docs/_data/nav.yml b/docs/_data/nav.yml index caa9704b..405b67df 100644 --- a/docs/_data/nav.yml +++ b/docs/_data/nav.yml @@ -85,6 +85,9 @@ - title: Run url: /bundles/code-review/run/ expertise: [intermediate, advanced] + - title: External Python runtimes + url: /guides/portable-project-runtime/ + expertise: [intermediate, advanced] - title: Ledger url: /bundles/code-review/ledger/ expertise: [intermediate, advanced] diff --git a/docs/bundles/code-review/run.md b/docs/bundles/code-review/run.md index 4d7212d1..8ca948b8 100644 --- a/docs/bundles/code-review/run.md +++ b/docs/bundles/code-review/run.md @@ -199,6 +199,12 @@ specfact code review run --scope changed --enforcement shadow --focus simplify - Simplify-focused reports keep advisory `ai_bloat` findings plus high-confidence `dry` and `kiss` findings that include deterministic simplification metadata. Metadata fields such as `rewrite_hint`, `canonical_pattern`, `intent_key`, `estimated_deletion_lines`, `related_locations`, `signal_trace`, `preserve_reasons`, and `remediation_packet` are additive; legacy consumers can keep reading the original finding fields. The report-level `cleanup_forecast` summarizes reviewed LOC, estimated deletion ranges, guidance-kind totals, normalized AI-bloat density, weighted bloat points, and cleanup-yield LOC per KLOC. Simplification findings remain score-neutral; enforce mode blocks only unresolved safe-mechanical cleanup candidates. +## External project runtimes + +Dependency-sensitive review automatically prepares an isolated project runtime. Use `--project-config PATH` to select a package manager, environment, extras, or test groups, and `--project-runtime PATH` to attach the descriptor returned by `specfact code review runtime prepare --json`. Inspect discovery without installation using `specfact code review runtime inspect --json`. + +See [Review external Python projects](/guides/portable-project-runtime/) for pip, Hatch, uv, Poetry, offline reuse, and incomplete-evidence diagnostics. Local runtime provenance cannot authorize protected `pr_range` evidence. + ## Related - [Code review ledger](/bundles/code-review/ledger/) diff --git a/docs/guides/portable-project-runtime.md b/docs/guides/portable-project-runtime.md new file mode 100644 index 00000000..18480b05 --- /dev/null +++ b/docs/guides/portable-project-runtime.md @@ -0,0 +1,123 @@ +--- +layout: default +title: Review external Python projects +permalink: /guides/portable-project-runtime/ +description: Prepare repository dependencies in an isolated sidecar runtime for capsule review. +keywords: [capsule, review, Python, pip, Hatch, uv, Poetry] +audience: [users, developers] +expertise_level: [intermediate] +doc_owner: specfact-cli-modules +tracks: + - packages/specfact-code-review/** +last_reviewed: 2026-09-14 +exempt: false +exempt_reason: "" +--- + +# Review external Python projects + +Portable project runtimes let the code review capsule use a repository's own dependencies and test configuration. Runtime acquisition runs in a disposable builder; analysis runs offline in separate target workers. The signed supervisor keeps its own dependencies. Analyzer-owned imports follow the recorded member graph and resolve from the verified analyzer installation before project paths or startup imports are considered; project-owned dependencies keep their selected versions. Member startup preserves ordinary editable hooks, restores analyzer lookup precedence afterward, and reports hooks that replace analyzer dispatch or protected import machinery, or add paths outside the copied runtime. Startup rejection remains incomplete analysis even when a hook prints output that resembles analyzer results. These startup checks do not sandbox arbitrary Python code within one interpreter. A namespace shared between analyzer-owned and project-owned portions is not currently combined; that configuration reports an origin mismatch and incomplete analysis. + +Execution requires Linux x86-64 with the capsule's supported Python 3.11, 3.12, or 3.13 ABI and working unprivileged user namespaces. A local descriptor does not grant protected pull-request authority. + +Windows and macOS host-to-worker setup is not implemented or validated by this change. Runtime inspection reads project metadata, but automatic preparation and capsule execution require the Linux environment above. A future container or VM handoff must validate source transfer, architecture, native dependencies, and returned evidence. Linux execution cannot establish Windows- or macOS-specific test behavior. + +## Inspect and review + +From the repository root, inspect the selected manager and input identities without installing dependencies: + +```bash +specfact code review runtime inspect --json +``` + +Existing review commands prepare the target runtime automatically before dependency-sensitive analysis: + +```bash +specfact code review run src/example.py tests/test_example.py --bug-hunt --json --out /tmp/review.json +``` + +A single declared test dependency group or test extra is selected automatically. Explicit groups and extras suppress automatic selection of the other category; competing choices require configuration. Hatchling alone is a build backend, so it does not imply a Hatch environment. Competing environment choices produce a diagnostic; select the intended environment explicitly. Discovery validates the metadata tables it consumes before choosing an environment, including when a manager is explicitly selected; malformed tables identify the source file and section. + +## Select an environment + +Store selection outside the reviewed source when possible. For a detached Hatch environment: + +```toml +manager = "hatch" +environment = "review" +``` + +For a uv project with a test group: + +```toml +manager = "uv" +groups = ["tests"] +``` + +For a Hatch test matrix, `environment = "hatch-test"` selects its unique native environment matching the selected Python version. Multiple matching matrix entries require a concrete name such as `hatch-test.py3.12-feature`. Selection uses Hatch's exported configuration. + +The `python` setting overrides `.python-version`; both are checked against `requires-python` and the exact signed interpreter version. A root `.python-version` also activates automatic preparation for source-only projects without packaging metadata. A compatible controller ABI is reused; an incompatible controller does not force that ABI onto the project. Base and head reviews select their workers independently. + +Poetry Python constraints are intersected with project `requires-python` before choosing a worker. PEP 440 ranges are supported; Poetry caret, tilde, union and table syntax currently produce an explicit unsupported-constraint diagnostic. Express the declaration as an equivalent PEP 440 range to proceed. + +Supported manager names are `pip`, `hatch`, `uv`, and `poetry`. Optional fields are `python`, `groups`, `extras`, `requirements`, `constraints`, `source_roots`, `native_libraries`, and `native_tools`. Explicit `requirements` and `constraints` fields apply to `pip`; other managers reject these fields with a diagnostic rather than ignore them. Paths are repository-relative. Native libraries use ELF names such as `libodbc.so.2`; unavailable libraries are named in the preparation diagnostic. Hatch extras and dependencies belong in the selected native Hatch environment. + +Repositories whose tests invoke external programs can explicitly declare the supported Linux capabilities `git`, `uname`, and `sed`: + +```toml +[tool.specfact.code-review] +native_tools = ["git", "uname", "sed"] +``` + +Alternatively, put this in an external `/tmp/review-runtime.toml` without editing the repository: + +```toml +native_tools = ["git", "uname", "sed"] +``` + +An explicit `native_tools` field in `--project-config` overrides the repository declaration; a configuration without that field retains it. Preparation captures the declared executables, Git HTTP and local transport helpers, and their ELF library closure before dependency builds. Their hashes and generated launchers are recorded in the runtime inventory and cache identity. Tools are installed into the target runtime after the build; conflicting package-provided command names fail explicitly. Git uses the selected capsule's sealed `/bin/sh`. Other programs and Git transports requiring additional external clients, such as SSH, remain unsupported unless separately available through declared project dependencies; host PATH directories are never imported. Target execution remains offline even when Git HTTP helpers are present. Workers retain the signed capsule's public certificate bundle so dependencies can initialize TLS; host trust stores and credentials are not imported. Missing capabilities and incompatible native binaries produce preparation diagnostics. + +```bash +specfact code review runtime prepare --project-config /tmp/review-runtime.toml --json +specfact code review run src/example.py tests/test_example.py --project-config /tmp/review-runtime.toml +``` + +Plain package-index dependencies do not require Git. Preparation stages available Git capabilities; a dependency that actually needs a missing Git executable or HTTP transport fails with a preparation diagnostic and installation remedy. A newly initialized repository without a first commit has no VCS version context; ordinary dependency discovery can still proceed. Missing requested revisions or corrupt Git state remain errors. + +Preparation returns the path to `project-runtime.json`. Generated environments, resolutions, and inventories stay in the private cache, outside customer source. Keep descriptors private: they contain project dependency and path metadata. Index credentials are excluded from published descriptor inventory; private failed-build logs are owner-readable. + +## Reuse and attach + +```bash +specfact code review runtime prepare --project-config /tmp/review-runtime.toml --offline --json +specfact code review run src/example.py tests/test_example.py --project-config /tmp/review-runtime.toml --project-runtime /path/from/prepare/project-runtime.json +``` + +Attachment verifies the payload, project inputs, ABI, and analyzer worker identity. Changed workspace source invalidates built project packages as well as changed dependency files. Git commits, tags, and shallow-history boundaries also participate in cache identity for dynamically versioned packages. Staged reviews bind the captured Git tree separately from its HEAD commit, so private build copies retain the staged index and sanitized VCS metadata even if the live index later changes. Private Git metadata contains only the object closure recorded by the selected commit, staged tree, tags, and shallow boundary; unrelated branch refs and unreachable objects are excluded. A corrupt cache is rejected; remove only the identified invalid artifact and prepare again. Interrupted preparations do not publish partial descriptors. + +## Interpret incomplete evidence + +Runtime failure preserves independent static findings. Affected analyzers reference the preparation diagnostic and remain incomplete. A real missing import after successful preparation remains a finding. A report can contain real failures while still showing incomplete required evidence. + +Runtime attachment adds import roots only when declared through pytest `pythonpath` or explicit review `source_roots`. It does not infer `src/` or repository-root overrides from layout: installed packages, build-generated contents, editable hooks, and native pytest import modes retain their normal precedence. + +Pytest configuration and selection controls remain active. Full reviews leave collection roots to pytest, including its configured path expansion and fallback when configured test paths are missing. Targeted reviews retain explicit test selection. Deselected tests are recorded separately from the selected execution inventory; distributed deselection visibility is labelled as controller-observed. Quoted pytest paths and file patterns preserve spaces using native argument rules. Malformed tables, invalid path/pattern value shapes, and unclosed quotes identify the section or option and configuration file. The report records collection, execution, setup and internal errors, configured options, versions, and coverage. Distribution-owned commands are available inside target workers. Localhost coordination for plugins stays inside the offline namespace. Collection-only runs, early stopping that leaves collected tests unexecuted, and absent required coverage do not become successful test evidence. + +## Compatibility evidence + +The checked-in corpus pins Requests, Hatch, Flask, and Poetry commits and their initial source/test slices. The customer workflow exercises supported Python ABIs, records host tests separately, checks cold preparation and offline attachment, and injects controlled defects. Untouched upstream acceptance requires completed applicable analysis; it does not require zero findings. + +Track release acceptance in [User Story #473](https://github.com/nold-ai/specfact-cli-modules/issues/473) and the original [customer bug #472](https://github.com/nold-ai/specfact-cli-modules/issues/472). The reconstructed Hatch fixture is not proof that the inaccessible original customer reproduction is resolved. + +Python subprocess options `-I`, `-E`, and `-S` disable the startup mechanism used to attach the selected runtime. The worker rejects these exact options with `project_python_option_unsupported` rather than running without project dependencies. Ordinary `-c`, `-m`, `-u`, and script invocations retain native Python argument handling. + +Source aliases must resolve to included repository content. Aliases into excluded `.env`, virtual environments, or Git metadata are rejected before dependency building. Valid internal links are preserved in the private source copy, including absolute links rebased into that copy; the copied bytes are verified before build hooks execute. + +Pip discovery recognizes `requirements.txt`, pip-tools `requirements.in`, and `pylock.toml`. Multiple lock/requirements alternatives require explicit selection. Standard lock handling is delegated to pip; pip 26.2.1 documents `pylock.toml` support as experimental ([pip install reference](https://pip.pypa.io/en/stable/cli/pip_install/), accessed 2026-09-14). Static `setup.cfg` Python constraints are imported when PEP 621 metadata does not provide them. + +Portable analysis uses a verified private source copy with local environments and excluded secret files removed. The corpus records artifact bytes and Linux host-interface transfer counters; those counters include concurrent host traffic and are not exact package download sizes. Its warm preparation and attachment commands additionally run in a non-root network namespace, independently of the offline option. + + +Portable reviews retain skipped, XFAIL, and XPASS outcomes as non-passing test evidence, even when other tests pass. Each reviewed production Python source needs coverage evidence of at least 80 percent, or the effective project threshold when higher. A package directory used for pytest discovery does not exempt its production files from coverage. Missing coverage remains incomplete evidence. + +Coverage provenance distinguishes native pytest-cov requests from review-only measurement. When the repository does not request coverage, the review collects private evidence through the supported pytest-cov lifecycle without activating its dormant aggregate `fail_under` policy. The per-reviewed-file floor above still applies. Native `--cov`, `--cov-reset`, `--cov-config`, source/include/omit settings, report destinations, threshold and precision retain their effect; actual native aggregate failures remain blocking findings. The report records both configured and effective measurement scopes. Explicit `--no-cov`, blocked coverage plugins, `--no-cov-on-fail`, excluded sources, missing distributed-worker data and unsupported plugin contracts leave required evidence incomplete. Native report write failures remain visible rather than replacing the requested output. The current lifecycle contract supports pytest-cov7.0.0 and7.1.0; xdist and configured Coverage subprocess collection use its native worker lifecycle. diff --git a/docs/reference/commands.generated.json b/docs/reference/commands.generated.json index 4e80a39b..a2e24066 100644 --- a/docs/reference/commands.generated.json +++ b/docs/reference/commands.generated.json @@ -979,7 +979,8 @@ "subcommands": [ "ledger", "rules", - "run" + "run", + "runtime" ] }, { @@ -1143,6 +1144,8 @@ "--path", "--pr-context-file", "--preview-fixes", + "--project-config", + "--project-runtime", "--requirements-evidence", "--scope", "--score-only", @@ -1155,6 +1158,58 @@ "source": "specfact_code_review.review.commands:review_app", "subcommands": [] }, + { + "arguments": [], + "bare_invocation": "requires-subcommand", + "command": "specfact code review runtime", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:review_app", + "subcommands": [ + "inspect", + "prepare" + ] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review runtime inspect", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--json", + "--project-config" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:review_app", + "subcommands": [] + }, + { + "arguments": [], + "bare_invocation": "executes", + "command": "specfact code review runtime prepare", + "deprecated": false, + "hidden": false, + "install_prerequisite": "specfact module install nold-ai/specfact-code-review", + "options": [ + "--json", + "--offline", + "--project-config" + ], + "owner_package": "nold-ai/specfact-code-review", + "owner_repo": "nold-ai/specfact-cli-modules", + "short_help": "", + "source": "specfact_code_review.review.commands:review_app", + "subcommands": [] + }, { "arguments": [], "bare_invocation": "requires-subcommand", diff --git a/docs/reference/commands.generated.md b/docs/reference/commands.generated.md index 978deb5b..729ba090 100644 --- a/docs/reference/commands.generated.md +++ b/docs/reference/commands.generated.md @@ -53,7 +53,7 @@ This file is generated from the current module command trees. Do not edit by han | `specfact code import from-code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: BUNDLE | - | | | `specfact code repro` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --budget, --crosshair-per-path-timeout, --crosshair-required, --fail-fast, --fix, --out, --repo, --sidecar, --sidecar-bundle, --verbose; args: - | setup | | | `specfact code repro setup` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --install-crosshair, --repo; args: - | - | | -| `specfact code review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --install-completion, --show-completion; args: - | ledger, rules, run | | +| `specfact code review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --install-completion, --show-completion; args: - | ledger, rules, run, runtime | | | `specfact code review ledger` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | reset, status, update | | | `specfact code review ledger reset` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --confirm; args: - | - | | | `specfact code review ledger status` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | @@ -62,7 +62,10 @@ This file is generated from the current module command trees. Do not edit by han | `specfact code review rules init` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | | `specfact code review rules show` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | | `specfact code review rules update` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | -| `specfact code review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --base-ref, --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --head-ref, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --pr-context-file, --preview-fixes, --requirements-evidence, --scope, --score-only, --suppress-noise, --with-mutation; args: FILES | - | | +| `specfact code review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --base-ref, --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --head-ref, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --pr-context-file, --preview-fixes, --project-config, --project-runtime, --requirements-evidence, --scope, --score-only, --suppress-noise, --with-mutation; args: FILES | - | | +| `specfact code review runtime` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | inspect, prepare | | +| `specfact code review runtime inspect` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --json, --project-config; args: - | - | | +| `specfact code review runtime prepare` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --json, --offline, --project-config; args: - | - | | | `specfact code validate` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | sidecar | | | `specfact code validate sidecar` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | init, run | | | `specfact code validate sidecar init` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: BUNDLE_NAME (required), REPO_PATH (required) | - | | diff --git a/llms.txt b/llms.txt index fd2268c3..09a1e802 100644 --- a/llms.txt +++ b/llms.txt @@ -57,7 +57,7 @@ This file is generated from the current module command trees. Do not edit by han | `specfact code import from-code` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --confidence, --enrich-for-speckit, --enrichment, --entry-point, --exclude-tests, --force, --include-tests, --key-format, --no-enrich-for-speckit, --no-revalidate-features, --repo, --report, --revalidate-features, --shadow-only; args: BUNDLE | - | | | `specfact code repro` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --budget, --crosshair-per-path-timeout, --crosshair-required, --fail-fast, --fix, --out, --repo, --sidecar, --sidecar-bundle, --verbose; args: - | setup | | | `specfact code repro setup` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | --install-crosshair, --repo; args: - | - | | -| `specfact code review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --install-completion, --show-completion; args: - | ledger, rules, run | | +| `specfact code review` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --install-completion, --show-completion; args: - | ledger, rules, run, runtime | | | `specfact code review ledger` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | reset, status, update | | | `specfact code review ledger reset` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --confirm; args: - | - | | | `specfact code review ledger status` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | @@ -66,7 +66,10 @@ This file is generated from the current module command trees. Do not edit by han | `specfact code review rules init` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | | `specfact code review rules show` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | - | | | `specfact code review rules update` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --ide; args: - | - | | -| `specfact code review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --base-ref, --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --head-ref, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --pr-context-file, --preview-fixes, --requirements-evidence, --scope, --score-only, --suppress-noise, --with-mutation; args: FILES | - | | +| `specfact code review run` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --base-ref, --bug-hunt, --enforcement, --exclude-tests, --fix, --focus, --head-ref, --include-noise, --include-tests, --instructions, --interactive, --json, --level, --mode, --no-tests, --out, --path, --pr-context-file, --preview-fixes, --project-config, --project-runtime, --requirements-evidence, --scope, --score-only, --suppress-noise, --with-mutation; args: FILES | - | | +| `specfact code review runtime` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | -; args: - | inspect, prepare | | +| `specfact code review runtime inspect` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --json, --project-config; args: - | - | | +| `specfact code review runtime prepare` | nold-ai/specfact-code-review | `specfact module install nold-ai/specfact-code-review` | --json, --offline, --project-config; args: - | - | | | `specfact code validate` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | sidecar | | | `specfact code validate sidecar` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: - | init, run | | | `specfact code validate sidecar init` | nold-ai/specfact-codebase | `specfact module install nold-ai/specfact-codebase` | -; args: BUNDLE_NAME (required), REPO_PATH (required) | - | | diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index 35113e09..a0b5165d 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -65,6 +65,12 @@ These archived specs are now the shipped basis for the flagship demo: run review produce JSON evidence, identify AI-bloat findings, hand remediation packets to an AI IDE, rerun, and compare improved evidence. +## Portable External Project Runtime + +| Change | Issue | Status | Dependencies | +|---|---|---|---| +| `code-review-16-portable-project-runtime` | [#473](https://github.com/nold-ai/specfact-cli-modules/issues/473) | Implementation authorized 2026-09-14; automatic customer runtime preparation and external corpus | No open prerequisites; blocks #472; #466/#459/#175 are baseline context | + ## Customer Capsule Corrective Track | Change | GitHub issue | Positioning | Dependencies | diff --git a/openspec/changes/code-review-16-portable-project-runtime/ACTIVE_CONTEXT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ACTIVE_CONTEXT_RED.txt new file mode 100644 index 00000000..91dcfbd8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ACTIVE_CONTEXT_RED.txt @@ -0,0 +1,34 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 20 items / 18 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_runtime_discovery.py F. [100%] + +=================================== FAILURES =================================== +________ test_verified_active_hatch_context_resolves_competing_signals _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-53/test_verified_active_hatch_con0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b856fd0> + + def test_verified_active_hatch_context_resolves_competing_signals(tmp_path: Path, monkeypatch) -> None: + from specfact_code_review.run import runtime_discovery + + environment = tmp_path / '.venv' + environment.mkdir() + (environment / 'pyvenv.cfg').write_text('home = /usr/bin\n') + (tmp_path / 'uv.lock').touch() + (tmp_path / 'hatch.toml').write_text('[envs.default]\n[envs.review]\ndetached=true\n') + monkeypatch.setenv('VIRTUAL_ENV', str(environment)) + monkeypatch.setenv('HATCH_ENV_ACTIVE', 'review') +> monkeypatch.setattr(runtime_discovery.sys, 'prefix', str(environment)) + ^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.runtime_discovery' has no attribute 'sys' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:156: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_verified_active_hatch_context_resolves_competing_signals +================== 1 failed, 1 passed, 18 deselected in 0.09s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/ANALYSIS_EXCLUSIONS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ANALYSIS_EXCLUSIONS_RED.txt new file mode 100644 index 00000000..01c5f532 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ANALYSIS_EXCLUSIONS_RED.txt @@ -0,0 +1,44 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/specfact_code_review/run/test_portable_snapshot.py F [100%] + +=================================== FAILURES =================================== +__________ test_analysis_source_copy_excludes_local_environment_files __________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-152/test_analysis_source_copy_excl0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109dca650> + + def test_analysis_source_copy_excludes_local_environment_files(tmp_path: Path, monkeypatch) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / ".env").write_text("SYNTHETIC_SECRET=fixture\n") + (source / ".venv").mkdir() + (source / ".venv/private.txt").write_text("synthetic excluded fixture") + observed = [] + + def analyze(_runtime, *, snapshot_root, files, options, settings): + assert snapshot_root != source + assert not (snapshot_root / ".env").exists() + assert not (snapshot_root / ".venv").exists() + assert files == [snapshot_root / "app.py"] + assert files[0].read_text() == "VALUE = 1\n" + observed.append(snapshot_root) + return runner.CapsuleSnapshotResult({}, {}) + + monkeypatch.setattr(runner, "_run_capsule_snapshot", analyze) + request = ProjectSnapshotRequest(source, [source / "app.py"], runner.ReviewOptions(), "explicit_files") +> portable_snapshot._run_in_private_source(object(), request, runner.CapsuleSnapshotSettings()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.portable_snapshot' has no attribute '_run_in_private_source' + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:148: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_analysis_source_copy_excludes_local_environment_files +============================== 1 failed in 0.83s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/ANALYSIS_SOURCE_BINDING_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ANALYSIS_SOURCE_BINDING_RED.txt new file mode 100644 index 00000000..33c0da4e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ANALYSIS_SOURCE_BINDING_RED.txt @@ -0,0 +1,47 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items / 3 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_portable_snapshot.py F [100%] + +=================================== FAILURES =================================== +_____ test_source_change_during_full_analysis_invalidates_runtime_binding ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-117/test_source_change_during_full0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d64f230> + + def test_source_change_during_full_analysis_invalidates_runtime_binding(tmp_path: Path, monkeypatch) -> None: + from specfact_code_review.run import portable_snapshot + from specfact_code_review.run.portable_worker import DEPENDENT_MEMBERS + from specfact_code_review.run.runtime_models import PreparedRuntime + + root = tmp_path / 'project' + root.mkdir() + source = root / 'app.py' + source.write_text('VALUE = 1\n') + artifact = tmp_path / 'artifact' + prepared = PreparedRuntime(artifact, artifact / 'project-runtime.json', 'sha256:' + 'b' * 64, {'inventory': {}}) + runtime = runner.CapsuleRuntime(root, 'sha256:' + 'a' * 64, 'linux-x86_64-cp312', 'python', 'bootstrap', SimpleNamespace()) + monkeypatch.setattr(portable_snapshot, 'prepare_runtime', lambda *args, **kwargs: prepared) + monkeypatch.setattr(portable_snapshot, 'load_runtime', lambda *args, **kwargs: prepared) + + def analyze(*args, **kwargs): + source.write_text('VALUE = 2\n') + return runner.CapsuleSnapshotResult({name: {'evidence_outcome': 'PASS'} for name in DEPENDENT_MEMBERS}, {}) + + monkeypatch.setattr(runner, '_run_capsule_snapshot', analyze) + snapshot, evidence = run_project_snapshot(runtime, ProjectSnapshotRequest(root, [source], runner.ReviewOptions(no_tests=True), 'full')) +> assert evidence['status'] == 'UNKNOWN' +E AssertionError: assert 'PASS' == 'UNKNOWN' +E +E - UNKNOWN +E + PASS + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:99: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_source_change_during_full_analysis_invalidates_runtime_binding +======================= 1 failed, 3 deselected in 0.19s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/ANALYZER_IMPORT_OWNERSHIP_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ANALYZER_IMPORT_OWNERSHIP_RED.txt new file mode 100644 index 00000000..ec6073ca --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ANALYZER_IMPORT_OWNERSHIP_RED.txt @@ -0,0 +1,941 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- /worktree/.venv/bin/python +cachedir: .pytest_cache +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collecting ... collected 29 items + +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_member_graph_excludes_unrelated_analyzer_packages PASSED [ 3%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_project_distribution_replaces_compatible_tool_dependency PASSED [ 6%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_missing_member_dependency_cannot_be_silently_omitted PASSED [ 10%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_pytest_worker_validates_selected_dependency_versions[7.0-True] PASSED [ 13%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_pytest_worker_validates_selected_dependency_versions[8.4-False] PASSED [ 17%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.py-None] FAILED [ 20%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.py-owned\nstale\n] FAILED [ 24%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.py-stale\n] FAILED [ 27%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-None] FAILED [ 31%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-owned\nstale\n] FAILED [ 34%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-stale\n] FAILED [ 37%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-None] FAILED [ 41%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-owned\nstale\n] FAILED [ 44%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-stale\n] FAILED [ 48%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-None] FAILED [ 51%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-owned\nstale\n] FAILED [ 55%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-stale\n] FAILED [ 58%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-None] FAILED [ 62%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-owned\nstale\n] FAILED [ 65%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-stale\n] FAILED [ 68%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-None] FAILED [ 72%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-owned\nstale\n] FAILED [ 75%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-stale\n] FAILED [ 79%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.so-None] FAILED [ 82%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.so-owned\nstale\n] FAILED [ 86%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.so-stale\n] FAILED [ 89%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_stale_top_level_cannot_claim_another_distributions_build_package FAILED [ 93%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_missing_recorded_module_is_not_admitted_from_stale_metadata FAILED [ 96%] +tests/unit/specfact_code_review/run/test_runtime_domains.py::test_missing_file_inventory_cannot_guess_analyzer_ownership FAILED [100%] + +=================================== FAILURES =================================== +___ test_import_ownership_comes_from_recorded_runtime_payload[owned.py-None] ___ + +tmp_path = PosixPath('/test_import_ownership_comes_fr0') +declaration = None, payload = 'owned.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.py-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr1') +declaration = 'owned\nstale\n', payload = 'owned.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.py-stale\n] __ + +tmp_path = PosixPath('/test_import_ownership_comes_fr2') +declaration = 'stale\n', payload = 'owned.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-None] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr3') +declaration = None, payload = 'owned/__init__.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr4') +declaration = 'owned\nstale\n', payload = 'owned/__init__.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-stale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr5') +declaration = 'stale\n', payload = 'owned/__init__.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-None] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr6') +declaration = None, payload = 'owned/component.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr7') +declaration = 'owned\nstale\n', payload = 'owned/component.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-stale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr8') +declaration = 'stale\n', payload = 'owned/component.py' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-None] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr9') +declaration = None, payload = 'owned.cpython-311-x86_64-linux-gnu.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr10') +declaration = 'owned\nstale\n' +payload = 'owned.cpython-311-x86_64-linux-gnu.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-stale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr11') +declaration = 'stale\n', payload = 'owned.cpython-311-x86_64-linux-gnu.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-None] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr12') +declaration = None, payload = 'owned.cp312-win_amd64.pyd' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr13') +declaration = 'owned\nstale\n', payload = 'owned.cp312-win_amd64.pyd' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-stale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr14') +declaration = 'stale\n', payload = 'owned.cp312-win_amd64.pyd' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-None] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr15') +declaration = None, payload = 'owned.abi3.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr16') +declaration = 'owned\nstale\n', payload = 'owned.abi3.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-stale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr17') +declaration = 'stale\n', payload = 'owned.abi3.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +___ test_import_ownership_comes_from_recorded_runtime_payload[owned.so-None] ___ + +tmp_path = PosixPath('/test_import_ownership_comes_fr18') +declaration = None, payload = 'owned.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'docs', 'inv...ned', 'stubs'} == {'owned'} +E +E Extra items in the left set: +E 'stubs' +E 'docs' +E 'invalid' +E +E Full diff:... +E +E ...Full output truncated (6 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.so-owned\nstale\n] _ + +tmp_path = PosixPath('/test_import_ownership_comes_fr19') +declaration = 'owned\nstale\n', payload = 'owned.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'owned', 'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E +E Full diff: +E { +E 'owned', +E + 'stale', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +_ test_import_ownership_comes_from_recorded_runtime_payload[owned.so-stale\n] __ + +tmp_path = PosixPath('/test_import_ownership_comes_fr20') +declaration = 'stale\n', payload = 'owned.so' + + @pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) + @pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], + ) + def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str + ) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'stale'} == {'owned'} +E +E Extra items in the left set: +E 'stale' +E Extra items in the right set: +E 'owned' +E +E Full diff:... +E +E ...Full output truncated (4 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_runtime_domains.py:98: AssertionError +____ test_stale_top_level_cannot_claim_another_distributions_build_package _____ + +tmp_path = PosixPath('/test_stale_top_level_cannot_cl0') + + def test_stale_top_level_cannot_claim_another_distributions_build_package(tmp_path: Path) -> None: + metadata = _distribution(tmp_path, "pylint") + (metadata / "top_level.txt").write_text("pylint\nbuild\n") + _distribution(tmp_path, "build", files=("build/__init__.py",)) + graph = member_dependency_graphs({"installed": []}, tmp_path)["pylint"] +> assert graph["sealed_imports"] == ["pylint"] +E AssertionError: assert ['build', 'pylint'] == ['pylint'] +E +E At index 0 diff: 'build' != 'pylint' +E Left contains one more item: 'pylint' +E +E Full diff: +E [ +E + 'build', +E 'pylint', +E ] + +tests/unit/specfact_code_review/run/test_runtime_domains.py:106: AssertionError +_______ test_missing_recorded_module_is_not_admitted_from_stale_metadata _______ + +tmp_path = PosixPath('/test_missing_recorded_module_i0') + + def test_missing_recorded_module_is_not_admitted_from_stale_metadata(tmp_path: Path) -> None: + metadata = _distribution(tmp_path, "fixture", files=("owned.py", "missing.py")) + (tmp_path / "missing.py").unlink() + (metadata / "top_level.txt").write_text("owned\nmissing\n") +> assert _top_level_imports(Distribution.at(metadata)) == {"owned"} +E AssertionError: assert {'missing', 'owned'} == {'owned'} +E +E Extra items in the left set: +E 'missing' +E +E Full diff: +E { +E + 'missing', +E 'owned', +E } + +tests/unit/specfact_code_review/run/test_runtime_domains.py:114: AssertionError +_________ test_missing_file_inventory_cannot_guess_analyzer_ownership __________ + +tmp_path = PosixPath('/test_missing_file_inventory_ca0') + + def test_missing_file_inventory_cannot_guess_analyzer_ownership(tmp_path: Path) -> None: + metadata = _distribution(tmp_path, "fixture") + (metadata / "RECORD").unlink() +> with pytest.raises(ProjectRuntimeError, match="project_analyzer_import_inventory_missing:fixture"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_domains.py:120: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.py-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.py-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.py-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/__init__.py-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned/component.py-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cpython-311-x86_64-linux-gnu.so-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.cp312-win_amd64.pyd-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.abi3.so-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.so-None] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.so-owned\nstale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_import_ownership_comes_from_recorded_runtime_payload[owned.so-stale\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_stale_top_level_cannot_claim_another_distributions_build_package +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_missing_recorded_module_is_not_admitted_from_stale_metadata +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_missing_file_inventory_cannot_guess_analyzer_ownership +========================= 24 failed, 5 passed in 0.81s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/ANALYZER_ORIGIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ANALYZER_ORIGIN_RED.txt new file mode 100644 index 00000000..ffc450bb --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ANALYZER_ORIGIN_RED.txt @@ -0,0 +1,318 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 18 items + +tests/unit/specfact_code_review/run/test_target_bootstrap.py ...FFF...FF [ 61%] +FF..... [100%] + +=================================== FAILURES =================================== +_ test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload[pylint-none] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_analyzer_dependency_origi0') +preload = 'none', module = 'pylint' + + @pytest.mark.parametrize("preload", ["none", "pth", "before"]) + @pytest.mark.parametrize("module", ["pylint", "project-python"]) + def test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload( + tmp_path: Path, preload: str, module: str + ) -> None: + project = tmp_path / "project" + installed = project / "site-packages" + snapshot = tmp_path / "snapshot" + analyzers = tmp_path / "analyzers" + for directory in (installed, snapshot, analyzers): + directory.mkdir(parents=True) + (snapshot / "packaging.py").write_text('OWNER = "snapshot"\n') + (analyzers / "packaging.py").write_text('OWNER = "analyzer"\n') + (installed / "project_owned.py").write_text('OWNER = "project"\n') + (analyzers / "project_owned.py").write_text('OWNER = "wrong-analyzer"\n') + if preload == "pth": + (installed / "preload.pth").write_text("import packaging; import project_owned\n") + descriptor = { + "project": {"source_roots": ["."]}, + "inventory": { + "member_graphs": { + "pylint": { + "sealed_imports": ["packaging"], + "installed": [ + {"name": "packaging", "origin": "analyzer"}, + {"name": "project-owned", "origin": "project"}, + ], + } + } + }, + } + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + script = f""" + import json, runpy, sys + from pathlib import Path + bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) + configure = bootstrap['_configure_runtime'] + configure.__globals__.update(PROJECT=Path({str(project)!r}), SNAPSHOT=Path({str(snapshot)!r}), ANALYZERS=Path({str(analyzers)!r})) + if {preload!r} == 'before': + sys.path.insert(0, {str(snapshot)!r}) + import packaging + sys.path.pop(0) + configure({module!r}) + preloaded = 'packaging' in sys.modules + import packaging, project_owned + print(json.dumps([packaging.OWNER, packaging.__file__, project_owned.OWNER, preloaded])) + """ + completed = subprocess.run([sys.executable, "-I", "-S", "-c", script], capture_output=True, text=True, check=False) + if module == "pylint" and preload == "before": + assert completed.returncode != 0 + assert "project_worker_analyzer_origin_mismatch:packaging" in completed.stderr + return + assert completed.returncode == 0, completed.stderr + owner, origin, project_owner, preloaded = json.loads(completed.stdout) + expected_root = analyzers if module == "pylint" else snapshot +> assert owner == ("analyzer" if module == "pylint" else "snapshot") +E AssertionError: assert 'snapshot' == 'analyzer' +E +E - analyzer +E + snapshot + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:105: AssertionError +_ test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload[pylint-pth] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_analyzer_dependency_origi1') +preload = 'pth', module = 'pylint' + + @pytest.mark.parametrize("preload", ["none", "pth", "before"]) + @pytest.mark.parametrize("module", ["pylint", "project-python"]) + def test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload( + tmp_path: Path, preload: str, module: str + ) -> None: + project = tmp_path / "project" + installed = project / "site-packages" + snapshot = tmp_path / "snapshot" + analyzers = tmp_path / "analyzers" + for directory in (installed, snapshot, analyzers): + directory.mkdir(parents=True) + (snapshot / "packaging.py").write_text('OWNER = "snapshot"\n') + (analyzers / "packaging.py").write_text('OWNER = "analyzer"\n') + (installed / "project_owned.py").write_text('OWNER = "project"\n') + (analyzers / "project_owned.py").write_text('OWNER = "wrong-analyzer"\n') + if preload == "pth": + (installed / "preload.pth").write_text("import packaging; import project_owned\n") + descriptor = { + "project": {"source_roots": ["."]}, + "inventory": { + "member_graphs": { + "pylint": { + "sealed_imports": ["packaging"], + "installed": [ + {"name": "packaging", "origin": "analyzer"}, + {"name": "project-owned", "origin": "project"}, + ], + } + } + }, + } + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + script = f""" + import json, runpy, sys + from pathlib import Path + bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) + configure = bootstrap['_configure_runtime'] + configure.__globals__.update(PROJECT=Path({str(project)!r}), SNAPSHOT=Path({str(snapshot)!r}), ANALYZERS=Path({str(analyzers)!r})) + if {preload!r} == 'before': + sys.path.insert(0, {str(snapshot)!r}) + import packaging + sys.path.pop(0) + configure({module!r}) + preloaded = 'packaging' in sys.modules + import packaging, project_owned + print(json.dumps([packaging.OWNER, packaging.__file__, project_owned.OWNER, preloaded])) + """ + completed = subprocess.run([sys.executable, "-I", "-S", "-c", script], capture_output=True, text=True, check=False) + if module == "pylint" and preload == "before": + assert completed.returncode != 0 + assert "project_worker_analyzer_origin_mismatch:packaging" in completed.stderr + return + assert completed.returncode == 0, completed.stderr + owner, origin, project_owner, preloaded = json.loads(completed.stdout) + expected_root = analyzers if module == "pylint" else snapshot +> assert owner == ("analyzer" if module == "pylint" else "snapshot") +E AssertionError: assert 'snapshot' == 'analyzer' +E +E - analyzer +E + snapshot + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:105: AssertionError +_ test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload[pylint-before] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_analyzer_dependency_origi2') +preload = 'before', module = 'pylint' + + @pytest.mark.parametrize("preload", ["none", "pth", "before"]) + @pytest.mark.parametrize("module", ["pylint", "project-python"]) + def test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload( + tmp_path: Path, preload: str, module: str + ) -> None: + project = tmp_path / "project" + installed = project / "site-packages" + snapshot = tmp_path / "snapshot" + analyzers = tmp_path / "analyzers" + for directory in (installed, snapshot, analyzers): + directory.mkdir(parents=True) + (snapshot / "packaging.py").write_text('OWNER = "snapshot"\n') + (analyzers / "packaging.py").write_text('OWNER = "analyzer"\n') + (installed / "project_owned.py").write_text('OWNER = "project"\n') + (analyzers / "project_owned.py").write_text('OWNER = "wrong-analyzer"\n') + if preload == "pth": + (installed / "preload.pth").write_text("import packaging; import project_owned\n") + descriptor = { + "project": {"source_roots": ["."]}, + "inventory": { + "member_graphs": { + "pylint": { + "sealed_imports": ["packaging"], + "installed": [ + {"name": "packaging", "origin": "analyzer"}, + {"name": "project-owned", "origin": "project"}, + ], + } + } + }, + } + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + script = f""" + import json, runpy, sys + from pathlib import Path + bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) + configure = bootstrap['_configure_runtime'] + configure.__globals__.update(PROJECT=Path({str(project)!r}), SNAPSHOT=Path({str(snapshot)!r}), ANALYZERS=Path({str(analyzers)!r})) + if {preload!r} == 'before': + sys.path.insert(0, {str(snapshot)!r}) + import packaging + sys.path.pop(0) + configure({module!r}) + preloaded = 'packaging' in sys.modules + import packaging, project_owned + print(json.dumps([packaging.OWNER, packaging.__file__, project_owned.OWNER, preloaded])) + """ + completed = subprocess.run([sys.executable, "-I", "-S", "-c", script], capture_output=True, text=True, check=False) + if module == "pylint" and preload == "before": +> assert completed.returncode != 0 +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,...00gn/T/pytest-of-dom/pytest-265/test_analyzer_dependency_origi2/snapshot/packaging.py", "project", true]\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:99: AssertionError +__________ test_sealed_dependency_rejects_unverified_origins[module] ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_sealed_dependency_rejects0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1083ffd10> +form = 'module' + + @pytest.mark.parametrize("form", ["module", "namespace", "symlink", "missing"]) + def test_sealed_dependency_rejects_unverified_origins(tmp_path: Path, monkeypatch, form: str) -> None: + analyzers = tmp_path / "analyzers" + hostile = tmp_path / "hostile" + analyzers.mkdir() + hostile.mkdir() + monkeypatch.setattr(target_bootstrap, "ANALYZERS", analyzers) + if form == "namespace": + (hostile / "needed").mkdir() + elif form != "missing": + (hostile / "needed.py").write_text("VALUE = 1\n") + if form == "symlink": + (analyzers / "needed.py").symlink_to(hostile / "needed.py") + finder = target_bootstrap.DomainFinder({"sealed_imports": ["needed"], "installed": []}) + search = [str(analyzers)] if form in {"symlink", "missing"} else [str(hostile)] +> with pytest.raises(ImportError, match="project_worker_analyzer_origin_mismatch:needed"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ImportError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:126: Failed +_________ test_sealed_dependency_rejects_unverified_origins[namespace] _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_sealed_dependency_rejects1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108a58270> +form = 'namespace' + + @pytest.mark.parametrize("form", ["module", "namespace", "symlink", "missing"]) + def test_sealed_dependency_rejects_unverified_origins(tmp_path: Path, monkeypatch, form: str) -> None: + analyzers = tmp_path / "analyzers" + hostile = tmp_path / "hostile" + analyzers.mkdir() + hostile.mkdir() + monkeypatch.setattr(target_bootstrap, "ANALYZERS", analyzers) + if form == "namespace": + (hostile / "needed").mkdir() + elif form != "missing": + (hostile / "needed.py").write_text("VALUE = 1\n") + if form == "symlink": + (analyzers / "needed.py").symlink_to(hostile / "needed.py") + finder = target_bootstrap.DomainFinder({"sealed_imports": ["needed"], "installed": []}) + search = [str(analyzers)] if form in {"symlink", "missing"} else [str(hostile)] +> with pytest.raises(ImportError, match="project_worker_analyzer_origin_mismatch:needed"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ImportError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:126: Failed +__________ test_sealed_dependency_rejects_unverified_origins[symlink] __________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_sealed_dependency_rejects2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108a587c0> +form = 'symlink' + + @pytest.mark.parametrize("form", ["module", "namespace", "symlink", "missing"]) + def test_sealed_dependency_rejects_unverified_origins(tmp_path: Path, monkeypatch, form: str) -> None: + analyzers = tmp_path / "analyzers" + hostile = tmp_path / "hostile" + analyzers.mkdir() + hostile.mkdir() + monkeypatch.setattr(target_bootstrap, "ANALYZERS", analyzers) + if form == "namespace": + (hostile / "needed").mkdir() + elif form != "missing": + (hostile / "needed.py").write_text("VALUE = 1\n") + if form == "symlink": + (analyzers / "needed.py").symlink_to(hostile / "needed.py") + finder = target_bootstrap.DomainFinder({"sealed_imports": ["needed"], "installed": []}) + search = [str(analyzers)] if form in {"symlink", "missing"} else [str(hostile)] +> with pytest.raises(ImportError, match="project_worker_analyzer_origin_mismatch:needed"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ImportError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:126: Failed +__________ test_sealed_dependency_rejects_unverified_origins[missing] __________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-265/test_sealed_dependency_rejects3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108a34b50> +form = 'missing' + + @pytest.mark.parametrize("form", ["module", "namespace", "symlink", "missing"]) + def test_sealed_dependency_rejects_unverified_origins(tmp_path: Path, monkeypatch, form: str) -> None: + analyzers = tmp_path / "analyzers" + hostile = tmp_path / "hostile" + analyzers.mkdir() + hostile.mkdir() + monkeypatch.setattr(target_bootstrap, "ANALYZERS", analyzers) + if form == "namespace": + (hostile / "needed").mkdir() + elif form != "missing": + (hostile / "needed.py").write_text("VALUE = 1\n") + if form == "symlink": + (analyzers / "needed.py").symlink_to(hostile / "needed.py") + finder = target_bootstrap.DomainFinder({"sealed_imports": ["needed"], "installed": []}) + search = [str(analyzers)] if form in {"symlink", "missing"} else [str(hostile)] +> with pytest.raises(ImportError, match="project_worker_analyzer_origin_mismatch:needed"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ImportError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:126: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload[pylint-none] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload[pylint-pth] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload[pylint-before] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_sealed_dependency_rejects_unverified_origins[module] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_sealed_dependency_rejects_unverified_origins[namespace] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_sealed_dependency_rejects_unverified_origins[symlink] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_sealed_dependency_rejects_unverified_origins[missing] +========================= 7 failed, 11 passed in 0.82s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/ARTIFACTS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ARTIFACTS_RED.txt new file mode 100644 index 00000000..acb11b3e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ARTIFACTS_RED.txt @@ -0,0 +1,20 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 0 items / 1 error + +==================================== ERRORS ==================================== +_ ERROR collecting tests/unit/specfact_code_review/run/test_runtime_artifacts.py _ +ImportError while importing test module '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/tests/unit/specfact_code_review/run/test_runtime_artifacts.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:8: in + from specfact_code_review.run.runtime_artifacts import load_runtime, seal_runtime +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_artifacts' +=========================== short test summary info ============================ +ERROR tests/unit/specfact_code_review/run/test_runtime_artifacts.py +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +=============================== 1 error in 0.11s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/AUTO_ENVIRONMENT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/AUTO_ENVIRONMENT_RED.txt new file mode 100644 index 00000000..7d908e9d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/AUTO_ENVIRONMENT_RED.txt @@ -0,0 +1,40 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 17 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 58%] +.....FF [100%] + +=================================== FAILURES =================================== +__________ test_single_declared_test_group_is_prepared_automatically ___________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-25/test_single_declared_test_grou0') + + def test_single_declared_test_group_is_prepared_automatically(tmp_path: Path) -> None: + (tmp_path / 'pyproject.toml').write_text('[dependency-groups]\ntest=["pytest"]\ndocs=["sphinx"]\n') +> assert discover_project(tmp_path).groups == ('test',) +E AssertionError: assert () == ('test',) +E +E Right contains one more item: 'test' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:127: AssertionError +_____________ test_competing_hatch_environments_require_selection ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-25/test_competing_hatch_environme0') + + def test_competing_hatch_environments_require_selection(tmp_path: Path) -> None: + (tmp_path / 'hatch.toml').write_text('[envs.default]\ndependencies=["pytest<8"]\n[envs.test]\ndependencies=["pytest>=9"]\n') +> with pytest.raises(ProjectRuntimeError, match=r'environment_ambiguous.*default.*test'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:132: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_single_declared_test_group_is_prepared_automatically +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_competing_hatch_environments_require_selection +========================= 2 failed, 15 passed in 0.13s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/BOUND_GIT_CLOSURE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BOUND_GIT_CLOSURE_RED.txt new file mode 100644 index 00000000..b71baf35 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BOUND_GIT_CLOSURE_RED.txt @@ -0,0 +1,182 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 17 items + +tests/unit/specfact_code_review/run/test_runtime_vcs.py ............FFFF [ 94%] +F [100%] + +=================================== FAILURES =================================== +_______ test_private_git_contains_only_bound_objects_and_tag_refs[False] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-267/test_private_git_contains_only0') +packed = False + + @pytest.mark.parametrize("packed", [False, True]) + def test_private_git_contains_only_bound_objects_and_tag_refs(tmp_path: Path, packed: bool) -> None: + root = tmp_path / "source" + _repository(root) + _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "tag", "-a", "v1", "-m", "bound annotated tag") + _git(root, "tag", "lightweight") + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected ancestry") + _git(root, "checkout", "--orphan", "unrelated") + _git(root, "rm", "-rf", ".") + (root / "private.txt").write_text("unbound branch data") + unrelated = _commit_fixture(root, "unrelated branch") + _git(root, "update-ref", "refs/remotes/origin/private", unrelated) + _git(root, "checkout", "--detach", selected) + if packed: + _git(root, "repack", "-ad") + secret = tmp_path / "unreachable.txt" + secret.write_text("unreachable object data") + unreachable = _git(root, "hash-object", "-w", str(secret)) + before = vcs_context(root, selected) + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied, selected) + expected = set(_git(root, "rev-list", "--objects", "--no-object-names", selected, "refs/tags/v1", "refs/tags/lightweight").splitlines()) + actual = set(_git(copied, "cat-file", "--batch-all-objects", "--batch-check=%(objectname)").splitlines()) +> assert actual == expected +E AssertionError: assert {'1550f105848...3ddf0b7', ...} == {'378237afc43...6ef7942', ...} +E +E Extra items in the left set: +E '6647d0cc9292d6e995ea4c5dfdbdee110a185105' +E 'c78dcb4a83bb1f5e86b16d9591553526b50804e7' +E '748edf51a0f9396595b334822f668bfaad13be3f' +E '1550f1058488ea05be9d7f58a4e6c6757649de50' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:288: AssertionError +----------------------------- Captured stderr call ----------------------------- +Switched to a new branch 'unrelated' +HEAD is now at aec8842 selected ancestry +_______ test_private_git_contains_only_bound_objects_and_tag_refs[True] ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-267/test_private_git_contains_only1') +packed = True + + @pytest.mark.parametrize("packed", [False, True]) + def test_private_git_contains_only_bound_objects_and_tag_refs(tmp_path: Path, packed: bool) -> None: + root = tmp_path / "source" + _repository(root) + _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "tag", "-a", "v1", "-m", "bound annotated tag") + _git(root, "tag", "lightweight") + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected ancestry") + _git(root, "checkout", "--orphan", "unrelated") + _git(root, "rm", "-rf", ".") + (root / "private.txt").write_text("unbound branch data") + unrelated = _commit_fixture(root, "unrelated branch") + _git(root, "update-ref", "refs/remotes/origin/private", unrelated) + _git(root, "checkout", "--detach", selected) + if packed: + _git(root, "repack", "-ad") + secret = tmp_path / "unreachable.txt" + secret.write_text("unreachable object data") + unreachable = _git(root, "hash-object", "-w", str(secret)) + before = vcs_context(root, selected) + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied, selected) + expected = set(_git(root, "rev-list", "--objects", "--no-object-names", selected, "refs/tags/v1", "refs/tags/lightweight").splitlines()) + actual = set(_git(copied, "cat-file", "--batch-all-objects", "--batch-check=%(objectname)").splitlines()) +> assert actual == expected +E AssertionError: assert {'1550f105848...3ddf0b7', ...} == {'378237afc43...6ef7942', ...} +E +E Extra items in the left set: +E '6647d0cc9292d6e995ea4c5dfdbdee110a185105' +E 'c78dcb4a83bb1f5e86b16d9591553526b50804e7' +E '748edf51a0f9396595b334822f668bfaad13be3f' +E '1550f1058488ea05be9d7f58a4e6c6757649de50' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:288: AssertionError +----------------------------- Captured stderr call ----------------------------- +Switched to a new branch 'unrelated' +HEAD is now at aec8842 selected ancestry +______ test_private_git_preserves_bound_shallow_history_and_annotated_tag ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-267/test_private_git_preserves_bou0') + + def test_private_git_preserves_bound_shallow_history_and_annotated_tag(tmp_path: Path) -> None: + original = tmp_path / "original" + _repository(original) + ancestor = _git(original, "rev-parse", "HEAD") + (original / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(original, "shallow tip") + _git(original, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "tag", "-a", "v2", "-m", "tip tag") + source = tmp_path / "shallow" + _git(original, "clone", "--quiet", "--depth=1", original.as_uri(), str(source)) + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(source, copied) + assert _git(copied, "rev-parse", "HEAD") == selected + assert _git(copied, "describe", "--tags", "HEAD") == "v2" + assert _git(copied, "rev-parse", "--is-shallow-repository") == "true" + assert _git(copied, "rev-list", "--count", "HEAD") == "1" +> assert vcs_context(copied) == vcs_context(source) +E AssertionError: assert {'commit': '4...50d7147975\n'} == {'commit': '4...50d7147975\n'} +E +E Omitting 3 identical items, use -vv to show +E Differing items: +E {'shallow': '49184c378d13047c85d995fb3ace1350d7147975\n49184c378d13047c85d995fb3ace1350d7147975\n'} != {'shallow': '49184c378d13047c85d995fb3ace1350d7147975\n'} +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:312: AssertionError +_______ test_unbound_replacement_ref_cannot_change_selected_vcs_context ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-267/test_unbound_replacement_ref_c0') + + def test_unbound_replacement_ref_cannot_change_selected_vcs_context(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + ancestor = _git(root, "rev-parse", "HEAD") + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + before = vcs_context(root) + _git(root, "replace", selected, ancestor) +> assert vcs_context(root) == before +E AssertionError: assert {'commit': '1...'shallow': ''} == {'commit': '1...'shallow': ''} +E +E Omitting 3 identical items, use -vv to show +E Differing items: +E {'tree': 'ae7df65c33f278677e0d7eb633690c0053ddf0b7'} != {'tree': 'dc72567ae5cda25891b9ab3e4fc6c31f523e86ee'} +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:326: AssertionError +___________ test_missing_promisor_object_does_not_run_host_transport ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-267/test_missing_promisor_object_d0') + + def test_missing_promisor_object_does_not_run_host_transport(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + blob = _git(root, "rev-parse", "HEAD:app.py") + marker = tmp_path / "transport-executed" + transport = tmp_path / "transport.sh" + transport.write_text(f"#!/bin/sh\ntouch '{marker}'\nexit 1\n") + transport.chmod(0o700) + _git(root, "config", "core.repositoryformatversion", "1") + _git(root, "config", "extensions.partialClone", "origin") + _git(root, "config", "remote.origin.promisor", "true") + _git(root, "config", "remote.origin.url", "ssh://example.invalid/project") + _git(root, "config", "core.sshCommand", str(transport)) + (root / ".git/objects" / blob[:2] / blob[2:]).unlink() + copied = tmp_path / "copy" + copied.mkdir() +> with pytest.raises(ValueError, match="project_git_snapshot_failed"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:350: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_private_git_contains_only_bound_objects_and_tag_refs[False] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_private_git_contains_only_bound_objects_and_tag_refs[True] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_private_git_preserves_bound_shallow_history_and_annotated_tag +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unbound_replacement_ref_cannot_change_selected_vcs_context +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_missing_promisor_object_does_not_run_host_transport +========================= 5 failed, 12 passed in 2.86s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/BOUND_GIT_ORIGIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BOUND_GIT_ORIGIN_RED.txt new file mode 100644 index 00000000..a77293da --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BOUND_GIT_ORIGIN_RED.txt @@ -0,0 +1,279 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- /.venv/bin/python +cachedir: .pytest_cache +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collecting ... collected 27 items / 23 deselected / 4 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_requires_captured_git_origin_after_verification[False-False] PASSED [ 25%] +tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_requires_captured_git_origin_after_verification[False-True] PASSED [ 50%] +tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_requires_captured_git_origin_after_verification[True-False] FAILED [ 75%] +tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_requires_captured_git_origin_after_verification[True-True] FAILED [100%] + +=================================== FAILURES =================================== +___ test_builder_requires_captured_git_origin_after_verification[True-False] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-284/test_builder_requires_captured2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d0c71d0> +immutable = False, remove_origin = True + + @pytest.mark.parametrize("immutable", [False, True]) + @pytest.mark.parametrize("remove_origin", [False, True]) + def test_builder_requires_captured_git_origin_after_verification( + tmp_path: Path, monkeypatch, immutable: bool, remove_origin: bool + ) -> None: + root = tmp_path / "source" + _repository(root) + snapshot = scope._materialize_commit(root, _git(root, "rev-parse", "HEAD")) if immutable else None + try: + plan = ( + discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + if snapshot is not None + else discover_project(root) + ) + staging = tmp_path / "staging" + staging.mkdir() + acquisition = [] + + def remove_git_after_verification(selected_plan): + verify_inputs(selected_plan) + if remove_origin: + (root / ".git").rename(tmp_path / "removed-git") + + def stop_before_acquisition(*_args, **_kwargs): + acquisition.append(True) + assert _git(staging / "project", "rev-parse", "HEAD") == plan.vcs["commit"] + raise RuntimeError("stop before acquisition") + + monkeypatch.setattr(runtime_builder, "verify_inputs", remove_git_after_verification) + monkeypatch.setattr(runtime_builder, "stage_git", stop_before_acquisition) + expected = ProjectRuntimeError if remove_origin else RuntimeError + diagnostic = "project_git_snapshot_origin_missing" if remove_origin else "stop before acquisition" + with pytest.raises(expected, match=diagnostic): +> runtime_builder._build(plan, object(), staging) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:515: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:135: in _build + stage_git(staging) +tests/unit/specfact_code_review/run/test_runtime_vcs.py:507: in stop_before_acquisition + assert _git(staging / "project", "rev-parse", "HEAD") == plan.vcs["commit"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:20: in _git + return subprocess.check_output( +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:473: in check_output + return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +input = None, capture_output = False, timeout = None, check = True +popenargs = (['git', '-C', '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-284/test_builder_requires_captured2/staging/project', 'rev-parse', 'HEAD'],) +kwargs = {'stdout': -1, 'text': True, 'env': {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...}} + + def run(*popenargs, + input=None, capture_output=False, timeout=None, check=False, **kwargs): + """Run command with arguments and return a CompletedProcess instance. + + The returned instance will have attributes args, returncode, stdout and + stderr. By default, stdout and stderr are not captured, and those attributes + will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them, + or pass capture_output=True to capture both. + + If check is True and the exit code was non-zero, it raises a + CalledProcessError. The CalledProcessError object will have the return code + in the returncode attribute, and output & stderr attributes if those streams + were captured. + + If timeout (seconds) is given and the process takes too long, + a TimeoutExpired exception will be raised. + + There is an optional argument "input", allowing you to + pass bytes or a string to the subprocess's stdin. If you use this argument + you may not also use the Popen constructor's "stdin" argument, as + it will be used internally. + + By default, all communication is in bytes, and therefore any "input" should + be bytes, and the stdout and stderr will be bytes. If in text mode, any + "input" should be a string, and stdout and stderr will be strings decoded + according to locale encoding, or by "encoding" if set. Text mode is + triggered by setting any of text, encoding, errors or universal_newlines. + + The other arguments are the same as for the Popen constructor. + """ + if input is not None: + if kwargs.get('stdin') is not None: + raise ValueError('stdin and input arguments may not both be used.') + kwargs['stdin'] = PIPE + + if capture_output: + if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None: + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + kwargs['stdout'] = PIPE + kwargs['stderr'] = PIPE + + with Popen(*popenargs, **kwargs) as process: + try: + stdout, stderr = process.communicate(input, timeout=timeout) + except TimeoutExpired as exc: + process.kill() + if _mswindows: + # Windows accumulates the output in a single blocking + # read() call run on child threads, with the timeout + # being done in a join() on those threads. communicate() + # _after_ kill() is required to collect that and add it + # to the exception. + exc.stdout, exc.stderr = process.communicate() + else: + # POSIX _communicate already populated the output so + # far into the TimeoutExpired exception. + process.wait() + raise + except: # Including KeyboardInterrupt, communicate handled that. + process.kill() + # We don't call process.wait() as .__exit__ does that for us. + raise + retcode = process.poll() + if check and retcode: +> raise CalledProcessError(retcode, process.args, + output=stdout, stderr=stderr) +E subprocess.CalledProcessError: Command '['git', '-C', '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-284/test_builder_requires_captured2/staging/project', 'rev-parse', 'HEAD']' returned non-zero exit status 128. + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:578: CalledProcessError +----------------------------- Captured stderr call ----------------------------- +fatal: not a git repository (or any of the parent directories): .git +___ test_builder_requires_captured_git_origin_after_verification[True-True] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-284/test_builder_requires_captured3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c6bfdf0> +immutable = True, remove_origin = True + + @pytest.mark.parametrize("immutable", [False, True]) + @pytest.mark.parametrize("remove_origin", [False, True]) + def test_builder_requires_captured_git_origin_after_verification( + tmp_path: Path, monkeypatch, immutable: bool, remove_origin: bool + ) -> None: + root = tmp_path / "source" + _repository(root) + snapshot = scope._materialize_commit(root, _git(root, "rev-parse", "HEAD")) if immutable else None + try: + plan = ( + discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + if snapshot is not None + else discover_project(root) + ) + staging = tmp_path / "staging" + staging.mkdir() + acquisition = [] + + def remove_git_after_verification(selected_plan): + verify_inputs(selected_plan) + if remove_origin: + (root / ".git").rename(tmp_path / "removed-git") + + def stop_before_acquisition(*_args, **_kwargs): + acquisition.append(True) + assert _git(staging / "project", "rev-parse", "HEAD") == plan.vcs["commit"] + raise RuntimeError("stop before acquisition") + + monkeypatch.setattr(runtime_builder, "verify_inputs", remove_git_after_verification) + monkeypatch.setattr(runtime_builder, "stage_git", stop_before_acquisition) + expected = ProjectRuntimeError if remove_origin else RuntimeError + diagnostic = "project_git_snapshot_origin_missing" if remove_origin else "stop before acquisition" + with pytest.raises(expected, match=diagnostic): +> runtime_builder._build(plan, object(), staging) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:515: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:135: in _build + stage_git(staging) +tests/unit/specfact_code_review/run/test_runtime_vcs.py:507: in stop_before_acquisition + assert _git(staging / "project", "rev-parse", "HEAD") == plan.vcs["commit"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:20: in _git + return subprocess.check_output( +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:473: in check_output + return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +input = None, capture_output = False, timeout = None, check = True +popenargs = (['git', '-C', '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-284/test_builder_requires_captured3/staging/project', 'rev-parse', 'HEAD'],) +kwargs = {'stdout': -1, 'text': True, 'env': {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...}} + + def run(*popenargs, + input=None, capture_output=False, timeout=None, check=False, **kwargs): + """Run command with arguments and return a CompletedProcess instance. + + The returned instance will have attributes args, returncode, stdout and + stderr. By default, stdout and stderr are not captured, and those attributes + will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them, + or pass capture_output=True to capture both. + + If check is True and the exit code was non-zero, it raises a + CalledProcessError. The CalledProcessError object will have the return code + in the returncode attribute, and output & stderr attributes if those streams + were captured. + + If timeout (seconds) is given and the process takes too long, + a TimeoutExpired exception will be raised. + + There is an optional argument "input", allowing you to + pass bytes or a string to the subprocess's stdin. If you use this argument + you may not also use the Popen constructor's "stdin" argument, as + it will be used internally. + + By default, all communication is in bytes, and therefore any "input" should + be bytes, and the stdout and stderr will be bytes. If in text mode, any + "input" should be a string, and stdout and stderr will be strings decoded + according to locale encoding, or by "encoding" if set. Text mode is + triggered by setting any of text, encoding, errors or universal_newlines. + + The other arguments are the same as for the Popen constructor. + """ + if input is not None: + if kwargs.get('stdin') is not None: + raise ValueError('stdin and input arguments may not both be used.') + kwargs['stdin'] = PIPE + + if capture_output: + if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None: + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + kwargs['stdout'] = PIPE + kwargs['stderr'] = PIPE + + with Popen(*popenargs, **kwargs) as process: + try: + stdout, stderr = process.communicate(input, timeout=timeout) + except TimeoutExpired as exc: + process.kill() + if _mswindows: + # Windows accumulates the output in a single blocking + # read() call run on child threads, with the timeout + # being done in a join() on those threads. communicate() + # _after_ kill() is required to collect that and add it + # to the exception. + exc.stdout, exc.stderr = process.communicate() + else: + # POSIX _communicate already populated the output so + # far into the TimeoutExpired exception. + process.wait() + raise + except: # Including KeyboardInterrupt, communicate handled that. + process.kill() + # We don't call process.wait() as .__exit__ does that for us. + raise + retcode = process.poll() + if check and retcode: +> raise CalledProcessError(retcode, process.args, + output=stdout, stderr=stderr) +E subprocess.CalledProcessError: Command '['git', '-C', '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-284/test_builder_requires_captured3/staging/project', 'rev-parse', 'HEAD']' returned non-zero exit status 128. + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:578: CalledProcessError +----------------------------- Captured stderr call ----------------------------- +fatal: not a git repository (or any of the parent directories): .git +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_requires_captured_git_origin_after_verification[True-False] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_requires_captured_git_origin_after_verification[True-True] +================== 2 failed, 2 passed, 23 deselected in 1.00s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILDER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILDER_RED.txt new file mode 100644 index 00000000..769d4ec8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILDER_RED.txt @@ -0,0 +1,20 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 0 items / 1 error + +==================================== ERRORS ==================================== +_ ERROR collecting tests/unit/specfact_code_review/run/test_runtime_builder.py _ +ImportError while importing test module '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/tests/unit/specfact_code_review/run/test_runtime_builder.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +tests/unit/specfact_code_review/run/test_runtime_builder.py:8: in + from specfact_code_review.run.runtime_builder import builder_command, copy_project +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_builder' +=========================== short test summary info ============================ +ERROR tests/unit/specfact_code_review/run/test_runtime_builder.py +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +=============================== 1 error in 0.11s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILDER_VCS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILDER_VCS_RED.txt new file mode 100644 index 00000000..0cd5fb73 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILDER_VCS_RED.txt @@ -0,0 +1,25 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 6 items + +tests/unit/specfact_code_review/run/test_runtime_builder.py .....F [100%] + +=================================== FAILURES =================================== +______ test_builder_provides_vcs_metadata_tool_without_host_configuration ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-29/test_builder_provides_vcs_meta0') + + def test_builder_provides_vcs_metadata_tool_without_host_configuration(tmp_path: Path) -> None: + runtime = SimpleNamespace(root=tmp_path / 'capsule', interpreter='/opt/specfact/python/bin/python') + command = builder_command(runtime, staging=tmp_path / 'staging', executable='/proc/self/fd/12') +> assert '/opt/specfact/output/builder-tools/bin:/opt/specfact/python/bin:/usr/bin:/bin' in command +E AssertionError: assert '/opt/specfact/output/builder-tools/bin:/opt/specfact/python/bin:/usr/bin:/bin' in ['/proc/self/fd/12', '--die-with-parent', '--new-session', '--unshare-all', '--share-net', '--ro-bind', ...] + +tests/unit/specfact_code_review/run/test_runtime_builder.py:64: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_builder_provides_vcs_metadata_tool_without_host_configuration +========================= 1 failed, 5 passed in 0.11s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILD_ARTIFACT_BOUNDARY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILD_ARTIFACT_BOUNDARY_RED.txt new file mode 100644 index 00000000..03fdc11a --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILD_ARTIFACT_BOUNDARY_RED.txt @@ -0,0 +1,357 @@ +Recorded before the artifact topology validator or integration call existed. Temporary checkout paths normalized; failure results unchanged. + +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 8 items + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py FF [ 25%] +FFFF.F [100%] + +=================================== FAILURES =================================== +_____ test_prepare_rejects_artifact_topology_before_inventory_access[root] _____ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bf25f30> +mutation = 'root' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:80: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:248: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_artifact_0/cache/.preparing-p29ze2xy/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("controller read inventory before rejecting untrusted artifact topology") +E AssertionError: controller read inventory before rejecting untrusted artifact topology + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:70: AssertionError +_ test_prepare_rejects_artifact_topology_before_inventory_access[native-directory] _ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bf26650> +mutation = 'native-directory' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:80: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:248: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_artifact_1/cache/.preparing-wl7rs41l/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("controller read inventory before rejecting untrusted artifact topology") +E AssertionError: controller read inventory before rejecting untrusted artifact topology + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:70: AssertionError +__ test_prepare_rejects_artifact_topology_before_inventory_access[inventory] ___ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bfd5130> +mutation = 'inventory' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:80: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:248: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_artifact_2/cache/.preparing-bjd7pq86/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("controller read inventory before rejecting untrusted artifact topology") +E AssertionError: controller read inventory before rejecting untrusted artifact topology + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:70: AssertionError +_ test_prepare_rejects_artifact_topology_before_inventory_access[nested-file] __ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bf7a9c0> +mutation = 'nested-file' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:80: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:248: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_artifact_3/cache/.preparing-1vj0vnn9/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("controller read inventory before rejecting untrusted artifact topology") +E AssertionError: controller read inventory before rejecting untrusted artifact topology + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:70: AssertionError +_____ test_prepare_rejects_artifact_topology_before_inventory_access[fifo] _____ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bf78f30> +mutation = 'fifo' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:80: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:248: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_artifact_4/cache/.preparing-8f809vcz/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("controller read inventory before rejecting untrusted artifact topology") +E AssertionError: controller read inventory before rejecting untrusted artifact topology + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:70: AssertionError +______ test_prepare_native_directory_cannot_overwrite_external_sentinels _______ + +tmp_path = PosixPath('/test_prepare_native_directory_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bfc2950> + + def test_prepare_native_directory_cannot_overwrite_external_sentinels( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside, system = tmp_path / "simulated-host", tmp_path / "system-fixtures" + outside.mkdir() + system.mkdir() + payload = bytearray(64) + payload[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" assert all((outside / name).read_bytes() == b"UNCHANGED-SENTINEL" for name in names) +E assert False +E + where False = all(. at 0x10bfa2c00>) + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:116: AssertionError +_____________ test_build_artifact_validation_never_reads_payloads ______________ + +tmp_path = PosixPath('/test_build_artifact_validation0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b115310> + + def test_build_artifact_validation_never_reads_payloads(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + artifact = _artifact(tmp_path) + + def forbidden_read(*_args, **_kwargs): + raise AssertionError("topology validation must not read payload bytes") + + monkeypatch.setattr(Path, "read_bytes", forbidden_read) + monkeypatch.setattr(Path, "read_text", forbidden_read) +> runtime_artifacts.validate_build_artifact(artifact) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.runtime_artifacts' has no attribute 'validate_build_artifact' + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:137: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[root] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[native-directory] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[inventory] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[nested-file] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[fifo] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_native_directory_cannot_overwrite_external_sentinels +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_build_artifact_validation_never_reads_payloads +========================= 7 failed, 1 passed in 0.17s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILD_HARDLINK_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILD_HARDLINK_RED.txt new file mode 100644 index 00000000..bbd8776d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILD_HARDLINK_RED.txt @@ -0,0 +1,284 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py F [ 14%] +FFFFF. [100%] + +=================================== FAILURES =================================== +______________ test_linked_payload_is_rejected_before_read[build] ______________ + +runtime_fixture = (ProjectPlan(root=PosixPath('/test_linked_payload_is_rejecte0/artifact/site-packages/customer.py')) +tmp_path = PosixPath('/test_linked_payload_is_rejecte0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107d76650> +phase = 'build' + + @pytest.mark.parametrize("phase", ["build", "seal", "load"]) + def test_linked_payload_is_rejected_before_read(runtime_fixture, tmp_path: Path, monkeypatch, phase: str) -> None: + plan, artifact, payload = runtime_fixture + prepared = _seal(plan, artifact) if phase == "load" else None + os.link(payload, tmp_path / "external-alias") + _guard_bytes(monkeypatch, payload) +> with pytest.raises(ProjectRuntimeError, match="hardlink"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:46: Failed +______________ test_linked_payload_is_rejected_before_read[seal] _______________ + +runtime_fixture = (ProjectPlan(root=PosixPath('/test_linked_payload_is_rejecte1/artifact/site-packages/customer.py')) +tmp_path = PosixPath('/test_linked_payload_is_rejecte1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107d75a70> +phase = 'seal' + + @pytest.mark.parametrize("phase", ["build", "seal", "load"]) + def test_linked_payload_is_rejected_before_read(runtime_fixture, tmp_path: Path, monkeypatch, phase: str) -> None: + plan, artifact, payload = runtime_fixture + prepared = _seal(plan, artifact) if phase == "load" else None + os.link(payload, tmp_path / "external-alias") + _guard_bytes(monkeypatch, payload) + with pytest.raises(ProjectRuntimeError, match="hardlink"): + if phase == "build": + runtime_artifacts.validate_build_artifact(artifact) + elif phase == "seal": +> _seal(plan, artifact) + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:50: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:26: in _seal + return runtime_artifacts.seal_runtime(artifact, plan=plan, environment_id="test-abi", worker_identity="worker") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py:91: in seal_runtime + "payload": _payload_manifest(root), + ^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py:66: in _payload_manifest + entries[name] = {"kind": "file", "mode": stat.S_IMODE(mode), "digest": content_digest(path.read_bytes())} + ^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_linked_payload_is_rejecte1/artifact/site-packages/customer.py') + + def read(path): + if path == forbidden: +> raise AssertionError("linked payload read before rejection") +E AssertionError: linked payload read before rejection + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:34: AssertionError +______________ test_linked_payload_is_rejected_before_read[load] _______________ + +runtime_fixture = (ProjectPlan(root=PosixPath('/test_linked_payload_is_rejecte2/artifact/site-packages/customer.py')) +tmp_path = PosixPath('/test_linked_payload_is_rejecte2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107e69250> +phase = 'load' + + @pytest.mark.parametrize("phase", ["build", "seal", "load"]) + def test_linked_payload_is_rejected_before_read(runtime_fixture, tmp_path: Path, monkeypatch, phase: str) -> None: + plan, artifact, payload = runtime_fixture + prepared = _seal(plan, artifact) if phase == "load" else None + os.link(payload, tmp_path / "external-alias") + _guard_bytes(monkeypatch, payload) + with pytest.raises(ProjectRuntimeError, match="hardlink"): + if phase == "build": + runtime_artifacts.validate_build_artifact(artifact) + elif phase == "seal": + _seal(plan, artifact) + else: +> runtime_artifacts.load_runtime( + prepared.descriptor_path, plan=plan, environment_id="test-abi", worker_identity="worker" + ) + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:52: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py:181: in load_runtime + if descriptor.get("payload") != _payload_manifest(root): + ^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py:66: in _payload_manifest + entries[name] = {"kind": "file", "mode": stat.S_IMODE(mode), "digest": content_digest(path.read_bytes())} + ^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_linked_payload_is_rejecte2/artifact/site-packages/customer.py') + + def read(path): + if path == forbidden: +> raise AssertionError("linked payload read before rejection") +E AssertionError: linked payload read before rejection + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:34: AssertionError +________________ test_linked_descriptor_is_rejected_before_read ________________ + +runtime_fixture = (ProjectPlan(root=PosixPath('/test_linked_descriptor_is_reje0/artifact/site-packages/customer.py')) +tmp_path = PosixPath('/test_linked_descriptor_is_reje0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107de3020> + + def test_linked_descriptor_is_rejected_before_read(runtime_fixture, tmp_path: Path, monkeypatch) -> None: + plan, artifact, _ = runtime_fixture + prepared = _seal(plan, artifact) + os.link(prepared.descriptor_path, tmp_path / "descriptor-alias") + original = Path.read_text + + def read(path, *args, **kwargs): + if path == prepared.descriptor_path: + raise AssertionError("linked descriptor read before rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError, match="hardlink"): +> runtime_artifacts.load_runtime( + prepared.descriptor_path, plan=plan, environment_id="test-abi", worker_identity="worker" + ) + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:70: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py:178: in load_runtime + descriptor = _read_descriptor(path) + ^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py:150: in _read_descriptor + descriptor = json.loads(path.read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_linked_descriptor_is_reje0/artifact/project-runtime.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def read(path, *args, **kwargs): + if path == prepared.descriptor_path: +> raise AssertionError("linked descriptor read before rejection") +E AssertionError: linked descriptor read before rejection + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:65: AssertionError +_______ test_prepare_rejects_log_alias_before_inventory[inventory.json] ________ + +tmp_path = PosixPath('/test_prepare_rejects_log_alias0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107de3570> +linked_name = 'inventory.json' + + @pytest.mark.parametrize("linked_name", ["inventory.json", "site-packages/retained.log"]) + def test_prepare_rejects_log_alias_before_inventory(tmp_path: Path, monkeypatch, linked_name: str) -> None: + source = tmp_path / "source" + source.mkdir() + plan = discover_project(source) + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="worker", root=tmp_path / "capsule") + cache = tmp_path / "cache" + + def build(_plan, _runtime, staging, *, build_log): + artifact = staging / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "inventory.json").write_text("{}", encoding="utf-8") + build_log.write("DISPOSABLE_PRIVATE_DIAGNOSTIC\n") + build_log.flush() + target = artifact / linked_name + target.unlink(missing_ok=True) + # Reconstruct the topology; this does not simulate Linux bind-mount traversal. + os.link(build_log.name, target) + return artifact + + original = Path.read_text + + def read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("inventory read before linked artifact rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:105: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:249: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_log_alias0/cache/.preparing-3p018vww/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("inventory read before linked artifact rejection") +E AssertionError: inventory read before linked artifact rejection + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:99: AssertionError +_ test_prepare_rejects_log_alias_before_inventory[site-packages/retained.log] __ + +tmp_path = PosixPath('/test_prepare_rejects_log_alias1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107e3f550> +linked_name = 'site-packages/retained.log' + + @pytest.mark.parametrize("linked_name", ["inventory.json", "site-packages/retained.log"]) + def test_prepare_rejects_log_alias_before_inventory(tmp_path: Path, monkeypatch, linked_name: str) -> None: + source = tmp_path / "source" + source.mkdir() + plan = discover_project(source) + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="worker", root=tmp_path / "capsule") + cache = tmp_path / "cache" + + def build(_plan, _runtime, staging, *, build_log): + artifact = staging / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "inventory.json").write_text("{}", encoding="utf-8") + build_log.write("DISPOSABLE_PRIVATE_DIAGNOSTIC\n") + build_log.flush() + target = artifact / linked_name + target.unlink(missing_ok=True) + # Reconstruct the topology; this does not simulate Linux bind-mount traversal. + os.link(build_log.name, target) + return artifact + + original = Path.read_text + + def read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("inventory read before linked artifact rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError) as error: +> runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:105: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:249: in prepare_runtime + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_prepare_rejects_log_alias1/cache/.preparing-u455xh1j/artifact/inventory.json') +args = (), kwargs = {'encoding': 'utf-8'} + + def read(path, *args, **kwargs): + if path.name == "inventory.json": +> raise AssertionError("inventory read before linked artifact rejection") +E AssertionError: inventory read before linked artifact rejection + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:99: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_linked_payload_is_rejected_before_read[build] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_linked_payload_is_rejected_before_read[seal] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_linked_payload_is_rejected_before_read[load] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_linked_descriptor_is_rejected_before_read +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_prepare_rejects_log_alias_before_inventory[inventory.json] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_prepare_rejects_log_alias_before_inventory[site-packages/retained.log] +========================= 6 failed, 1 passed in 1.31s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILD_LOG_DOWNSTREAM_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILD_LOG_DOWNSTREAM_RED.txt new file mode 100644 index 00000000..9b3399b0 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILD_LOG_DOWNSTREAM_RED.txt @@ -0,0 +1,113 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 16 items / 12 deselected / 4 selected + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py FFFF [100%] + +=================================== FAILURES =================================== +_ test_downstream_failure_retains_log_and_original_diagnostic[inventory_native] _ + +tmp_path = PosixPath('/test_downstream_failure_retain0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c82d6e0> +stage = 'inventory_native' + + @pytest.mark.parametrize("stage", ["inventory_native", "seal_runtime", "publish_artifact", "load_runtime"]) + def test_downstream_failure_retains_log_and_original_diagnostic(tmp_path: Path, monkeypatch, stage: str) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, SUCCESS_SCRIPT) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def failed_stage(*_args, **_kwargs): + raise ProjectRuntimeError("project_native_library_missing:libcustomer.so") + + monkeypatch.setattr(runtime_builder, stage, failed_stage) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) +> assert len(logs) == 1 and logs[0].read_text() == "success\n" +E assert (0 == 1) +E + where 0 = len([]) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:192: AssertionError +__ test_downstream_failure_retains_log_and_original_diagnostic[seal_runtime] ___ + +tmp_path = PosixPath('/test_downstream_failure_retain1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c82d940> +stage = 'seal_runtime' + + @pytest.mark.parametrize("stage", ["inventory_native", "seal_runtime", "publish_artifact", "load_runtime"]) + def test_downstream_failure_retains_log_and_original_diagnostic(tmp_path: Path, monkeypatch, stage: str) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, SUCCESS_SCRIPT) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def failed_stage(*_args, **_kwargs): + raise ProjectRuntimeError("project_native_library_missing:libcustomer.so") + + monkeypatch.setattr(runtime_builder, stage, failed_stage) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) +> assert len(logs) == 1 and logs[0].read_text() == "success\n" +E assert (0 == 1) +E + where 0 = len([]) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:192: AssertionError +_ test_downstream_failure_retains_log_and_original_diagnostic[publish_artifact] _ + +tmp_path = PosixPath('/test_downstream_failure_retain2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c7a7ad0> +stage = 'publish_artifact' + + @pytest.mark.parametrize("stage", ["inventory_native", "seal_runtime", "publish_artifact", "load_runtime"]) + def test_downstream_failure_retains_log_and_original_diagnostic(tmp_path: Path, monkeypatch, stage: str) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, SUCCESS_SCRIPT) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def failed_stage(*_args, **_kwargs): + raise ProjectRuntimeError("project_native_library_missing:libcustomer.so") + + monkeypatch.setattr(runtime_builder, stage, failed_stage) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) +> assert len(logs) == 1 and logs[0].read_text() == "success\n" +E assert (0 == 1) +E + where 0 = len([]) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:192: AssertionError +__ test_downstream_failure_retains_log_and_original_diagnostic[load_runtime] ___ + +tmp_path = PosixPath('/test_downstream_failure_retain3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c8658c0> +stage = 'load_runtime' + + @pytest.mark.parametrize("stage", ["inventory_native", "seal_runtime", "publish_artifact", "load_runtime"]) + def test_downstream_failure_retains_log_and_original_diagnostic(tmp_path: Path, monkeypatch, stage: str) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, SUCCESS_SCRIPT) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def failed_stage(*_args, **_kwargs): + raise ProjectRuntimeError("project_native_library_missing:libcustomer.so") + + monkeypatch.setattr(runtime_builder, stage, failed_stage) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) +> assert len(logs) == 1 and logs[0].read_text() == "success\n" +E assert (0 == 1) +E + where 0 = len([]) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:192: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_downstream_failure_retains_log_and_original_diagnostic[inventory_native] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_downstream_failure_retains_log_and_original_diagnostic[seal_runtime] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_downstream_failure_retains_log_and_original_diagnostic[publish_artifact] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_downstream_failure_retains_log_and_original_diagnostic[load_runtime] +======================= 4 failed, 12 deselected in 0.23s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILD_LOG_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILD_LOG_RED.txt new file mode 100644 index 00000000..cc7a67bd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILD_LOG_RED.txt @@ -0,0 +1,606 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 10 items + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py FFFF [ 40%] +FFFFFF [100%] + +=================================== FAILURES =================================== +_________ test_staging_log_symlink_cannot_write_or_read_host_sentinel __________ + +tmp_path = PosixPath('/test_staging_log_symlink_canno0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a56d5b0> + + def test_staging_log_symlink_cannot_write_or_read_host_sentinel(tmp_path: Path, monkeypatch) -> None: + sentinel = tmp_path / "sentinel" + sentinel.write_text("PRIVATE_SENTINEL_UNCHANGED\n") + script = f"(staging/'build.log').symlink_to({str(sentinel)!r}); print('private-builder-output'); sys.exit(1)" + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, script) + error = _failed_build(plan, runtime, cache) +> assert sentinel.read_text() == "PRIVATE_SENTINEL_UNCHANGED\n" +E AssertionError: assert 'PRIVATE_SENT...lder-output\n' == 'PRIVATE_SENTINEL_UNCHANGED\n' +E +E PRIVATE_SENTINEL_UNCHANGED +E + private-builder-output + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:64: AssertionError +____________ test_controller_never_opens_special_staging_log[fifo] _____________ + +tmp_path = PosixPath('/test_controller_never_opens_sp0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a56ce90> +kind = 'fifo' + + @pytest.mark.parametrize("kind", ["fifo", "directory"]) + def test_controller_never_opens_special_staging_log(tmp_path: Path, monkeypatch, kind: str) -> None: + operation = "os.mkfifo(staging/'build.log')" if kind == "fifo" else "(staging/'build.log').mkdir()" + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, operation + "; print('build-failed'); sys.exit(1)") + native_open = Path.open + + def checked_open(path, *args, **kwargs): + if path.name == "build.log" and path.parent.name.startswith(".preparing-"): + raise AssertionError("controller attempted an untrusted special-file open") + return native_open(path, *args, **kwargs) + + monkeypatch.setattr(Path, "open", checked_open) +> _failed_build(plan, runtime, cache) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:86: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:53: in _failed_build + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:241: in prepare_runtime + artifact = _build(plan, runtime, staging) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:157: in _build + with (staging / "build.log").open("a", encoding="utf-8") as stream: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_controller_never_opens_sp0/cache/.preparing-elf33638/build.log') +args = ('a',), kwargs = {'encoding': 'utf-8'} + + def checked_open(path, *args, **kwargs): + if path.name == "build.log" and path.parent.name.startswith(".preparing-"): +> raise AssertionError("controller attempted an untrusted special-file open") +E AssertionError: controller attempted an untrusted special-file open + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:82: AssertionError +__________ test_controller_never_opens_special_staging_log[directory] __________ + +tmp_path = PosixPath('/test_controller_never_opens_sp1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a4dfbf0> +kind = 'directory' + + @pytest.mark.parametrize("kind", ["fifo", "directory"]) + def test_controller_never_opens_special_staging_log(tmp_path: Path, monkeypatch, kind: str) -> None: + operation = "os.mkfifo(staging/'build.log')" if kind == "fifo" else "(staging/'build.log').mkdir()" + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, operation + "; print('build-failed'); sys.exit(1)") + native_open = Path.open + + def checked_open(path, *args, **kwargs): + if path.name == "build.log" and path.parent.name.startswith(".preparing-"): + raise AssertionError("controller attempted an untrusted special-file open") + return native_open(path, *args, **kwargs) + + monkeypatch.setattr(Path, "open", checked_open) +> _failed_build(plan, runtime, cache) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:86: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:53: in _failed_build + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:241: in prepare_runtime + artifact = _build(plan, runtime, staging) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:157: in _build + with (staging / "build.log").open("a", encoding="utf-8") as stream: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/test_controller_never_opens_sp1/cache/.preparing-4phz4s1z/build.log') +args = ('a',), kwargs = {'encoding': 'utf-8'} + + def checked_open(path, *args, **kwargs): + if path.name == "build.log" and path.parent.name.startswith(".preparing-"): +> raise AssertionError("controller attempted an untrusted special-file open") +E AssertionError: controller attempted an untrusted special-file open + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:82: AssertionError +____ test_failure_retention_never_reopens_predictable_destination[symlink] _____ + +tmp_path = PosixPath('/test_failure_retention_never_r0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a59d8c0> +kind = 'symlink' + + @pytest.mark.parametrize("kind", ["symlink", "fifo", "directory"]) + def test_failure_retention_never_reopens_predictable_destination(tmp_path: Path, monkeypatch, kind: str) -> None: + sentinel = tmp_path / "sentinel" + sentinel.write_text("UNCHANGED\n") + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, "print('build-failed'); sys.exit(1)") + cache.mkdir() + old_name = cache / f"failed-{KEY}.log" + if kind == "symlink": + old_name.symlink_to(sentinel) + elif kind == "fifo": + os.mkfifo(old_name) + else: + old_name.mkdir() + error = _failed_build(plan, runtime, cache) +> assert sentinel.read_text() == "UNCHANGED\n" +E AssertionError: assert 'build-failed\n' == 'UNCHANGED\n' +E +E - UNCHANGED +E + build-failed + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:104: AssertionError +______ test_failure_retention_never_reopens_predictable_destination[fifo] ______ + +plan = ProjectPlan(root=PosixPath('/test_fai...oups=(), extras=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={}) +runtime = namespace(root=PosixPath('/test_failu...nvironment_id='linux-x86_64-cp312', identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +cache_root = PosixPath('/test_failure_retention_never_r1/cache') +offline = False + + @ensure(lambda result, plan: result.descriptor["project_identity"] == plan.identity) + def prepare_runtime( + plan: ProjectPlan, + *, + runtime: Any, + cache_root: Path | None = None, + offline: bool = False, + ) -> PreparedRuntime: + """Build or validate reusable local dependencies for the exact capsule ABI.""" + environment = str(runtime.environment_id) + _validate_python(plan, environment) + cache = cache_root or Path.home() / ".cache/specfact/code-review/project-runtimes-v2" + cache.mkdir(parents=True, exist_ok=True, mode=0o700) + if cache.is_symlink(): + raise ProjectRuntimeError("project_runtime_cache_symlink") + key = document_digest( + { + "project": plan.identity, + "environment": environment, + "worker": runtime.identity, + "manager": MANAGER_REQUIREMENTS[plan.manager], + "builder": { + name: content_digest(Path(__file__).with_name(name).read_bytes()) + for name in ( + "runtime_build_driver.py", + "runtime_builder.py", + "runtime_native.py", + "runtime_adapters.py", + "runtime_domains.py", + "runtime_vcs.py", + "runtime_git.py", + "target_bootstrap.py", + "target_launch.py", + "target_pytest.py", + "target_pylint.py", + "sitecustomize.py", + ) + }, + "git": git_identity(), + } + )[7:] + destination = cache / key + descriptor_path = destination / "project-runtime.json" + if destination.exists(): + return load_runtime(descriptor_path, plan=plan, environment_id=environment, worker_identity=runtime.identity) + if offline: + raise ProjectRuntimeError("project_runtime_offline_cache_miss: run runtime prepare with network first") + with tempfile.TemporaryDirectory(prefix=".preparing-", dir=cache) as directory: + staging = Path(directory) + try: +> artifact = _build(plan, runtime, staging) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:241: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/test_fai...oups=(), extras=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={}) +runtime = namespace(root=PosixPath('/test_failu...nvironment_id='linux-x86_64-cp312', identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +staging = PosixPath('/test_failure_retention_never_r1/cache/.preparing-lvfq7u0n') + + def _build(plan: ProjectPlan, runtime: Any, staging: Path) -> Path: + verify_inputs(plan) + copy_project(plan.root, staging / "project", include_vcs=False) + if source_identity(staging / "project") != plan.source_identity: + raise ProjectRuntimeError("project_runtime_source_changed_during_copy") + if plan.vcs: + copy_vcs_context( + plan.vcs_repository or plan.root, + staging / "project", + plan.vcs["commit"], + tree=plan.vcs.get("tree"), + bound_vcs=plan.vcs, + ) + stage_git(staging) + (staging / "home").mkdir() + driver = Path(__file__).with_name("runtime_build_driver.py") + shutil.copyfile(driver, staging / "build_driver.py") + config = { + "manager": plan.manager, + "environment": plan.environment, + "interpreter": runtime.interpreter, + "manager_requirements": MANAGER_REQUIREMENTS[plan.manager], + "commands": install_commands(plan, python="/opt/specfact/output/tools/bin/python"), + } + (staging / "build.json").write_text(json.dumps(config), encoding="utf-8") + descriptor = sandbox._verified_bubblewrap_descriptor(runtime.root, runtime.bubblewrap) + try: + completed = subprocess.run( + builder_command(runtime, staging=staging, executable=f"/proc/self/fd/{descriptor}"), + pass_fds=(descriptor,), + capture_output=True, + text=True, + check=False, + timeout=4500, + ) + finally: + os.close(descriptor) + if completed.returncode: + with (staging / "build.log").open("a", encoding="utf-8") as stream: + stream.write(completed.stdout + completed.stderr) + # Build logs stay private; repository-controlled errors may contain index credentials. +> raise ProjectRuntimeError( + f"project_runtime_prepare_failed:exit={completed.returncode}; " + f"inspect private build log {staging / 'build.log'}" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_runtime_prepare_failed:exit=1; inspect private build log /test_failure_retention_never_r1/cache/.preparing-lvfq7u0n/build.log + +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:160: ProjectRuntimeError + +During handling of the above exception, another exception occurred: + +tmp_path = PosixPath('/test_failure_retention_never_r1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a59dd00> +kind = 'fifo' + + @pytest.mark.parametrize("kind", ["symlink", "fifo", "directory"]) + def test_failure_retention_never_reopens_predictable_destination(tmp_path: Path, monkeypatch, kind: str) -> None: + sentinel = tmp_path / "sentinel" + sentinel.write_text("UNCHANGED\n") + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, "print('build-failed'); sys.exit(1)") + cache.mkdir() + old_name = cache / f"failed-{KEY}.log" + if kind == "symlink": + old_name.symlink_to(sentinel) + elif kind == "fifo": + os.mkfifo(old_name) + else: + old_name.mkdir() +> error = _failed_build(plan, runtime, cache) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:103: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:53: in _failed_build + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:246: in prepare_runtime + shutil.copyfile(log, retained) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +src = PosixPath('/test_failure_retention_never_r1/cache/.preparing-lvfq7u0n/build.log') +dst = PosixPath('/test_failure_retention_never_r1/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.log') +follow_symlinks = True + + def copyfile(src, dst, *, follow_symlinks=True): + """Copy data from src to dst in the most efficient way possible. + + If follow_symlinks is not set and src is a symbolic link, a new + symlink will be created instead of copying the file it points to. + + """ + sys.audit("shutil.copyfile", src, dst) + + if _samefile(src, dst): + raise SameFileError("{!r} and {!r} are the same file".format(src, dst)) + + file_size = 0 + for i, fn in enumerate([src, dst]): + try: + st = _stat(fn) + except OSError: + # File most likely does not exist + pass + else: + # XXX What about other special files? (sockets, devices...) + if stat.S_ISFIFO(st.st_mode): + fn = fn.path if isinstance(fn, os.DirEntry) else fn +> raise SpecialFileError("`%s` is a named pipe" % fn) +E shutil.SpecialFileError: `/test_failure_retention_never_r1/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.log` is a named pipe + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/shutil.py:306: SpecialFileError +___ test_failure_retention_never_reopens_predictable_destination[directory] ____ + +plan = ProjectPlan(root=PosixPath('/test_fai...oups=(), extras=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={}) +runtime = namespace(root=PosixPath('/test_failu...nvironment_id='linux-x86_64-cp312', identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +cache_root = PosixPath('/test_failure_retention_never_r2/cache') +offline = False + + @ensure(lambda result, plan: result.descriptor["project_identity"] == plan.identity) + def prepare_runtime( + plan: ProjectPlan, + *, + runtime: Any, + cache_root: Path | None = None, + offline: bool = False, + ) -> PreparedRuntime: + """Build or validate reusable local dependencies for the exact capsule ABI.""" + environment = str(runtime.environment_id) + _validate_python(plan, environment) + cache = cache_root or Path.home() / ".cache/specfact/code-review/project-runtimes-v2" + cache.mkdir(parents=True, exist_ok=True, mode=0o700) + if cache.is_symlink(): + raise ProjectRuntimeError("project_runtime_cache_symlink") + key = document_digest( + { + "project": plan.identity, + "environment": environment, + "worker": runtime.identity, + "manager": MANAGER_REQUIREMENTS[plan.manager], + "builder": { + name: content_digest(Path(__file__).with_name(name).read_bytes()) + for name in ( + "runtime_build_driver.py", + "runtime_builder.py", + "runtime_native.py", + "runtime_adapters.py", + "runtime_domains.py", + "runtime_vcs.py", + "runtime_git.py", + "target_bootstrap.py", + "target_launch.py", + "target_pytest.py", + "target_pylint.py", + "sitecustomize.py", + ) + }, + "git": git_identity(), + } + )[7:] + destination = cache / key + descriptor_path = destination / "project-runtime.json" + if destination.exists(): + return load_runtime(descriptor_path, plan=plan, environment_id=environment, worker_identity=runtime.identity) + if offline: + raise ProjectRuntimeError("project_runtime_offline_cache_miss: run runtime prepare with network first") + with tempfile.TemporaryDirectory(prefix=".preparing-", dir=cache) as directory: + staging = Path(directory) + try: +> artifact = _build(plan, runtime, staging) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:241: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/test_fai...oups=(), extras=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={}) +runtime = namespace(root=PosixPath('/test_failu...nvironment_id='linux-x86_64-cp312', identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') +staging = PosixPath('/test_failure_retention_never_r2/cache/.preparing-wvbop8va') + + def _build(plan: ProjectPlan, runtime: Any, staging: Path) -> Path: + verify_inputs(plan) + copy_project(plan.root, staging / "project", include_vcs=False) + if source_identity(staging / "project") != plan.source_identity: + raise ProjectRuntimeError("project_runtime_source_changed_during_copy") + if plan.vcs: + copy_vcs_context( + plan.vcs_repository or plan.root, + staging / "project", + plan.vcs["commit"], + tree=plan.vcs.get("tree"), + bound_vcs=plan.vcs, + ) + stage_git(staging) + (staging / "home").mkdir() + driver = Path(__file__).with_name("runtime_build_driver.py") + shutil.copyfile(driver, staging / "build_driver.py") + config = { + "manager": plan.manager, + "environment": plan.environment, + "interpreter": runtime.interpreter, + "manager_requirements": MANAGER_REQUIREMENTS[plan.manager], + "commands": install_commands(plan, python="/opt/specfact/output/tools/bin/python"), + } + (staging / "build.json").write_text(json.dumps(config), encoding="utf-8") + descriptor = sandbox._verified_bubblewrap_descriptor(runtime.root, runtime.bubblewrap) + try: + completed = subprocess.run( + builder_command(runtime, staging=staging, executable=f"/proc/self/fd/{descriptor}"), + pass_fds=(descriptor,), + capture_output=True, + text=True, + check=False, + timeout=4500, + ) + finally: + os.close(descriptor) + if completed.returncode: + with (staging / "build.log").open("a", encoding="utf-8") as stream: + stream.write(completed.stdout + completed.stderr) + # Build logs stay private; repository-controlled errors may contain index credentials. +> raise ProjectRuntimeError( + f"project_runtime_prepare_failed:exit={completed.returncode}; " + f"inspect private build log {staging / 'build.log'}" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_runtime_prepare_failed:exit=1; inspect private build log /test_failure_retention_never_r2/cache/.preparing-wvbop8va/build.log + +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:160: ProjectRuntimeError + +During handling of the above exception, another exception occurred: + +tmp_path = PosixPath('/test_failure_retention_never_r2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a7d9050> +kind = 'directory' + + @pytest.mark.parametrize("kind", ["symlink", "fifo", "directory"]) + def test_failure_retention_never_reopens_predictable_destination(tmp_path: Path, monkeypatch, kind: str) -> None: + sentinel = tmp_path / "sentinel" + sentinel.write_text("UNCHANGED\n") + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, "print('build-failed'); sys.exit(1)") + cache.mkdir() + old_name = cache / f"failed-{KEY}.log" + if kind == "symlink": + old_name.symlink_to(sentinel) + elif kind == "fifo": + os.mkfifo(old_name) + else: + old_name.mkdir() +> error = _failed_build(plan, runtime, cache) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:103: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:53: in _failed_build + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:246: in prepare_runtime + shutil.copyfile(log, retained) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +src = PosixPath('/test_failure_retention_never_r2/cache/.preparing-wvbop8va/build.log') +dst = PosixPath('/test_failure_retention_never_r2/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.log') +follow_symlinks = True + + def copyfile(src, dst, *, follow_symlinks=True): + """Copy data from src to dst in the most efficient way possible. + + If follow_symlinks is not set and src is a symbolic link, a new + symlink will be created instead of copying the file it points to. + + """ + sys.audit("shutil.copyfile", src, dst) + + if _samefile(src, dst): + raise SameFileError("{!r} and {!r} are the same file".format(src, dst)) + + file_size = 0 + for i, fn in enumerate([src, dst]): + try: + st = _stat(fn) + except OSError: + # File most likely does not exist + pass + else: + # XXX What about other special files? (sockets, devices...) + if stat.S_ISFIFO(st.st_mode): + fn = fn.path if isinstance(fn, os.DirEntry) else fn + raise SpecialFileError("`%s` is a named pipe" % fn) + if _WINDOWS and i == 0: + file_size = st.st_size + + if not follow_symlinks and _islink(src): + os.symlink(os.readlink(src), dst) + else: + with open(src, 'rb') as fsrc: + try: +> with open(dst, 'wb') as fdst: + ^^^^^^^^^^^^^^^ +E IsADirectoryError: [Errno 21] Is a directory: '/test_failure_retention_never_r2/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.log' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/shutil.py:315: IsADirectoryError +______ test_timeout_retains_private_partial_output_and_closes_descriptor _______ + +tmp_path = PosixPath('/test_timeout_retains_private_p0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a597350> + + def test_timeout_retains_private_partial_output_and_closes_descriptor(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder( + tmp_path, monkeypatch, "print('partial-builder-output', flush=True); time.sleep(10)", timeout=True + ) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) +> assert len(logs) == 1 and logs[0].read_text() == "partial-builder-output\n" +E assert (0 == 1) +E + where 0 = len([]) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:117: AssertionError +______________ test_success_closes_and_removes_private_build_log _______________ + +tmp_path = PosixPath('/test_success_closes_and_remove0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5af020> + + def test_success_closes_and_removes_private_build_log(tmp_path: Path, monkeypatch) -> None: + script = "(staging/'artifact/site-packages').mkdir(parents=True); (staging/'artifact/inventory.json').write_text('{}'); print('success')" + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, script) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + prepared = runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + assert prepared.descriptor_path.is_file() + assert not list(cache.glob("failed-*.log")) and not list(cache.glob(".preparing-*")) +> assert captures[0] is not None and captures[0].closed +E assert (None is not None) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:131: AssertionError +_________ test_driver_forwards_diagnostics_without_opening_builder_log _________ + +tmp_path = PosixPath('/test_driver_forwards_diagnosti0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a7cc140> +capsys = <_pytest.capture.CaptureFixture object at 0x10a52dd30> + + def test_driver_forwards_diagnostics_without_opening_builder_log(tmp_path: Path, monkeypatch, capsys) -> None: + (tmp_path / "project").mkdir() + sentinel = tmp_path / "sentinel" + sentinel.write_text("UNCHANGED\n") + (tmp_path / "build.log").symlink_to(sentinel) + monkeypatch.setattr(runtime_build_driver, "ROOT", tmp_path) + output = runtime_build_driver._run([sys.executable, "-c", "import sys; print('native-out'); print('native-err', file=sys.stderr)"]) + assert output == "native-out" +> assert sentinel.read_text() == "UNCHANGED\n" +E AssertionError: assert 'UNCHANGED\nn...nnative-err\n' == 'UNCHANGED\n' +E +E UNCHANGED +E + native-out +E + native-err + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:142: AssertionError +______________ test_driver_forwards_native_timeout_partial_output ______________ + +tmp_path = PosixPath('/test_driver_forwards_native_ti0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5db150> +capsys = <_pytest.capture.CaptureFixture object at 0x10a759950> + + def test_driver_forwards_native_timeout_partial_output(tmp_path: Path, monkeypatch, capsys) -> None: + (tmp_path / "project").mkdir() + monkeypatch.setattr(runtime_build_driver, "ROOT", tmp_path) + native_run = subprocess.run + + def timed_run(command, **kwargs): + return native_run(command, **{**kwargs, "timeout": 0.3}) + + monkeypatch.setattr(runtime_build_driver.subprocess, "run", timed_run) + with pytest.raises(subprocess.TimeoutExpired): + runtime_build_driver._run([sys.executable, "-c", "import time; print('native-partial', flush=True); time.sleep(10)"]) +> assert capsys.readouterr().out == "native-partial\n" +E AssertionError: assert '' == 'native-partial\n' +E +E - native-partial + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:157: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_staging_log_symlink_cannot_write_or_read_host_sentinel +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_controller_never_opens_special_staging_log[fifo] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_controller_never_opens_special_staging_log[directory] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_failure_retention_never_reopens_predictable_destination[symlink] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_failure_retention_never_reopens_predictable_destination[fifo] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_failure_retention_never_reopens_predictable_destination[directory] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_timeout_retains_private_partial_output_and_closes_descriptor +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_success_closes_and_removes_private_build_log +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_driver_forwards_diagnostics_without_opening_builder_log +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_driver_forwards_native_timeout_partial_output +============================== 10 failed in 1.02s ============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILD_PIPE_READINESS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILD_PIPE_READINESS_RED.txt new file mode 100644 index 00000000..a95fc5ed --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILD_PIPE_READINESS_RED.txt @@ -0,0 +1,50 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 22 items / 21 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py F [100%] + +=================================== FAILURES =================================== +_______ test_pipe_readiness_race_retries_without_blocking_past_deadline ________ + +tmp_path = PosixPath('/test_pipe_readiness_race_retri0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a871e00> + + def test_pipe_readiness_race_retries_without_blocking_past_deadline(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, _ = _builder( + tmp_path, monkeypatch, "print('before', flush=True); time.sleep(0.05); print('after', flush=True); sys.exit(1)" + ) + native_popen = subprocess.Popen + native_read = os.read + stdout_descriptors = [] + observed_blocking = [] + + @contextmanager + def record_stdout(*args, **kwargs): + with native_popen(*args, **kwargs) as process: + stdout_descriptors.append(process.stdout.fileno()) + yield process + + def transiently_drained(fd, size): + if fd in stdout_descriptors and not observed_blocking: + observed_blocking.append(os.get_blocking(fd)) + raise BlockingIOError("another reader drained the pipe after readiness") + return native_read(fd, size) + + monkeypatch.setattr(subprocess, "Popen", record_stdout) + monkeypatch.setattr(os, "read", transiently_drained) + _failed_build(plan, runtime, cache) +> assert _retained_logs(cache)[0].read_text() == "before\nafter\n" +E AssertionError: assert '' == 'before\nafter\n' +E +E - before +E - after + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:338: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_pipe_readiness_race_retries_without_blocking_past_deadline +======================= 1 failed, 21 deselected in 0.14s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/BUILD_PIPE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/BUILD_PIPE_RED.txt new file mode 100644 index 00000000..354cced1 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/BUILD_PIPE_RED.txt @@ -0,0 +1,39 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 18 items / 16 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py Fs [100%] + +=================================== FAILURES =================================== +______________ test_builder_stdout_is_pipe_not_private_log_inode _______________ + +tmp_path = PosixPath('/test_builder_stdout_is_pipe_no0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ca35e00> + + def test_builder_stdout_is_pipe_not_private_log_inode(tmp_path: Path, monkeypatch) -> None: + script = ( + "import stat; " + "assert stat.S_ISFIFO(os.fstat(1).st_mode), 'controller log inode exposed'; " + "print('PIPE_BOUNDARY_OK', flush=True); sys.exit(1)" + ) + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, script) + _failed_build(plan, runtime, cache) +> assert _retained_logs(cache)[0].read_text() == "PIPE_BOUNDARY_OK\n" +E assert 'Traceback (m...ode exposed\n' == 'PIPE_BOUNDARY_OK\n' +E +E - PIPE_BOUNDARY_OK +E + Traceback (most recent call last): +E + File "", line 1, in +E + import os,sys,time; from pathlib import Path; staging=Path(sys.argv[1]); import stat; assert stat.S_ISFIFO(os.fstat(1).st_mode), 'controller log inode exposed'; print('PIPE_BOUNDARY_OK', flush=True); sys.exit(1) +E + ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^ +E + AssertionError: controller log inode exposed + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:231: AssertionError +=========================== short test summary info ============================ +SKIPPED [1] tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:234: Linux proc descriptor contract +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_builder_stdout_is_pipe_not_private_log_inode +================= 1 failed, 1 skipped, 16 deselected in 1.00s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CACHED_COMMAND_VCS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CACHED_COMMAND_VCS_RED.txt new file mode 100644 index 00000000..b0c4bca5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CACHED_COMMAND_VCS_RED.txt @@ -0,0 +1,141 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 12 items / 9 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py FFF [100%] + +=================================== FAILURES =================================== +__ test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance[head] __ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_real_cached_analysi...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_real_cached_analysis_snap0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d123950> +changed_state = 'head' + + @pytest.mark.parametrize("changed_state", ["head", "index"]) + def test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance( + staged_project, tmp_path: Path, monkeypatch, changed_state: str + ) -> None: + root, resolution = staged_project + monkeypatch.chdir(root) + destination = tmp_path / "cached" + destination.mkdir() + snapshot = runner._cached_analysis_snapshot([Path("renamed.py")], destination) + assert snapshot is not None + if changed_state == "head": + _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "-c", "commit.gpgsign=false", "commit", "-qm", "advance HEAD") + else: + _git(root, "read-tree", "HEAD") +> plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/tests/unit/specfact_code_review/run/test_runtime_vcs.py:200: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_real_cached_analysis_snap0/cached') +config_path = None +source_snapshot = CachedAnalysisSnapshot(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/py...e='ae7df65c33f278677e0d7eb633690c0053ddf0b7', index_tree='14dd0b830997655370b92b481566b173120d1c06', caller_prefix='')) + + @require(lambda root: root.is_dir()) + def discover_snapshot(root: Path, *, config_path: Path | None, source_snapshot: Any = None) -> ProjectPlan: + """Attach selected VCS metadata without changing the immutable source tree.""" + plan = discover_project(root, config_path=config_path) + repository = getattr(source_snapshot, "repository", None) + if repository is None: + return plan + commit = getattr(source_snapshot, "vcs_commit", "") or source_snapshot.commit + if commit.startswith(("index:", "index-")): +> raise ProjectRuntimeError("project_index_vcs_commit_missing: recapture the review index") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_index_vcs_commit_missing: recapture the review index + +/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:56: ProjectRuntimeError +_ test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance[index] __ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_real_cached_analysi...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_real_cached_analysis_snap1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d384050> +changed_state = 'index' + + @pytest.mark.parametrize("changed_state", ["head", "index"]) + def test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance( + staged_project, tmp_path: Path, monkeypatch, changed_state: str + ) -> None: + root, resolution = staged_project + monkeypatch.chdir(root) + destination = tmp_path / "cached" + destination.mkdir() + snapshot = runner._cached_analysis_snapshot([Path("renamed.py")], destination) + assert snapshot is not None + if changed_state == "head": + _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "-c", "commit.gpgsign=false", "commit", "-qm", "advance HEAD") + else: + _git(root, "read-tree", "HEAD") +> plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/tests/unit/specfact_code_review/run/test_runtime_vcs.py:200: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_real_cached_analysis_snap1/cached') +config_path = None +source_snapshot = CachedAnalysisSnapshot(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/py...e='ae7df65c33f278677e0d7eb633690c0053ddf0b7', index_tree='14dd0b830997655370b92b481566b173120d1c06', caller_prefix='')) + + @require(lambda root: root.is_dir()) + def discover_snapshot(root: Path, *, config_path: Path | None, source_snapshot: Any = None) -> ProjectPlan: + """Attach selected VCS metadata without changing the immutable source tree.""" + plan = discover_project(root, config_path=config_path) + repository = getattr(source_snapshot, "repository", None) + if repository is None: + return plan + commit = getattr(source_snapshot, "vcs_commit", "") or source_snapshot.commit + if commit.startswith(("index:", "index-")): +> raise ProjectRuntimeError("project_index_vcs_commit_missing: recapture the review index") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_index_vcs_commit_missing: recapture the review index + +/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:56: ProjectRuntimeError +______ test_cached_capture_rejects_head_commit_change_with_identical_tree ______ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_cached_capture_reje...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_cached_capture_rejects_he0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d54c3b0> + + def test_cached_capture_rejects_head_commit_change_with_identical_tree(staged_project, tmp_path: Path, monkeypatch) -> None: + root, _resolution = staged_project + monkeypatch.chdir(root) + destination = tmp_path / "cached-race" + destination.mkdir() + original = runner._git_tree_identity + + def move_head_after_index(repository: Path, revision: str): + result = original(repository, revision) + if revision == "index": + previous = _git(root, "rev-parse", "HEAD") + tree = _git(root, "rev-parse", "HEAD^{tree}") + commit = _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit-tree", tree, "-p", previous, "-m", "same tree new commit") + _git(root, "update-ref", "HEAD", commit) + return result + + monkeypatch.setattr(runner, "_git_tree_identity", move_head_after_index) +> assert runner._cached_analysis_snapshot([Path("renamed.py")], destination) is None +E AssertionError: assert CachedAnalysisSnapshot(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/py...e='ae7df65c33f278677e0d7eb633690c0053ddf0b7', index_tree='14dd0b830997655370b92b481566b173120d1c06', caller_prefix='')) is None +E + where CachedAnalysisSnapshot(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/py...e='ae7df65c33f278677e0d7eb633690c0053ddf0b7', index_tree='14dd0b830997655370b92b481566b173120d1c06', caller_prefix='')) = ([PosixPath('renamed.py')], PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-254/test_cached_capture_rejects_he0/cached-race')) +E + where = runner._cached_analysis_snapshot + +/tests/unit/specfact_code_review/run/test_runtime_vcs.py:229: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance[head] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance[index] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_cached_capture_rejects_head_commit_change_with_identical_tree +======================= 3 failed, 9 deselected in 0.57s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CANDIDATE_790C_EVIDENCE.json b/openspec/changes/code-review-16-portable-project-runtime/CANDIDATE_790C_EVIDENCE.json new file mode 100644 index 00000000..9996b357 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CANDIDATE_790C_EVIDENCE.json @@ -0,0 +1,653 @@ +{ + "candidate_sha": "790c39eb47b89046f6525cce9f7345f746896706", + "run_url": "https://github.com/nold-ai/specfact-cli-modules/actions/runs/34850206113", + "public_release_acceptance": false, + "network_evidence": "Offline flag and cache reuse only; independent network denial is added in the subsequent candidate.", + "entries": [ + { + "artifact": "capsule-candidate-cp3.11-2", + "repository": "flask", + "status": "PASS", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:ae03a700cddecfe7ebe4267c6e1ec5281b5ea59775f18d187215a118ef515b79", + "artifact_bytes": 23339947, + "assurance_status": "PASS", + "findings": 38, + "collected": 19, + "test_calls": 19, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.8655445570000211, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 62.12832506099994, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 24.051680019999935, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 53.10843620199989, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 54.973957939000115, + "uid": 1001 + } + }, + "report_sha256": "3bca0871445301d0872928d0e444449264c6903b2324f7e191f340bed3855b89" + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "repository": "hatch", + "status": "PASS", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:1257851cadb4ac58c1deb992b767698d207737ce6b8ec29f1fce92713299d88e", + "artifact_bytes": 93538449, + "assurance_status": "PASS", + "findings": 29, + "collected": 5, + "test_calls": 5, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 1.3550481190000028, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 72.45076273200004, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 24.373922411999956, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 54.76910508699996, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 74.03236068899992, + "uid": 1001 + } + }, + "report_sha256": "3d64c1c8fd390cd0e7ea38d82320a9b15819c14cdeedf26f52bddcdd950fd299" + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "repository": "hatch-detached", + "status": "PASS", + "commit": "reconstructed-sha256:ab12ae4c46bb9bd24b4e2888c24e69ae6e74fbb362aa31ab4083a5622fe4d8c9", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:7e38ed23da9e51ffd91445347a3fbdfaeb5ae35d3b2f4ee3572dd73700df5c2c", + "artifact_bytes": 25182148, + "assurance_status": "FAIL", + "findings": 4, + "collected": 1, + "test_calls": 1, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.5101921289999609, + "uid": 1001 + }, + "cold-auto": { + "exit": 1, + "seconds": 65.52007576200003, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 25.015769412000054, + "uid": 1001 + }, + "warm-attach": { + "exit": 1, + "seconds": 45.76698404600006, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 66.83649849900007, + "uid": 1001 + } + }, + "report_sha256": "a4328435658843e532e834d8c2357a63898f77fb4f5c0b4a364f64e2b37e287f" + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "repository": "poetry", + "status": "PASS", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:89bc7c82a655a7531c9f5b9308424d89a133c9f5553b937b9f775ab3617fb3f6", + "artifact_bytes": 81202817, + "assurance_status": "PASS", + "findings": 17, + "collected": 7, + "test_calls": 7, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 4.061343480000005, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 83.482156218, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 24.598323703000005, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 61.010466065999935, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 84.68167394500006, + "uid": 1001 + } + }, + "report_sha256": "6f4e0acada070cd298679e8e9a0f639170df9a85d9905a3f1dc8a8b2e3373def" + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "repository": "requests", + "status": "PASS", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:1fd559fb3b50083db28582ed931d1ccfb613b62ac4de57cb75895fea681794d9", + "artifact_bytes": 54169978, + "assurance_status": "FAIL", + "findings": 76, + "collected": 24, + "test_calls": 24, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.8437639669999726, + "uid": 1001 + }, + "cold-auto": { + "exit": 1, + "seconds": 63.777593712, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 24.226139390999947, + "uid": 1001 + }, + "warm-attach": { + "exit": 1, + "seconds": 48.458951126000045, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 62.373512699, + "uid": 1001 + } + }, + "report_sha256": "b8ae803c40a2bac14f0b99827ce99d2547b7e801fca0abb388f02b59109a146e" + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "repository": "flask", + "status": "PASS", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:0ac9968de38afa1a38875fd08ea39ba422defd5e90019f6f7199b4b0ce16aa64", + "artifact_bytes": 17459850, + "assurance_status": "PASS", + "findings": 38, + "collected": 19, + "test_calls": 19, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.7229073040000458, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 52.58311882199996, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 23.802622895000013, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 46.12251091799999, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 49.56557891399996, + "uid": 1001 + } + }, + "report_sha256": "cd1efae20d844289addbdb98830e7fb4a657b18a5f497dd007a443633aa060e1" + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "repository": "hatch", + "status": "PASS", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:94d5825793ec77e6e0c88888f4af58ad35d14d7454137d42712f7372c5b2b56e", + "artifact_bytes": 93337858, + "assurance_status": "PASS", + "findings": 29, + "collected": 5, + "test_calls": 5, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 1.2149815500000614, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 67.35375056400005, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 24.181192329000055, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 49.60144850200004, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 95.840027889, + "uid": 1001 + } + }, + "report_sha256": "06318d652991594ad69b7ffd68eed16acf7557453b333b42211ff90386d08f4f" + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "repository": "hatch-detached", + "status": "PASS", + "commit": "reconstructed-sha256:ab12ae4c46bb9bd24b4e2888c24e69ae6e74fbb362aa31ab4083a5622fe4d8c9", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:ce26a9b8d004aed4807221539962d987559da9fee89ba57ad412628ead7db79d", + "artifact_bytes": 22320028, + "assurance_status": "FAIL", + "findings": 4, + "collected": 1, + "test_calls": 1, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.38110269300000255, + "uid": 1001 + }, + "cold-auto": { + "exit": 1, + "seconds": 62.84380643999998, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 23.88717918799989, + "uid": 1001 + }, + "warm-attach": { + "exit": 1, + "seconds": 41.872338608000064, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 97.43103900999995, + "uid": 1001 + } + }, + "report_sha256": "0f80a85f29cc99dd4824524900f194b354b72fdb8fde6f9d2446478918c767a9" + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "repository": "poetry", + "status": "PASS", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:44ff272ae83f781f207f9c60a629a1ce9619125ccd52b69df9a8358abec3bdc8", + "artifact_bytes": 75073564, + "assurance_status": "PASS", + "findings": 17, + "collected": 7, + "test_calls": 7, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 3.2802367209999375, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 76.30958475900002, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 24.161435861999962, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 55.53275712499999, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 78.62037464700006, + "uid": 1001 + } + }, + "report_sha256": "a3de9cffde2c14a530a4f0a3e2aa6c019ffaef377ef69d14160114cb406ebbdc" + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "repository": "requests", + "status": "PASS", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:321a0ecc1660563aae95be6ac36da1a7c08af26ecc3bd77d032e5d65c547572e", + "artifact_bytes": 48155980, + "assurance_status": "FAIL", + "findings": 76, + "collected": 24, + "test_calls": 24, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.6940615159999766, + "uid": 1001 + }, + "cold-auto": { + "exit": 1, + "seconds": 60.05805215800001, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 23.860793911999963, + "uid": 1001 + }, + "warm-attach": { + "exit": 1, + "seconds": 44.90659736800001, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 58.515139234, + "uid": 1001 + } + }, + "report_sha256": "fd91a4802160181968ffddaf1933d958df9c89032c8721c54aa9acb4e3467bee" + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "repository": "flask", + "status": "PASS", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:e530e2f77b02b50b8a9ae3826aecc6b2e69e67b791993d8d3a2841c790b5f715", + "artifact_bytes": 17129672, + "assurance_status": "PASS", + "findings": 38, + "collected": 19, + "test_calls": 19, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.9522328820000894, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 65.59216172000004, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 28.240223164999975, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 58.41699450100009, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 59.893755801, + "uid": 1001 + } + }, + "report_sha256": "da86fbb2088fb3ca70f08fcb09d97a2ba19fa23f1e13a44ef81e0bf4c82e526b" + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "repository": "hatch", + "status": "PASS", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:53be990533d25ee71e91de5f77d9ab68d81e85ef6ebf3e2e1aa43bd678167169", + "artifact_bytes": 93330864, + "assurance_status": "PASS", + "findings": 29, + "collected": 5, + "test_calls": 5, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 1.4603343349999705, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 77.93810508299998, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 28.432799085000056, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 61.59329719799996, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 78.14387591400009, + "uid": 1001 + } + }, + "report_sha256": "a3aade877cfc2b709008083b56ce543debde587e20125d6aab1fd9cfb4242195" + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "repository": "hatch-detached", + "status": "PASS", + "commit": "reconstructed-sha256:ab12ae4c46bb9bd24b4e2888c24e69ae6e74fbb362aa31ab4083a5622fe4d8c9", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:3b19e4568c49a2a2eb1b9961f79f65680d1df185f60e60805ca4d278d510b12e", + "artifact_bytes": 22154590, + "assurance_status": "FAIL", + "findings": 4, + "collected": 1, + "test_calls": 1, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.5152638150000257, + "uid": 1001 + }, + "cold-auto": { + "exit": 1, + "seconds": 67.02093121899998, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 28.24764240000013, + "uid": 1001 + }, + "warm-attach": { + "exit": 1, + "seconds": 51.17695420199993, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 66.37384881899993, + "uid": 1001 + } + }, + "report_sha256": "de979e32e5fbf03fe35768725f37cac96f78acba932b51636ada0585c12f95a0" + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "repository": "poetry", + "status": "PASS", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:79ed95d9a0d0b24f55c63e56a46c5f2158b0fc1e7629e8c2c0bb237d20126e02", + "artifact_bytes": 74528534, + "assurance_status": "PASS", + "findings": 17, + "collected": 7, + "test_calls": 7, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 4.568728027000134, + "uid": 1001 + }, + "cold-auto": { + "exit": 0, + "seconds": 88.8511830970001, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 28.745056191999993, + "uid": 1001 + }, + "warm-attach": { + "exit": 0, + "seconds": 68.16085630199996, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 87.39499571399983, + "uid": 1001 + } + }, + "report_sha256": "e91ed3e4f437e90efbf9379b3b5fdcf48d73792bd96b86aa0510ef4b13468e61" + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "repository": "requests", + "status": "PASS", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "mode": "candidate", + "source_unchanged": true, + "runtime_identity": "sha256:63f2963d0f8992cd7b5e32a187f788c11884c43b1385375b78e3e7a778bb4f5b", + "artifact_bytes": 47832822, + "assurance_status": "FAIL", + "findings": 76, + "collected": 24, + "test_calls": 24, + "pytest_version": "9.1.1", + "phases": { + "host-tests": { + "exit": 0, + "seconds": 0.7305600509999977, + "uid": 1001 + }, + "cold-auto": { + "exit": 1, + "seconds": 69.65158838100001, + "uid": 1001 + }, + "offline-prepare": { + "exit": 0, + "seconds": 28.390059093000048, + "uid": 1001 + }, + "warm-attach": { + "exit": 1, + "seconds": 55.32578684699996, + "uid": 1001 + }, + "controlled-defect": { + "exit": 1, + "seconds": 66.79110196400006, + "uid": 1001 + } + }, + "report_sha256": "2be26e057cb8334abcf223354160c94d0cd555dc26a39578a4c4e1e12feb3201" + } + ] +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/CANDIDATE_A413_EVIDENCE.json b/openspec/changes/code-review-16-portable-project-runtime/CANDIDATE_A413_EVIDENCE.json new file mode 100644 index 00000000..c73ad975 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CANDIDATE_A413_EVIDENCE.json @@ -0,0 +1,10587 @@ +{ + "candidate_sha": "a413f700855e081aa0edec8dea35e3a90be41d7d", + "tested_merge_sha": "34db09df344d7d76cd764df7c2b05ebdee40fc1c", + "base_sha": "976460c0541b685f5fae256faef4cec32fe9ba7e", + "merge_tree_matches_candidate": true, + "run_url": "https://github.com/nold-ai/specfact-cli-modules/actions/runs/34869521909", + "public_release_acceptance": false, + "installation": "Signed baseline0.49.85; candidate source execution, not public0.50.0 installation.", + "network_evidence": "Offline prepare and warm attachment independently deny networking; transfer counters are host-wide and include concurrent traffic.", + "entries": [ + { + "artifact": "capsule-candidate-cp3.11-2", + "abi": "cp311", + "repository": "flask", + "status": "PASS", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:601a17a4b66bc3afb40f408ef9137d97572cabae68491fb3f2c0ba09fb93c856", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.11.14", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.11.14", + "python_version": "3.11", + "sys_platform": "linux" + }, + "descriptor_sha256": "20beb0dbb6c8b8ab03e2190bc8138c827dfb7603ca07e36b5e1229f2dc2c9097", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-checkouts/flask/.venv/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/host-junit.xml", + "tests/test_config.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-checkouts/flask/.venv/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/host-junit.xml", + "tests/test_config.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.6489358949999087, + "network": { + "received_bytes": 402, + "sent_bytes": 216, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 43.48355505200004, + "network": { + "received_bytes": 47598550, + "sent_bytes": 339006, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 16.716526972999986, + "network": { + "received_bytes": 23740, + "sent_bytes": 33982, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/71f5559efc73ce3e14a9dad83236cd690bdca073780304fa07f36e34de87282e/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/71f5559efc73ce3e14a9dad83236cd690bdca073780304fa07f36e34de87282e/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 36.07312626999999, + "network": { + "received_bytes": 71802, + "sent_bytes": 85838, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/flask/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 39.76811511999995, + "network": { + "received_bytes": 47592172, + "sent_bytes": 302614, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "604f0e6a7b19d30b9552e4e8f91eb2df364106721e759226c6486c1009bbec60", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:601a17a4b66bc3afb40f408ef9137d97572cabae68491fb3f2c0ba09fb93c856", + "selected": [ + "tests/test_config.py::test_config_from_class", + "tests/test_config.py::test_config_from_envvar", + "tests/test_config.py::test_config_from_envvar_missing", + "tests/test_config.py::test_config_from_file_json", + "tests/test_config.py::test_config_from_file_toml", + "tests/test_config.py::test_config_from_mapping", + "tests/test_config.py::test_config_from_object", + "tests/test_config.py::test_config_from_pyfile", + "tests/test_config.py::test_config_missing", + "tests/test_config.py::test_config_missing_file", + "tests/test_config.py::test_custom_config_class", + "tests/test_config.py::test_from_prefixed_env", + "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "tests/test_config.py::test_from_prefixed_env_nested", + "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "tests/test_config.py::test_get_namespace", + "tests/test_config.py::test_session_lifetime" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_pyfile", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_object", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_json", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_toml", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_nested", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_mapping", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing_file", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_custom_config_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_session_lifetime", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_get_namespace", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_config.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "f9d84ecad6e4e468b8a1851c4d253975d31e6c14742f0681aa2842bc8b138872", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:601a17a4b66bc3afb40f408ef9137d97572cabae68491fb3f2c0ba09fb93c856", + "selected": [ + "tests/test_config.py::test_config_from_class", + "tests/test_config.py::test_config_from_envvar", + "tests/test_config.py::test_config_from_envvar_missing", + "tests/test_config.py::test_config_from_file_json", + "tests/test_config.py::test_config_from_file_toml", + "tests/test_config.py::test_config_from_mapping", + "tests/test_config.py::test_config_from_object", + "tests/test_config.py::test_config_from_pyfile", + "tests/test_config.py::test_config_missing", + "tests/test_config.py::test_config_missing_file", + "tests/test_config.py::test_custom_config_class", + "tests/test_config.py::test_from_prefixed_env", + "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "tests/test_config.py::test_from_prefixed_env_nested", + "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "tests/test_config.py::test_get_namespace", + "tests/test_config.py::test_session_lifetime" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_pyfile", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_object", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_json", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_toml", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_nested", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_mapping", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing_file", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_custom_config_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_session_lifetime", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_get_namespace", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_config.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "842d73bca464f00856d40c5dc63b08f29a54f28f14714a17518d2cef8129f8bf", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:0252a1d16915879fdb2a335c4ab11ef3d16f806f46823b5a3ee61050abccef3d", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "abi": "cp311", + "repository": "hatch", + "status": "PASS", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:5154a43facbd58e74a5e9ce05425c79bf98ffdcf264fa8ecf0693e226673f7d8", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.11.14", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.11.14", + "python_version": "3.11", + "sys_platform": "linux" + }, + "descriptor_sha256": "2e184caf482e2cec65a39ed0aed1c7a57d639d17f74dfda45b1c9364b4bb37e4", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/hatch/bin/python", + "-m", + "hatch", + "-e", + "hatch-test.py3.11", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/host-junit.xml", + "tests/utils/test_structures.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/hatch/bin/python", + "-m", + "hatch", + "-e", + "hatch-test.py3.11", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/host-junit.xml", + "tests/utils/test_structures.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 1.0061129100000699, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 56.06217007399994, + "network": { + "received_bytes": 159777698, + "sent_bytes": 901620, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 16.884467664, + "network": { + "received_bytes": 23400, + "sent_bytes": 37632, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/a3b04a95e316f7f4113acc692af4ec69be463c3601241a70ade5a2d876763a5c/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/a3b04a95e316f7f4113acc692af4ec69be463c3601241a70ade5a2d876763a5c/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 38.385164508, + "network": { + "received_bytes": 61314, + "sent_bytes": 79852, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 52.961032813999964, + "network": { + "received_bytes": 159676046, + "sent_bytes": 877072, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "919458595814972d8a88e5913dfa131b26a3320f4750056d138356519032832f", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:5154a43facbd58e74a5e9ce05425c79bf98ffdcf264fa8ecf0693e226673f7d8", + "selected": [ + "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "tests/utils/test_structures.py::TestEnvVars::test_include", + "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "tests/utils/test_structures.py::TestEnvVars::test_set" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_set", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_include", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "5a4a7db3a4196de1eafd8d5c3d8dab3066e125e030d8f48c1824d99e30a84838", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:5154a43facbd58e74a5e9ce05425c79bf98ffdcf264fa8ecf0693e226673f7d8", + "selected": [ + "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "tests/utils/test_structures.py::TestEnvVars::test_include", + "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "tests/utils/test_structures.py::TestEnvVars::test_set" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_include", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_set", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "4a220c5400fd8e57c3dfc34c882e6d19221433402bb2491d1562a7f949a01e91", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:026c166081461f1742c847f5a0846c770b4dee5966245f7f9643eeee0c5d2103", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\n/opt/specfact/snapshot/tests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "abi": "cp311", + "repository": "hatch-detached", + "status": "PASS", + "commit": "reconstructed-sha256:ac6b4d614246fd1d87f9a3e8336ebd9591293d63cb8f58b59e78e3c48ea80cbf", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:41919cda9af01a039231bb1ce1edbfddab154a3b05fd03d7de0fc19d7fe2e373", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.11.14", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.11.14", + "python_version": "3.11", + "sys_platform": "linux" + }, + "descriptor_sha256": "bf6d846ef756ac3609ed336797b18c956f741969109544f3c5669f34fae60bd6", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/hatch-detached/bin/python", + "-m", + "hatch", + "-e", + "review", + "run", + "python", + "-m", + "pytest", + "-p", + "no:asyncio", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/host-junit.xml", + "tests/test_core.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/hatch-detached/bin/python", + "-m", + "hatch", + "-e", + "review", + "run", + "python", + "-m", + "pytest", + "-p", + "no:asyncio", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/host-junit.xml", + "tests/test_core.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.360708134999868, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 45.93960248799999, + "network": { + "received_bytes": 80715304, + "sent_bytes": 540310, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 16.7990536289999, + "network": { + "received_bytes": 23596, + "sent_bytes": 37974, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/8d966f5e90dde21c20ae3a181b8e5de7256e402349cf5706e533a857f94027dc/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/8d966f5e90dde21c20ae3a181b8e5de7256e402349cf5706e533a857f94027dc/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 1, + "seconds": 31.853108936000126, + "network": { + "received_bytes": 63966, + "sent_bytes": 68578, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/hatch-detached/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 46.751263250999955, + "network": { + "received_bytes": 80718680, + "sent_bytes": 582426, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "08a76df92b23ddeaeee8c789829087769d62b311d6efba3564f6ea2a406f857d", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:41919cda9af01a039231bb1ce1edbfddab154a3b05fd03d7de0fc19d7fe2e373", + "selected": [ + "tests/test_core.py::test_runtime_dependencies" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_core.py::test_runtime_dependencies", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_core.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "f9518548e10e9c53af60a62f81d39830181f979dcb8bbf97b98b98225980f93e", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:41919cda9af01a039231bb1ce1edbfddab154a3b05fd03d7de0fc19d7fe2e373", + "selected": [ + "tests/test_core.py::test_runtime_dependencies" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_core.py::test_runtime_dependencies", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_core.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "ef6d41955cd5cadf9f7536fc1f1060fe0ddf0062aaa3b39bb53438f22e132acb", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:f6aee48b006efaefa0fecd9467852daa5af5e8af21565655e7eb97167dd4d78e", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "abi": "cp311", + "repository": "poetry", + "status": "PASS", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:29f4bcaadcc7313a382bab102880bdedcb9d478ddf9e1c253050c30046d032e1", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.11.14", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.11.14", + "python_version": "3.11", + "sys_platform": "linux" + }, + "descriptor_sha256": "1c5207107b82011d4a9ac8f4631bbddffde3fa4f5bc8cd61e44d687f5e520e2a", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/poetry/bin/python", + "-m", + "poetry", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/host-junit.xml", + "tests/utils/test_extras.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/poetry/bin/python", + "-m", + "poetry", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/host-junit.xml", + "tests/utils/test_extras.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 2.9125207519999776, + "network": { + "received_bytes": 7250, + "sent_bytes": 10338, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 63.47296071200003, + "network": { + "received_bytes": 108713048, + "sent_bytes": 1036466, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 17.034773202999986, + "network": { + "received_bytes": 15626, + "sent_bytes": 22964, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/08d041221495c6c33a51f5fc1bd03ad8ed2378b25df68856531f8117e4a0cf4a/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/08d041221495c6c33a51f5fc1bd03ad8ed2378b25df68856531f8117e4a0cf4a/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 43.23381471799996, + "network": { + "received_bytes": 88780, + "sent_bytes": 133882, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 60.94012131599993, + "network": { + "received_bytes": 108666994, + "sent_bytes": 1104572, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "696cc2f51338013409a2ca5ce9e6a882b4ed0ecdb9423117a1276467f4649856", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:29f4bcaadcc7313a382bab102880bdedcb9d478ddf9e1c253050c30046d032e1", + "selected": [ + "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_extras.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "45e99287cffb495ae06db8ff60f829e5018d34abd643772852c9fb0aa507d823", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:29f4bcaadcc7313a382bab102880bdedcb9d478ddf9e1c253050c30046d032e1", + "selected": [ + "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_extras.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "e3e338905263ac4fa336588ca37bdf8c6110c7864698e3de869e14c80141dc80", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:c4cfff1424978cbe5239d763840a08de4f1721b2008e1fb946a48738fd3253b6", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "[gw0] linux -- Python 3.11.14 /opt/specfact/project-runtime/bin/python\n\n def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\n/opt/specfact/snapshot/tests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.11-2", + "abi": "cp311", + "repository": "requests", + "status": "PASS", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:da2abb12e5c769b45be19b41e451ade0d2b3b0c7fd9a0c98b9eefa04be10c435", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.11.14", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.11.14", + "python_version": "3.11", + "sys_platform": "linux" + }, + "descriptor_sha256": "f4a32ea0572ff7bce68cf4303bbe0c9fcf9b9d58a9a6449e9a949a437698d324", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/requests/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/host-junit.xml", + "tests/test_structures.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.11/external/host-environments/requests/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/host-junit.xml", + "tests/test_structures.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.7827689910000117, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 47.41986234400002, + "network": { + "received_bytes": 34245618, + "sent_bytes": 461476, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 16.86727898999999, + "network": { + "received_bytes": 39684, + "sent_bytes": 48872, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/d2a0fd401c58ce636e9bfda95fa51327e3af5d9b2b224058c53c377fa1151d8e/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.11/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.11/home/.cache/specfact/code-review/project-runtimes-v2/d2a0fd401c58ce636e9bfda95fa51327e3af5d9b2b224058c53c377fa1151d8e/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 1, + "seconds": 34.75919676799998, + "network": { + "received_bytes": 57326, + "sent_bytes": 75860, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/requests/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 45.298841036, + "network": { + "received_bytes": 34259620, + "sent_bytes": 425880, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.11.16 (main, Aug 13 2026, 02:46:14) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "853089e6ba06e252602054d735fe6fdd1647e943fc3e275ef795c96bb518ffc2", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:da2abb12e5c769b45be19b41e451ade0d2b3b0c7fd9a0c98b9eefa04be10c435", + "selected": [ + "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getattr", + "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_hasattr", + "tests/test_structures.py::TestLookupDict::test_repr" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_hasattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "b2cba0e5e09a18a16144062c12743dbfb08c323c25b2d0094d053370354915cd", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:da2abb12e5c769b45be19b41e451ade0d2b3b0c7fd9a0c98b9eefa04be10c435", + "selected": [ + "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getattr", + "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_hasattr", + "tests/test_structures.py::TestLookupDict::test_repr" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_hasattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "4b1d1d8b73a57df90f5f833c633a67ac4842a2be9e4ec736f3b0642e512c8294", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:d2956e053a85b9079b2fc73b2afa839a17df2b21285a598a6731f2136532ab0f", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "abi": "cp312", + "repository": "flask", + "status": "PASS", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:9caa9c1a13cf8e606e137d974b4ef9c03019f98e46dd4360d660b93dbf50507d", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.12.13", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.12.13", + "python_version": "3.12", + "sys_platform": "linux" + }, + "descriptor_sha256": "113d3a8bb610a1b1db4099065056e2dd8bb8a5ac00685038d09d72dc45742246", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-checkouts/flask/.venv/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/host-junit.xml", + "tests/test_config.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-checkouts/flask/.venv/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/host-junit.xml", + "tests/test_config.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.6907471739999664, + "network": { + "received_bytes": 402, + "sent_bytes": 216, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 47.883217100000024, + "network": { + "received_bytes": 47708224, + "sent_bytes": 489930, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 21.382277971000008, + "network": { + "received_bytes": 56820, + "sent_bytes": 63008, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/b994910fdd6a99ffdfef6e5b9c588627fcc55b403141b380cb877283837da61f/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/b994910fdd6a99ffdfef6e5b9c588627fcc55b403141b380cb877283837da61f/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 41.47684803699997, + "network": { + "received_bytes": 53900, + "sent_bytes": 82640, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/flask/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 45.09905681999999, + "network": { + "received_bytes": 47711112, + "sent_bytes": 469356, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "420d7230f43663e3c0be96d52fbcc7de54097327fbeab87e2bbd57d446cb3cc3", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:9caa9c1a13cf8e606e137d974b4ef9c03019f98e46dd4360d660b93dbf50507d", + "selected": [ + "tests/test_config.py::test_config_from_class", + "tests/test_config.py::test_config_from_envvar", + "tests/test_config.py::test_config_from_envvar_missing", + "tests/test_config.py::test_config_from_file_json", + "tests/test_config.py::test_config_from_file_toml", + "tests/test_config.py::test_config_from_mapping", + "tests/test_config.py::test_config_from_object", + "tests/test_config.py::test_config_from_pyfile", + "tests/test_config.py::test_config_missing", + "tests/test_config.py::test_config_missing_file", + "tests/test_config.py::test_custom_config_class", + "tests/test_config.py::test_from_prefixed_env", + "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "tests/test_config.py::test_from_prefixed_env_nested", + "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "tests/test_config.py::test_get_namespace", + "tests/test_config.py::test_session_lifetime" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_pyfile", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_object", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_json", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_toml", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_nested", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_mapping", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing_file", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_custom_config_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_session_lifetime", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_get_namespace", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_config.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "a5d3cf041eac85353e24a35dfc3384d7f72d1a960059339ce89a1f556fad1a74", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:9caa9c1a13cf8e606e137d974b4ef9c03019f98e46dd4360d660b93dbf50507d", + "selected": [ + "tests/test_config.py::test_config_from_class", + "tests/test_config.py::test_config_from_envvar", + "tests/test_config.py::test_config_from_envvar_missing", + "tests/test_config.py::test_config_from_file_json", + "tests/test_config.py::test_config_from_file_toml", + "tests/test_config.py::test_config_from_mapping", + "tests/test_config.py::test_config_from_object", + "tests/test_config.py::test_config_from_pyfile", + "tests/test_config.py::test_config_missing", + "tests/test_config.py::test_config_missing_file", + "tests/test_config.py::test_custom_config_class", + "tests/test_config.py::test_from_prefixed_env", + "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "tests/test_config.py::test_from_prefixed_env_nested", + "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "tests/test_config.py::test_get_namespace", + "tests/test_config.py::test_session_lifetime" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_pyfile", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_object", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_json", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_toml", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_nested", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_mapping", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing_file", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_custom_config_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_session_lifetime", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_get_namespace", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_config.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "8e6970975f45e561ca91b127ebed3e717d1576f981cc35c04817cfd44f27ed46", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:5246e6846739e60a4085c92e2d6f2b274bba6640dfcecff9ed76a67d8eb5a723", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "abi": "cp312", + "repository": "hatch", + "status": "PASS", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:58b001d00fa88454ce33f06162eca19ac66b7cd31c4fa6515edefaa945620c4f", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.12.13", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.12.13", + "python_version": "3.12", + "sys_platform": "linux" + }, + "descriptor_sha256": "d62cfa4fda93cd699072e87bece36764a7f48557df1a676152d12cf51ae1c464", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/hatch/bin/python", + "-m", + "hatch", + "-e", + "hatch-test.py3.12", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/host-junit.xml", + "tests/utils/test_structures.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/hatch/bin/python", + "-m", + "hatch", + "-e", + "hatch-test.py3.12", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/host-junit.xml", + "tests/utils/test_structures.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 1.4694074090000413, + "network": { + "received_bytes": 1168, + "sent_bytes": 7534, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 75.566762756, + "network": { + "received_bytes": 159367924, + "sent_bytes": 1207414, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 21.097493874000065, + "network": { + "received_bytes": 56390, + "sent_bytes": 76424, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/7323420e318485f100ab51deec0ad476a4d3ca261d36f85f4d4b29ca8e08acbf/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/7323420e318485f100ab51deec0ad476a4d3ca261d36f85f4d4b29ca8e08acbf/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 44.32004251800004, + "network": { + "received_bytes": 55124, + "sent_bytes": 108212, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 66.25998799199999, + "network": { + "received_bytes": 159459900, + "sent_bytes": 1220124, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "d0932ace41a9ad0f1060845174eb2aabd390e4d4989d727663de129715f11649", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:58b001d00fa88454ce33f06162eca19ac66b7cd31c4fa6515edefaa945620c4f", + "selected": [ + "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "tests/utils/test_structures.py::TestEnvVars::test_include", + "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "tests/utils/test_structures.py::TestEnvVars::test_set" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_set", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_include", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "dce3ef55aac519f75a7015e709e5f18ef88374c4636ffa6c120d5bfacea80b6b", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:58b001d00fa88454ce33f06162eca19ac66b7cd31c4fa6515edefaa945620c4f", + "selected": [ + "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "tests/utils/test_structures.py::TestEnvVars::test_include", + "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "tests/utils/test_structures.py::TestEnvVars::test_set" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_set", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_include", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "512e173c3017d810abb97f7c67ada1bb9b507d014206daae2406ca2a10462507", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:b24f6a90df55afef3a4edd439a3acaf2508076f0d19c086a89980a3bcbc3bf34", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\n/opt/specfact/snapshot/tests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "abi": "cp312", + "repository": "hatch-detached", + "status": "PASS", + "commit": "reconstructed-sha256:ac6b4d614246fd1d87f9a3e8336ebd9591293d63cb8f58b59e78e3c48ea80cbf", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:32f0f1e2eac0074838358f23effdc641f7d95835e7c3d06476e006c33fbf9f24", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.12.13", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.12.13", + "python_version": "3.12", + "sys_platform": "linux" + }, + "descriptor_sha256": "005caaafe456be2a4e221518c2c31fb4656d7a7861972f70b9c301acd56348a8", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/hatch-detached/bin/python", + "-m", + "hatch", + "-e", + "review", + "run", + "python", + "-m", + "pytest", + "-p", + "no:asyncio", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/host-junit.xml", + "tests/test_core.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/hatch-detached/bin/python", + "-m", + "hatch", + "-e", + "review", + "run", + "python", + "-m", + "pytest", + "-p", + "no:asyncio", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/host-junit.xml", + "tests/test_core.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.35208986599991476, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 61.63473302700004, + "network": { + "received_bytes": 80503504, + "sent_bytes": 775962, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 20.899567796999918, + "network": { + "received_bytes": 24438, + "sent_bytes": 49120, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/6b94ff3f6d75f954c1ff7edfd67bfb6917cbc73e63b3d8c8593011d3863c1fe7/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/6b94ff3f6d75f954c1ff7edfd67bfb6917cbc73e63b3d8c8593011d3863c1fe7/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 1, + "seconds": 36.582104313999935, + "network": { + "received_bytes": 61364, + "sent_bytes": 84438, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/hatch-detached/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 61.269883131999904, + "network": { + "received_bytes": 80491144, + "sent_bytes": 733908, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "d736f5b37060c25acef952cbdd512b64a184f232af4ae1c23bc342c0799f0e0c", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:32f0f1e2eac0074838358f23effdc641f7d95835e7c3d06476e006c33fbf9f24", + "selected": [ + "tests/test_core.py::test_runtime_dependencies" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_core.py::test_runtime_dependencies", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_core.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "77a2d8bbdd577d56c3840eddeff9ac29f2a7ef3d66375268cb84e2399e3cdb0e", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:32f0f1e2eac0074838358f23effdc641f7d95835e7c3d06476e006c33fbf9f24", + "selected": [ + "tests/test_core.py::test_runtime_dependencies" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_core.py::test_runtime_dependencies", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_core.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "69bf3e6b3182532be1b1b8a69aff64eedb9e447e1bc9412b7735f4698cc3b520", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:a8fe8a36a6109ddd235dae3e744db979afa231cfe50818b91d761fffa95dea06", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "abi": "cp312", + "repository": "poetry", + "status": "PASS", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:2414ed73cab0c8d79ff080d1086df77e520c960b53762bcbd9486977f4a5a5e7", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.12.13", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.12.13", + "python_version": "3.12", + "sys_platform": "linux" + }, + "descriptor_sha256": "49bc634c3bbde53e71e93778a90991c88ad2efda40e7f5136b98e3c75ceab5ad", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/poetry/bin/python", + "-m", + "poetry", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/host-junit.xml", + "tests/utils/test_extras.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/poetry/bin/python", + "-m", + "poetry", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/host-junit.xml", + "tests/utils/test_extras.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 3.0137175340000795, + "network": { + "received_bytes": 17990, + "sent_bytes": 16146, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 77.68784770100001, + "network": { + "received_bytes": 108187456, + "sent_bytes": 1193074, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 21.268796803999976, + "network": { + "received_bytes": 30680, + "sent_bytes": 49430, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/c7258f0d8ea46da6da691f76be88070a97e41f20d38ddf11f26e203f81788c8f/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/c7258f0d8ea46da6da691f76be88070a97e41f20d38ddf11f26e203f81788c8f/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 49.397856047000005, + "network": { + "received_bytes": 88852, + "sent_bytes": 113232, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 73.11599280900009, + "network": { + "received_bytes": 108193458, + "sent_bytes": 1160636, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "083d7b7b343673c807872a73a06d5965765765b5d51756d08653f73d018cfc04", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:2414ed73cab0c8d79ff080d1086df77e520c960b53762bcbd9486977f4a5a5e7", + "selected": [ + "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_extras.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "77f231f7d1d1a9329383cd7a91e6aaa30027e827e9af6a54194cf4dc59a53719", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:2414ed73cab0c8d79ff080d1086df77e520c960b53762bcbd9486977f4a5a5e7", + "selected": [ + "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_extras.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "dd8147d79f9c7c44b49e3319e09234f7306cc991ece75f94115d33b65ccef8f2", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:eeb23d85d0958a1cad6836d23a1f689600bab9acc58e1fa34e806bf523d7faa8", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "[gw0] linux -- Python 3.12.13 /opt/specfact/project-runtime/bin/python\n\n def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\n/opt/specfact/snapshot/tests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "abi": "cp312", + "repository": "requests", + "status": "PASS", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:815cce729f9bf05f184f0816dbb41d10cd6554485ef8791ce97b0845010bc994", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.12.13", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.12.13", + "python_version": "3.12", + "sys_platform": "linux" + }, + "descriptor_sha256": "fa634247dd424d1cb695a272a898d4f2b2215017f4fce3cb3e6c67fc2d90437e", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/requests/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/host-junit.xml", + "tests/test_structures.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.12/external/host-environments/requests/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/host-junit.xml", + "tests/test_structures.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.9762422219999962, + "network": { + "received_bytes": 284, + "sent_bytes": 264, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 56.356880918, + "network": { + "received_bytes": 34319980, + "sent_bytes": 546234, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 20.98808552099996, + "network": { + "received_bytes": 31240, + "sent_bytes": 44630, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/9e6f0e89fd5250d746629c2ec453652a1a936e05d2442568d6c6900ebbce8aa8/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.12/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.12/home/.cache/specfact/code-review/project-runtimes-v2/9e6f0e89fd5250d746629c2ec453652a1a936e05d2442568d6c6900ebbce8aa8/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 1, + "seconds": 40.19934691200001, + "network": { + "received_bytes": 80716, + "sent_bytes": 101692, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/requests/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 62.64041201600003, + "network": { + "received_bytes": 34306520, + "sent_bytes": 525100, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.12.14 (main, Aug 13 2026, 02:47:42) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "d90ed64b1cb4344fd7f9fb397e0b42a4dd233d574bdc3b884faeb63b79b938bc", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:815cce729f9bf05f184f0816dbb41d10cd6554485ef8791ce97b0845010bc994", + "selected": [ + "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getattr", + "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_hasattr", + "tests/test_structures.py::TestLookupDict::test_repr" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_hasattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "d51eebd67cf7ced5f415a89a52417769ae6c433467faaf597aebf839cf9bccc7", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:815cce729f9bf05f184f0816dbb41d10cd6554485ef8791ce97b0845010bc994", + "selected": [ + "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getattr", + "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_hasattr", + "tests/test_structures.py::TestLookupDict::test_repr" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_hasattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "b3d012df5f1f8bf9955cdea897fb381e44450042689097bf7cbe1b039fed6d9c", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:a71fbea9909f982f0a64fef3e577e4328b6b525f201e38a8e472d7094f7c459a", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "abi": "cp313", + "repository": "flask", + "status": "PASS", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:9ab994d4d38140ae48f4a808ec3f7a65379f6e2fe3ea3a9bdd6128af19803fbc", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.13.11", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.13.11", + "python_version": "3.13", + "sys_platform": "linux" + }, + "descriptor_sha256": "8b17a0871e1cee4fc4887fd1e22573128d82cb22914a6c07abcd1f3c05afcbf0", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-checkouts/flask/.venv/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/host-junit.xml", + "tests/test_config.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-checkouts/flask/.venv/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/host-junit.xml", + "tests/test_config.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.9721126349999167, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 64.49480093800003, + "network": { + "received_bytes": 47586541, + "sent_bytes": 350592, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 27.778548039000043, + "network": { + "received_bytes": 25990, + "sent_bytes": 60378, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/2c4e2d10e332be94970dd86637e7d8d22005ea16a31d6a77dbe0fa74cdf87013/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/flask/config.py", + "tests/test_config.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/2c4e2d10e332be94970dd86637e7d8d22005ea16a31d6a77dbe0fa74cdf87013/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 56.421114465999835, + "network": { + "received_bytes": 59059, + "sent_bytes": 125694, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/flask/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 58.43678807900005, + "network": { + "received_bytes": 47569071, + "sent_bytes": 337246, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "6ca2180c736f881b5cfd6b4095f307b15adcdf14c1a1271be1c781c5cf4b8a15", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:9ab994d4d38140ae48f4a808ec3f7a65379f6e2fe3ea3a9bdd6128af19803fbc", + "selected": [ + "tests/test_config.py::test_config_from_class", + "tests/test_config.py::test_config_from_envvar", + "tests/test_config.py::test_config_from_envvar_missing", + "tests/test_config.py::test_config_from_file_json", + "tests/test_config.py::test_config_from_file_toml", + "tests/test_config.py::test_config_from_mapping", + "tests/test_config.py::test_config_from_object", + "tests/test_config.py::test_config_from_pyfile", + "tests/test_config.py::test_config_missing", + "tests/test_config.py::test_config_missing_file", + "tests/test_config.py::test_custom_config_class", + "tests/test_config.py::test_from_prefixed_env", + "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "tests/test_config.py::test_from_prefixed_env_nested", + "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "tests/test_config.py::test_get_namespace", + "tests/test_config.py::test_session_lifetime" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_pyfile", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_object", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_json", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_toml", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_nested", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_mapping", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing_file", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_custom_config_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_session_lifetime", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_get_namespace", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_config.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "43651248838632495d4b7369bd534df6f66828220aa1ee3227da19911dd817a3", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:9ab994d4d38140ae48f4a808ec3f7a65379f6e2fe3ea3a9bdd6128af19803fbc", + "selected": [ + "tests/test_config.py::test_config_from_class", + "tests/test_config.py::test_config_from_envvar", + "tests/test_config.py::test_config_from_envvar_missing", + "tests/test_config.py::test_config_from_file_json", + "tests/test_config.py::test_config_from_file_toml", + "tests/test_config.py::test_config_from_mapping", + "tests/test_config.py::test_config_from_object", + "tests/test_config.py::test_config_from_pyfile", + "tests/test_config.py::test_config_missing", + "tests/test_config.py::test_config_missing_file", + "tests/test_config.py::test_custom_config_class", + "tests/test_config.py::test_from_prefixed_env", + "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "tests/test_config.py::test_from_prefixed_env_nested", + "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "tests/test_config.py::test_get_namespace", + "tests/test_config.py::test_session_lifetime" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_pyfile", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_object", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_json", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_file_toml", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_custom_prefix", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_prefixed_env_nested", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_mapping", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_from_envvar_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_config_missing_file", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_custom_config_class", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_session_lifetime", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_get_namespace", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[utf-8]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[iso-8859-15]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_config.py::test_from_pyfile_weird_encoding[latin-1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_config.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "3f1c41ab1802188e87fd0ccb44faa0a0a0cb5ef08d419dff0ca14d1114460cc2", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:b3e9339fa1456310b0bb8e16ab0578fddef1b14a05152087c698e94d1a9bafd7", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "abi": "cp313", + "repository": "hatch", + "status": "PASS", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:3c5e395e41a37b84a4f725d1c0405dc901f479ba4720923355d7b37e998042a1", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.13.11", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.13.11", + "python_version": "3.13", + "sys_platform": "linux" + }, + "descriptor_sha256": "f7b69cb135e38127752e6b3a91ebdf6a21603013d06156b0563f8a6aed12040a", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/hatch/bin/python", + "-m", + "hatch", + "-e", + "hatch-test.py3.13", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/host-junit.xml", + "tests/utils/test_structures.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/hatch/bin/python", + "-m", + "hatch", + "-e", + "hatch-test.py3.13", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/host-junit.xml", + "tests/utils/test_structures.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 1.4571591630000285, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 76.04352530300002, + "network": { + "received_bytes": 158991128, + "sent_bytes": 854538, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 27.756202701999996, + "network": { + "received_bytes": 41120, + "sent_bytes": 75252, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/2b53063e7640c1ae7112a7bba7547d959276ca285935b934ff0ed93004ac44f5/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/2b53063e7640c1ae7112a7bba7547d959276ca285935b934ff0ed93004ac44f5/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 60.41479355599995, + "network": { + "received_bytes": 48375, + "sent_bytes": 131308, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 76.24213641799997, + "network": { + "received_bytes": 158989958, + "sent_bytes": 820454, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "64a9f6c0f526db9c356989c1a70f79d582b4d9c383ec328f036e3c1e8748b155", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:3c5e395e41a37b84a4f725d1c0405dc901f479ba4720923355d7b37e998042a1", + "selected": [ + "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "tests/utils/test_structures.py::TestEnvVars::test_include", + "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "tests/utils/test_structures.py::TestEnvVars::test_set" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_include", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_set", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "f32bea4dd7bd75d6fd72f2f2027745be25746e4a5c51dfc3ba5c43048a44b743", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:3c5e395e41a37b84a4f725d1c0405dc901f479ba4720923355d7b37e998042a1", + "selected": [ + "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "tests/utils/test_structures.py::TestEnvVars::test_include", + "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "tests/utils/test_structures.py::TestEnvVars::test_set" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_precedence", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_set", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_restoration", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_include", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_structures.py::TestEnvVars::test_exclude", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "fe08ba2be5fac7118e6d5126fae7a6b80149c7e058beca61009152a2d0e428b9", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:5953893ba9af8e7d896fe1aa3b21b6e18f2929c41b48e69488684f9a6c9ce826", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\n/opt/specfact/snapshot/tests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "-p", + "anyio", + "-p", + "rerunfailures", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-p", + "randomly", + "-p", + "pytest_cov", + "--dist", + "worksteal", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "abi": "cp313", + "repository": "hatch-detached", + "status": "PASS", + "commit": "reconstructed-sha256:ac6b4d614246fd1d87f9a3e8336ebd9591293d63cb8f58b59e78e3c48ea80cbf", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:437f277b1f1abc022d03ef4fbef37513a5ebfcbcbc6ac0e8568e99d24c40b1c8", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.13.11", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.13.11", + "python_version": "3.13", + "sys_platform": "linux" + }, + "descriptor_sha256": "c9558c007ce961438e104161ab0e79ad47e12a7e7d37406562f09bef97506c9d", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/hatch-detached/bin/python", + "-m", + "hatch", + "-e", + "review", + "run", + "python", + "-m", + "pytest", + "-p", + "no:asyncio", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/host-junit.xml", + "tests/test_core.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/hatch-detached/bin/python", + "-m", + "hatch", + "-e", + "review", + "run", + "python", + "-m", + "pytest", + "-p", + "no:asyncio", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/host-junit.xml", + "tests/test_core.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.48777335399995536, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 65.450946531, + "network": { + "received_bytes": 80349174, + "sent_bytes": 544694, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 27.608998334000034, + "network": { + "received_bytes": 20356, + "sent_bytes": 54864, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/60b626ee1f3dd77ae0bf8e4dc0a52f0cc893f20c5af432fb89c3f66d3931144d/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/portable_customer/core.py", + "tests/test_core.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/60b626ee1f3dd77ae0bf8e4dc0a52f0cc893f20c5af432fb89c3f66d3931144d/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 1, + "seconds": 49.31469812299997, + "network": { + "received_bytes": 41668, + "sent_bytes": 106654, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/hatch-detached/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 64.55728419399998, + "network": { + "received_bytes": 80322385, + "sent_bytes": 560660, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "56786bba5070e34ffe88495ac08b50860e1a1c053ac863f4cdaf02c9d2b0e41f", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:437f277b1f1abc022d03ef4fbef37513a5ebfcbcbc6ac0e8568e99d24c40b1c8", + "selected": [ + "tests/test_core.py::test_runtime_dependencies" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_core.py::test_runtime_dependencies", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_core.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "c08a35d197277746fddb708e827aba68ed0f6addbe3bf170d08222bbc8ffa4ec", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:437f277b1f1abc022d03ef4fbef37513a5ebfcbcbc6ac0e8568e99d24c40b1c8", + "selected": [ + "tests/test_core.py::test_runtime_dependencies" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_core.py::test_runtime_dependencies", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_core.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "6919575c8a97c3a64e7406f4c9f97b69a133ceea78f3c35b851e9ffbd256021b", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:92e6d194e0be533410b8bdbdf9562b8a0dcf6b2659ee395c611ba8df02abef48", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.15.4", + "pytest_cov_version": "7.1.0", + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "pytest_asyncio.plugin", + "-p", + "pytest_cov", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "abi": "cp313", + "repository": "poetry", + "status": "PASS", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:d3fd19037fc810f0d790c88d797abaab5c9d6e6e982eddf15cdafbaf6580ac0e", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.13.11", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.13.11", + "python_version": "3.13", + "sys_platform": "linux" + }, + "descriptor_sha256": "c40e752cfc383c0a455dc4c95edb9339eaf00e4388f8788cfa5db486bf13a235", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/poetry/bin/python", + "-m", + "poetry", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/host-junit.xml", + "tests/utils/test_extras.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/poetry/bin/python", + "-m", + "poetry", + "run", + "python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/host-junit.xml", + "tests/utils/test_extras.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 4.367349051000019, + "network": { + "received_bytes": 3874, + "sent_bytes": 10470, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 88.88199616299994, + "network": { + "received_bytes": 107768966, + "sent_bytes": 1022158, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 28.408695913999964, + "network": { + "received_bytes": 21834, + "sent_bytes": 66038, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/a56055088836ffbc9e42d5a04c65b909c08d6b2a5dc9e8adeae8fd0391995c8c/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/a56055088836ffbc9e42d5a04c65b909c08d6b2a5dc9e8adeae8fd0391995c8c/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 66.78421710999987, + "network": { + "received_bytes": 66320, + "sent_bytes": 159768, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 87.15448126399997, + "network": { + "received_bytes": 107760676, + "sent_bytes": 1046720, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "4c7d16e89315f1aff1af01a0380aebd5266d5488f2ce1b93889f6b5e453ede67", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:d3fd19037fc810f0d790c88d797abaab5c9d6e6e982eddf15cdafbaf6580ac0e", + "selected": [ + "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_extras.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "3106f95d798a4e6750c75072b7b147343022bd85f7508528d3203efc9b2c113e", + "verdict": "PASS_WITH_ADVISORY", + "assurance_status": "PASS", + "runtime_identity": "sha256:d3fd19037fc810f0d790c88d797abaab5c9d6e6e982eddf15cdafbaf6580ac0e", + "selected": [ + "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages3-extras3-extra_names3-expected_extra_package_names3]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages1-extras1-extra_names1-expected_extra_package_names1]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages0-extras0-extra_names0-expected_extra_package_names0]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages2-extras2-extra_names2-expected_extra_package_names2]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages5-extras5-extra_names5-expected_extra_package_names5]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages6-extras6-extra_names6-expected_extra_package_names6]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/utils/test_extras.py::test_get_extra_package_names[packages4-extras4-extra_names4-expected_extra_package_names4]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/utils/test_extras.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "b24d31689a10b603368bc7dc379517a23b151682de419a93e525ded09e8aa012", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:a37c0a82c171e818216fe827e45c6b63d979e72bb1fed08ef00d3d4275eb1475", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "[gw0] linux -- Python 3.13.11 /opt/specfact/project-runtime/bin/python\n\n def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\n/opt/specfact/snapshot/tests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.0", + "pytest_cov_version": "7.1.0", + "argv": [ + "-n", + "logical", + "-ra", + "-p", + "pytest_cov", + "-p", + "anyio", + "-p", + "randomly", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "abi": "cp313", + "repository": "requests", + "status": "PASS", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "mode": "candidate", + "source_unchanged": true, + "environment": { + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001, + "core": "0.55.4", + "mode": "candidate", + "commit": "34db09df344d7d76cd764df7c2b05ebdee40fc1c" + }, + "runtime_identity": "sha256:4ed664cba2aa205388c6fe78443df7857c4e977759fc43c818d936da3bbeca27", + "runtime_environment": { + "implementation_name": "cpython", + "implementation_version": "3.13.11", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-1022-azure", + "platform_system": "Linux", + "platform_version": "#22-Ubuntu SMP Mon Jul 27 17:24:03 UTC 2026", + "python_full_version": "3.13.11", + "python_version": "3.13", + "sys_platform": "linux" + }, + "descriptor_sha256": "95d10537492f3328e7d036ea055877d5d4e80a0c73eeb7d09fbef3dfd8f8073a", + "phases": { + "host-tests": { + "argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/requests/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/host-junit.xml", + "tests/test_structures.py" + ], + "execution_argv": [ + "/home/runner/work/_temp/specfact-customer-3.13/external/host-environments/requests/bin/python", + "-m", + "pytest", + "--junitxml=/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/host-junit.xml", + "tests/test_structures.py" + ], + "offline_network_verified": false, + "exit": 0, + "seconds": 0.9561242560000665, + "network": { + "received_bytes": 0, + "sent_bytes": 0, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "cold-auto": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/cold-auto-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/cold-auto-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 68.51306731800003, + "network": { + "received_bytes": 34196485, + "sent_bytes": 408310, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "offline-prepare": { + "argv": [ + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--offline", + "--json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "runtime", + "prepare", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--offline", + "--json" + ], + "offline_network_verified": true, + "exit": 0, + "seconds": 27.95440160299995, + "network": { + "received_bytes": 27082, + "sent_bytes": 67846, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "warm-attach": { + "argv": [ + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/de31f35a1040fed19dae77798451cfe54195d5baf49d17dd790a4c5faa73198d/project-runtime.json" + ], + "execution_argv": [ + "/specfact-corpus-offline/bwrap", + "--unshare-user", + "--unshare-net", + "--uid", + "1001", + "--gid", + "1001", + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + "/home/runner/work/_temp/specfact-customer-3.13/venv/bin/python", + "-c", + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno(),0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); probe_socket.close(); valid or sys.exit(77); print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); os.execvp(sys.argv[2],sys.argv[2:])", + "net:[4026531833]", + "specfact", + "code", + "review", + "run", + "src/requests/structures.py", + "tests/test_structures.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/warm-attach-report.json", + "--bug-hunt", + "--project-runtime", + "/home/runner/work/_temp/specfact-customer-3.13/home/.cache/specfact/code-review/project-runtimes-v2/de31f35a1040fed19dae77798451cfe54195d5baf49d17dd790a4c5faa73198d/project-runtime.json" + ], + "offline_network_verified": true, + "exit": 1, + "seconds": 53.92958033699995, + "network": { + "received_bytes": 57599, + "sent_bytes": 138516, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + }, + "controlled-defect": { + "argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/controlled-defect-report.json", + "--bug-hunt" + ], + "execution_argv": [ + "specfact", + "code", + "review", + "run", + "specfact_controlled.py", + "tests/test_specfact_controlled.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/requests/controlled-defect-report.json", + "--bug-hunt" + ], + "offline_network_verified": false, + "exit": 1, + "seconds": 65.35341020200008, + "network": { + "received_bytes": 34193814, + "sent_bytes": 417192, + "scope": "host_non_loopback_interface_counters" + }, + "python": "3.13.15 (main, Aug 6 2026, 02:15:18) [GCC 13.3.0]", + "platform": "Linux-6.17.0-1022-azure-x86_64-with-glibc2.39", + "uid": 1001 + } + }, + "reports": { + "cold-auto": { + "sha256": "c17a60f09291d22821356489dedb4c9e6d188c8ae73750cc5fb90d35cd05aacf", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:4ed664cba2aa205388c6fe78443df7857c4e977759fc43c818d936da3bbeca27", + "selected": [ + "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getattr", + "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_hasattr", + "tests/test_structures.py::TestLookupDict::test_repr" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_hasattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "warm-attach": { + "sha256": "48e0c4de36b1db3f8da2c88c22ff69828d4e82578c42c6793556ace1dfa12d68", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:4ed664cba2aa205388c6fe78443df7857c4e977759fc43c818d936da3bbeca27", + "selected": [ + "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getattr", + "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "tests/test_structures.py::TestLookupDict::test_hasattr", + "tests/test_structures.py::TestLookupDict::test_repr" + ], + "test_calls": [ + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_list", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_getitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[ACCEPT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[aCcEpT]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_delitem[Accept]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_lower_items", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_copy", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other0-True]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[other1-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestCaseInsensitiveDict::test_instance_equality[None-False]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_repr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getitem[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_get[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_hasattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[bad_gateway-502]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + }, + { + "detail": "", + "nodeid": "tests/test_structures.py::TestLookupDict::test_getattr_default[not_a_key-None]", + "outcome": "passed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_structures.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "PASS" + } + ] + }, + "controlled-defect": { + "sha256": "28a9fada9fb712890060985a662894d9b309ea79a4cce7ceb0cdcf9fe608562b", + "verdict": "FAIL", + "assurance_status": "FAIL", + "runtime_identity": "sha256:09b51ba1fd1f7bf7a8b9327ad87ef1f9e2064ce82b4c16e86cfadec8d721ac4f", + "selected": [ + "tests/test_specfact_controlled.py::test_specfact_controlled_failure" + ], + "test_calls": [ + { + "detail": "def test_specfact_controlled_failure():\n> assert False, \"SPECFACT_CONTROLLED_473\"\nE AssertionError: SPECFACT_CONTROLLED_473\nE assert False\n\ntests/test_specfact_controlled.py:2: AssertionError", + "nodeid": "tests/test_specfact_controlled.py::test_specfact_controlled_failure", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "pytest_version": "9.1.1", + "coverage_version": "7.16.1", + "pytest_cov_version": "7.1.0", + "argv": [ + "--doctest-modules", + "-p", + "pytest_cov", + "-p", + "httpbin", + "-p", + "pytest_mock", + "-p", + "xdist", + "-p", + "xdist.looponfail", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/test_specfact_controlled.py" + ], + "analyzers": [ + { + "id": "ruff", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "radon", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-clean", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ai-bloat-ast", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "ast-clean-code", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "basedpyright", + "state": "ran", + "outcome": "FAIL" + }, + { + "id": "pylint", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "contracts", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "semgrep-bugs", + "state": "ran", + "outcome": "PASS" + }, + { + "id": "targeted-pytest-coverage", + "state": "ran", + "outcome": "FAIL" + } + ] + } + } + } + ] +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/CHANGE_VALIDATION.md b/openspec/changes/code-review-16-portable-project-runtime/CHANGE_VALIDATION.md new file mode 100644 index 00000000..de26a715 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CHANGE_VALIDATION.md @@ -0,0 +1,3 @@ +# Change validation + +Accepted plan explicitly authorized 2026-09-14. #473 is native User Story under #163/#162, assigned djm81 and in SpecFact CLI #1; native #472 blocked-by #473. No overlapping open modules PRs were found. #466/#459/#175 are baseline context and are not reopened. Fresh hierarchy cache is local ephemeral state. Original customer PR is inaccessible; keep #472 open pending that validation. Target platform: Linux x86-64 CPython 3.11/3.12/3.13. Implementation changes belong in the dedicated worktree, not dev. diff --git a/openspec/changes/code-review-16-portable-project-runtime/CLI_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CLI_RED.txt new file mode 100644 index 00000000..abfdcfae --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CLI_RED.txt @@ -0,0 +1,149 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_runtime_cli.py FFF [100%] + +=================================== FAILURES =================================== +______________________ test_runtime_inspect_is_read_only _______________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-3/test_runtime_inspect_is_read_o0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108efeb10> + + def test_runtime_inspect_is_read_only(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / 'pyproject.toml').write_text('[project]\nname="consumer"\n') + result = CliRunner().invoke(app, ['review', 'runtime', 'inspect', '--json']) +> assert result.exit_code == 0, result.output +E AssertionError: Usage: root review [OPTIONS] COMMAND [ARGS]... +E Try 'root review --help' for help. +E ╭─ Error ──────────────────────────────────────────────────────────────────────╮ +E │ No such command 'runtime'. Did you mean 'run'? │ +E ╰──────────────────────────────────────────────────────────────────────────────╯ +E +E assert 2 == 0 +E + where 2 = .exit_code + +/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/tests/unit/specfact_code_review/run/test_runtime_cli.py:17: AssertionError +___________________ test_review_help_exposes_project_handoff ___________________ + + def test_review_help_exposes_project_handoff() -> None: + result = CliRunner().invoke(app, ['review', 'run', '--help'], env={'TERM': 'dumb', 'NO_COLOR': '1'}) + assert result.exit_code == 0 +> assert '--project-config' in result.output +E AssertionError: assert '--project-config' in ' \n Usage: root review run [OPTIONS] [... exit. │\n╰──────────────────────────────────────────────────────────────────────────────╯\n\n' +E + where ' \n Usage: root review run [OPTIONS] [... exit. │\n╰──────────────────────────────────────────────────────────────────────────────╯\n\n' = .output + +tests/unit/specfact_code_review/run/test_runtime_cli.py:25: AssertionError +_________________ test_review_request_carries_runtime_options __________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-3/test_review_request_carries_ru0') + + def test_review_request_carries_runtime_options(tmp_path: Path) -> None: + config, descriptor = tmp_path / 'review.toml', tmp_path / 'project-runtime.json' +> request = _build_review_run_request([], {'project_config': config, 'project_runtime': descriptor}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_cli.py:31: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +files = [] +kwargs = {'project_config': PosixPath('/pytest-of-dom/pytest-3/test_rev...ders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-3/test_review_request_carries_ru0/project-runtime.json')} + + def _build_review_run_request( + files: list[Path], + kwargs: dict[str, object], + ) -> ReviewRunRequest: + # Validate files is a list of Path instances + if not isinstance(files, list): + raise RunCommandError(f"files must be a list, got {type(files).__name__}") + if not all(isinstance(file_path, Path) for file_path in files): + raise RunCommandError("files must contain only Path instances") + + request_kwargs = dict(kwargs) + + # Validate and extract known boolean flags with proper type checking + def _get_bool_param(name: str, default: bool = False) -> bool: + value = request_kwargs.pop(name, default) + if value is None: + return default + if not isinstance(value, bool): + raise RunCommandError(f"{name} must be a boolean, got {type(value).__name__}") + return value + + # Validate and extract known path/scope parameters + def _get_optional_param(name: str, validator: Callable[[object], object], default: object = None) -> object: + value = request_kwargs.pop(name, default) + if value is None or value == default: + return default + return validator(value) + + # Get include_tests with proper default + include_tests_value = request_kwargs.pop("include_tests", None) + include_tests = False # default value + if include_tests_value is not None: + if not isinstance(include_tests_value, bool): + raise RunCommandError(f"include_tests must be a boolean, got {type(include_tests_value).__name__}") + include_tests = include_tests_value + + # Get optional parameters with proper type casting + scope_value = _get_optional_param("scope", _as_auto_scope) + path_filters_value = _get_optional_param("path_filters", _as_path_filters) + out_value = _get_optional_param("out", _as_optional_path) + requirements_evidence_value = _get_optional_param("requirements_evidence", _as_optional_path) + base_ref_value = request_kwargs.pop("base_ref", None) + head_ref_value = request_kwargs.pop("head_ref", None) + if base_ref_value is not None and not isinstance(base_ref_value, str): + raise RunCommandError("base_ref must be a string") + if head_ref_value is not None and not isinstance(head_ref_value, str): + raise RunCommandError("head_ref must be a string") + pr_context_file_value = _get_optional_param("pr_context_file", _as_optional_path) + + # Cast the optional parameters to their proper types + scope = cast(AutoScope | None, scope_value) + path_filters = cast(list[Path] | None, path_filters_value) + out = cast(Path | None, out_value) + requirements_evidence = cast(Path | None, requirements_evidence_value) + + focus_facets = cast(tuple[str, ...], _as_focus_facets(request_kwargs.pop("focus_facets", None))) + + request = ReviewRunRequest( + files=files, + include_tests=include_tests, + scope=scope, + path_filters=path_filters, + base_ref=cast(str | None, base_ref_value), + head_ref=cast(str | None, head_ref_value), + pr_context_file=cast(Path | None, pr_context_file_value), + include_noise=_get_bool_param("include_noise"), + json_output=_get_bool_param("json_output"), + out=out, + score_only=_get_bool_param("score_only"), + no_tests=_get_bool_param("no_tests"), + fix=_get_bool_param("fix"), + preview_fixes=_get_bool_param("preview_fixes"), + with_mutation=_get_bool_param("with_mutation"), + bug_hunt=_get_bool_param("bug_hunt"), + review_mode=_as_review_mode(request_kwargs.pop("review_mode", "changed")), + review_level=_as_review_level(request_kwargs.pop("review_level", None)), + focus_facets=focus_facets, + review_focus=_review_focus_from_facets(focus_facets), + requirements_evidence=requirements_evidence, + ) + + # Reject any unexpected keyword arguments + if request_kwargs: + unexpected = ", ".join(sorted(request_kwargs)) +> raise RunCommandError(f"Unexpected keyword arguments: {unexpected}") +E specfact_code_review.run.scope.RunCommandError: Unexpected keyword arguments: project_config, project_runtime + +packages/specfact-code-review/src/specfact_code_review/run/commands.py:752: RunCommandError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_cli.py::test_runtime_inspect_is_read_only +FAILED tests/unit/specfact_code_review/run/test_runtime_cli.py::test_review_help_exposes_project_handoff +FAILED tests/unit/specfact_code_review/run/test_runtime_cli.py::test_review_request_carries_runtime_options +============================== 3 failed in 0.85s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/COMPATIBILITY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/COMPATIBILITY_RED.txt new file mode 100644 index 00000000..d84cfef3 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/COMPATIBILITY_RED.txt @@ -0,0 +1,33 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 2 items + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py FF [100%] + +=================================== FAILURES =================================== +_________ test_incompatible_dependency_identifies_only_affected_member _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-36/test_incompatible_dependency_i0') + + def test_incompatible_dependency_identifies_only_affected_member(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_compatibility import analyzer_dependency_conflicts +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_compatibility' + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:7: ModuleNotFoundError +____________ test_compatible_common_project_package_is_not_rejected ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-36/test_compatible_common_project0') + + def test_compatible_common_project_package_is_not_rejected(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_compatibility import analyzer_dependency_conflicts +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_compatibility' + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:19: ModuleNotFoundError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_incompatible_dependency_identifies_only_affected_member +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_compatible_common_project_package_is_not_rejected +============================== 2 failed in 0.10s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CONSUMED_METADATA_TABLES_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CONSUMED_METADATA_TABLES_RED.txt new file mode 100644 index 00000000..527dae97 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CONSUMED_METADATA_TABLES_RED.txt @@ -0,0 +1,4069 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 233 items / 137 deselected / 96 selected + +tests/unit/specfact_code_review/run/test_runtime_discovery.py FFFFFFFFFF [ 10%] +FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF [ 85%] +FFFFFF [ 91%] +tests/unit/specfact_code_review/run/test_portable_snapshot.py .FF..... [100%] + +=================================== FAILURES =================================== +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_0') +filename = 'pyproject.toml', section = 'tool', value = '1', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_0') +project = {'tool': 1}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() +> if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + ^^^^^^^^^^^^^ +E TypeError: argument of type 'int' is not a container or iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:132: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_1') +filename = 'pyproject.toml', section = 'project', value = '1', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_1') +project = {'project': 1}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_2') +filename = 'pyproject.toml', section = 'dependency-groups', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'dependency-groups': 1}, hatch = {}, values = {}, manager = 'pip' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) +> tests = sorted(set(groups) & {"test", "tests", "testing"}) + ^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:271: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_3') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = '1', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'project': {'optional-dependencies': 1}}, values = {} +manager = 'pip', groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": + declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} +> candidates = sorted(set(declared) & {"test", "tests", "testing"}) + ^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:319: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_4') +filename = 'pyproject.toml', section = 'tool.hatch', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_4') +project = {'tool': {'hatch': 1}}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") +> if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:136: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_5') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'hatch': {'envs': 1}}}, hatch = {}, values = {} +manager = 'hatch' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) +> if len(environments) > 1: + ^^^^^^^^^^^^^^^^^ +E TypeError: object of type 'int' has no len() + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:258: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_6') +filename = 'pyproject.toml', section = 'tool.poetry', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': 1}}, hatch = {}, values = {}, manager = 'poetry' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": +> groups = project.get("tool", {}).get("poetry", {}).get("group", groups) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:270: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_7') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_7') +project = {'tool': {'poetry': {'dependencies': 1}}}, manager = 'poetry' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_8') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': {'group': 1}}}, hatch = {}, values = {} +manager = 'poetry' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) +> tests = sorted(set(groups) & {"test", "tests", "testing"}) + ^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:271: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_9') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = '1' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': {'extras': 1}}}, values = {}, manager = 'poetry' +groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": +> declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not a mapping + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:318: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_10') +filename = 'hatch.toml', section = 'envs', value = '1', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {}, hatch = {'envs': 1}, values = {}, manager = 'hatch' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) +> if len(environments) > 1: + ^^^^^^^^^^^^^^^^^ +E TypeError: object of type 'int' has no len() + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:258: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_11') +filename = 'pyproject.toml', section = 'tool', value = 'false', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_11') +project = {'tool': False}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() +> if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + ^^^^^^^^^^^^^ +E TypeError: argument of type 'bool' is not a container or iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:132: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_12') +filename = 'pyproject.toml', section = 'project', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_12') +project = {'project': False}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_13') +filename = 'pyproject.toml', section = 'dependency-groups', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'dependency-groups': False}, hatch = {}, values = {}, manager = 'pip' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) +> tests = sorted(set(groups) & {"test", "tests", "testing"}) + ^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:271: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_14') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = 'false', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'project': {'optional-dependencies': False}}, values = {} +manager = 'pip', groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": + declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} +> candidates = sorted(set(declared) & {"test", "tests", "testing"}) + ^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:319: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_15') +filename = 'pyproject.toml', section = 'tool.hatch', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_15') +project = {'tool': {'hatch': False}}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") +> if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:136: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_16') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_17') +filename = 'pyproject.toml', section = 'tool.poetry', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': False}}, hatch = {}, values = {} +manager = 'poetry' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": +> groups = project.get("tool", {}).get("poetry", {}).get("group", groups) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:270: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_18') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies' +value = 'false', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_18') +project = {'tool': {'poetry': {'dependencies': False}}}, manager = 'poetry' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_19') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': {'group': False}}}, hatch = {}, values = {} +manager = 'poetry' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) +> tests = sorted(set(groups) & {"test", "tests", "testing"}) + ^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:271: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_20') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = 'false' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': {'extras': False}}}, values = {} +manager = 'poetry', groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": +> declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not a mapping + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:318: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_21') +filename = 'hatch.toml', section = 'envs', value = 'false', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_22') +filename = 'pyproject.toml', section = 'tool', value = '[]', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_22') +project = {'tool': []}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") +> if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + ^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:136: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_23') +filename = 'pyproject.toml', section = 'project', value = '[]', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_23') +project = {'project': []}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_24') +filename = 'pyproject.toml', section = 'dependency-groups', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_25') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = '[]', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_26') +filename = 'pyproject.toml', section = 'tool.hatch', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_26') +project = {'tool': {'hatch': []}}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") +> if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:136: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_27') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_28') +filename = 'pyproject.toml', section = 'tool.poetry', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': []}}, hatch = {}, values = {}, manager = 'poetry' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": +> groups = project.get("tool", {}).get("poetry", {}).get("group", groups) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:270: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_29') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_29') +project = {'tool': {'poetry': {'dependencies': []}}}, manager = 'poetry' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_30') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_31') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = '[]' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': {'extras': []}}}, values = {}, manager = 'poetry' +groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": +> declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'list' object is not a mapping + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:318: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_32') +filename = 'hatch.toml', section = 'envs', value = '[]', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_33') +filename = 'pyproject.toml', section = 'tool', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_33') +project = {'tool': 'invalid'}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") +> if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + ^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:136: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_34') +filename = 'pyproject.toml', section = 'project', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_34') +project = {'project': 'invalid'}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_35') +filename = 'pyproject.toml', section = 'dependency-groups', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_36') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = '"invalid"', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_37') +filename = 'pyproject.toml', section = 'tool.hatch', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_37') +project = {'tool': {'hatch': 'invalid'}}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") +> if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:136: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_38') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_config_invalid:pyproject.toml:tool.hatch.envs; expected a table' +E Actual message: 'project_environment_ambiguous:a,d,i,i,l,n,v; select environment in --project-config' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: AssertionError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_39') +filename = 'pyproject.toml', section = 'tool.poetry', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': 'invalid'}}, hatch = {}, values = {} +manager = 'poetry' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": +> groups = project.get("tool", {}).get("poetry", {}).get("group", groups) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:270: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_40') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies' +value = '"invalid"', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_40') +project = {'tool': {'poetry': {'dependencies': 'invalid'}}}, manager = 'poetry' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_41') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_42') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'tool': {'poetry': {'extras': 'invalid'}}}, values = {} +manager = 'poetry', groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": +> declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'str' object is not a mapping + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:318: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_43') +filename = 'hatch.toml', section = 'envs', value = '"invalid"', explicit = False + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_config_invalid:hatch.toml:envs; expected a table' +E Actual message: 'project_environment_ambiguous:a,d,i,i,l,n,v; select environment in --project-config' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: AssertionError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_44') +filename = 'pyproject.toml', section = 'tool', value = '1', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:349: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_44') +project = {'tool': 1} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(_pytest_table(_toml(root / name).get("pytest", {}), name, "pytest"), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": +> pytest = _pytest_table(project.get("tool", {}).get("pytest", {}), name, "tool.pytest") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:226: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_45') +filename = 'pyproject.toml', section = 'project', value = '1', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_45') +project = {'project': 1}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_46') +filename = 'pyproject.toml', section = 'dependency-groups', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'dependency-groups': 1}, hatch = {}, values = {'manager': 'pip'} +manager = 'pip' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) +> tests = sorted(set(groups) & {"test", "tests", "testing"}) + ^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:271: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_47') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = '1', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'project': {'optional-dependencies': 1}}, values = {'manager': 'pip'} +manager = 'pip', groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": + declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} +> candidates = sorted(set(declared) & {"test", "tests", "testing"}) + ^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:319: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_48') +filename = 'pyproject.toml', section = 'tool.hatch', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_49') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_50') +filename = 'pyproject.toml', section = 'tool.poetry', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_50') +project = {'tool': {'poetry': 1}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_51') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_51') +project = {'tool': {'poetry': {'dependencies': 1}}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_52') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_53') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = '1' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_54') +filename = 'hatch.toml', section = 'envs', value = '1', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_55') +filename = 'pyproject.toml', section = 'tool', value = 'false', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:349: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_55') +project = {'tool': False} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(_pytest_table(_toml(root / name).get("pytest", {}), name, "pytest"), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": +> pytest = _pytest_table(project.get("tool", {}).get("pytest", {}), name, "tool.pytest") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:226: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_56') +filename = 'pyproject.toml', section = 'project', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_56') +project = {'project': False}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_57') +filename = 'pyproject.toml', section = 'dependency-groups', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:353: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'dependency-groups': False}, hatch = {}, values = {'manager': 'pip'} +manager = 'pip' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) +> tests = sorted(set(groups) & {"test", "tests", "testing"}) + ^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:271: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_58') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = 'false', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:365: in discover_project + extras=_test_extras(project, selection, manager, groups), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'project': {'optional-dependencies': False}} +values = {'manager': 'pip'}, manager = 'pip', groups = () + + def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] + ) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": + declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} +> candidates = sorted(set(declared) & {"test", "tests", "testing"}) + ^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:319: TypeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_59') +filename = 'pyproject.toml', section = 'tool.hatch', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_60') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_61') +filename = 'pyproject.toml', section = 'tool.poetry', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_61') +project = {'tool': {'poetry': False}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_62') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies' +value = 'false', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_62') +project = {'tool': {'poetry': {'dependencies': False}}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_63') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_64') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = 'false' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_65') +filename = 'hatch.toml', section = 'envs', value = 'false', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_66') +filename = 'pyproject.toml', section = 'tool', value = '[]', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:349: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_66') +project = {'tool': []} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(_pytest_table(_toml(root / name).get("pytest", {}), name, "pytest"), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": +> pytest = _pytest_table(project.get("tool", {}).get("pytest", {}), name, "tool.pytest") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:226: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_67') +filename = 'pyproject.toml', section = 'project', value = '[]', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_67') +project = {'project': []}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_68') +filename = 'pyproject.toml', section = 'dependency-groups', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_69') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = '[]', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_70') +filename = 'pyproject.toml', section = 'tool.hatch', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_71') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_72') +filename = 'pyproject.toml', section = 'tool.poetry', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_72') +project = {'tool': {'poetry': []}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_73') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_73') +project = {'tool': {'poetry': {'dependencies': []}}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_74') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_75') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = '[]' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_76') +filename = 'hatch.toml', section = 'envs', value = '[]', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_77') +filename = 'pyproject.toml', section = 'tool', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:349: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_77') +project = {'tool': 'invalid'} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(_pytest_table(_toml(root / name).get("pytest", {}), name, "pytest"), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": +> pytest = _pytest_table(project.get("tool", {}).get("pytest", {}), name, "tool.pytest") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:226: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-project] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_78') +filename = 'pyproject.toml', section = 'project', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_78') +project = {'project': 'invalid'}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: +> declared = project.get("project", {}).get("requires-python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:290: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-dependency-groups] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_79') +filename = 'pyproject.toml', section = 'dependency-groups', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-project.optional-dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_80') +filename = 'pyproject.toml', section = 'project.optional-dependencies' +value = '"invalid"', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_81') +filename = 'pyproject.toml', section = 'tool.hatch', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.hatch.envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_82') +filename = 'pyproject.toml', section = 'tool.hatch.envs', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_83') +filename = 'pyproject.toml', section = 'tool.poetry', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_83') +project = {'tool': {'poetry': 'invalid'}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry.dependencies] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_84') +filename = 'pyproject.toml', section = 'tool.poetry.dependencies' +value = '"invalid"', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path, config_path=config if explicit else None) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:384: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:363: in discover_project + requires_python=_python_constraint(root, project, manager), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_84') +project = {'tool': {'poetry': {'dependencies': 'invalid'}}}, manager = 'pip' + + def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") +> poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:293: AttributeError +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry.group] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_85') +filename = 'pyproject.toml', section = 'tool.poetry.group', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry.extras] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_86') +filename = 'pyproject.toml', section = 'tool.poetry.extras', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-hatch.toml-envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_consumed_metadata_tables_87') +filename = 'hatch.toml', section = 'envs', value = '"invalid"', explicit = True + + @pytest.mark.parametrize("filename,section", [ + ("pyproject.toml", "tool"), ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.hatch"), ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ]) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:383: Failed +_ test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-tool=1\n-project_config_invalid:pyproject.toml:tool] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_malformed_configuration_p1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10970afd0> +filename = 'pyproject.toml', contents = 'tool=1\n' +diagnostic = 'project_config_invalid:pyproject.toml:tool' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pyproject.toml", "tool=1\n", "project_config_invalid:pyproject.toml:tool"), + ("hatch.toml", "envs=false\n", "project_config_invalid:hatch.toml:envs"), + ("pytest.toml", "pytest=1\n", "project_pytest_config_invalid:pytest.toml:pytest"), + ("pyproject.toml", "[tool]\npytest=false\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest"), + ( + "pyproject.toml", + "[tool.pytest]\nini_options=[]\n", + "project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\npythonpath=[1]\n", + "project_pytest_config_invalid:pyproject.toml:pythonpath", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\ntestpaths=1\n", + "project_pytest_config_invalid:pyproject.toml:testpaths", + ), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:196: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:81: in run_project_snapshot + plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:50: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:341: in discover_project + manager = _manager(root, project, hatch, selection) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_malformed_configuration_p1') +project = {'tool': 1}, hatch = {}, config = {} + + def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() +> if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + ^^^^^^^^^^^^^ +E TypeError: argument of type 'int' is not a container or iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:132: TypeError +_ test_malformed_configuration_preserves_independent_analyzer_evidence[hatch.toml-envs=false\n-project_config_invalid:hatch.toml:envs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-275/test_malformed_configuration_p2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1098220f0> +filename = 'hatch.toml', contents = 'envs=false\n' +diagnostic = 'project_config_invalid:hatch.toml:envs' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pyproject.toml", "tool=1\n", "project_config_invalid:pyproject.toml:tool"), + ("hatch.toml", "envs=false\n", "project_config_invalid:hatch.toml:envs"), + ("pytest.toml", "pytest=1\n", "project_pytest_config_invalid:pytest.toml:pytest"), + ("pyproject.toml", "[tool]\npytest=false\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest"), + ( + "pyproject.toml", + "[tool.pytest]\nini_options=[]\n", + "project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\npythonpath=[1]\n", + "project_pytest_config_invalid:pyproject.toml:pythonpath", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\ntestpaths=1\n", + "project_pytest_config_invalid:pyproject.toml:testpaths", + ), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + assert "ruff" in calls and not set(calls) & DEPENDENT_MEMBERS +> assert evidence["diagnostic"].startswith(diagnostic) +E assert False +E + where False = ('project_config_invalid:hatch.toml:envs') +E + where = "[Errno 1] Operation not permitted: '/.cache/specfact'".startswith + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:200: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-project] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-dependency-groups] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-project.optional-dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.hatch.envs] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry.dependencies] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry.group] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.poetry.extras] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-hatch.toml-envs] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-tool=1\n-project_config_invalid:pyproject.toml:tool] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[hatch.toml-envs=false\n-project_config_invalid:hatch.toml:envs] +================= 90 failed, 6 passed, 137 deselected in 1.07s ================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/CORPUS_CONTINUATION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_CONTINUATION_RED.txt new file mode 100644 index 00000000..7965781e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_CONTINUATION_RED.txt @@ -0,0 +1,134 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 5 items / 4 deselected / 1 selected + +tests/unit/test_external_capsule_corpus.py F [100%] + +=================================== FAILURES =================================== +_________ test_corpus_attempts_remaining_repositories_after_a_failure __________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-77/test_corpus_attempts_remaining0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108ff7e10> + + def test_corpus_attempts_remaining_repositories_after_a_failure(tmp_path: Path, monkeypatch) -> None: + import json + + module = _load() + manifest = tmp_path / 'corpus.json' + manifest.write_text(json.dumps({'python': ['3.12'], 'repositories': [{'name': 'first'}, {'name': 'second'}]})) + monkeypatch.setattr(module, 'MANIFEST', manifest) + monkeypatch.setattr(module.sys, 'argv', ['corpus', '--workspace', str(tmp_path / 'work')]) + monkeypatch.setattr(module.sys, 'version_info', type('Version', (), {'major': 3, 'minor': 12})()) + monkeypatch.setattr(module.platform, 'system', lambda: 'Linux') + monkeypatch.setattr(module.platform, 'machine', lambda: 'x86_64') + monkeypatch.setattr(module.os, 'getuid', lambda: 1000) + attempted = [] + + def run_entry(entry, workspace): + attempted.append(entry['name']) + if entry['name'] == 'first': + raise ValueError('controlled incomplete analysis') + + monkeypatch.setattr(module, 'run_entry', run_entry) +> assert module.main() == 1 + ^^^^^^^^^^^^^ + +tests/unit/test_external_capsule_corpus.py:82: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +scripts/external_capsule_corpus.py:293: in main + run_entry(entry, args.workspace.resolve()) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +entry = {'name': 'first'} +workspace = PosixPath('/pytest-of-dom/pytest-77/test_corpus_attempts_remaining0/work') + + def run_entry(entry, workspace): + attempted.append(entry['name']) + if entry['name'] == 'first': +> raise ValueError('controlled incomplete analysis') +E ValueError: controlled incomplete analysis + +tests/unit/test_external_capsule_corpus.py:79: ValueError + +During handling of the above exception, another exception occurred: + +cls = +func = . at 0x1096cd6f0> +when = 'call' +reraise = (, ) + + @classmethod + def from_call( + cls, + func: Callable[[], TResult], + when: Literal["collect", "setup", "call", "teardown"], + reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None, + ) -> CallInfo[TResult]: + """Call func, wrapping the result in a CallInfo. + + :param func: + The function to call. Called without arguments. + :type func: Callable[[], _pytest.runner.TResult] + :param when: + The phase in which the function is called. + :param reraise: + Exception or exceptions that shall propagate if raised by the + function, instead of being wrapped in the CallInfo. + """ + excinfo = None + instant = timing.Instant() + try: +> result: TResult | None = func() + ^^^^^^ + +../specfact-473-worktree/.venv/lib/python3.14/site-packages/_pytest/runner.py:361: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/_pytest/runner.py:250: in + lambda: runtest_hook(item=item, **kwds), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/pluggy/_hooks.py:512: in __call__ + return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/pluggy/_manager.py:120: in _hookexec + return self._inner_hookexec(hook_name, methods, kwargs, firstresult) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/_pytest/logging.py:865: in pytest_runtest_call + yield +../specfact-473-worktree/.venv/lib/python3.14/site-packages/_pytest/capture.py:900: in pytest_runtest_call + return (yield) + ^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/_pytest/skipping.py:268: in pytest_runtest_call + return (yield) + ^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +item = + + def pytest_runtest_call(item: Item) -> None: + _update_current_test_var(item, "call") + try: + del sys.last_type + del sys.last_value + del sys.last_traceback + if sys.version_info >= (3, 12, 0): + del sys.last_exc # type:ignore[attr-defined] + except AttributeError: + pass + try: + item.runtest() + except Exception as e: + # Store trace info to allow postmortem debugging + sys.last_type = type(e) + sys.last_value = e +> if sys.version_info >= (3, 12, 0): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: '>=' not supported between instances of 'Version' and 'tuple' + +../specfact-473-worktree/.venv/lib/python3.14/site-packages/_pytest/runner.py:189: TypeError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_corpus_attempts_remaining_repositories_after_a_failure +======================= 1 failed, 4 deselected in 0.04s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CORPUS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_RED.txt new file mode 100644 index 00000000..899936e8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_RED.txt @@ -0,0 +1,61 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 2 items + +tests/unit/test_external_capsule_corpus.py FF [100%] + +=================================== FAILURES =================================== +____________ test_external_gate_rejects_unknown_despite_fail_status ____________ + + def test_external_gate_rejects_unknown_despite_fail_status() -> None: +> module = _load() + ^^^^^^^ + +tests/unit/test_external_capsule_corpus.py:18: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/test_external_capsule_corpus.py:13: in _load + spec.loader.exec_module(module) +:755: in exec_module + ??? +:892: in get_code + ??? +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <_frozen_importlib_external.SourceFileLoader object at 0x1076b14f0> +path = '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/scripts/external_capsule_corpus.py' + +> ??? +E FileNotFoundError: [Errno 2] No such file or directory: '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/scripts/external_capsule_corpus.py' + +:950: FileNotFoundError +______________ test_external_gate_requires_actual_test_execution _______________ + + def test_external_gate_requires_actual_test_execution() -> None: +> module = _load() + ^^^^^^^ + +tests/unit/test_external_capsule_corpus.py:24: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/test_external_capsule_corpus.py:13: in _load + spec.loader.exec_module(module) +:755: in exec_module + ??? +:892: in get_code + ??? +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <_frozen_importlib_external.SourceFileLoader object at 0x1076b1730> +path = '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/scripts/external_capsule_corpus.py' + +> ??? +E FileNotFoundError: [Errno 2] No such file or directory: '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/scripts/external_capsule_corpus.py' + +:950: FileNotFoundError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_external_gate_rejects_unknown_despite_fail_status +FAILED tests/unit/test_external_capsule_corpus.py::test_external_gate_requires_actual_test_execution +============================== 2 failed in 0.02s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CORPUS_ROSTER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_ROSTER_RED.txt new file mode 100644 index 00000000..c778f7cf --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_ROSTER_RED.txt @@ -0,0 +1,33 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/test_external_capsule_corpus.py ..F [100%] + +=================================== FAILURES =================================== +_______ test_external_gate_rejects_missing_analyzer_even_with_real_tests _______ + + def test_external_gate_rejects_missing_analyzer_even_with_real_tests() -> None: + report = { + 'assurance_status': 'PASS', + 'analyzer_evidence': [{ + 'id': 'targeted-pytest-coverage', 'evidence_outcome': 'PASS', + 'target_execution': { + 'collected': ['test_app.py::test_app'], + 'records': [{'nodeid': 'test_app.py::test_app', 'phase': 'call', 'outcome': 'passed'}], + 'coverage': {'files': {'app.py': {}}}, + }, + }], + } +> with pytest.raises(ValueError, match='roster'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:42: Failed +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_external_gate_rejects_missing_analyzer_even_with_real_tests +========================= 1 failed, 2 passed in 0.02s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CORPUS_TRANSFER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_TRANSFER_RED.txt new file mode 100644 index 00000000..5b473ea3 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CORPUS_TRANSFER_RED.txt @@ -0,0 +1,27 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/test_external_capsule_corpus.py F [100%] + +=================================== FAILURES =================================== +______ test_network_evidence_excludes_loopback_and_reports_counter_deltas ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-146/test_network_evidence_excludes0') + + def test_network_evidence_excludes_loopback_and_reports_counter_deltas(tmp_path: Path) -> None: + module = _load() + counters = tmp_path / "net-dev" + counters.write_text("Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n lo: 900 0 0 0 0 0 0 0 800 0 0 0 0 0 0 0\n eth0: 120 0 0 0 0 0 0 0 70 0 0 0 0 0 0 0\n") +> before = module.network_counters(counters) + ^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'external_capsule_corpus' has no attribute 'network_counters' + +tests/unit/test_external_capsule_corpus.py:113: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_network_evidence_excludes_loopback_and_reports_counter_deltas +============================== 1 failed in 0.03s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/CUSTOMER_ACTIONS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/CUSTOMER_ACTIONS_RED.txt new file mode 100644 index 00000000..85b2e59f --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/CUSTOMER_ACTIONS_RED.txt @@ -0,0 +1,44 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items + +tests/unit/specfact_code_review/run/test_runtime_cli.py ...F [100%] + +=================================== FAILURES =================================== +________ test_customer_actions_source_layout_does_not_select_publisher _________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d1782b0> + + def test_customer_actions_source_layout_does_not_select_publisher(monkeypatch) -> None: + from specfact_code_review.run import runner + marker = object() + monkeypatch.setenv('GITHUB_ACTIONS', 'true') + monkeypatch.setenv('GITHUB_REPOSITORY', 'customer/project') + monkeypatch.setattr(runner, '_official_installed_payload', lambda: (marker, '')) + monkeypatch.setattr(runner, '_protected_candidate_payload', lambda: (_ for _ in ()).throw(AssertionError('publisher selected'))) +> assert runner._selected_module_payload().payload is marker + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_cli.py:45: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runner.py:655: in _selected_module_payload + return _protected_candidate_payload() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +tests/unit/specfact_code_review/run/test_runtime_cli.py:44: in + monkeypatch.setattr(runner, '_protected_candidate_payload', lambda: (_ for _ in ()).throw(AssertionError('publisher selected'))) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +.0 = + +> monkeypatch.setattr(runner, '_protected_candidate_payload', lambda: (_ for _ in ()).throw(AssertionError('publisher selected'))) +E AssertionError: publisher selected + +tests/unit/specfact_code_review/run/test_runtime_cli.py:44: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_cli.py::test_customer_actions_source_layout_does_not_select_publisher +========================= 1 failed, 3 passed in 0.30s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/DEVELOPMENT_CONTEXT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/DEVELOPMENT_CONTEXT_RED.txt new file mode 100644 index 00000000..65f2ce04 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/DEVELOPMENT_CONTEXT_RED.txt @@ -0,0 +1,21 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 8 items / 7 deselected / 1 selected + +tests/unit/specfact_code_review/test_capsule_tool_commands.py F [100%] + +=================================== FAILURES =================================== +_ test_development_snapshot_uses_its_verified_runtime_without_leaking_context __ + + def test_development_snapshot_uses_its_verified_runtime_without_leaking_context() -> None: +> from specfact_code_review._review_utils import _development_runtime, analyzer_command +E ImportError: cannot import name '_development_runtime' from 'specfact_code_review._review_utils' (/packages/specfact-code-review/src/specfact_code_review/_review_utils.py) + +tests/unit/specfact_code_review/test_capsule_tool_commands.py:48: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/test_capsule_tool_commands.py::test_development_snapshot_uses_its_verified_runtime_without_leaking_context +======================= 1 failed, 7 deselected in 0.08s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/DEVELOPMENT_TYPECHECK_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/DEVELOPMENT_TYPECHECK_RED.txt new file mode 100644 index 00000000..962eddfd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/DEVELOPMENT_TYPECHECK_RED.txt @@ -0,0 +1,35 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/specfact_code_review/test_capsule_tool_commands.py F [100%] + +=================================== FAILURES =================================== +______ test_development_context_runs_basedpyright_with_import_resolution _______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-51/test_development_context_runs_0') + + def test_development_context_runs_basedpyright_with_import_resolution(tmp_path) -> None: + import json + import subprocess + import sys + + source = tmp_path / 'app.py' + source.write_text('from icontract import require\n@require(lambda x: x > 0)\ndef positive(x: int) -> int:\n return x\n') + with _review_utils._development_runtime(Path(sys.prefix)): + command = _review_utils.analyzer_command(['basedpyright', '--outputjson', str(source)]) + result = subprocess.run(command, cwd=tmp_path, text=True, capture_output=True, check=False) +> assert result.returncode in (0, 1), result.stderr +E AssertionError: 'pythonpath' option cannot be used with 'venvpath' option +E +E assert 4 in (0, 1) +E + where 4 = CompletedProcess(args=['basedpyright', '--outputjson', '/pytes...tree/.venv/bin/python'], returncode=4, stdout='', stderr="'pythonpath' option cannot be used with 'venvpath' option\n").returncode + +tests/unit/specfact_code_review/test_capsule_tool_commands.py:70: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/test_capsule_tool_commands.py::test_development_context_runs_basedpyright_with_import_resolution +============================== 1 failed in 0.27s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/DISCOVERY_HARDENING_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/DISCOVERY_HARDENING_RED.txt new file mode 100644 index 00000000..5eb5fb51 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/DISCOVERY_HARDENING_RED.txt @@ -0,0 +1,61 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 15 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 66%] +..FFF [100%] + +=================================== FAILURES =================================== +__________________ test_pyproject_pytest_config_precedes_tox ___________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-9/test_pyproject_pytest_config_p0') + + def test_pyproject_pytest_config_precedes_tox(tmp_path: Path) -> None: + (tmp_path / 'pyproject.toml').write_text('[tool.pytest.ini_options]\naddopts="--import-mode=importlib"\n') + (tmp_path / 'tox.ini').write_text('[pytest]\naddopts=--collect-only\n') +> assert discover_project(tmp_path).pytest_config['addopts'] == '--import-mode=importlib' +E AssertionError: assert '--collect-only' == '--import-mode=importlib' +E +E - --import-mode=importlib +E + --collect-only + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:105: AssertionError +___________ test_changed_local_workspace_source_invalidates_runtime ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-9/test_changed_local_workspace_s0') + + def test_changed_local_workspace_source_invalidates_runtime(tmp_path: Path) -> None: + (tmp_path / 'pyproject.toml').write_text('[project]\nname="consumer"\n') + workspace = tmp_path / 'components' / 'local' + workspace.mkdir(parents=True) + source = workspace / 'library.py' + source.write_text('VALUE = 1\n') + before = discover_project(tmp_path) + source.write_text('VALUE = 2\n') +> assert before.identity != discover_project(tmp_path).identity +E AssertionError: assert 'sha256:1b349ddec1db6145ffcbdbdc3b992ea94312dc6ea0a91d4b02d3a0d1437c4642' != 'sha256:1b349ddec1db6145ffcbdbdc3b992ea94312dc6ea0a91d4b02d3a0d1437c4642' +E + where 'sha256:1b349ddec1db6145ffcbdbdc3b992ea94312dc6ea0a91d4b02d3a0d1437c4642' = ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-9/test_chang...inputs={'pyproject.toml': 'sha256:2a272a03e24cd0556a28c509db721ce1c78a2ea398f3d8d301ee54c9f9a1a692'}, pytest_config={}).identity +E + and 'sha256:1b349ddec1db6145ffcbdbdc3b992ea94312dc6ea0a91d4b02d3a0d1437c4642' = ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-9/test_chang...inputs={'pyproject.toml': 'sha256:2a272a03e24cd0556a28c509db721ce1c78a2ea398f3d8d301ee54c9f9a1a692'}, pytest_config={}).identity +E + where ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-9/test_chang...inputs={'pyproject.toml': 'sha256:2a272a03e24cd0556a28c509db721ce1c78a2ea398f3d8d301ee54c9f9a1a692'}, pytest_config={}) = discover_project(PosixPath('/pytest-of-dom/pytest-9/test_changed_local_workspace_s0')) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:116: AssertionError +___________________ test_pytest_paths_cannot_escape_snapshot ___________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-9/test_pytest_paths_cannot_escap0') + + def test_pytest_paths_cannot_escape_snapshot(tmp_path: Path) -> None: + (tmp_path / 'pytest.ini').write_text('[pytest]\ntestpaths=../outside\n') +> with pytest.raises(ProjectRuntimeError, match='escape'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:121: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_pyproject_pytest_config_precedes_tox +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_changed_local_workspace_source_invalidates_runtime +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_pytest_paths_cannot_escape_snapshot +========================= 3 failed, 12 passed in 0.36s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/DISCOVERY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/DISCOVERY_RED.txt new file mode 100644 index 00000000..353e0734 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/DISCOVERY_RED.txt @@ -0,0 +1,20 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 0 items / 1 error + +==================================== ERRORS ==================================== +_ ERROR collecting tests/unit/specfact_code_review/run/test_runtime_discovery.py _ +ImportError while importing test module '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/tests/unit/specfact_code_review/run/test_runtime_discovery.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +tests/unit/specfact_code_review/run/test_runtime_discovery.py:7: in + from specfact_code_review.run.runtime_discovery import discover_project +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_discovery' +=========================== short test summary info ============================ +ERROR tests/unit/specfact_code_review/run/test_runtime_discovery.py +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +=============================== 1 error in 0.27s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/EMPTY_XDIST_INVENTORY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/EMPTY_XDIST_INVENTORY_RED.txt new file mode 100644 index 00000000..b1733010 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/EMPTY_XDIST_INVENTORY_RED.txt @@ -0,0 +1,26 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py .... [ 57%] +..F [100%] + +=================================== FAILURES =================================== +__ test_empty_distributed_selection_has_controller_only_deselection_inventory __ + + def test_empty_distributed_selection_has_controller_only_deselection_inventory() -> None: + observer = target_pytest.Observer() + observer.pytest_xdist_node_collection_finished(None, []) + assert not observer.collected +> assert observer.deselection_inventory == "controller_observed" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'Observer' object has no attribute 'deselection_inventory' + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py:97: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_pytest_selection.py::test_empty_distributed_selection_has_controller_only_deselection_inventory +========================= 1 failed, 6 passed in 0.50s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/ENVIRONMENT_TEST_SELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/ENVIRONMENT_TEST_SELECTION_RED.txt new file mode 100644 index 00000000..3b022703 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/ENVIRONMENT_TEST_SELECTION_RED.txt @@ -0,0 +1,128 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 16 items + +tests/unit/specfact_code_review/run/test_portable_worker.py ............ [ 75%] +.FFF [100%] + +=================================== FAILURES =================================== +________ test_default_test_discovery_excludes_environment_tests[.venv] _________ + +tmp_path = PosixPath('/pytest-of-user/pytest-201/test_default_test_discovery_ex0') +name = '.venv' + + @pytest.mark.parametrize("name", [".venv", "venv", "custom-python"]) + def test_default_test_discovery_excludes_environment_tests(tmp_path: Path, name: str) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + environment = tmp_path / name + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home=/usr/bin\n") + (environment / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:139: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:57: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'.venv/test_app.py', 'test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:34: ProjectRuntimeError +_________ test_default_test_discovery_excludes_environment_tests[venv] _________ + +tmp_path = PosixPath('/pytest-of-user/pytest-201/test_default_test_discovery_ex1') +name = 'venv' + + @pytest.mark.parametrize("name", [".venv", "venv", "custom-python"]) + def test_default_test_discovery_excludes_environment_tests(tmp_path: Path, name: str) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + environment = tmp_path / name + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home=/usr/bin\n") + (environment / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:139: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:57: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'test_app.py', 'venv/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:34: ProjectRuntimeError +____ test_default_test_discovery_excludes_environment_tests[custom-python] _____ + +tmp_path = PosixPath('/pytest-of-user/pytest-201/test_default_test_discovery_ex2') +name = 'custom-python' + + @pytest.mark.parametrize("name", [".venv", "venv", "custom-python"]) + def test_default_test_discovery_excludes_environment_tests(tmp_path: Path, name: str) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + environment = tmp_path / name + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home=/usr/bin\n") + (environment / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:139: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:57: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'custom-python/test_app.py', 'test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:34: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_test_discovery_excludes_environment_tests[.venv] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_test_discovery_excludes_environment_tests[venv] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_test_discovery_excludes_environment_tests[custom-python] +========================= 3 failed, 13 passed in 0.14s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/EXECUTABLE_INVENTORY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/EXECUTABLE_INVENTORY_RED.txt new file mode 100644 index 00000000..64b91b82 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/EXECUTABLE_INVENTORY_RED.txt @@ -0,0 +1,23 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py ...F [100%] + +=================================== FAILURES =================================== +_____ test_distribution_executables_are_rebased_and_unowned_files_excluded _____ + +tmp_path = PosixPath('/pytest-of-dom/pytest-85/test_distribution_executables_0') + + def test_distribution_executables_are_rebased_and_unowned_files_excluded(tmp_path) -> None: +> from specfact_code_review.run.runtime_build_driver import copy_executables +E ImportError: cannot import name 'copy_executables' from 'specfact_code_review.run.runtime_build_driver' (/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py) + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:45: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_distribution_executables_are_rebased_and_unowned_files_excluded +========================= 1 failed, 3 passed in 0.09s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/FALLBACK_SANITIZATION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/FALLBACK_SANITIZATION_RED.txt new file mode 100644 index 00000000..4f504a6c --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/FALLBACK_SANITIZATION_RED.txt @@ -0,0 +1,211 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 13 items + +tests/unit/specfact_code_review/run/test_portable_snapshot.py .......... [ 76%] +FFF [100%] + +=================================== FAILURES =================================== +_ test_preparation_fallback_uses_only_sanitized_source[project_config_invalid:setup.cfg] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10892fe30> +reason = 'project_config_invalid:setup.cfg' + + @pytest.mark.parametrize("reason", ["project_config_invalid:setup.cfg", "project_native_library_missing:libodbc.so.2"]) + def test_preparation_fallback_uses_only_sanitized_source(tmp_path: Path, monkeypatch, reason: str) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + (tmp_path / ".env").write_text("PRIVATE=fixture\n") + (tmp_path / ".git/objects").mkdir(parents=True) + observed = [] + + def dispatch(request, **_kwargs): + assert request.snapshot_root != tmp_path + assert not (request.snapshot_root / ".env").exists() + assert not (request.snapshot_root / ".git").exists() + assert request.files[0].read_text() == "VALUE = 1\n" + observed.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = portable_snapshot._failed_project_snapshot( + runtime, snapshot_root=tmp_path, files=[source], options=runner.ReviewOptions(), reason=reason + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:206: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:92: in _failed_project_snapshot + snapshot = _run_capsule_snapshot( +packages/specfact-code-review/src/specfact_code_review/run/runner.py:1294: in _run_capsule_snapshot + raw = _dispatch_capsule_member( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +request = CapsuleMemberExecutionRequest(runtime=namespace(identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...), config_roots=(), adapter_argv=(), complete_pytest_inventory=False, project_runtime_root=None, portable_runtime=True) +_kwargs = {'applicability': SnapshotInputClassification(members=(SnapshotMemberState(id='ruff', status='PASS', required=True), S...SnapshotMemberState(id='contracts.icontract-static-scan', status='PASS', required=True))), 'sealed_bugs_policy': False} + + def dispatch(request, **_kwargs): +> assert request.snapshot_root != tmp_path +E AssertionError: assert PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses0') != PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses0') +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses0') = CapsuleMemberExecutionRequest(runtime=namespace(identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...), config_roots=(), adapter_argv=(), complete_pytest_inventory=False, project_runtime_root=None, portable_runtime=True).snapshot_root + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:197: AssertionError +_ test_preparation_fallback_uses_only_sanitized_source[project_native_library_missing:libodbc.so.2] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108b0d630> +reason = 'project_native_library_missing:libodbc.so.2' + + @pytest.mark.parametrize("reason", ["project_config_invalid:setup.cfg", "project_native_library_missing:libodbc.so.2"]) + def test_preparation_fallback_uses_only_sanitized_source(tmp_path: Path, monkeypatch, reason: str) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + (tmp_path / ".env").write_text("PRIVATE=fixture\n") + (tmp_path / ".git/objects").mkdir(parents=True) + observed = [] + + def dispatch(request, **_kwargs): + assert request.snapshot_root != tmp_path + assert not (request.snapshot_root / ".env").exists() + assert not (request.snapshot_root / ".git").exists() + assert request.files[0].read_text() == "VALUE = 1\n" + observed.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = portable_snapshot._failed_project_snapshot( + runtime, snapshot_root=tmp_path, files=[source], options=runner.ReviewOptions(), reason=reason + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:206: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:92: in _failed_project_snapshot + snapshot = _run_capsule_snapshot( +packages/specfact-code-review/src/specfact_code_review/run/runner.py:1294: in _run_capsule_snapshot + raw = _dispatch_capsule_member( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +request = CapsuleMemberExecutionRequest(runtime=namespace(identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...), config_roots=(), adapter_argv=(), complete_pytest_inventory=False, project_runtime_root=None, portable_runtime=True) +_kwargs = {'applicability': SnapshotInputClassification(members=(SnapshotMemberState(id='ruff', status='PASS', required=True), S...SnapshotMemberState(id='contracts.icontract-static-scan', status='PASS', required=True))), 'sealed_bugs_policy': False} + + def dispatch(request, **_kwargs): +> assert request.snapshot_root != tmp_path +E AssertionError: assert PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses1') != PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses1') +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_preparation_fallback_uses1') = CapsuleMemberExecutionRequest(runtime=namespace(identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...), config_roots=(), adapter_argv=(), complete_pytest_inventory=False, project_runtime_root=None, portable_runtime=True).snapshot_root + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:197: AssertionError +____________ test_unsafe_fallback_source_never_dispatches_analyzer _____________ + +runtime = namespace(identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', environment_id='linux-x86_64-cp312') +request = ProjectSnapshotRequest(snapshot_root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom...='full', focus=None, project_config=None, project_runtime=None), assurance_kind='explicit_files', source_snapshot=None) + + @require(lambda request: request.snapshot_root.is_dir()) + def run_project_snapshot(runtime: Any, request: ProjectSnapshotRequest) -> tuple[Any, dict[str, Any]]: + """Bind every review side to its selected signed Python worker.""" + snapshot_root, files, options = request.snapshot_root, request.files, request.options + source_snapshot = request.source_snapshot + try: +> plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:77: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:50: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:323: in discover_project + source_identity=source_identity(root), + ^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py:75: in source_identity + identity = _source_entry(path, root) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py:58: in _source_entry + return "link:" + source_link_target(path, root).relative_to(root).as_posix() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_unsafe_fallback_source_ne0/alias.py') +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_unsafe_fallback_source_ne0') + + @ensure(lambda result, root: result.is_relative_to(root)) + def source_link_target(path: Path, root: Path) -> Path: + """Apply the same exclusion boundary to aliases and ordinary source paths.""" + relative = path.relative_to(root).as_posix() + try: + target = path.resolve(strict=True) + except (OSError, RuntimeError) as exc: + raise ProjectRuntimeError(f"project_source_symlink_invalid:{relative}") from exc + if not target.is_relative_to(root) or target == root or path.is_relative_to(target): + raise ProjectRuntimeError(f"project_source_symlink_escape:{relative}") + if any( + is_excluded_source(entry) for entry in (target, *target.parents) if entry != root and entry.is_relative_to(root) + ): +> raise ProjectRuntimeError(f"project_source_symlink_excluded:{relative}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_source_symlink_excluded:alias.py + +packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py:51: ProjectRuntimeError + +During handling of the above exception, another exception occurred: + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-210/test_unsafe_fallback_source_ne0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108b0cf30> + + def test_unsafe_fallback_source_never_dispatches_analyzer(tmp_path: Path, monkeypatch) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + (tmp_path / ".env").write_text("PRIVATE=fixture\n") + (tmp_path / "alias.py").symlink_to(".env") + + def dispatch(*_args, **_kwargs): + pytest.fail("unsafe original source reached an analyzer") + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:225: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:81: in run_project_snapshot + return _failed_project_snapshot( +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:92: in _failed_project_snapshot + snapshot = _run_capsule_snapshot( +packages/specfact-code-review/src/specfact_code_review/run/runner.py:1294: in _run_capsule_snapshot + raw = _dispatch_capsule_member( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (CapsuleMemberExecutionRequest(runtime=namespace(identity='sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... config_roots=(), adapter_argv=(), complete_pytest_inventory=False, project_runtime_root=None, portable_runtime=True),) +_kwargs = {'applicability': SnapshotInputClassification(members=(SnapshotMemberState(id='ruff', status='PASS', required=True), S...SnapshotMemberState(id='contracts.icontract-static-scan', status='PASS', required=True))), 'sealed_bugs_policy': False} + + def dispatch(*_args, **_kwargs): +> pytest.fail("unsafe original source reached an analyzer") +E Failed: unsafe original source reached an analyzer + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:221: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_preparation_fallback_uses_only_sanitized_source[project_config_invalid:setup.cfg] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_preparation_fallback_uses_only_sanitized_source[project_native_library_missing:libodbc.so.2] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_unsafe_fallback_source_never_dispatches_analyzer +========================= 3 failed, 10 passed in 0.93s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/FROZEN_GIT_CONTEXT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/FROZEN_GIT_CONTEXT_RED.txt new file mode 100644 index 00000000..54209a3b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/FROZEN_GIT_CONTEXT_RED.txt @@ -0,0 +1,212 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 23 items / 20 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py FFF [100%] + +=================================== FAILURES =================================== +_____________ test_frozen_vcs_tags_exclude_newly_added_tag_history _____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_frozen_vcs_tags_exclude_n0') + + def test_frozen_vcs_tags_exclude_newly_added_tag_history(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + bound = vcs_context(root) + unrelated = _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit-tree", bound["tree"], "-m", "unbound commit") + _git(root, "tag", "late", unrelated) + copied = tmp_path / "copy" + copied.mkdir() +> copy_vcs_context(root, copied, bound_vcs=bound) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:419: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +args = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_frozen_vcs_tags_exc...rivate/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_frozen_vcs_tags_exclude_n0/copy')) +kwargs = {'bound_vcs': {'commit': '9a2c9027fec277397df7d92be4d73fc4d438dcdf', 'tree': 'ae7df65c33f278677e0d7eb633690c0053ddf0b7', 'tags': '', 'shallow': ''}} + + def wrapper(*args, **kwargs): # type: ignore + """Wrap func by checking the preconditions and postconditions.""" + kwargs_error = _assert_no_invalid_kwargs(kwargs) + if kwargs_error: + raise kwargs_error + + # We need to create a new in-progress set if it is None as the ``ContextVar`` does not accept + # a factory function for the default argument. If we didn't do this, and simply set an empty + # set as the default, ``ContextVar`` would always point to the same set by copying the default + # by reference. + in_progress = _IN_PROGRESS.get() + if in_progress is None: + in_progress = set() + _IN_PROGRESS.set(in_progress) + + # Use try-finally instead of ExitStack for performance. + try: + # If the wrapper is already checking the contracts for the wrapped function, avoid a recursive loop + # by skipping any subsequent contract checks for the same function. + if id_func in in_progress: + return func(*args, **kwargs) + + in_progress.add(id_func) + + (preconditions, snapshots, postconditions) = _unpack_pre_snap_posts( + wrapper + ) + + resolved_kwargs = kwargs_from_call( + param_names=param_names, + kwdefaults=kwdefaults, + args=args, + kwargs=kwargs, + ) + + type_error = _assert_resolved_kwargs_valid( + postconditions=postconditions, resolved_kwargs=resolved_kwargs + ) + if type_error: + raise type_error + + violation_error = _assert_preconditions( + preconditions=preconditions, + resolved_kwargs=resolved_kwargs, + func=func, + ) + if violation_error: + raise violation_error + + # Capture the snapshots + if postconditions and snapshots: + resolved_kwargs["OLD"] = _capture_old( + snapshots=snapshots, resolved_kwargs=resolved_kwargs, func=func + ) + + # Ideally, we would catch any exception here and strip the checkers from the traceback. + # Unfortunately, this can not be done in Python 3, see + # https://stackoverflow.com/questions/44813333/how-can-i-elide-a-function-wrapper-from-the-traceback-in-python-3 +> result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +E TypeError: copy_vcs_context() got an unexpected keyword argument 'bound_vcs' + +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: TypeError +____________ test_frozen_shallow_boundary_controls_object_traversal ____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_frozen_shallow_boundary_c0') + + def test_frozen_shallow_boundary_controls_object_traversal(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + bound = vcs_context(root) + (root / ".git/shallow").write_text(selected + "\n") + copied = tmp_path / "copy" + copied.mkdir() +> copy_vcs_context(root, copied, bound_vcs=bound) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:433: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +args = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_frozen_shallow_boun...rivate/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_frozen_shallow_boundary_c0/copy')) +kwargs = {'bound_vcs': {'commit': '2d07f386619e5b43d329436856d0ada35427ab99', 'tree': 'dc72567ae5cda25891b9ab3e4fc6c31f523e86ee', 'tags': '', 'shallow': ''}} + + def wrapper(*args, **kwargs): # type: ignore + """Wrap func by checking the preconditions and postconditions.""" + kwargs_error = _assert_no_invalid_kwargs(kwargs) + if kwargs_error: + raise kwargs_error + + # We need to create a new in-progress set if it is None as the ``ContextVar`` does not accept + # a factory function for the default argument. If we didn't do this, and simply set an empty + # set as the default, ``ContextVar`` would always point to the same set by copying the default + # by reference. + in_progress = _IN_PROGRESS.get() + if in_progress is None: + in_progress = set() + _IN_PROGRESS.set(in_progress) + + # Use try-finally instead of ExitStack for performance. + try: + # If the wrapper is already checking the contracts for the wrapped function, avoid a recursive loop + # by skipping any subsequent contract checks for the same function. + if id_func in in_progress: + return func(*args, **kwargs) + + in_progress.add(id_func) + + (preconditions, snapshots, postconditions) = _unpack_pre_snap_posts( + wrapper + ) + + resolved_kwargs = kwargs_from_call( + param_names=param_names, + kwdefaults=kwdefaults, + args=args, + kwargs=kwargs, + ) + + type_error = _assert_resolved_kwargs_valid( + postconditions=postconditions, resolved_kwargs=resolved_kwargs + ) + if type_error: + raise type_error + + violation_error = _assert_preconditions( + preconditions=preconditions, + resolved_kwargs=resolved_kwargs, + func=func, + ) + if violation_error: + raise violation_error + + # Capture the snapshots + if postconditions and snapshots: + resolved_kwargs["OLD"] = _capture_old( + snapshots=snapshots, resolved_kwargs=resolved_kwargs, func=func + ) + + # Ideally, we would catch any exception here and strip the checkers from the traceback. + # Unfortunately, this can not be done in Python 3, see + # https://stackoverflow.com/questions/44813333/how-can-i-elide-a-function-wrapper-from-the-traceback-in-python-3 +> result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +E TypeError: copy_vcs_context() got an unexpected keyword argument 'bound_vcs' + +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: TypeError +________ test_builder_copies_frozen_plan_refs_after_input_verification _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_builder_copies_frozen_pla0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1096e0b00> + + def test_builder_copies_frozen_plan_refs_after_input_verification(tmp_path: Path, monkeypatch) -> None: + root = tmp_path / "source" + _repository(root) + plan = discover_project(root) + unrelated = _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit-tree", plan.vcs["tree"], "-m", "unbound commit") + staging = tmp_path / "staging" + staging.mkdir() + + def change_tags_after_verification(selected_plan): + verify_inputs(selected_plan) + _git(root, "tag", "late", unrelated) + + def stop_before_acquisition(*_args, **_kwargs): + raise RuntimeError("stop before acquisition") + + monkeypatch.setattr(runtime_builder, "verify_inputs", change_tags_after_verification) + monkeypatch.setattr(runtime_builder, "stage_git", stop_before_acquisition) + with pytest.raises(RuntimeError, match="stop before acquisition"): + runtime_builder._build(plan, object(), staging) +> assert not _git(staging / "project", "for-each-ref", "--format=%(refname)") +E AssertionError: assert not 'refs/tags/late' +E + where 'refs/tags/late' = _git((PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-273/test_builder_copies_frozen_pla0/staging') / 'project'), 'for-each-ref', '--format=%(refname)') + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:457: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_frozen_vcs_tags_exclude_newly_added_tag_history +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_frozen_shallow_boundary_controls_object_traversal +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_copies_frozen_plan_refs_after_input_verification +======================= 3 failed, 20 deselected in 0.81s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/GIT_EXPORT_ERRORS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/GIT_EXPORT_ERRORS_RED.txt new file mode 100644 index 00000000..fe0262d0 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/GIT_EXPORT_ERRORS_RED.txt @@ -0,0 +1,73 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 20 items / 18 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py FF [100%] + +=================================== FAILURES =================================== +______ test_git_export_process_failure_uses_runtime_diagnostic[failure0] _______ + +tmp_path = PosixPath('/test_git_export_process_failur0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107217820> +failure = TimeoutExpired('git', 120) + + @pytest.mark.parametrize("failure", [subprocess.TimeoutExpired("git", 120, stderr=b"private transport detail"), OSError("private filesystem detail")]) + def test_git_export_process_failure_uses_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + def fail_process(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_vcs.subprocess, "run", fail_process) + with pytest.raises(ProjectRuntimeError, match="project_git_snapshot_failed:rev-list") as caught: +> runtime_vcs._git_bytes(tmp_path, "rev-list", "--objects", "HEAD") + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:405: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:17: in _git_bytes + result = subprocess.run( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (['git', '-C', '/test_git_export_process_failur0', '-c', 'core.hooksPath=/dev/null', 'rev-list', ...],) +_kwargs = {'env': {'PATH': '/bin:/usr/bin', 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_NO_REPLACE_OBJECTS': '1', ...}, 'input': None, 'capture_output': True, 'check': False, ...} + + def fail_process(*_args, **_kwargs): +> raise failure +E subprocess.TimeoutExpired: Command 'git' timed out after 120 seconds + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:401: TimeoutExpired +______ test_git_export_process_failure_uses_runtime_diagnostic[failure1] _______ + +tmp_path = PosixPath('/test_git_export_process_failur1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107c29480> +failure = OSError('private filesystem detail') + + @pytest.mark.parametrize("failure", [subprocess.TimeoutExpired("git", 120, stderr=b"private transport detail"), OSError("private filesystem detail")]) + def test_git_export_process_failure_uses_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + def fail_process(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_vcs.subprocess, "run", fail_process) + with pytest.raises(ProjectRuntimeError, match="project_git_snapshot_failed:rev-list") as caught: +> runtime_vcs._git_bytes(tmp_path, "rev-list", "--objects", "HEAD") + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:405: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:17: in _git_bytes + result = subprocess.run( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (['git', '-C', '/test_git_export_process_failur1', '-c', 'core.hooksPath=/dev/null', 'rev-list', ...],) +_kwargs = {'env': {'PATH': '/bin:/usr/bin', 'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': '/dev/null', 'GIT_NO_REPLACE_OBJECTS': '1', ...}, 'input': None, 'capture_output': True, 'check': False, ...} + + def fail_process(*_args, **_kwargs): +> raise failure +E OSError: private filesystem detail + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:401: OSError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_git_export_process_failure_uses_runtime_diagnostic[failure0] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_git_export_process_failure_uses_runtime_diagnostic[failure1] +======================= 2 failed, 18 deselected in 0.19s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/GIT_GRAFTS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/GIT_GRAFTS_RED.txt new file mode 100644 index 00000000..cbdccf3f --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/GIT_GRAFTS_RED.txt @@ -0,0 +1,117 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 18 items / 17 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py F [100%] + +=================================== FAILURES =================================== +_____________ test_unbound_grafts_cannot_remove_selected_ancestry ______________ + +tmp_path = PosixPath('/test_unbound_grafts_cannot_rem0') + + def test_unbound_grafts_cannot_remove_selected_ancestry(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + before = vcs_context(root) + (root / ".git/info/grafts").write_text(selected + "\n") + assert vcs_context(root) == before + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied) +> assert _git(copied, "rev-list", "--count", "HEAD") == "2" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:394: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:19: in _git + return subprocess.check_output( +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:473: in check_output + return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +input = None, capture_output = False, timeout = None, check = True +popenargs = (['git', '-C', '/test_unbound_grafts_cannot_rem0/copy', 'rev-list', '--count', 'HEAD'],) +kwargs = {'stdout': -1, 'text': True, 'env': {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...}} + + def run(*popenargs, + input=None, capture_output=False, timeout=None, check=False, **kwargs): + """Run command with arguments and return a CompletedProcess instance. + + The returned instance will have attributes args, returncode, stdout and + stderr. By default, stdout and stderr are not captured, and those attributes + will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them, + or pass capture_output=True to capture both. + + If check is True and the exit code was non-zero, it raises a + CalledProcessError. The CalledProcessError object will have the return code + in the returncode attribute, and output & stderr attributes if those streams + were captured. + + If timeout (seconds) is given and the process takes too long, + a TimeoutExpired exception will be raised. + + There is an optional argument "input", allowing you to + pass bytes or a string to the subprocess's stdin. If you use this argument + you may not also use the Popen constructor's "stdin" argument, as + it will be used internally. + + By default, all communication is in bytes, and therefore any "input" should + be bytes, and the stdout and stderr will be bytes. If in text mode, any + "input" should be a string, and stdout and stderr will be strings decoded + according to locale encoding, or by "encoding" if set. Text mode is + triggered by setting any of text, encoding, errors or universal_newlines. + + The other arguments are the same as for the Popen constructor. + """ + if input is not None: + if kwargs.get('stdin') is not None: + raise ValueError('stdin and input arguments may not both be used.') + kwargs['stdin'] = PIPE + + if capture_output: + if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None: + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + kwargs['stdout'] = PIPE + kwargs['stderr'] = PIPE + + with Popen(*popenargs, **kwargs) as process: + try: + stdout, stderr = process.communicate(input, timeout=timeout) + except TimeoutExpired as exc: + process.kill() + if _mswindows: + # Windows accumulates the output in a single blocking + # read() call run on child threads, with the timeout + # being done in a join() on those threads. communicate() + # _after_ kill() is required to collect that and add it + # to the exception. + exc.stdout, exc.stderr = process.communicate() + else: + # POSIX _communicate already populated the output so + # far into the TimeoutExpired exception. + process.wait() + raise + except: # Including KeyboardInterrupt, communicate handled that. + process.kill() + # We don't call process.wait() as .__exit__ does that for us. + raise + retcode = process.poll() + if check and retcode: +> raise CalledProcessError(retcode, process.args, + output=stdout, stderr=stderr) +E subprocess.CalledProcessError: Command '['git', '-C', '/test_unbound_grafts_cannot_rem0/copy', 'rev-list', '--count', 'HEAD']' returned non-zero exit status 128. + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:578: CalledProcessError +----------------------------- Captured stderr call ----------------------------- +error: Could not read 4b9d889dc86c80decda73345368196a28b26db2a +fatal: Failed to traverse parents of commit a1e801eb190abc352e94a002dcb9450874fec3fd +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unbound_grafts_cannot_remove_selected_ancestry +======================= 1 failed, 17 deselected in 0.43s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/GROUP_SELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/GROUP_SELECTION_RED.txt new file mode 100644 index 00000000..20395612 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/GROUP_SELECTION_RED.txt @@ -0,0 +1,27 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py ......F [100%] + +=================================== FAILURES =================================== +____________ test_explicit_groups_do_not_install_unrelated_defaults ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-24/test_explicit_groups_do_not_in0') + + def test_explicit_groups_do_not_install_unrelated_defaults(tmp_path: Path) -> None: + from specfact_code_review.run.runtime_adapters import install_commands + from specfact_code_review.run.runtime_models import ProjectPlan + uv = install_commands(ProjectPlan(root=tmp_path, manager='uv', groups=('tests',)), python='python')[0] + poetry = install_commands(ProjectPlan(root=tmp_path, manager='poetry', groups=('test',)), python='python')[0] +> assert '--no-default-groups' in uv +E AssertionError: assert '--no-default-groups' in ('python', '-m', 'uv', 'sync', '--active', '--no-editable', ...) + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:109: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_explicit_groups_do_not_install_unrelated_defaults +========================= 1 failed, 6 passed in 0.11s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/HATCH_EXECUTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HATCH_EXECUTION_RED.txt new file mode 100644 index 00000000..70a6fa06 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HATCH_EXECUTION_RED.txt @@ -0,0 +1,85 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 16 items + +tests/unit/specfact_code_review/run/test_runtime_adapters.py ..F. [ 25%] +tests/unit/specfact_code_review/run/test_runtime_build_driver.py .FF [ 43%] +tests/unit/specfact_code_review/run/test_target_pytest.py .F [ 56%] +tests/unit/test_external_capsule_corpus.py .....FF [100%] + +=================================== FAILURES =================================== +_____________ test_hatch_prepare_uses_selected_native_environment ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-83/test_hatch_prepare_uses_select0') + + def test_hatch_prepare_uses_selected_native_environment(tmp_path: Path) -> None: + (tmp_path / "hatch.toml").write_text("[envs.review]\ndetached=true\n") + config = tmp_path / "review.toml" + config.write_text('manager="hatch"\nenvironment="review"\n') + commands = install_commands(discover_project(tmp_path, config_path=config), python="/runtime/python") +> assert commands == (("/runtime/python", "-m", "hatch", "env", "create", "review"),) +E AssertionError: assert (('/runtime/p... 'env', ...),) == (('/runtime/p...', 'review'),) +E +E At index 0 diff: ('/runtime/python', '-m', 'hatch', '-e', 'review', 'env', 'create') != ('/runtime/python', '-m', 'hatch', 'env', 'create', 'review') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:33: AssertionError +___________ test_hatch_export_selects_one_native_matrix_environment ____________ + + def test_hatch_export_selects_one_native_matrix_environment() -> None: +> from specfact_code_review.run.runtime_build_driver import select_hatch_environment +E ImportError: cannot import name 'select_hatch_environment' from 'specfact_code_review.run.runtime_build_driver' (/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py) + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:31: ImportError +__________________ test_hatch_export_rejects_ambiguous_matrix __________________ + + def test_hatch_export_rejects_ambiguous_matrix() -> None: + import pytest +> from specfact_code_review.run.runtime_build_driver import select_hatch_environment +E ImportError: cannot import name 'select_hatch_environment' from 'specfact_code_review.run.runtime_build_driver' (/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py) + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:38: ImportError +_________________ test_observer_retains_setup_failure_details __________________ + + def test_observer_retains_setup_failure_details() -> None: +> observer = target_pytest.Observer() + ^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute 'Observer' + +tests/unit/specfact_code_review/run/test_target_pytest.py:20: AttributeError +______ test_host_baseline_rejects_startup_failure_without_test_execution _______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-83/test_host_baseline_rejects_sta0') + + def test_host_baseline_rejects_startup_failure_without_test_execution(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="host.*execution"): +> _load().assert_host_execution(tmp_path / "missing.xml") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'external_capsule_corpus' has no attribute 'assert_host_execution' + +tests/unit/test_external_capsule_corpus.py:88: AttributeError +_________________ test_host_baseline_rejects_only_setup_errors _________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-83/test_host_baseline_rejects_onl0') + + def test_host_baseline_rejects_only_setup_errors(tmp_path: Path) -> None: + result = tmp_path / "tests.xml" + result.write_text('') + with pytest.raises(ValueError, match="host.*execution"): +> _load().assert_host_execution(result) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'external_capsule_corpus' has no attribute 'assert_host_execution' + +tests/unit/test_external_capsule_corpus.py:95: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_hatch_prepare_uses_selected_native_environment +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_hatch_export_selects_one_native_matrix_environment +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_hatch_export_rejects_ambiguous_matrix +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_observer_retains_setup_failure_details +FAILED tests/unit/test_external_capsule_corpus.py::test_host_baseline_rejects_startup_failure_without_test_execution +FAILED tests/unit/test_external_capsule_corpus.py::test_host_baseline_rejects_only_setup_errors +========================= 6 failed, 10 passed in 0.12s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/HATCH_INSTALLER_DOMAIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HATCH_INSTALLER_DOMAIN_RED.txt new file mode 100644 index 00000000..783f9b59 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HATCH_INSTALLER_DOMAIN_RED.txt @@ -0,0 +1,21 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 6 items + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py .....F [100%] + +=================================== FAILURES =================================== +_____ test_hatch_dependencies_are_not_redirected_into_the_pip_environment ______ + + def test_hatch_dependencies_are_not_redirected_into_the_pip_environment() -> None: +> from specfact_code_review.run.runtime_build_driver import manager_environment +E ImportError: cannot import name 'manager_environment' from 'specfact_code_review.run.runtime_build_driver' (/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py) + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:84: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_hatch_dependencies_are_not_redirected_into_the_pip_environment +========================= 1 failed, 5 passed in 0.08s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/HATCH_NATIVE_EXPORT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HATCH_NATIVE_EXPORT_RED.txt new file mode 100644 index 00000000..c4d812fa --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HATCH_NATIVE_EXPORT_RED.txt @@ -0,0 +1,33 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 5 items + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py ....F [100%] + +=================================== FAILURES =================================== +________________ test_hatch_export_has_its_owned_uv_executable _________________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1083fdcd0> + + def test_hatch_export_has_its_owned_uv_executable(monkeypatch) -> None: + from specfact_code_review.run import runtime_build_driver as driver + observed = [] + + def run(command, *, env=None): + observed.append(env.copy()) + return '{"review": {}}' + + monkeypatch.setattr(driver, '_run', run) + driver._prepare_hatch({'environment': 'review'}, '/private/tools/bin/python', {}) +> assert observed[0]['HATCH_UV'] == '/private/tools/bin/uv' + ^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'HATCH_UV' + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:78: KeyError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_hatch_export_has_its_owned_uv_executable +========================= 1 failed, 4 passed in 0.09s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/HATCH_PYTEST_OPTIONS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HATCH_PYTEST_OPTIONS_RED.txt new file mode 100644 index 00000000..b4866740 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HATCH_PYTEST_OPTIONS_RED.txt @@ -0,0 +1,40 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 11 items + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py ......F [ 63%] + [ 63%] +tests/unit/specfact_code_review/run/test_target_pytest.py ...F [100%] + +=================================== FAILURES =================================== +________________ test_hatch_native_extra_arguments_are_retained ________________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f9a3f0> + + def test_hatch_native_extra_arguments_are_retained(monkeypatch) -> None: + from specfact_code_review.run import runtime_build_driver as driver + monkeypatch.setattr(driver, '_run', lambda *args, **kwargs: '{"review": {"extra-args": ["--dist", "worksteal", "-p", "no:randomly"]}}') + config = {'environment': 'review'} + driver._prepare_hatch(config, '/tools/bin/python', {}) +> assert config['pytest_arguments'] == ['--dist', 'worksteal', '-p', 'no:randomly'] + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'pytest_arguments' + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:98: KeyError +______ test_native_hatch_plugin_options_participate_in_explicit_selection ______ + + def test_native_hatch_plugin_options_participate_in_explicit_selection() -> None: + descriptor = {'project': {'pytest_config': {'addopts': '-ra'}}, 'inventory': {'pytest_arguments': ['-p', 'no:randomly']}} +> assert target_pytest._effective_pytest_config(descriptor)['addopts'] == ['-ra', '-p', 'no:randomly'] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_effective_pytest_config' + +tests/unit/specfact_code_review/run/test_target_pytest.py:45: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_hatch_native_extra_arguments_are_retained +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_native_hatch_plugin_options_participate_in_explicit_selection +========================= 2 failed, 9 passed in 0.30s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/HOOK_INDEX_ISOLATION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HOOK_INDEX_ISOLATION_RED.txt new file mode 100644 index 00000000..d529ed76 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HOOK_INDEX_ISOLATION_RED.txt @@ -0,0 +1,48 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 410 items / 408 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py F [ 50%] +tests/unit/specfact_code_review/run/test_runner.py F [100%] + +=================================== FAILURES =================================== +____________ test_vcs_fixtures_never_change_the_calling_hooks_index ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-113/test_vcs_fixtures_never_change0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10e9de650> + + def test_vcs_fixtures_never_change_the_calling_hooks_index(tmp_path: Path, monkeypatch) -> None: + parent = tmp_path / 'parent' + _repository(parent) + index = parent / '.git/index' + before = index.read_bytes() + monkeypatch.setenv('GIT_INDEX_FILE', str(index)) + _repository(tmp_path / 'child') +> assert index.read_bytes() == before +E assert b"DIRC\x00\x0...dc\x83\x0b'\n" == b'DIRC\x00\x0...b\xe4\xce\xc5' +E +E At index 16 diff: b'\x12' != b'\x11' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:80: AssertionError +____________ test_pytest_worker_does_not_inherit_calling_git_index _____________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10e9de2c0> + + def test_pytest_worker_does_not_inherit_calling_git_index(monkeypatch: MonkeyPatch) -> None: + runner_api = _c14_runner() + monkeypatch.setenv('GIT_INDEX_FILE', '/private/controller/index') +> assert 'GIT_INDEX_FILE' not in runner_api._pytest_env() +E AssertionError: assert 'GIT_INDEX_FILE' not in {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...} +E + where {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...} = () +E + where = /packages/specfact-code-review/src/specfact_code_review/run/runner.py'>._pytest_env + +tests/unit/specfact_code_review/run/test_runner.py:7731: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_vcs_fixtures_never_change_the_calling_hooks_index +FAILED tests/unit/specfact_code_review/run/test_runner.py::test_pytest_worker_does_not_inherit_calling_git_index +====================== 2 failed, 408 deselected in 0.94s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/HOSTED_RUNNER_ANCESTOR_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HOSTED_RUNNER_ANCESTOR_RED.txt new file mode 100644 index 00000000..baddf627 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HOSTED_RUNNER_ANCESTOR_RED.txt @@ -0,0 +1,52 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/test_external_capsule_corpus.py F [100%] + +=================================== FAILURES =================================== +_______ test_offline_provisioning_avoids_writable_hosted_runner_ancestor _______ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10becc3e0> + + def test_offline_provisioning_avoids_writable_hosted_runner_ancestor(monkeypatch) -> None: + module = _load() + payload = b"administrator-provisioned launcher fixture" + + def permissions(path): + mode = 0o100555 if path.name in {"bwrap", "bwrap.sha256"} else 0o40755 + if path == Path("/opt"): + mode = 0o40775 + return SimpleNamespace(st_uid=0, st_mode=mode) + + monkeypatch.setattr(Path, "lstat", permissions) + monkeypatch.setattr(Path, "read_bytes", lambda _path: payload) + monkeypatch.setattr(Path, "read_text", lambda _path, **_kwargs: hashlib.sha256(payload).hexdigest()) + monkeypatch.setattr(module.os, "access", lambda *_args: True) + launcher = module.OFFLINE_ROOT / "bwrap" +> assert module._verified_offline_launcher(str(launcher)) == launcher + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/test_external_capsule_corpus.py:229: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +scripts/external_capsule_corpus.py:103: in _verified_offline_launcher + _check_offline_path(directory, directory=True) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/opt'), directory = True + + def _check_offline_path(path: Path, *, directory: bool = False) -> None: + metadata = path.lstat() + expected_type = stat.S_ISDIR if directory else stat.S_ISREG + if metadata.st_uid != 0 or metadata.st_mode & 0o022 or not expected_type(metadata.st_mode): +> raise ValueError(f"offline_launcher_untrusted:{path.name}") +E ValueError: offline_launcher_untrusted:opt + +scripts/external_capsule_corpus.py:94: ValueError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_offline_provisioning_avoids_writable_hosted_runner_ancestor +============================== 1 failed in 0.03s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/HOST_PLUGIN_ALIAS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/HOST_PLUGIN_ALIAS_RED.txt new file mode 100644 index 00000000..06e6efc7 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/HOST_PLUGIN_ALIAS_RED.txt @@ -0,0 +1,2 @@ +CI run 34845391063, cp312, reconstructed Hatch native host baseline +ValueError: Plugin already registered under a different name: asyncio= diff --git a/openspec/changes/code-review-16-portable-project-runtime/IMMUTABLE_PROJECT_LINKS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/IMMUTABLE_PROJECT_LINKS_RED.txt new file mode 100644 index 00000000..6c8cdf0c --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/IMMUTABLE_PROJECT_LINKS_RED.txt @@ -0,0 +1,412 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 138 items / 128 deselected / 10 selected + +tests/unit/specfact_code_review/run/test_scope.py FFFFFFFF.. [100%] + +=================================== FAILURES =================================== +____ test_portable_immutable_snapshots_retain_package_and_data_links[range] ____ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh0/repo') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh0') +scope_name = 'range' + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + def test_portable_immutable_snapshots_retain_package_and_data_links(git_repo: Path, tmp_path: Path, scope_name: str) -> None: + package = git_repo / "src/customer" + package.mkdir() + (package / "__init__.py").write_text("VALUE = 1\n") + (package / "data.txt").write_text("original") + (git_repo / "customer").symlink_to("src/customer", target_is_directory=True) + (git_repo / "data-link").symlink_to("src/customer/data.txt") + base = _commit(git_repo, "project links") + (git_repo / "src/app.py").write_text("VALUE = 2\n") + (package / "data.txt").write_text("updated") + _git(git_repo, "add", "-A") + head = _commit(git_repo, "update project") if scope_name == "range" else None + request = scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + ) + result = scope.resolve_scope(request) + try: + assert result.status == "PASS" + for side, snapshot in (("base", result.base_snapshot), ("head", result.head_snapshot)): + assert snapshot is not None +> assert (snapshot.root / "customer").is_symlink() +E AssertionError: assert False +E + where False = is_symlink() +E + where is_symlink = (PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-505ee4d76064-elpyq46k') / 'customer').is_symlink +E + where PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-505ee4d76064-elpyq46k') = Snapshot(root=PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-505ee4d76064-elpyq46k'), com...rivate/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh0/repo')).root + +tests/unit/specfact_code_review/run/test_scope.py:1405: AssertionError +____ test_portable_immutable_snapshots_retain_package_and_data_links[index] ____ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh1/repo') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh1') +scope_name = 'index' + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + def test_portable_immutable_snapshots_retain_package_and_data_links(git_repo: Path, tmp_path: Path, scope_name: str) -> None: + package = git_repo / "src/customer" + package.mkdir() + (package / "__init__.py").write_text("VALUE = 1\n") + (package / "data.txt").write_text("original") + (git_repo / "customer").symlink_to("src/customer", target_is_directory=True) + (git_repo / "data-link").symlink_to("src/customer/data.txt") + base = _commit(git_repo, "project links") + (git_repo / "src/app.py").write_text("VALUE = 2\n") + (package / "data.txt").write_text("updated") + _git(git_repo, "add", "-A") + head = _commit(git_repo, "update project") if scope_name == "range" else None + request = scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + ) + result = scope.resolve_scope(request) + try: + assert result.status == "PASS" + for side, snapshot in (("base", result.base_snapshot), ("head", result.head_snapshot)): + assert snapshot is not None +> assert (snapshot.root / "customer").is_symlink() +E AssertionError: assert False +E + where False = is_symlink() +E + where is_symlink = (PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-325d6f0f0e1b-a14sl_6b') / 'customer').is_symlink +E + where PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-325d6f0f0e1b-a14sl_6b') = Snapshot(root=PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-325d6f0f0e1b-a14sl_6b'), com...rivate/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh1/repo')).root + +tests/unit/specfact_code_review/run/test_scope.py:1405: AssertionError +_ test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[data-link-../outside.toml-range] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh2/repo') +scope_name = 'range', link_name = 'data-link', target = '../outside.toml' +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1083bda70> + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + @pytest.mark.parametrize("link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")]) + def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: str, link_name: str, target: str, monkeypatch + ) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) +> result = scope.resolve_scope(scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + )) + +tests/unit/specfact_code_review/run/test_scope.py:1432: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2876: in resolve_scope + return _resolve_range(normalized) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2373: in _resolve_range + resolved = _materialized_range(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2154: in _materialized_range + additional_policy_paths = _range_policy_closure(request, target_snapshot.root, head_snapshot.root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (ScopeRequest(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/...9be733-ime8j2a6'), PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-562c39ec89b0-3d30ds8f')) +_kwargs = {} + + def forbid_policy_read(*_args, **_kwargs): +> pytest.fail("unsafe source links reached policy discovery") +E Failed: unsafe source links reached policy discovery + +tests/unit/specfact_code_review/run/test_scope.py:1428: Failed +_ test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[data-link-../outside.toml-index] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh3/repo') +scope_name = 'index', link_name = 'data-link', target = '../outside.toml' +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1083be650> + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + @pytest.mark.parametrize("link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")]) + def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: str, link_name: str, target: str, monkeypatch + ) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) +> result = scope.resolve_scope(scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + )) + +tests/unit/specfact_code_review/run/test_scope.py:1432: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2878: in resolve_scope + return _resolve_index(normalized) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2557: in _resolve_index + context = _materialized_index_context(request.repository) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2575: in _materialized_index_context + return _materialized_index_context_from_capture(repository, capture) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2724: in _materialized_index_context_from_capture + base_policy_paths, _ = _resolved_index_policy_paths(base_snapshot.root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-6f8d129be733-n3o_e0_y'),) +_kwargs = {} + + def forbid_policy_read(*_args, **_kwargs): +> pytest.fail("unsafe source links reached policy discovery") +E Failed: unsafe source links reached policy discovery + +tests/unit/specfact_code_review/run/test_scope.py:1428: Failed +_ test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[pyproject.toml-../outside.toml-range] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh4/repo') +scope_name = 'range', link_name = 'pyproject.toml', target = '../outside.toml' +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108c78050> + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + @pytest.mark.parametrize("link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")]) + def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: str, link_name: str, target: str, monkeypatch + ) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) +> result = scope.resolve_scope(scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + )) + +tests/unit/specfact_code_review/run/test_scope.py:1432: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2876: in resolve_scope + return _resolve_range(normalized) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2373: in _resolve_range + resolved = _materialized_range(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2154: in _materialized_range + additional_policy_paths = _range_policy_closure(request, target_snapshot.root, head_snapshot.root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (ScopeRequest(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/...9be733-m4shzcjq'), PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-c97865fc1489-aeqqpkll')) +_kwargs = {} + + def forbid_policy_read(*_args, **_kwargs): +> pytest.fail("unsafe source links reached policy discovery") +E Failed: unsafe source links reached policy discovery + +tests/unit/specfact_code_review/run/test_scope.py:1428: Failed +_ test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[pyproject.toml-../outside.toml-index] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh5/repo') +scope_name = 'index', link_name = 'pyproject.toml', target = '../outside.toml' +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1082d7460> + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + @pytest.mark.parametrize("link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")]) + def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: str, link_name: str, target: str, monkeypatch + ) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) +> result = scope.resolve_scope(scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + )) + +tests/unit/specfact_code_review/run/test_scope.py:1432: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2878: in resolve_scope + return _resolve_index(normalized) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2557: in _resolve_index + context = _materialized_index_context(request.repository) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2575: in _materialized_index_context + return _materialized_index_context_from_capture(repository, capture) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2724: in _materialized_index_context_from_capture + base_policy_paths, _ = _resolved_index_policy_paths(base_snapshot.root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-6f8d129be733-rz9eo14y'),) +_kwargs = {} + + def forbid_policy_read(*_args, **_kwargs): +> pytest.fail("unsafe source links reached policy discovery") +E Failed: unsafe source links reached policy discovery + +tests/unit/specfact_code_review/run/test_scope.py:1428: Failed +_ test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[setup.cfg-.env-range] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh6/repo') +scope_name = 'range', link_name = 'setup.cfg', target = '.env' +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108d94490> + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + @pytest.mark.parametrize("link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")]) + def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: str, link_name: str, target: str, monkeypatch + ) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) +> result = scope.resolve_scope(scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + )) + +tests/unit/specfact_code_review/run/test_scope.py:1432: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2876: in resolve_scope + return _resolve_range(normalized) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2373: in _resolve_range + resolved = _materialized_range(request) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2154: in _materialized_range + additional_policy_paths = _range_policy_closure(request, target_snapshot.root, head_snapshot.root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (ScopeRequest(repository=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/...9be733-ch71ybbw'), PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-8d5bdf7a7f25-wtdxobef')) +_kwargs = {} + + def forbid_policy_read(*_args, **_kwargs): +> pytest.fail("unsafe source links reached policy discovery") +E Failed: unsafe source links reached policy discovery + +tests/unit/specfact_code_review/run/test_scope.py:1428: Failed +_ test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[setup.cfg-.env-index] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-223/test_portable_immutable_snapsh7/repo') +scope_name = 'index', link_name = 'setup.cfg', target = '.env' +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10841a050> + + @pytest.mark.parametrize("scope_name", ["range", "index"]) + @pytest.mark.parametrize("link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")]) + def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: str, link_name: str, target: str, monkeypatch + ) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) +> result = scope.resolve_scope(scope.ScopeRequest( + repository=git_repo, scope=scope_name, base_ref=base if head else None, + head_ref=head, portable_project_runtime=True, + )) + +tests/unit/specfact_code_review/run/test_scope.py:1432: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2878: in resolve_scope + return _resolve_index(normalized) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2557: in _resolve_index + context = _materialized_index_context(request.repository) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2575: in _materialized_index_context + return _materialized_index_context_from_capture(repository, capture) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/scope.py:2724: in _materialized_index_context_from_capture + base_policy_paths, _ = _resolved_index_policy_paths(base_snapshot.root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-5922e0c13c3d-6k2k8t2t'),) +_kwargs = {} + + def forbid_policy_read(*_args, **_kwargs): +> pytest.fail("unsafe source links reached policy discovery") +E Failed: unsafe source links reached policy discovery + +tests/unit/specfact_code_review/run/test_scope.py:1428: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_retain_package_and_data_links[range] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_retain_package_and_data_links[index] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[data-link-../outside.toml-range] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[data-link-../outside.toml-index] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[pyproject.toml-../outside.toml-range] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[pyproject.toml-../outside.toml-index] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[setup.cfg-.env-range] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads[setup.cfg-.env-index] +================= 8 failed, 2 passed, 128 deselected in 2.34s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/INDEX_CONTEXT_CLEANUP_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/INDEX_CONTEXT_CLEANUP_RED.txt new file mode 100644 index 00000000..25f17806 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/INDEX_CONTEXT_CLEANUP_RED.txt @@ -0,0 +1,127 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 141 items / 138 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_scope.py FFF [100%] + +=================================== FAILURES =================================== +__ test_index_discovery_failure_removes_both_materialized_roots[_range_paths] __ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-251/test_index_discovery_failure_r0/repo') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-251/test_index_discovery_failure_r0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109c20b00> +failure_stage = '_range_paths' + + @pytest.mark.parametrize("failure_stage", ["_range_paths", "_discovered_index_policy_paths", "_snapshot_input_manifest"]) + def test_index_discovery_failure_removes_both_materialized_roots( + git_repo: Path, tmp_path: Path, monkeypatch, failure_stage: str + ) -> None: + (git_repo / "src/app.py").write_text("VALUE = 2\n") + _git(git_repo, "add", "-A") + created = [] + + def tracked_mkdtemp(*, prefix: str) -> str: + root = tmp_path / f"{prefix}{len(created)}" + root.mkdir() + created.append(root) + return str(root) + + def fail_discovery(*_args, **_kwargs): + raise scope.GitResolutionError("simulated immutable input read failure") + + monkeypatch.setattr(scope.tempfile, "mkdtemp", tracked_mkdtemp) + monkeypatch.setattr(scope, failure_stage, fail_discovery) + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=True)) + assert result.status == "UNKNOWN" + assert result.reason == "git_resolution_failed" + assert "simulated immutable input read failure" in result.diagnostics + snapshots = [root for root in created if root.name.startswith("specfact-review-")] + assert len(snapshots) == 2 +> assert all(not root.exists() for root in created) +E assert False +E + where False = all(. at 0x109c64fb0>) + +tests/unit/specfact_code_review/run/test_scope.py:1504: AssertionError +_ test_index_discovery_failure_removes_both_materialized_roots[_discovered_index_policy_paths] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-251/test_index_discovery_failure_r1/repo') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-251/test_index_discovery_failure_r1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109c209d0> +failure_stage = '_discovered_index_policy_paths' + + @pytest.mark.parametrize("failure_stage", ["_range_paths", "_discovered_index_policy_paths", "_snapshot_input_manifest"]) + def test_index_discovery_failure_removes_both_materialized_roots( + git_repo: Path, tmp_path: Path, monkeypatch, failure_stage: str + ) -> None: + (git_repo / "src/app.py").write_text("VALUE = 2\n") + _git(git_repo, "add", "-A") + created = [] + + def tracked_mkdtemp(*, prefix: str) -> str: + root = tmp_path / f"{prefix}{len(created)}" + root.mkdir() + created.append(root) + return str(root) + + def fail_discovery(*_args, **_kwargs): + raise scope.GitResolutionError("simulated immutable input read failure") + + monkeypatch.setattr(scope.tempfile, "mkdtemp", tracked_mkdtemp) + monkeypatch.setattr(scope, failure_stage, fail_discovery) + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=True)) + assert result.status == "UNKNOWN" + assert result.reason == "git_resolution_failed" + assert "simulated immutable input read failure" in result.diagnostics + snapshots = [root for root in created if root.name.startswith("specfact-review-")] + assert len(snapshots) == 2 +> assert all(not root.exists() for root in created) +E assert False +E + where False = all(. at 0x109c658a0>) + +tests/unit/specfact_code_review/run/test_scope.py:1504: AssertionError +_ test_index_discovery_failure_removes_both_materialized_roots[_snapshot_input_manifest] _ + +git_repo = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-251/test_index_discovery_failure_r2/repo') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-251/test_index_discovery_failure_r2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109bc6b10> +failure_stage = '_snapshot_input_manifest' + + @pytest.mark.parametrize("failure_stage", ["_range_paths", "_discovered_index_policy_paths", "_snapshot_input_manifest"]) + def test_index_discovery_failure_removes_both_materialized_roots( + git_repo: Path, tmp_path: Path, monkeypatch, failure_stage: str + ) -> None: + (git_repo / "src/app.py").write_text("VALUE = 2\n") + _git(git_repo, "add", "-A") + created = [] + + def tracked_mkdtemp(*, prefix: str) -> str: + root = tmp_path / f"{prefix}{len(created)}" + root.mkdir() + created.append(root) + return str(root) + + def fail_discovery(*_args, **_kwargs): + raise scope.GitResolutionError("simulated immutable input read failure") + + monkeypatch.setattr(scope.tempfile, "mkdtemp", tracked_mkdtemp) + monkeypatch.setattr(scope, failure_stage, fail_discovery) + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=True)) + assert result.status == "UNKNOWN" + assert result.reason == "git_resolution_failed" + assert "simulated immutable input read failure" in result.diagnostics + snapshots = [root for root in created if root.name.startswith("specfact-review-")] + assert len(snapshots) == 2 +> assert all(not root.exists() for root in created) +E assert False +E + where False = all(. at 0x109c660c0>) + +tests/unit/specfact_code_review/run/test_scope.py:1504: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_discovery_failure_removes_both_materialized_roots[_range_paths] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_discovery_failure_removes_both_materialized_roots[_discovered_index_policy_paths] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_discovery_failure_removes_both_materialized_roots[_snapshot_input_manifest] +====================== 3 failed, 138 deselected in 0.53s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/INVENTORY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/INVENTORY_RED.txt new file mode 100644 index 00000000..cbc316bb --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/INVENTORY_RED.txt @@ -0,0 +1,21 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 5 items + +tests/unit/specfact_code_review/run/test_runtime_builder.py ....F [100%] + +=================================== FAILURES =================================== +______________ test_inventory_never_serializes_index_credentials _______________ + + def test_inventory_never_serializes_index_credentials() -> None: +> from specfact_code_review.run.runtime_build_driver import clean_inventory +E ImportError: cannot import name 'clean_inventory' from 'specfact_code_review.run.runtime_build_driver' (/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py) + +tests/unit/specfact_code_review/run/test_runtime_builder.py:52: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_inventory_never_serializes_index_credentials +========================= 1 failed, 4 passed in 0.11s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/JUNIT_PARAMETER_IDENTITIES_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/JUNIT_PARAMETER_IDENTITIES_RED.txt new file mode 100644 index 00000000..3f0744cf --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/JUNIT_PARAMETER_IDENTITIES_RED.txt @@ -0,0 +1,63 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /repository +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/specfact_code_review/run/test_runner.py F [100%] + +=================================== FAILURES =================================== +_______ test_pytest_parameter_delimiters_preserve_real_junit_identities ________ + +tmp_path = PosixPath('/test_pytest_parameter_delimite0') + + def test_pytest_parameter_delimiters_preserve_real_junit_identities(tmp_path: Path) -> None: + runner_api = _c14_runner() + tests_root = tmp_path / "tests" + tests_root.mkdir() + test_file = tests_root / "test_param.py" + test_file.write_text( + "import pytest\n" + "parameters = pytest.mark.parametrize('value', ['plain', 'left::right', '[left::right]'])\n" + "@parameters\n" + "def test_value(value):\n" + " assert value\n" + "class TestValues:\n" + " @parameters\n" + " def test_value(self, value):\n" + " assert value\n", + encoding="utf-8", + ) + + process, coverage_path, observer_path, junit_path = runner_api._run_pytest_selection_with_coverage( + (str(test_file),), + coverage_source=tmp_path, + policy_argv=("--rootdir", str(tmp_path)), + ) + try: + observer, junit = runner_api._load_pytest_outcome_evidence(observer_path, junit_path) + result = runner_api.reconcile_pytest_outcomes( + observer=observer, + junit=junit, + process_exit=process.returncode, + planned=("tests/test_param.py::test_value", "tests/test_param.py::TestValues::test_value"), + ) + finally: + coverage_path.unlink(missing_ok=True) + observer_path.unlink(missing_ok=True) + junit_path.unlink(missing_ok=True) + + assert process.returncode == 0, process.stdout + process.stderr + assert len(junit) == 6 +> assert result.status == "PASS" +E AssertionError: assert 'UNKNOWN' == 'PASS' +E +E - PASS +E + UNKNOWN + +tests/unit/specfact_code_review/run/test_runner.py:7336: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runner.py::test_pytest_parameter_delimiters_preserve_real_junit_identities +============================== 1 failed in 1.18s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/LOCAL_LINK_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/LOCAL_LINK_RED.txt new file mode 100644 index 00000000..d1de1b8b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/LOCAL_LINK_RED.txt @@ -0,0 +1,47 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items + +tests/unit/specfact_code_review/run/test_runtime_builder.py ...F [100%] + +=================================== FAILURES =================================== +_____________ test_copy_project_supports_repository_local_symlink ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-13/test_copy_project_supports_rep0') + + def test_copy_project_supports_repository_local_symlink(tmp_path: Path) -> None: + source = tmp_path / 'source' + source.mkdir() + (source / 'cert.pem').write_text('test certificate fixture') + (source / 'cert-link.pem').symlink_to('cert.pem') +> copy_project(source, tmp_path / 'copy') + +tests/unit/specfact_code_review/run/test_runtime_builder.py:47: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:34: in copy_project + shutil.copytree(source, destination, ignore=ignored) +/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/shutil.py:654: in copytree + return _copytree(entries=entries, src=src, dst=dst, symlinks=symlinks, +/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/shutil.py:548: in _copytree + ignored_names = ignore(os.fspath(src), [x.name for x in entries]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +directory = '/pytest-of-dom/pytest-13/test_copy_project_supports_rep0/source' +names = ['cert.pem', 'cert-link.pem'] + + def ignored(directory: str, names: list[str]) -> set[str]: + excluded = set(names) & IGNORED_INPUTS + for name in set(names) - excluded: + if (Path(directory) / name).is_symlink(): +> raise ProjectRuntimeError(f"project_source_symlink:{Path(directory) / name}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_source_symlink:/pytest-of-dom/pytest-13/test_copy_project_supports_rep0/source/cert-link.pem + +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:31: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_copy_project_supports_repository_local_symlink +========================= 1 failed, 3 passed in 0.11s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/LOCK_RANGE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/LOCK_RANGE_RED.txt new file mode 100644 index 00000000..1ab1e259 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/LOCK_RANGE_RED.txt @@ -0,0 +1,71 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 127 items / 126 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_scope.py F [100%] + +=================================== FAILURES =================================== +___________ test_portable_lock_only_range_remains_analyzable_preview ___________ + +scope_api = /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/packages/specfact-code-review/src/specfact_code_review/run/scope.py'> +git_repo = PosixPath('/pytest-of-dom/pytest-27/test_portable_lock_only_range_0/repo') + + def test_portable_lock_only_range_remains_analyzable_preview(scope_api: Any, git_repo: Path) -> None: + from dataclasses import replace + base, head = _make_range(git_repo, path='uv.lock', content='version = 1\n') +> request = replace(_range_request(scope_api, git_repo, base, head), portable_project_runtime=True) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_scope.py:1355: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/dataclasses.py:1779: in replace + return _replace(obj, **changes) + ^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = ScopeRequest(repository=PosixPath('/pytest-of-dom/pytest-27/te...iew_fixes=False, with_mutation=False, pr_context_file=None, repository_slug=None, project_runtime_source_lock_paths=()) +changes = {'portable_project_runtime': True, 'repository': PosixPath('/pytest-of-dom/pytest-27/test_portable_lock_only_range_0/repo'), 'scope': 'range', 'files': (), ...} + + def _replace(self, /, **changes): + # We're going to mutate 'changes', but that's okay because it's a + # new dict, even if called with 'replace(self, **my_changes)'. + + # It's an error to have init=False fields in 'changes'. + # If a field is not in 'changes', read its value from the provided 'self'. + + for f in getattr(self, _FIELDS).values(): + # Only consider normal fields or InitVars. + if f._field_type is _FIELD_CLASSVAR: + continue + + if not f.init: + # Error if this field is specified in changes. + if f.name in changes: + raise TypeError(f'field {f.name} is declared with ' + f'init=False, it cannot be specified with ' + f'replace()') + continue + + if f.name not in changes: + if f._field_type is _FIELD_INITVAR and f.default is MISSING: + raise TypeError(f"InitVar {f.name!r} " + f'must be specified with replace()') + changes[f.name] = getattr(self, f.name) + + # Create the new object, which calls __init__() and + # __post_init__() (if defined), using all of the init fields we've + # added and/or left in 'changes'. If there are values supplied in + # changes that aren't fields, this will correctly raise a + # TypeError. +> return self.__class__(**changes) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: ScopeRequest.__init__() got an unexpected keyword argument 'portable_project_runtime' + +/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/dataclasses.py:1813: TypeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_lock_only_range_remains_analyzable_preview +====================== 1 failed, 126 deselected in 0.25s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/MEMBER_CLOSURE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/MEMBER_CLOSURE_RED.txt new file mode 100644 index 00000000..4b4638ac --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/MEMBER_CLOSURE_RED.txt @@ -0,0 +1,28 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_runtime_domains.py ..F [100%] + +=================================== FAILURES =================================== +__________ test_missing_member_dependency_cannot_be_silently_omitted ___________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-150/test_missing_member_dependency0') + + def test_missing_member_dependency_cannot_be_silently_omitted(tmp_path: Path) -> None: + import pytest + from specfact_code_review.run.runtime_models import ProjectRuntimeError + + _distribution(tmp_path, "pylint", dependencies=("absent>=1",)) +> with pytest.raises(ProjectRuntimeError, match="project_analyzer_dependency_missing:pylint:absent"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_domains.py:43: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_missing_member_dependency_cannot_be_silently_omitted +========================= 1 failed, 2 passed in 0.10s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/MEMBER_IMPORT_DOMAIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/MEMBER_IMPORT_DOMAIN_RED.txt new file mode 100644 index 00000000..01a343e5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/MEMBER_IMPORT_DOMAIN_RED.txt @@ -0,0 +1,49 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items / 2 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_target_bootstrap.py FF [100%] + +=================================== FAILURES =================================== +___________ test_member_fallback_rejects_unrelated_analyzer_imports ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-73/test_member_fallback_rejects_u0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b20ba80> + + def test_member_fallback_rejects_unrelated_analyzer_imports(tmp_path: Path, monkeypatch) -> None: + (tmp_path / 'needed.py').write_text('VALUE = 1\n') + (tmp_path / 'unrelated.py').write_text('VALUE = 2\n') + monkeypatch.setattr(target_bootstrap, 'ANALYZERS', tmp_path) +> finder = target_bootstrap.DomainFinder({'sealed_imports': ['needed'], 'installed': []}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_bootstrap' has no attribute 'DomainFinder' + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:41: AttributeError +_____ test_member_metadata_lookup_cannot_expand_explicit_target_inventory ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-73/test_member_metadata_lookup_ca0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bfb6060> + + def test_member_metadata_lookup_cannot_expand_explicit_target_inventory(tmp_path: Path, monkeypatch) -> None: + from importlib.metadata import DistributionFinder + + metadata = tmp_path / 'needed-1.0.dist-info' + metadata.mkdir() + (metadata / 'METADATA').write_text('Metadata-Version: 2.1\nName: needed\nVersion: 1.0\n') + monkeypatch.setattr(target_bootstrap, 'ANALYZERS', tmp_path) +> finder = target_bootstrap.DomainFinder({ + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 'sealed_imports': ['needed'], + 'installed': [{'name': 'needed', 'version': '1.0', 'origin': 'analyzer'}], + }) +E AttributeError: module 'specfact_code_review.run.target_bootstrap' has no attribute 'DomainFinder' + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:53: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_member_fallback_rejects_unrelated_analyzer_imports +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_member_metadata_lookup_cannot_expand_explicit_target_inventory +======================= 2 failed, 2 deselected in 0.46s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/MEMBER_PATH_INTROSPECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/MEMBER_PATH_INTROSPECTION_RED.txt new file mode 100644 index 00000000..1fcd7c4d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/MEMBER_PATH_INTROSPECTION_RED.txt @@ -0,0 +1,35 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 6 items + +tests/unit/specfact_code_review/run/test_target_launch.py .....F [100%] + +=================================== FAILURES =================================== +__________ test_member_mounts_expose_only_recorded_distribution_files __________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-125/test_member_mounts_expose_only0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a82b020> + + def test_member_mounts_expose_only_recorded_distribution_files(tmp_path, monkeypatch) -> None: + import json + from specfact_code_review.run import target_launch + + sealed = tmp_path / "sealed" + sealed.mkdir() + for name in ("pylint", "pylint-4.0.7.dist-info", "unrelated", "unrelated-1.0.dist-info"): + (sealed / name).mkdir() + (tmp_path / "project-runtime.json").write_text(json.dumps({"inventory": {"member_graphs": { + "pylint": {"sealed_imports": ["pylint"], "installed": [{"name": "pylint", "origin": "analyzer"}]} + }}})) + monkeypatch.setattr(target_launch, "PROJECT", tmp_path) +> monkeypatch.setattr(target_launch, "SEALED", sealed) +E AttributeError: /packages/specfact-code-review/src/specfact_code_review/run/target_launch.py'> has no attribute 'SEALED' + +tests/unit/specfact_code_review/run/test_target_launch.py:87: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_member_mounts_expose_only_recorded_distribution_files +========================= 1 failed, 5 passed in 0.09s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/NATIVE_6B733_FAILURE.json b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_6B733_FAILURE.json new file mode 100644 index 00000000..67eaffdd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_6B733_FAILURE.json @@ -0,0 +1,126 @@ +{ + "candidate_sha": "6b733865cbefdcfb7633b7390779c3b0eb8f969e", + "tested_merge_sha": "cceae5b2d0ac101d0782a7f7bc54c7d5dff83141", + "run_id": 34881298236, + "run_url": "https://github.com/nold-ai/specfact-cli-modules/actions/runs/34881298236", + "root_cause": "Our sealed-import ownership trusts stale CrossHair top_level.txt names. CrossHair declares build without owning installed build files. Prioritizing that false ownership blocks Poetry project build import.", + "not_customer_dependency_failure": true, + "rows": [ + { + "artifact": "capsule-candidate-cp3.11-2", + "runtime_preparation": "PASS", + "contracts": { + "id": "contracts", + "execution_state": "error", + "evidence_outcome": "UNKNOWN", + "version": "crosshair-tool-0.0.109+icontract-2.7.1", + "diagnostic": "analyzer_reported_incomplete_execution", + "sandbox_invocation": "fresh", + "capsule_identity": "sha256:691ab0f395ea838bf4775b975901b217764470b80e7f7e2b87fec0fe71982536", + "environment_id": "linux-x86_64-cp311" + }, + "poetry_tests": { + "collected": 7, + "calls": 7, + "exit": 0 + }, + "host_test_exit": 0, + "cold_command": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.11/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "cold_exit": 1, + "diagnostic": "project_worker_analyzer_origin_mismatch:build", + "report": "/capsule-candidate-cp3.11-2/external/evidence/poetry/cold-auto-report.json", + "report_sha256": "297d7c283e3a61ab203a730f7822c0ffe54ec0cb32542ca85777495af5d72a27" + }, + { + "artifact": "capsule-candidate-cp3.12-2", + "runtime_preparation": "PASS", + "contracts": { + "id": "contracts", + "execution_state": "error", + "evidence_outcome": "UNKNOWN", + "version": "crosshair-tool-0.0.109+icontract-2.7.1", + "diagnostic": "analyzer_reported_incomplete_execution", + "sandbox_invocation": "fresh", + "capsule_identity": "sha256:2cff9f4ede1635f5aed0ca8dd25115526dca40e9f934ea6aeaa104bcc7ca902f", + "environment_id": "linux-x86_64-cp312" + }, + "poetry_tests": { + "collected": 7, + "calls": 7, + "exit": 0 + }, + "host_test_exit": 0, + "cold_command": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.12/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "cold_exit": 1, + "diagnostic": "project_worker_analyzer_origin_mismatch:build", + "report": "/capsule-candidate-cp3.12-2/external/evidence/poetry/cold-auto-report.json", + "report_sha256": "4c2419655bc8189c30edb04e3628df7f459ea91a77d7819cbbd3923d5c81fcce" + }, + { + "artifact": "capsule-candidate-cp3.13-2", + "runtime_preparation": "PASS", + "contracts": { + "id": "contracts", + "execution_state": "error", + "evidence_outcome": "UNKNOWN", + "version": "crosshair-tool-0.0.109+icontract-2.7.1", + "diagnostic": "analyzer_reported_incomplete_execution", + "sandbox_invocation": "fresh", + "capsule_identity": "sha256:05fdb1a37bae1666205fb6b8da5706e42c72ae1d6d60ae2a980d9243db95a6a2", + "environment_id": "linux-x86_64-cp313" + }, + "poetry_tests": { + "collected": 7, + "calls": 7, + "exit": 0 + }, + "host_test_exit": 0, + "cold_command": [ + "specfact", + "code", + "review", + "run", + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py", + "--project-config", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/project-runtime.toml", + "--json", + "--out", + "/home/runner/work/_temp/specfact-customer-3.13/external/evidence/poetry/cold-auto-report.json", + "--bug-hunt" + ], + "cold_exit": 1, + "diagnostic": "project_worker_analyzer_origin_mismatch:build", + "report": "/capsule-candidate-cp3.13-2/external/evidence/poetry/cold-auto-report.json", + "report_sha256": "26eee2347fe335d26f20349ad5c37934c9e961410ab74defd8e0ab368b8515ea" + } + ], + "limit": "Native failure evidence at historical candidate 6b733; not a local native Poetry reproduction or successful/public release acceptance.", + "report_hashes": "SHA-256 of original downloaded report bytes; report paths are relative to the downloaded artifact root." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/NATIVE_IMPORT_PRECEDENCE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_IMPORT_PRECEDENCE_RED.txt new file mode 100644 index 00000000..e78e6ce6 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_IMPORT_PRECEDENCE_RED.txt @@ -0,0 +1,193 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 283 items / 265 deselected / 18 selected + +tests/unit/specfact_code_review/run/test_runtime_discovery.py FF [ 11%] +tests/unit/specfact_code_review/run/test_target_bootstrap.py FF......... [ 72%] +..... [100%] + +=================================== FAILURES =================================== +________ test_undeclared_pythonpath_does_not_invent_import_roots[flat] _________ + +tmp_path = PosixPath('/test_undeclared_pythonpath_doe0') +layout = 'flat' + + @pytest.mark.parametrize("layout", ["flat", "src"]) + def test_undeclared_pythonpath_does_not_invent_import_roots(tmp_path: Path, layout: str) -> None: + if layout == "src": + (tmp_path / "src").mkdir() + (tmp_path / "requirements.txt").touch() +> assert discover_project(tmp_path).source_roots == () +E AssertionError: assert ('.',) == () +E +E Left contains one more item: '.' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:439: AssertionError +_________ test_undeclared_pythonpath_does_not_invent_import_roots[src] _________ + +tmp_path = PosixPath('/test_undeclared_pythonpath_doe1') +layout = 'src' + + @pytest.mark.parametrize("layout", ["flat", "src"]) + def test_undeclared_pythonpath_does_not_invent_import_roots(tmp_path: Path, layout: str) -> None: + if layout == "src": + (tmp_path / "src").mkdir() + (tmp_path / "requirements.txt").touch() +> assert discover_project(tmp_path).source_roots == () +E AssertionError: assert ('src', '.') == () +E +E Left contains 2 more items, first extra item: 'src' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:439: AssertionError +_ test_target_pytest_preserves_built_or_explicit_source_imports[undeclared-prepend] _ + +tmp_path = PosixPath('/test_target_pytest_preserves_b0') +paths_mode = 'undeclared', import_mode = 'prepend' + + @pytest.mark.parametrize("import_mode", ["prepend", "importlib"]) + @pytest.mark.parametrize("paths_mode", ["undeclared", "empty", "pytest", "explicit", "editable"]) + def test_target_pytest_preserves_built_or_explicit_source_imports( + tmp_path: Path, paths_mode: str, import_mode: str + ) -> None: + from specfact_code_review.run.runtime_discovery import discover_project + + project, snapshot = tmp_path / "runtime", tmp_path / "snapshot" + project.mkdir() + source = snapshot / "src with spaces" if paths_mode == "pytest" else snapshot / "src" + package = source / "capsule_origin" + package.mkdir(parents=True) + (package / "__init__.py").write_text('ORIGIN = "raw"\n') + (snapshot / "tests").mkdir() + _install_generated_package(project) + expected = "raw" if paths_mode in {"pytest", "explicit", "editable"} else "built" + (snapshot / "tests/test_origin.py").write_text( + f'import capsule_origin\ndef test_origin():\n assert capsule_origin.ORIGIN == {expected!r}\n' + ) + configuration = None + if paths_mode == "pytest": + (snapshot / "pytest.ini").write_text('[pytest]\npythonpath = "src with spaces"\n') + elif paths_mode == "empty": + (snapshot / "pytest.ini").write_text('[pytest]\npythonpath =\n') + elif paths_mode == "explicit": + configuration = snapshot / "review.toml" + configuration.write_text('source_roots = ["src"]\n') + elif paths_mode == "editable": + # The selected environment's ordinary .pth editable mapping owns this path. + (project / "site-packages/capsule-editable.pth").write_text(str(source) + "\n") + # A real editable installation supplies source instead of the built package. + import shutil + + shutil.rmtree(project / "site-packages/capsule_origin") + plan = discover_project(snapshot, config_path=configuration) + descriptor = {"project": plan.document(), "inventory": {"member_graphs": { + "pytest-observe": {"sealed_imports": [], "installed": []} + }}} + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + paths = {"PROJECT": project, "SNAPSHOT": snapshot, "ANALYZERS": Path(pytest.__file__).parent.parent} + if paths_mode != "explicit": + native = _run_origin_pytest(paths, capsule=False, import_mode=import_mode) + assert native.returncode == 0, native.stdout + native.stderr + observed = _run_origin_pytest(paths, capsule=True, import_mode=import_mode) +> assert observed.returncode == 0, observed.stdout + observed.stderr +E AssertionError: F [100%] +E =================================== FAILURES =================================== +E _________________________________ test_origin __________________________________ +E +E def test_origin(): +E > assert capsule_origin.ORIGIN == 'built' +E E AssertionError: assert 'raw' == 'built' +E E +E E - built +E E + raw +E +E tests/test_origin.py:3: AssertionError +E =========================== short test summary info ============================ +E FAILED tests/test_origin.py::test_origin - AssertionError: assert 'raw' == 'b... +E 1 failed in 0.01s +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...===\nFAILED tests/test_origin.py::test_origin - AssertionError: assert 'raw' == 'b...\n1 failed in 0.01s\n", stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:387: AssertionError +_ test_target_pytest_preserves_built_or_explicit_source_imports[undeclared-importlib] _ + +tmp_path = PosixPath('/test_target_pytest_preserves_b1') +paths_mode = 'undeclared', import_mode = 'importlib' + + @pytest.mark.parametrize("import_mode", ["prepend", "importlib"]) + @pytest.mark.parametrize("paths_mode", ["undeclared", "empty", "pytest", "explicit", "editable"]) + def test_target_pytest_preserves_built_or_explicit_source_imports( + tmp_path: Path, paths_mode: str, import_mode: str + ) -> None: + from specfact_code_review.run.runtime_discovery import discover_project + + project, snapshot = tmp_path / "runtime", tmp_path / "snapshot" + project.mkdir() + source = snapshot / "src with spaces" if paths_mode == "pytest" else snapshot / "src" + package = source / "capsule_origin" + package.mkdir(parents=True) + (package / "__init__.py").write_text('ORIGIN = "raw"\n') + (snapshot / "tests").mkdir() + _install_generated_package(project) + expected = "raw" if paths_mode in {"pytest", "explicit", "editable"} else "built" + (snapshot / "tests/test_origin.py").write_text( + f'import capsule_origin\ndef test_origin():\n assert capsule_origin.ORIGIN == {expected!r}\n' + ) + configuration = None + if paths_mode == "pytest": + (snapshot / "pytest.ini").write_text('[pytest]\npythonpath = "src with spaces"\n') + elif paths_mode == "empty": + (snapshot / "pytest.ini").write_text('[pytest]\npythonpath =\n') + elif paths_mode == "explicit": + configuration = snapshot / "review.toml" + configuration.write_text('source_roots = ["src"]\n') + elif paths_mode == "editable": + # The selected environment's ordinary .pth editable mapping owns this path. + (project / "site-packages/capsule-editable.pth").write_text(str(source) + "\n") + # A real editable installation supplies source instead of the built package. + import shutil + + shutil.rmtree(project / "site-packages/capsule_origin") + plan = discover_project(snapshot, config_path=configuration) + descriptor = {"project": plan.document(), "inventory": {"member_graphs": { + "pytest-observe": {"sealed_imports": [], "installed": []} + }}} + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + paths = {"PROJECT": project, "SNAPSHOT": snapshot, "ANALYZERS": Path(pytest.__file__).parent.parent} + if paths_mode != "explicit": + native = _run_origin_pytest(paths, capsule=False, import_mode=import_mode) + assert native.returncode == 0, native.stdout + native.stderr + observed = _run_origin_pytest(paths, capsule=True, import_mode=import_mode) +> assert observed.returncode == 0, observed.stdout + observed.stderr +E AssertionError: F [100%] +E =================================== FAILURES =================================== +E _________________________________ test_origin __________________________________ +E +E def test_origin(): +E > assert capsule_origin.ORIGIN == 'built' +E E AssertionError: assert 'raw' == 'built' +E E +E E - built +E E + raw +E +E tests/test_origin.py:3: AssertionError +E =========================== short test summary info ============================ +E FAILED tests/test_origin.py::test_origin - AssertionError: assert 'raw' == 'b... +E 1 failed in 0.01s +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...===\nFAILED tests/test_origin.py::test_origin - AssertionError: assert 'raw' == 'b...\n1 failed in 0.01s\n", stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:387: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_undeclared_pythonpath_does_not_invent_import_roots[flat] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_undeclared_pythonpath_does_not_invent_import_roots[src] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_target_pytest_preserves_built_or_explicit_source_imports[undeclared-prepend] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_target_pytest_preserves_built_or_explicit_source_imports[undeclared-importlib] +================= 4 failed, 14 passed, 265 deselected in 4.88s ================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/NATIVE_LOADER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_LOADER_RED.txt new file mode 100644 index 00000000..020ec1f0 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_LOADER_RED.txt @@ -0,0 +1,102 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items / 2 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_native.py F [100%] + +=================================== FAILURES =================================== +__________ test_project_native_libraries_use_their_own_loader_closure __________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-79/test_project_native_libraries_0') + + def test_project_native_libraries_use_their_own_loader_closure(tmp_path: Path) -> None: + from specfact_code_review.run.runtime_native import inventory_native + + artifact, capsule, system = (tmp_path / name for name in ('artifact', 'capsule', 'system')) + for root in (artifact, capsule, system): + root.mkdir() + _elf(system / 'libodbc.so.2', ('libc.so.6',)) + _elf(system / 'libc.so.6', ('ld-linux-x86-64.so.2',)) + _elf(system / 'ld-linux-x86-64.so.2', ()) + _elf(capsule / 'libc.so.6', ()) + original = (capsule / 'libc.so.6').read_bytes() +> records = inventory_native( + artifact, capsule_root=capsule, declared=('libodbc.so.2',), system_roots=(system,), target_loader=True + ) + +tests/unit/specfact_code_review/run/test_runtime_native.py:61: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +args = (PosixPath('/pytest-of-dom/pytest-79/test_project_native_libraries_0/artifact'),) +kwargs = {'capsule_root': PosixPath('/pytest-of-dom/pytest-79/test_proj...sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-79/test_project_native_libraries_0/system'),), 'target_loader': True} + + def wrapper(*args, **kwargs): # type: ignore + """Wrap func by checking the preconditions and postconditions.""" + kwargs_error = _assert_no_invalid_kwargs(kwargs) + if kwargs_error: + raise kwargs_error + + # We need to create a new in-progress set if it is None as the ``ContextVar`` does not accept + # a factory function for the default argument. If we didn't do this, and simply set an empty + # set as the default, ``ContextVar`` would always point to the same set by copying the default + # by reference. + in_progress = _IN_PROGRESS.get() + if in_progress is None: + in_progress = set() + _IN_PROGRESS.set(in_progress) + + # Use try-finally instead of ExitStack for performance. + try: + # If the wrapper is already checking the contracts for the wrapped function, avoid a recursive loop + # by skipping any subsequent contract checks for the same function. + if id_func in in_progress: + return func(*args, **kwargs) + + in_progress.add(id_func) + + (preconditions, snapshots, postconditions) = _unpack_pre_snap_posts( + wrapper + ) + + resolved_kwargs = kwargs_from_call( + param_names=param_names, + kwdefaults=kwdefaults, + args=args, + kwargs=kwargs, + ) + + type_error = _assert_resolved_kwargs_valid( + postconditions=postconditions, resolved_kwargs=resolved_kwargs + ) + if type_error: + raise type_error + + violation_error = _assert_preconditions( + preconditions=preconditions, + resolved_kwargs=resolved_kwargs, + func=func, + ) + if violation_error: + raise violation_error + + # Capture the snapshots + if postconditions and snapshots: + resolved_kwargs["OLD"] = _capture_old( + snapshots=snapshots, resolved_kwargs=resolved_kwargs, func=func + ) + + # Ideally, we would catch any exception here and strip the checkers from the traceback. + # Unfortunately, this can not be done in Python 3, see + # https://stackoverflow.com/questions/44813333/how-can-i-elide-a-function-wrapper-from-the-traceback-in-python-3 +> result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +E TypeError: inventory_native() got an unexpected keyword argument 'target_loader' + +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: TypeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_project_native_libraries_use_their_own_loader_closure +======================= 1 failed, 2 deselected in 0.09s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/NATIVE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_RED.txt new file mode 100644 index 00000000..266e8666 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_RED.txt @@ -0,0 +1,33 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 2 items + +tests/unit/specfact_code_review/run/test_runtime_native.py FF [100%] + +=================================== FAILURES =================================== +_________ test_declared_missing_native_library_names_exact_requirement _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-19/test_declared_missing_native_l0') + + def test_declared_missing_native_library_names_exact_requirement(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_native import inventory_native +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_native' + +tests/unit/specfact_code_review/run/test_runtime_native.py:8: ModuleNotFoundError +__________ test_native_inventory_does_not_copy_unrequested_host_files __________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-19/test_native_inventory_does_not0') + + def test_native_inventory_does_not_copy_unrequested_host_files(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_native import inventory_native +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_native' + +tests/unit/specfact_code_review/run/test_runtime_native.py:16: ModuleNotFoundError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_declared_missing_native_library_names_exact_requirement +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_native_inventory_does_not_copy_unrequested_host_files +============================== 2 failed in 0.09s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/NATIVE_WORKER_LAUNCH_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_WORKER_LAUNCH_RED.txt new file mode 100644 index 00000000..c5487534 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NATIVE_WORKER_LAUNCH_RED.txt @@ -0,0 +1,29 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items / 2 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_target_launch.py F [100%] + +=================================== FAILURES =================================== +_____ test_native_worker_uses_matching_loader_without_mutating_supervisor ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-81/test_native_worker_uses_matchi0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108717e10> + + def test_native_worker_uses_matching_loader_without_mutating_supervisor(tmp_path, monkeypatch) -> None: + from specfact_code_review.run import target_launch + + loader = tmp_path / 'native/ld-linux-x86-64.so.2' + loader.parent.mkdir() + loader.touch() +> monkeypatch.setattr(target_launch, 'PROJECT', tmp_path) +E AttributeError: /packages/specfact-code-review/src/specfact_code_review/run/target_launch.py'> has no attribute 'PROJECT' + +tests/unit/specfact_code_review/run/test_target_launch.py:34: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_native_worker_uses_matching_loader_without_mutating_supervisor +======================= 1 failed, 2 deselected in 0.08s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/NESTED_PATH_CONTAINER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NESTED_PATH_CONTAINER_RED.txt new file mode 100644 index 00000000..a3ed0af1 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NESTED_PATH_CONTAINER_RED.txt @@ -0,0 +1,46 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 48 items / 47 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_target_launch.py F [100%] + +=================================== FAILURES =================================== +_______ test_caller_path_prepend_does_not_invoke_project_container_hooks _______ + +tmp_path = PosixPath('') + + def test_caller_path_prepend_does_not_invoke_project_container_hooks(tmp_path): + wrapper = _native_nested_wrapper(tmp_path, "pylint") + installed = wrapper.parents[1] / "site-packages" + (installed / "analyzer_only.py").write_text("VALUE = 'counterfeit'\n") + hook = ( + "class HookedPaths(list):\n" + " def __setitem__(self, key, value):\n" + " sys.meta_path.pop(0)\n" + " return super().__setitem__(key, value)\n" + "sys.path = HookedPaths(sys.path)\n" + ) + (installed / "container_hook.pth").write_text(f"import sys; exec({hook!r})\n") + result = subprocess.run( + [str(wrapper), "-c", "import analyzer_only; print(analyzer_only.VALUE)"], + env={}, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert result.returncode == 0, result.stderr +> assert result.stdout.strip() == "member-owned" +E AssertionError: assert 'counterfeit' == 'member-owned' +E +E - member-owned +E + counterfeit + +tests/unit/specfact_code_review/run/test_target_launch.py:356: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_caller_path_prepend_does_not_invoke_project_container_hooks +======================= 1 failed, 47 deselected in 0.49s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/NESTED_PYTHONPATH_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NESTED_PYTHONPATH_RED.txt new file mode 100644 index 00000000..176e6bdd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NESTED_PYTHONPATH_RED.txt @@ -0,0 +1,698 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 47 items / 27 deselected / 20 selected + +tests/unit/specfact_code_review/run/test_target_launch.py FFFFFFFFFFFFFF [ 70%] +FFFFFF [100%] + +=================================== FAILURES =================================== +_ test_nested_pythonpath_matches_native_imports[False-absolute-project-python] _ + +tmp_path = PosixPath('/test_nested_pythonpath_matches0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109eeaea0> +domain = 'project-python', path_case = 'absolute', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches0/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...s0/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_____ test_nested_pythonpath_matches_native_imports[False-absolute-pylint] _____ + +tmp_path = PosixPath('/test_nested_pythonpath_matches1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109eead70> +domain = 'pylint', path_case = 'absolute', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches1/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...s1/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_ test_nested_pythonpath_matches_native_imports[False-relative-project-python] _ + +tmp_path = PosixPath('/test_nested_pythonpath_matches2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a590050> +domain = 'project-python', path_case = 'relative', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches2/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...s2/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_____ test_nested_pythonpath_matches_native_imports[False-relative-pylint] _____ + +tmp_path = PosixPath('/test_nested_pythonpath_matches3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a4e9590> +domain = 'pylint', path_case = 'relative', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches3/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...s3/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_ test_nested_pythonpath_matches_native_imports[False-empty-component-project-python] _ + +tmp_path = PosixPath('/test_nested_pythonpath_matches4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a4e9d00> +domain = 'project-python', path_case = 'empty-component', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches4/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...s4/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_ test_nested_pythonpath_matches_native_imports[False-empty-component-pylint] __ + +tmp_path = PosixPath('/test_nested_pythonpath_matches5') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5bd850> +domain = 'pylint', path_case = 'empty-component', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches5/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...s5/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +__ test_nested_pythonpath_matches_native_imports[False-empty-project-python] ___ + +tmp_path = PosixPath('/test_nested_pythonpath_matches6') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5bde50> +domain = 'project-python', path_case = 'empty', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[""]\n' +E +E - [""] +E + ["/test_nested_pythonpath_matches6/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +______ test_nested_pythonpath_matches_native_imports[False-empty-pylint] _______ + +tmp_path = PosixPath('/test_nested_pythonpath_matches7') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5a1f40> +domain = 'pylint', path_case = 'empty', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[""]\n' +E +E - [""] +E + ["/test_nested_pythonpath_matches7/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +_ test_nested_pythonpath_matches_native_imports[False-omitted-project-python] __ + +tmp_path = PosixPath('/test_nested_pythonpath_matches8') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5a2c60> +domain = 'project-python', path_case = 'omitted', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[null]\n' +E +E - [null] +E + ["/test_nested_pythonpath_matches8/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +_____ test_nested_pythonpath_matches_native_imports[False-omitted-pylint] ______ + +tmp_path = PosixPath('/test_nested_pythonpath_matches9') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a541c50> +domain = 'pylint', path_case = 'omitted', inherited = False + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[null]\n' +E +E - [null] +E + ["/test_nested_pythonpath_matches9/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +_ test_nested_pythonpath_matches_native_imports[True-absolute-project-python] __ + +tmp_path = PosixPath('/test_nested_pythonpath_matches10') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5119b0> +domain = 'project-python', path_case = 'absolute', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches10/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...10/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_____ test_nested_pythonpath_matches_native_imports[True-absolute-pylint] ______ + +tmp_path = PosixPath('/test_nested_pythonpath_matches11') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a62be10> +domain = 'pylint', path_case = 'absolute', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches11/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...11/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_ test_nested_pythonpath_matches_native_imports[True-relative-project-python] __ + +tmp_path = PosixPath('/test_nested_pythonpath_matches12') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f03a10> +domain = 'project-python', path_case = 'relative', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches12/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...12/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_____ test_nested_pythonpath_matches_native_imports[True-relative-pylint] ______ + +tmp_path = PosixPath('/test_nested_pythonpath_matches13') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f03890> +domain = 'pylint', path_case = 'relative', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches13/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...13/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +_ test_nested_pythonpath_matches_native_imports[True-empty-component-project-python] _ + +tmp_path = PosixPath('/test_nested_pythonpath_matches14') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5c9bd0> +domain = 'project-python', path_case = 'empty-component', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches14/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...14/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +__ test_nested_pythonpath_matches_native_imports[True-empty-component-pylint] __ + +tmp_path = PosixPath('/test_nested_pythonpath_matches15') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5c9180> +domain = 'pylint', path_case = 'empty-component', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) +> assert attached.returncode == 0, attached.stderr +E AssertionError: Traceback (most recent call last): +E File "/test_nested_pythonpath_matches15/probe.py", line 3, in +E import caller_module +E ModuleNotFoundError: No module named 'caller_module' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/test_nested_...15/probe.py", line 3, in \n import caller_module\nModuleNotFoundError: No module named \'caller_module\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_launch.py:331: AssertionError +___ test_nested_pythonpath_matches_native_imports[True-empty-project-python] ___ + +tmp_path = PosixPath('/test_nested_pythonpath_matches16') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a583cf0> +domain = 'project-python', path_case = 'empty', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[""]\n' +E +E - [""] +E + ["/test_nested_pythonpath_matches16/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +_______ test_nested_pythonpath_matches_native_imports[True-empty-pylint] _______ + +tmp_path = PosixPath('/test_nested_pythonpath_matches17') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5fc9b0> +domain = 'pylint', path_case = 'empty', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[""]\n' +E +E - [""] +E + ["/test_nested_pythonpath_matches17/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +__ test_nested_pythonpath_matches_native_imports[True-omitted-project-python] __ + +tmp_path = PosixPath('/test_nested_pythonpath_matches18') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1097dc170> +domain = 'project-python', path_case = 'omitted', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[null]\n' +E +E - [null] +E + ["/test_nested_pythonpath_matches18/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +______ test_nested_pythonpath_matches_native_imports[True-omitted-pylint] ______ + +tmp_path = PosixPath('/test_nested_pythonpath_matches19') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a5b44d0> +domain = 'pylint', path_case = 'omitted', inherited = True + + @pytest.mark.parametrize("domain", ["project-python", "pylint"]) + @pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) + @pytest.mark.parametrize("inherited", [False, True]) + def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = dict(cwd=cwd, env=environment, capture_output=True, text=True, timeout=30, check=False) + native = subprocess.run([sys.executable, "-s", str(probe)], **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], **options) + assert attached.returncode == 0, attached.stderr +> assert attached.stdout == native.stdout +E assert '["/private/v...eview/run"]\n' == '[null]\n' +E +E - [null] +E + ["/test_nested_pythonpath_matches19/builtin/specfact_code_review/run"] + +tests/unit/specfact_code_review/run/test_target_launch.py:332: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-absolute-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-absolute-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-relative-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-relative-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-empty-component-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-empty-component-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-empty-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-empty-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-omitted-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[False-omitted-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-absolute-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-absolute-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-relative-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-relative-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-empty-component-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-empty-component-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-empty-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-empty-pylint] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-omitted-project-python] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_pythonpath_matches_native_imports[True-omitted-pylint] +====================== 20 failed, 27 deselected in 8.78s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/NESTED_PYTHON_CONTEXT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NESTED_PYTHON_CONTEXT_RED.txt new file mode 100644 index 00000000..04cfa951 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NESTED_PYTHON_CONTEXT_RED.txt @@ -0,0 +1,256 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 17 items / 6 deselected / 11 selected + +tests/unit/specfact_code_review/run/test_target_launch.py FFFF.FFFFFF [100%] + +=================================== FAILURES =================================== +_ test_nested_python_preserves_caller_environment_cwd_and_files[False-snapshot/subdirectory] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_python_preserves_c0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bc168b0> +location = 'snapshot/subdirectory', remove_home = False + + @pytest.mark.parametrize("location", ["snapshot/subdirectory", "private-tmp/case"]) + @pytest.mark.parametrize("remove_home", [False, True]) + def test_nested_python_preserves_caller_environment_cwd_and_files(tmp_path, monkeypatch, location, remove_home): + observed, cwd = _nested_child_policy(tmp_path, monkeypatch, location, remove_home) +> assert observed == ["caller-overlay", None if remove_home else "caller-home", cwd, "parent-data"] +E AssertionError: assert [None, '/opt/...apshot', None] == ['caller-over...'parent-data'] +E +E At index 0 diff: None != 'caller-overlay' +E Use -v to get more diff + +/private/tmp/specfact-473-delivery/tests/unit/specfact_code_review/run/test_target_launch.py:170: AssertionError +_ test_nested_python_preserves_caller_environment_cwd_and_files[False-private-tmp/case] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_python_preserves_c1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bc16c40> +location = 'private-tmp/case', remove_home = False + + @pytest.mark.parametrize("location", ["snapshot/subdirectory", "private-tmp/case"]) + @pytest.mark.parametrize("remove_home", [False, True]) + def test_nested_python_preserves_caller_environment_cwd_and_files(tmp_path, monkeypatch, location, remove_home): + observed, cwd = _nested_child_policy(tmp_path, monkeypatch, location, remove_home) +> assert observed == ["caller-overlay", None if remove_home else "caller-home", cwd, "parent-data"] +E AssertionError: assert [None, '/opt/...apshot', None] == ['caller-over...'parent-data'] +E +E At index 0 diff: None != 'caller-overlay' +E Use -v to get more diff + +/private/tmp/specfact-473-delivery/tests/unit/specfact_code_review/run/test_target_launch.py:170: AssertionError +_ test_nested_python_preserves_caller_environment_cwd_and_files[True-snapshot/subdirectory] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_python_preserves_c2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bc5bad0> +location = 'snapshot/subdirectory', remove_home = True + + @pytest.mark.parametrize("location", ["snapshot/subdirectory", "private-tmp/case"]) + @pytest.mark.parametrize("remove_home", [False, True]) + def test_nested_python_preserves_caller_environment_cwd_and_files(tmp_path, monkeypatch, location, remove_home): + observed, cwd = _nested_child_policy(tmp_path, monkeypatch, location, remove_home) +> assert observed == ["caller-overlay", None if remove_home else "caller-home", cwd, "parent-data"] +E AssertionError: assert [None, '/opt/...apshot', None] == ['caller-over...'parent-data'] +E +E At index 0 diff: None != 'caller-overlay' +E Use -v to get more diff + +/private/tmp/specfact-473-delivery/tests/unit/specfact_code_review/run/test_target_launch.py:170: AssertionError +_ test_nested_python_preserves_caller_environment_cwd_and_files[True-private-tmp/case] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_python_preserves_c3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c231040> +location = 'private-tmp/case', remove_home = True + + @pytest.mark.parametrize("location", ["snapshot/subdirectory", "private-tmp/case"]) + @pytest.mark.parametrize("remove_home", [False, True]) + def test_nested_python_preserves_caller_environment_cwd_and_files(tmp_path, monkeypatch, location, remove_home): + observed, cwd = _nested_child_policy(tmp_path, monkeypatch, location, remove_home) +> assert observed == ["caller-overlay", None if remove_home else "caller-home", cwd, "parent-data"] +E AssertionError: assert [None, '/opt/...apshot', None] == ['caller-over...'parent-data'] +E +E At index 0 diff: None != 'caller-overlay' +E Use -v to get more diff + +/private/tmp/specfact-473-delivery/tests/unit/specfact_code_review/run/test_target_launch.py:170: AssertionError +_ test_nested_member_domain_survives_environment_replacement[project-python-run/target_bootstrap.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_member_domain_surv0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c25ea50> +domain = 'project-python', relative = 'run/target_bootstrap.py' + + @pytest.mark.parametrize( + "domain,relative", + [ + ("project-python", "run/target_bootstrap.py"), + ("pytest-observe", "run/target_pytest.py"), + ("pylint", "run/target_pylint.py"), + ("basedpyright", "tools/basedpyright_runner.py"), + ("crosshair", "tools/contract_runner.py"), + ], + ) + def test_nested_member_domain_survives_environment_replacement(tmp_path, monkeypatch, domain, relative): + from pathlib import Path + + builtin = Path(target_launch.__file__).parents[2] + context = tmp_path / "context" + context.symlink_to(builtin / "specfact_code_review" / relative) + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + monkeypatch.setattr(target_launch, "BUILTIN", builtin, raising=False) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST", raising=False) +> assert target_launch.execution_domain() == domain + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_launch' has no attribute 'execution_domain' + +tests/unit/specfact_code_review/run/test_target_launch.py:202: AttributeError +_ test_nested_member_domain_survives_environment_replacement[pytest-observe-run/target_pytest.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_member_domain_surv1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c25f350> +domain = 'pytest-observe', relative = 'run/target_pytest.py' + + @pytest.mark.parametrize( + "domain,relative", + [ + ("project-python", "run/target_bootstrap.py"), + ("pytest-observe", "run/target_pytest.py"), + ("pylint", "run/target_pylint.py"), + ("basedpyright", "tools/basedpyright_runner.py"), + ("crosshair", "tools/contract_runner.py"), + ], + ) + def test_nested_member_domain_survives_environment_replacement(tmp_path, monkeypatch, domain, relative): + from pathlib import Path + + builtin = Path(target_launch.__file__).parents[2] + context = tmp_path / "context" + context.symlink_to(builtin / "specfact_code_review" / relative) + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + monkeypatch.setattr(target_launch, "BUILTIN", builtin, raising=False) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST", raising=False) +> assert target_launch.execution_domain() == domain + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_launch' has no attribute 'execution_domain' + +tests/unit/specfact_code_review/run/test_target_launch.py:202: AttributeError +_ test_nested_member_domain_survives_environment_replacement[pylint-run/target_pylint.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_member_domain_surv2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c2c3d40> +domain = 'pylint', relative = 'run/target_pylint.py' + + @pytest.mark.parametrize( + "domain,relative", + [ + ("project-python", "run/target_bootstrap.py"), + ("pytest-observe", "run/target_pytest.py"), + ("pylint", "run/target_pylint.py"), + ("basedpyright", "tools/basedpyright_runner.py"), + ("crosshair", "tools/contract_runner.py"), + ], + ) + def test_nested_member_domain_survives_environment_replacement(tmp_path, monkeypatch, domain, relative): + from pathlib import Path + + builtin = Path(target_launch.__file__).parents[2] + context = tmp_path / "context" + context.symlink_to(builtin / "specfact_code_review" / relative) + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + monkeypatch.setattr(target_launch, "BUILTIN", builtin, raising=False) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST", raising=False) +> assert target_launch.execution_domain() == domain + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_launch' has no attribute 'execution_domain' + +tests/unit/specfact_code_review/run/test_target_launch.py:202: AttributeError +_ test_nested_member_domain_survives_environment_replacement[basedpyright-tools/basedpyright_runner.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_member_domain_surv3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c2e4aa0> +domain = 'basedpyright', relative = 'tools/basedpyright_runner.py' + + @pytest.mark.parametrize( + "domain,relative", + [ + ("project-python", "run/target_bootstrap.py"), + ("pytest-observe", "run/target_pytest.py"), + ("pylint", "run/target_pylint.py"), + ("basedpyright", "tools/basedpyright_runner.py"), + ("crosshair", "tools/contract_runner.py"), + ], + ) + def test_nested_member_domain_survives_environment_replacement(tmp_path, monkeypatch, domain, relative): + from pathlib import Path + + builtin = Path(target_launch.__file__).parents[2] + context = tmp_path / "context" + context.symlink_to(builtin / "specfact_code_review" / relative) + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + monkeypatch.setattr(target_launch, "BUILTIN", builtin, raising=False) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST", raising=False) +> assert target_launch.execution_domain() == domain + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_launch' has no attribute 'execution_domain' + +tests/unit/specfact_code_review/run/test_target_launch.py:202: AttributeError +_ test_nested_member_domain_survives_environment_replacement[crosshair-tools/contract_runner.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_nested_member_domain_surv4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c258750> +domain = 'crosshair', relative = 'tools/contract_runner.py' + + @pytest.mark.parametrize( + "domain,relative", + [ + ("project-python", "run/target_bootstrap.py"), + ("pytest-observe", "run/target_pytest.py"), + ("pylint", "run/target_pylint.py"), + ("basedpyright", "tools/basedpyright_runner.py"), + ("crosshair", "tools/contract_runner.py"), + ], + ) + def test_nested_member_domain_survives_environment_replacement(tmp_path, monkeypatch, domain, relative): + from pathlib import Path + + builtin = Path(target_launch.__file__).parents[2] + context = tmp_path / "context" + context.symlink_to(builtin / "specfact_code_review" / relative) + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + monkeypatch.setattr(target_launch, "BUILTIN", builtin, raising=False) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST", raising=False) +> assert target_launch.execution_domain() == domain + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_launch' has no attribute 'execution_domain' + +tests/unit/specfact_code_review/run/test_target_launch.py:202: AttributeError +_______________ test_unrecognized_namespace_context_is_rejected ________________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-484/test_unrecognized_namespace_co0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c2583d0> + + def test_unrecognized_namespace_context_is_rejected(tmp_path, monkeypatch): + context = tmp_path / "context" + context.write_text("pytest-observe") + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + with pytest.raises(RuntimeError, match="project_python_context"): +> target_launch.execution_domain() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_launch' has no attribute 'execution_domain' + +tests/unit/specfact_code_review/run/test_target_launch.py:212: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_python_preserves_caller_environment_cwd_and_files[False-snapshot/subdirectory] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_python_preserves_caller_environment_cwd_and_files[False-private-tmp/case] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_python_preserves_caller_environment_cwd_and_files[True-snapshot/subdirectory] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_python_preserves_caller_environment_cwd_and_files[True-private-tmp/case] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_member_domain_survives_environment_replacement[project-python-run/target_bootstrap.py] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_member_domain_survives_environment_replacement[pytest-observe-run/target_pytest.py] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_member_domain_survives_environment_replacement[pylint-run/target_pylint.py] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_member_domain_survives_environment_replacement[basedpyright-tools/basedpyright_runner.py] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_nested_member_domain_survives_environment_replacement[crosshair-tools/contract_runner.py] +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_unrecognized_namespace_context_is_rejected +================== 10 failed, 1 passed, 6 deselected in 0.28s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/NESTED_SCOPE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/NESTED_SCOPE_RED.txt new file mode 100644 index 00000000..83299018 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/NESTED_SCOPE_RED.txt @@ -0,0 +1,27 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 405 items / 404 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runner.py F [100%] + +=================================== FAILURES =================================== +________ test_pytest_worker_does_not_inherit_controller_diff_selection _________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x106de76f0> + + def test_pytest_worker_does_not_inherit_controller_diff_selection(monkeypatch: MonkeyPatch) -> None: + runner_api = _c14_runner() + monkeypatch.setenv('SPECFACT_CODE_REVIEW_CHANGED_DIFF', 'cached') +> assert 'SPECFACT_CODE_REVIEW_CHANGED_DIFF' not in runner_api._pytest_env() +E AssertionError: assert 'SPECFACT_CODE_REVIEW_CHANGED_DIFF' not in {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...} +E + where {'RUST_LOG': 'warn', 'CODEX_SANDBOX_NETWORK_DISABLED': '1', 'TERM': 'dumb', 'SHELL': '/bin/zsh', ...} = () +E + where = /packages/specfact-code-review/src/specfact_code_review/run/runner.py'>._pytest_env + +tests/unit/specfact_code_review/run/test_runner.py:7724: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runner.py::test_pytest_worker_does_not_inherit_controller_diff_selection +====================== 1 failed, 404 deselected in 0.42s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_CORPUS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_CORPUS_RED.txt new file mode 100644 index 00000000..67a91097 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_CORPUS_RED.txt @@ -0,0 +1,85 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 11 items + +tests/unit/test_external_capsule_corpus.py .........FF [100%] + +=================================== FAILURES =================================== +_______________ test_controlled_defect_preserves_separate_report _______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-158/test_controlled_defect_preserv0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1088d3820> + + def test_controlled_defect_preserves_separate_report(tmp_path: Path, monkeypatch) -> None: + module = _load() + root = tmp_path / "upstream" + (root / "tests").mkdir(parents=True) + evidence = tmp_path / "evidence" + evidence.mkdir() + (evidence / "cold-auto-report.json").write_text('{"untouched": true}') + + def execute(argv, **_context): + output = Path(argv[argv.index("--out") + 1]) + output.write_text(json.dumps({"findings": [ + {"rule": "TEST_OUTCOME_NOT_PASS"}, + {"tool": "basedpyright", "file": "specfact_controlled.py"}, + ]})) + return "" + + monkeypatch.setattr(module, "execute", execute) + monkeypatch.setattr(module, "assert_completed_report", lambda _report: None) +> module.controlled_defect(root, evidence, tmp_path / "config.toml", tmp_path) + +tests/unit/test_external_capsule_corpus.py:144: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/pytest-of-dom/pytest-158/test_controlled_defect_preserv0/upstream') +evidence = PosixPath('/pytest-of-dom/pytest-158/test_controlled_defect_preserv0/evidence') +config = PosixPath('/pytest-of-dom/pytest-158/test_controlled_defect_preserv0/config.toml') +workspace = PosixPath('/pytest-of-dom/pytest-158/test_controlled_defect_preserv0') + + def controlled_defect(root: Path, evidence: Path, config: Path, workspace: Path) -> None: + """Require actual test failure and static defect detection in an identified copy.""" + copy = workspace / "controlled" / root.name + shutil.copytree(root, copy, symlinks=True) + (copy / "specfact_controlled.py").write_text( + 'def controlled() -> int:\n return "injected wrong type"\n', encoding="utf-8" + ) + (copy / "tests/test_specfact_controlled.py").write_text( + 'def test_specfact_controlled_failure():\n assert False, "SPECFACT_CONTROLLED_473"\n', encoding="utf-8" + ) +> report = review( + copy, + evidence, + config, + ["specfact_controlled.py", "tests/test_specfact_controlled.py"], + name="controlled-defect", + ) +E TypeError: review() got an unexpected keyword argument 'name' + +scripts/external_capsule_corpus.py:291: TypeError +_______________ test_offline_corpus_rejects_unverified_launcher ________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-158/test_offline_corpus_rejects_un0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108f4f530> + + def test_offline_corpus_rejects_unverified_launcher(tmp_path: Path, monkeypatch) -> None: + module = _load() + monkeypatch.setenv("SPECFACT_CODE_REVIEW_CAPSULE_CACHE", str(tmp_path)) + launcher = tmp_path / "root/opt/specfact/bin/bwrap-static" + launcher.parent.mkdir(parents=True) + launcher.write_bytes(b"not the signed launcher") + with pytest.raises(ValueError, match="verified.*launcher"): +> module.offline_command(["specfact", "code", "review", "runtime", "prepare", "--offline"]) + ^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'external_capsule_corpus' has no attribute 'offline_command' + +tests/unit/test_external_capsule_corpus.py:156: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_defect_preserves_separate_report +FAILED tests/unit/test_external_capsule_corpus.py::test_offline_corpus_rejects_unverified_launcher +========================= 2 failed, 9 passed in 0.04s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_DEVICES_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_DEVICES_RED.txt new file mode 100644 index 00000000..efaf807b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_DEVICES_RED.txt @@ -0,0 +1,11 @@ +2026-09-14, local Ubuntu 24.04 x86-64 component host, UID 1001. +Before the wrapper correction, the strengthened offline wrapper used --bind / / without a private /dev. + +Command: +bwrap --unshare-user --unshare-net --uid 1001 --gid 1001 --bind / / -- git --version +Observed stderr: +fatal: could not open '/dev/null' for reading and writing: Permission denied + +The same host outside the wrapper returned git version 2.43.0. +The component preparation command inside the wrapper reported core_installed_module_handoff_unavailable. Unwrapped diagnostic inspection traced it to GitPython's Bad git executable ImportError, not dependency resolution or a missing signed module. +This is failing-before harness evidence, not a capsule acceptance result. diff --git a/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_HOSTS_MOUNT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_HOSTS_MOUNT_RED.txt new file mode 100644 index 00000000..7c1876fd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_HOSTS_MOUNT_RED.txt @@ -0,0 +1,24 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items + +tests/unit/specfact_code_review/run/test_target_launch.py ...F [100%] + +=================================== FAILURES =================================== +_____________ test_offline_target_has_private_localhost_resolution _____________ + + def test_offline_target_has_private_localhost_resolution() -> None: + from specfact_code_review.run.target_launch import target_command + + command = target_command("pytest-observe", []) +> assert "/opt/specfact/project-runtime/worker-config" in command +E AssertionError: assert '/opt/specfact/project-runtime/worker-config' in ['/opt/specfact/bin/bwrap-static', '--die-with-parent', '--new-session', '--unshare-all', '--ro-bind', '/', ...] + +tests/unit/specfact_code_review/run/test_target_launch.py:54: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_offline_target_has_private_localhost_resolution +========================= 1 failed, 3 passed in 0.09s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_PLUGIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_PLUGIN_RED.txt new file mode 100644 index 00000000..c06dac6d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/OFFLINE_PLUGIN_RED.txt @@ -0,0 +1,34 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items + +tests/unit/specfact_code_review/run/test_target_launch.py ...F [ 57%] +tests/unit/specfact_code_review/run/test_target_pytest.py ..F [100%] + +=================================== FAILURES =================================== +_____________ test_offline_target_has_private_localhost_resolution _____________ + + def test_offline_target_has_private_localhost_resolution() -> None: + from specfact_code_review.run.target_launch import target_command + command = target_command('pytest-observe', []) +> assert '/opt/specfact/project-runtime/worker-config/hosts' in command +E AssertionError: assert '/opt/specfact/project-runtime/worker-config/hosts' in ['/opt/specfact/bin/bwrap-static', '--die-with-parent', '--new-session', '--unshare-all', '--ro-bind', '/', ...] + +tests/unit/specfact_code_review/run/test_target_launch.py:53: AssertionError +_________________ test_observer_retains_internal_plugin_errors _________________ + + def test_observer_retains_internal_plugin_errors() -> None: + observer = target_pytest.Observer() +> observer.pytest_internalerror('socket.gaierror: localhost unavailable', None) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'Observer' object has no attribute 'pytest_internalerror' + +tests/unit/specfact_code_review/run/test_target_pytest.py:39: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_offline_target_has_private_localhost_resolution +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_observer_retains_internal_plugin_errors +========================= 2 failed, 5 passed in 0.73s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/OPTIONAL_GIT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/OPTIONAL_GIT_RED.txt new file mode 100644 index 00000000..7851e0a5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/OPTIONAL_GIT_RED.txt @@ -0,0 +1,132 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items + +tests/unit/specfact_code_review/run/test_runtime_git.py FF.... [ 85%] +tests/unit/specfact_code_review/run/test_runtime_builder.py F [100%] + +=================================== FAILURES =================================== +______________ test_missing_git_https_helper_only_fails_when_used ______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-244/test_missing_git_https_helper_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bd109d0> + + def test_missing_git_https_helper_only_fails_when_used(tmp_path: Path, monkeypatch) -> None: + git = tmp_path / "git" + git.write_bytes(b"\x7fELFgit") + (tmp_path / "git-remote-http").write_bytes(b"\x7fELFhttp") + monkeypatch.setattr(runtime_git, "GIT", git) + run = subprocess.run + monkeypatch.setattr(runtime_git.subprocess, "run", lambda *_args, **_kwargs: SimpleNamespace(stdout=str(tmp_path))) +> assert set(runtime_git.git_identity()) == {"bin/git", "git-core/git-remote-http"} + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_git.py:21: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:44: in git_identity + return {name: content_digest(path.read_bytes()) for name, path in _programs().items()} + ^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def _programs() -> dict[str, Path]: + if not GIT.is_file(): + raise ProjectRuntimeError("project_builder_tool_missing:git; install Git on the builder") + result = subprocess.run( + [str(GIT), "--exec-path"], env={"PATH": "/usr/bin:/bin"}, capture_output=True, text=True, check=True, timeout=10 + ) + directory = Path(result.stdout.strip()) + programs = {"bin/git": GIT} + for name in ("git-remote-http", "git-remote-https"): + path = directory / name + if not path.is_file(): +> raise ProjectRuntimeError(f"project_builder_tool_missing:{name}; install Git HTTP transport helpers") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_builder_tool_missing:git-remote-https; install Git HTTP transport helpers + +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:30: ProjectRuntimeError +____________ test_missing_git_is_optional_for_identity_and_staging _____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-244/test_missing_git_is_optional_f0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bd10b00> + + def test_missing_git_is_optional_for_identity_and_staging(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(runtime_git, "GIT", tmp_path / "unavailable-git") + assert runtime_git.git_identity() == {} +> runtime_git.stage_git(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_git.py:36: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:57: in stage_git + for name, source in _programs().items(): + ^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def _programs() -> dict[str, Path]: + if not GIT.is_file(): +> raise ProjectRuntimeError("project_builder_tool_missing:git; install Git on the builder") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_builder_tool_missing:git; install Git on the builder + +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:21: ProjectRuntimeError +____________________ test_plain_pip_build_runs_without_git _____________________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-244/test_plain_pip_build_runs_with0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10be34850> + + def test_plain_pip_build_runs_without_git(tmp_path: Path, monkeypatch) -> None: + from specfact_code_review.run import runtime_builder, runtime_git + from specfact_code_review.run.runtime_sources import source_identity + + source = tmp_path / "source" + source.mkdir() + (source / "requirements.txt").write_text("example-package==1.0\n") + plan = ProjectPlan(source, manager="pip", requirements=("requirements.txt",), source_identity=source_identity(source)) + staging = tmp_path / "staging" + staging.mkdir() + runtime = SimpleNamespace(root=tmp_path / "capsule", interpreter="/opt/specfact/python/bin/python", bubblewrap="bwrap") + monkeypatch.setattr(runtime_git, "GIT", tmp_path / "unavailable-git") + monkeypatch.setattr( + runtime_builder.sandbox, "_verified_bubblewrap_descriptor", lambda *_args: os.open(os.devnull, os.O_RDONLY) + ) + launched = [] + + def acquire(command, **_kwargs): + launched.append(command) + (staging / "artifact/site-packages").mkdir(parents=True) + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(runtime_builder.subprocess, "run", acquire) +> assert runtime_builder._build(plan, runtime, staging) == staging / "artifact" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_builder.py:39: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:126: in _build + stage_git(staging) +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:57: in stage_git + for name, source in _programs().items(): + ^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def _programs() -> dict[str, Path]: + if not GIT.is_file(): +> raise ProjectRuntimeError("project_builder_tool_missing:git; install Git on the builder") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_builder_tool_missing:git; install Git on the builder + +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:21: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_git.py::test_missing_git_https_helper_only_fails_when_used +FAILED tests/unit/specfact_code_review/run/test_runtime_git.py::test_missing_git_is_optional_for_identity_and_staging +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_plain_pip_build_runs_without_git +========================= 3 failed, 4 passed in 0.77s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PAIR_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PAIR_RED.txt new file mode 100644 index 00000000..0640497e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PAIR_RED.txt @@ -0,0 +1,39 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_portable_snapshot.py ..F [100%] + +=================================== FAILURES =================================== +_______ test_immutable_pair_prepares_each_side_and_downgrades_authority ________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-7/test_immutable_pair_prepares_e0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c74e3f0> + + def test_immutable_pair_prepares_each_side_and_downgrades_authority(tmp_path: Path, monkeypatch) -> None: + from specfact_code_review.run import portable_snapshot + base_root, head_root = tmp_path / 'base', tmp_path / 'head' + base_root.mkdir() + head_root.mkdir() + calls = [] + def run_side(runtime, **kwargs): + calls.append((kwargs['snapshot_root'], kwargs['options'].project_runtime)) + return runner.CapsuleSnapshotResult({}, {}), {'identity': str(kwargs['snapshot_root'])} + monkeypatch.setattr(portable_snapshot, 'run_project_snapshot', run_side) + monkeypatch.setattr(runner, '_snapshot_python_files', lambda *args: []) + monkeypatch.setattr(runner, '_classify_range_findings', lambda *args: ({}, {})) + monkeypatch.setattr(runner, '_capsule_report', lambda *args, **kwargs: kwargs['scope_evidence']) + resolution = SimpleNamespace(base_snapshot=SimpleNamespace(root=base_root), head_snapshot=SimpleNamespace(root=head_root)) + descriptor = tmp_path / 'project-runtime.json' +> observed = portable_snapshot.run_project_scope_pair(resolution, runtime=object(), options=runner.ReviewOptions(project_runtime=descriptor), scope_evidence={'assurance_kind': 'range_candidate'}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.portable_snapshot' has no attribute 'run_project_scope_pair' + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:52: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_immutable_pair_prepares_each_side_and_downgrades_authority +========================= 1 failed, 2 passed in 0.16s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PIP_METADATA_SELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PIP_METADATA_SELECTION_RED.txt new file mode 100644 index 00000000..1f829d59 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PIP_METADATA_SELECTION_RED.txt @@ -0,0 +1,145 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 25 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 40%] +..........FFFFF [100%] + +=================================== FAILURES =================================== +_________________ test_unlocked_pip_tools_input_is_discovered __________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-142/test_unlocked_pip_tools_input_0') + + def test_unlocked_pip_tools_input_is_discovered(tmp_path: Path) -> None: + (tmp_path / "requirements.in").write_text("requests<3\n") + plan = discover_project(tmp_path) +> assert plan.requirements == ("requirements.in",) +E AssertionError: assert () == ('requirements.in',) +E +E Right contains one more item: 'requirements.in' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:171: AssertionError +_______________ test_legacy_static_python_constraint_is_imported _______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-142/test_legacy_static_python_cons0') + + def test_legacy_static_python_constraint_is_imported(tmp_path: Path) -> None: + (tmp_path / "setup.cfg").write_text("[options]\npython_requires = >=3.11,<3.12\n") +> assert discover_project(tmp_path).requires_python == ">=3.11,<3.12" +E AssertionError: assert '' == '>=3.11,<3.12' +E +E - >=3.11,<3.12 + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:179: AssertionError +________________ test_one_test_extra_is_prepared_automatically _________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-142/test_one_test_extra_is_prepare0') + + def test_one_test_extra_is_prepared_automatically(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[project.optional-dependencies]\ntest=["pytest"]\n') +> assert discover_project(tmp_path).extras == ("test",) +E AssertionError: assert () == ('test',) +E +E Right contains one more item: 'test' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:184: AssertionError +_______________ test_group_and_extra_require_explicit_selection ________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-142/test_group_and_extra_require_e0') + + def test_group_and_extra_require_explicit_selection(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[project.optional-dependencies]\ntest=["pytest"]\n[dependency-groups]\ntest=["pytest<9"]\n') +> with pytest.raises(ProjectRuntimeError, match="test_dependencies_ambiguous"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:189: Failed +___________ test_executable_source_mode_is_part_of_runtime_identity ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-142/test_executable_source_mode_is0') + + def test_executable_source_mode_is_part_of_runtime_identity(tmp_path: Path) -> None: + script = tmp_path / "manage" + script.write_text("#!/usr/bin/env python\nprint('ready')\n") + script.chmod(0o644) + before = discover_project(tmp_path).identity + script.chmod(0o755) +> assert discover_project(tmp_path).identity != before +E AssertionError: assert 'sha256:66295c9551e122e11574b6c4b47fd29cb86881d7c0f9eac36ffe087770cf5fae' != 'sha256:66295c9551e122e11574b6c4b47fd29cb86881d7c0f9eac36ffe087770cf5fae' +E + where 'sha256:66295c9551e122e11574b6c4b47fd29cb86881d7c0f9eac36ffe087770cf5fae' = ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-142/test_exe...=(), extras=(), requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={}).identity +E + where ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-142/test_exe...=(), extras=(), requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={}) = discover_project(PosixPath('/pytest-of-dom/pytest-142/test_executable_source_mode_is0')) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:203: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_unlocked_pip_tools_input_is_discovered +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_legacy_static_python_constraint_is_imported +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_one_test_extra_is_prepared_automatically +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_group_and_extra_require_explicit_selection +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_executable_source_mode_is_part_of_runtime_identity +========================= 5 failed, 20 passed in 0.34s ========================= +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 32 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 31%] +...............FF [ 84%] +tests/unit/specfact_code_review/run/test_portable_snapshot.py ....F [100%] + +=================================== FAILURES =================================== +______ test_pylock_uses_native_pip_input_without_requirement_line_parsing ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-144/test_pylock_uses_native_pip_in0') + + def test_pylock_uses_native_pip_input_without_requirement_line_parsing(tmp_path: Path) -> None: + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\ncreated-by="pip"\n[[packages]]\nname="requests"\nversion="2.32.5"\n') +> assert discover_project(tmp_path).requirements == ("pylock.toml",) +E AssertionError: assert () == ('pylock.toml',) +E +E Right contains one more item: 'pylock.toml' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:208: AssertionError +_________ test_remote_requirement_include_diagnostic_omits_credentials _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-144/test_remote_requirement_includ0') + + def test_remote_requirement_include_diagnostic_omits_credentials(tmp_path: Path) -> None: + (tmp_path / "requirements.txt").write_text("-r https://username:secret-token@example.invalid/requirements.txt\n") + with pytest.raises(ProjectRuntimeError) as error: + discover_project(tmp_path) +> assert "remote_include" in str(error.value) +E AssertionError: assert 'remote_include' in 'project_input_missing:https:/username:secret-token@example.invalid/requirements.txt' +E + where 'project_input_missing:https:/username:secret-token@example.invalid/requirements.txt' = str(ProjectRuntimeError('project_input_missing:https:/username:secret-token@example.invalid/requirements.txt')) +E + where ProjectRuntimeError('project_input_missing:https:/username:secret-token@example.invalid/requirements.txt') = .value + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:218: AssertionError +____________ test_pip_tools_source_input_triggers_automatic_runtime ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-144/test_pip_tools_source_input_tr0') + + def test_pip_tools_source_input_triggers_automatic_runtime(tmp_path: Path) -> None: + from types import SimpleNamespace + from specfact_code_review.run.portable_snapshot import project_runtime_requested + + (tmp_path / "requirements.in").write_text("requests\n") +> assert project_runtime_requested(tmp_path, SimpleNamespace(project_config=None, project_runtime=None)) +E AssertionError: assert False +E + where False = (PosixPath('/pytest-of-dom/pytest-144/test_pip_tools_source_input_tr0'), namespace(project_config=None, project_runtime=None)) +E + where namespace(project_config=None, project_runtime=None) = (project_config=None, project_runtime=None) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:126: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_pylock_uses_native_pip_input_without_requirement_line_parsing +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_remote_requirement_include_diagnostic_omits_credentials +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_pip_tools_source_input_triggers_automatic_runtime +========================= 3 failed, 29 passed in 0.74s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/POETRY_MANAGER_INPUTS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/POETRY_MANAGER_INPUTS_RED.txt new file mode 100644 index 00000000..55e64d4b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/POETRY_MANAGER_INPUTS_RED.txt @@ -0,0 +1,476 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 69 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 14%] +............FFFFF.FFFFFFFFFFFF............. [ 76%] +tests/unit/specfact_code_review/run/test_runtime_adapters.py ....FFFFFF [ 91%] +tests/unit/specfact_code_review/run/test_runtime_interpreter.py ....FF [100%] + +=================================== FAILURES =================================== +__________ test_poetry_python_constraint_intersects_package_metadata ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_poetry_python_constraint_0') + + def test_poetry_python_constraint_intersects_package_metadata(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project]\nrequires-python=">=3.12"\n[tool.poetry.dependencies]\npython=">=3.11,<3.13"\n' + ) + constraint = SpecifierSet(discover_project(tmp_path).requires_python) + assert "3.12.1" in constraint +> assert "3.11.1" not in constraint and "3.13.1" not in constraint +E AssertionError: assert ('3.11.1' not in =3.12')> and '3.13.1' not in =3.12')>) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:190: AssertionError +________ test_unsupported_poetry_python_syntax_has_exact_remedy[^3.12] _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_unsupported_poetry_python0') +declared = '^3.12' + + @pytest.mark.parametrize("declared", ["^3.12", "~3.12", ">=3.11,<3.12 || >=3.13", {"version": ">=3.12"}]) + def test_unsupported_poetry_python_syntax_has_exact_remedy(tmp_path: Path, declared) -> None: + text = '{version=">=3.12"}' if isinstance(declared, dict) else f'"{declared}"' + (tmp_path / "pyproject.toml").write_text(f"[tool.poetry.dependencies]\npython={text}\n") +> with pytest.raises(ProjectRuntimeError, match="project_python_constraint_unsupported") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:197: Failed +________ test_unsupported_poetry_python_syntax_has_exact_remedy[~3.12] _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_unsupported_poetry_python1') +declared = '~3.12' + + @pytest.mark.parametrize("declared", ["^3.12", "~3.12", ">=3.11,<3.12 || >=3.13", {"version": ">=3.12"}]) + def test_unsupported_poetry_python_syntax_has_exact_remedy(tmp_path: Path, declared) -> None: + text = '{version=">=3.12"}' if isinstance(declared, dict) else f'"{declared}"' + (tmp_path / "pyproject.toml").write_text(f"[tool.poetry.dependencies]\npython={text}\n") +> with pytest.raises(ProjectRuntimeError, match="project_python_constraint_unsupported") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:197: Failed +_ test_unsupported_poetry_python_syntax_has_exact_remedy[>=3.11,<3.12 || >=3.13] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_unsupported_poetry_python2') +declared = '>=3.11,<3.12 || >=3.13' + + @pytest.mark.parametrize("declared", ["^3.12", "~3.12", ">=3.11,<3.12 || >=3.13", {"version": ">=3.12"}]) + def test_unsupported_poetry_python_syntax_has_exact_remedy(tmp_path: Path, declared) -> None: + text = '{version=">=3.12"}' if isinstance(declared, dict) else f'"{declared}"' + (tmp_path / "pyproject.toml").write_text(f"[tool.poetry.dependencies]\npython={text}\n") +> with pytest.raises(ProjectRuntimeError, match="project_python_constraint_unsupported") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:197: Failed +______ test_unsupported_poetry_python_syntax_has_exact_remedy[declared3] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_unsupported_poetry_python3') +declared = {'version': '>=3.12'} + + @pytest.mark.parametrize("declared", ["^3.12", "~3.12", ">=3.11,<3.12 || >=3.13", {"version": ">=3.12"}]) + def test_unsupported_poetry_python_syntax_has_exact_remedy(tmp_path: Path, declared) -> None: + text = '{version=">=3.12"}' if isinstance(declared, dict) else f'"{declared}"' + (tmp_path / "pyproject.toml").write_text(f"[tool.poetry.dependencies]\npython={text}\n") +> with pytest.raises(ProjectRuntimeError, match="project_python_constraint_unsupported") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:197: Failed +__ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields0-uv] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_0') +manager = 'uv', fields = ('requirements',) + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:uv' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +_ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields0-hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_1') +manager = 'hatch', fields = ('requirements',) + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:hatch' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +_ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields0-poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_2') +manager = 'poetry', fields = ('requirements',) + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:poetry' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +__ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields1-uv] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_3') +manager = 'uv', fields = ('constraints',) + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:uv' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +_ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields1-hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_4') +manager = 'hatch', fields = ('constraints',) + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:hatch' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +_ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields1-poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_5') +manager = 'poetry', fields = ('constraints',) + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:poetry' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +__ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields2-uv] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_6') +manager = 'uv', fields = ('requirements', 'constraints') + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:uv' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +_ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields2-hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_7') +manager = 'hatch', fields = ('requirements', 'constraints') + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:hatch' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +_ test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields2-poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_non_pip_rejects_explicit_8') +manager = 'poetry', fields = ('requirements', 'constraints') + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) + def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_manager_inputs_unsupported:poetry' +E Actual message: 'project_input_missing:absent.txt' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:217: AssertionError +________ test_native_manager_does_not_select_unrelated_pip_exports[uv] _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_manager_does_not_s0') +manager = 'uv' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + def test_native_manager_does_not_select_unrelated_pip_exports(tmp_path: Path, manager: str) -> None: + (tmp_path / "pyproject.toml").write_text(f"[tool.{manager}]\n" if manager != "hatch" else "[tool.hatch.envs.test]\n") + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\n') + (tmp_path / "requirements.txt").write_text("-r absent.txt\n") +> plan = discover_project(tmp_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:228: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:310: in discover_project + requirements = tuple(values["requirements"] if "requirements" in values else _default_requirements(root)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_manager_does_not_s0') + + def _default_requirements(root: Path) -> list[str]: + texts = [name for name in ("requirements.txt", "requirements.in") if (root / name).is_file()] + if (root / "pylock.toml").is_file() and texts: +> raise ProjectRuntimeError( + f"project_requirements_ambiguous:pylock.toml,{','.join(texts)}; select requirements in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_requirements_ambiguous:pylock.toml,requirements.txt; select requirements in --project-config + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:262: ProjectRuntimeError +_______ test_native_manager_does_not_select_unrelated_pip_exports[hatch] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_manager_does_not_s1') +manager = 'hatch' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + def test_native_manager_does_not_select_unrelated_pip_exports(tmp_path: Path, manager: str) -> None: + (tmp_path / "pyproject.toml").write_text(f"[tool.{manager}]\n" if manager != "hatch" else "[tool.hatch.envs.test]\n") + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\n') + (tmp_path / "requirements.txt").write_text("-r absent.txt\n") +> plan = discover_project(tmp_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:228: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:310: in discover_project + requirements = tuple(values["requirements"] if "requirements" in values else _default_requirements(root)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_manager_does_not_s1') + + def _default_requirements(root: Path) -> list[str]: + texts = [name for name in ("requirements.txt", "requirements.in") if (root / name).is_file()] + if (root / "pylock.toml").is_file() and texts: +> raise ProjectRuntimeError( + f"project_requirements_ambiguous:pylock.toml,{','.join(texts)}; select requirements in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_requirements_ambiguous:pylock.toml,requirements.txt; select requirements in --project-config + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:262: ProjectRuntimeError +______ test_native_manager_does_not_select_unrelated_pip_exports[poetry] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_manager_does_not_s2') +manager = 'poetry' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + def test_native_manager_does_not_select_unrelated_pip_exports(tmp_path: Path, manager: str) -> None: + (tmp_path / "pyproject.toml").write_text(f"[tool.{manager}]\n" if manager != "hatch" else "[tool.hatch.envs.test]\n") + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\n') + (tmp_path / "requirements.txt").write_text("-r absent.txt\n") +> plan = discover_project(tmp_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:228: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:310: in discover_project + requirements = tuple(values["requirements"] if "requirements" in values else _default_requirements(root)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_manager_does_not_s2') + + def _default_requirements(root: Path) -> list[str]: + texts = [name for name in ("requirements.txt", "requirements.in") if (root / name).is_file()] + if (root / "pylock.toml").is_file() and texts: +> raise ProjectRuntimeError( + f"project_requirements_ambiguous:pylock.toml,{','.join(texts)}; select requirements in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_requirements_ambiguous:pylock.toml,requirements.txt; select requirements in --project-config + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:262: ProjectRuntimeError +__ test_native_adapter_rejects_unconsumed_dependency_inputs[requirements-uv] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_adapter_rejects_un0') +manager = 'uv', field = 'requirements' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("field", ["requirements", "constraints"]) + def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan(root=tmp_path, manager=manager, **{field: ("dependencies.txt",)}) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:51: Failed +_ test_native_adapter_rejects_unconsumed_dependency_inputs[requirements-hatch] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_adapter_rejects_un1') +manager = 'hatch', field = 'requirements' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("field", ["requirements", "constraints"]) + def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan(root=tmp_path, manager=manager, **{field: ("dependencies.txt",)}) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:51: Failed +_ test_native_adapter_rejects_unconsumed_dependency_inputs[requirements-poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_adapter_rejects_un2') +manager = 'poetry', field = 'requirements' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("field", ["requirements", "constraints"]) + def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan(root=tmp_path, manager=manager, **{field: ("dependencies.txt",)}) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:51: Failed +___ test_native_adapter_rejects_unconsumed_dependency_inputs[constraints-uv] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_adapter_rejects_un3') +manager = 'uv', field = 'constraints' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("field", ["requirements", "constraints"]) + def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan(root=tmp_path, manager=manager, **{field: ("dependencies.txt",)}) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:51: Failed +_ test_native_adapter_rejects_unconsumed_dependency_inputs[constraints-hatch] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_adapter_rejects_un4') +manager = 'hatch', field = 'constraints' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("field", ["requirements", "constraints"]) + def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan(root=tmp_path, manager=manager, **{field: ("dependencies.txt",)}) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:51: Failed +_ test_native_adapter_rejects_unconsumed_dependency_inputs[constraints-poetry] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_native_adapter_rejects_un5') +manager = 'poetry', field = 'constraints' + + @pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) + @pytest.mark.parametrize("field", ["requirements", "constraints"]) + def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan(root=tmp_path, manager=manager, **{field: ("dependencies.txt",)}) +> with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:51: Failed +__________ test_poetry_constraint_selects_the_only_compatible_worker ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_poetry_constraint_selects0') + + def test_poetry_constraint_selects_the_only_compatible_worker(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[tool.poetry.dependencies]\npython=">=3.12,<3.13"\n') +> assert select_environment(discover_project(tmp_path), current="linux-x86_64-cp313") == "linux-x86_64-cp312" +E AssertionError: assert 'linux-x86_64-cp313' == 'linux-x86_64-cp312' +E +E - linux-x86_64-cp312 +E ? ^ +E + linux-x86_64-cp313 +E ? ^ + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py:41: AssertionError +_________ test_poetry_constraint_preserves_ambiguous_worker_selection __________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-209/test_poetry_constraint_preserv0') + + def test_poetry_constraint_preserves_ambiguous_worker_selection(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[tool.poetry.dependencies]\npython=">=3.11,<3.13"\n') +> with pytest.raises(ValueError, match="project_python_ambiguous"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py:46: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_poetry_python_constraint_intersects_package_metadata +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_unsupported_poetry_python_syntax_has_exact_remedy[^3.12] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_unsupported_poetry_python_syntax_has_exact_remedy[~3.12] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_unsupported_poetry_python_syntax_has_exact_remedy[>=3.11,<3.12 || >=3.13] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_unsupported_poetry_python_syntax_has_exact_remedy[declared3] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields0-uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields0-hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields0-poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields1-uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields1-hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields1-poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields2-uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields2-hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_non_pip_rejects_explicit_dependency_inputs_before_reading[fields2-poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_native_manager_does_not_select_unrelated_pip_exports[uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_native_manager_does_not_select_unrelated_pip_exports[hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_native_manager_does_not_select_unrelated_pip_exports[poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_native_adapter_rejects_unconsumed_dependency_inputs[requirements-uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_native_adapter_rejects_unconsumed_dependency_inputs[requirements-hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_native_adapter_rejects_unconsumed_dependency_inputs[requirements-poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_native_adapter_rejects_unconsumed_dependency_inputs[constraints-uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_native_adapter_rejects_unconsumed_dependency_inputs[constraints-hatch] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_native_adapter_rejects_unconsumed_dependency_inputs[constraints-poetry] +FAILED tests/unit/specfact_code_review/run/test_runtime_interpreter.py::test_poetry_constraint_selects_the_only_compatible_worker +FAILED tests/unit/specfact_code_review/run/test_runtime_interpreter.py::test_poetry_constraint_preserves_ambiguous_worker_selection +======================== 25 failed, 44 passed in 0.89s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PORTABLE_RANGE_SELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PORTABLE_RANGE_SELECTION_RED.txt new file mode 100644 index 00000000..6d465baf --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PORTABLE_RANGE_SELECTION_RED.txt @@ -0,0 +1,645 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 31 items / 23 deselected / 8 selected + +tests/unit/specfact_code_review/run/test_portable_snapshot.py FF.FFF.F [100%] + +=================================== FAILURES =================================== +_ test_portable_range_keeps_absent_selection_out_of_unrelated_findings[False-deleted] _ + +tmp_path = PosixPath('/test_portable_range_keeps_abse0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108f7b6f0> +change = 'deleted', preparation_fails = False + + @pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) + @pytest.mark.parametrize("preparation_fails", [False, True]) + def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool + ) -> None: + source = b"def check():\n return missing_name\n" + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + resolution = SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={selected[0]: "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] +> assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings +E AssertionError: assert [('unrelated....py', 'fixed')] == [('deleted.py', 'fixed')] +E +E At index 0 diff: ('unrelated.py', 'introduced') != ('deleted.py', 'fixed') +E Left contains one more item: ('deleted.py', 'fixed') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:177: AssertionError +_ test_portable_range_keeps_absent_selection_out_of_unrelated_findings[False-added] _ + +tmp_path = PosixPath('/test_portable_range_keeps_abse1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1096f0e90> +change = 'added', preparation_fails = False + + @pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) + @pytest.mark.parametrize("preparation_fails", [False, True]) + def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool + ) -> None: + source = b"def check():\n return missing_name\n" + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + resolution = SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={selected[0]: "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] +> assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings +E AssertionError: assert [('added.py',...py', 'fixed')] == [('added.py', 'introduced')] +E +E Left contains one more item: ('unrelated.py', 'fixed') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:177: AssertionError +_ test_portable_range_keeps_absent_selection_out_of_unrelated_findings[False-empty] _ + +tmp_path = PosixPath('/test_portable_range_keeps_abse3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ae738a0> +change = 'empty', preparation_fails = False + + @pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) + @pytest.mark.parametrize("preparation_fails", [False, True]) + def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool + ) -> None: + source = b"def check():\n return missing_name\n" + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + resolution = SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={selected[0]: "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] +> assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings +E AssertionError: assert [('unrelated.... 'unchanged')] == [] +E +E Left contains one more item: ('unrelated.py', 'unchanged') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:177: AssertionError +_ test_portable_range_keeps_absent_selection_out_of_unrelated_findings[True-deleted] _ + +tmp_path = PosixPath('/test_portable_range_keeps_abse4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ae73df0> +change = 'deleted', preparation_fails = True + + @pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) + @pytest.mark.parametrize("preparation_fails", [False, True]) + def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool + ) -> None: + source = b"def check():\n return missing_name\n" + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + resolution = SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={selected[0]: "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] +> assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings +E AssertionError: assert [('unrelated....py', 'fixed')] == [('deleted.py', 'fixed')] +E +E At index 0 diff: ('unrelated.py', 'introduced') != ('deleted.py', 'fixed') +E Left contains one more item: ('deleted.py', 'fixed') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:177: AssertionError +_ test_portable_range_keeps_absent_selection_out_of_unrelated_findings[True-added] _ + +tmp_path = PosixPath('/test_portable_range_keeps_abse5') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10afab750> +change = 'added', preparation_fails = True + + @pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) + @pytest.mark.parametrize("preparation_fails", [False, True]) + def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool + ) -> None: + source = b"def check():\n return missing_name\n" + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + resolution = SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={selected[0]: "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] +> assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings +E AssertionError: assert [('added.py',...py', 'fixed')] == [('added.py', 'introduced')] +E +E Left contains one more item: ('unrelated.py', 'fixed') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:177: AssertionError +_ test_portable_range_keeps_absent_selection_out_of_unrelated_findings[True-empty] _ + +tmp_path = PosixPath('/test_portable_range_keeps_abse7') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10afae4e0> +change = 'empty', preparation_fails = True + + @pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) + @pytest.mark.parametrize("preparation_fails", [False, True]) + def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool + ) -> None: + source = b"def check():\n return missing_name\n" + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + resolution = SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={selected[0]: "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] +> assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings +E AssertionError: assert [('unrelated.... 'unchanged')] == [] +E +E Left contains one more item: ('unrelated.py', 'unchanged') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:177: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_portable_range_keeps_absent_selection_out_of_unrelated_findings[False-deleted] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_portable_range_keeps_absent_selection_out_of_unrelated_findings[False-added] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_portable_range_keeps_absent_selection_out_of_unrelated_findings[False-empty] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_portable_range_keeps_absent_selection_out_of_unrelated_findings[True-deleted] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_portable_range_keeps_absent_selection_out_of_unrelated_findings[True-added] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_portable_range_keeps_absent_selection_out_of_unrelated_findings[True-empty] +================== 6 failed, 2 passed, 23 deselected in 0.29s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_ATTACHED_VCS_GIT_EVIDENCE.md b/openspec/changes/code-review-16-portable-project-runtime/PR478_ATTACHED_VCS_GIT_EVIDENCE.md new file mode 100644 index 00000000..d0c5e71a --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_ATTACHED_VCS_GIT_EVIDENCE.md @@ -0,0 +1,42 @@ +# PR #478: retain declared Git during sanitized VCS export + +The native run `35081982712` exposed `FileNotFoundError: git` in VCS context +inspection: the worker exposes declared Git in its private runtime `bin`, while +VCS inspection intentionally resets PATH to the system default. The canonical +OpenSpec scenarios were added before the regression tests and production edit. + +`PR478_ATTACHED_VCS_GIT_RED.txt` records seven failing regression cases and one +passing host-isolation control before the correction. The positive regression +executes actual Git through a fixture launcher while the system-default PATH +contains no Git; it reproduced the original missing-executable failure. +Negative cases require precise rejection of undeclared, differently owned, +changed, missing or symlinked launchers and a copied rather than inherited +worker-context inode. The synthetic mount layout is a host test fixture; it is +not Linux namespace acceptance evidence. + +The correction reuses `target_launch.execution_domain()` to verify the existing +read-only startup context. Only in that context can VCS select the fixed +attached `bin/git`, and only when the runtime descriptor declares Git and the +controller-generated inventory digest matches a contained regular launcher. +The complete artifact was already verified before worker entry; this check does +not attempt a new independent whole-artifact attestation. Host lookup still +uses only `os.defpath`. Neither ambient PATH nor environment markers select the +attached executable. All existing Git configuration, hook, replacement-object, +graft, network acquisition and prompting controls remain unchanged. + +The positive regression was then parameterized by Git isolation control to +retain individually attributable checks without duplicating the production +configuration literal. Final focused validation on 2026-09-16: + +```sh +SPECFACT_CLI_REPO=/private/tmp/specfact-473-core .venv/bin/python -m pytest \ + tests/unit/specfact_code_review/run/test_runtime_vcs.py \ + tests/unit/specfact_code_review/run/test_snapshot_activation.py -q --tb=short +``` + +Result: **69 passed in 10.09 seconds**, macOS, Python 3.14.7, pytest 9.1.1. +Focused BasedPyright found zero errors or warnings; Ruff lint/format passed. +Direct Pylint reported only ten unchanged protected-member accesses in earlier +VCS tests, and no production or newly added test findings. New production helper +complexity is eight and three. The subsequent real Linux capsule run remains +required before claiming native acceptance. diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_ATTACHED_VCS_GIT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_ATTACHED_VCS_GIT_RED.txt new file mode 100644 index 00000000..d3300306 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_ATTACHED_VCS_GIT_RED.txt @@ -0,0 +1,93 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 49 items / 41 deselected / 8 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py FFFFFF.F [100%] + +=================================== FAILURES =================================== +________________ test_attached_vcs_git_survives_sanitized_path _________________ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:20: in _git_result + return subprocess.run( +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:555: in run + with Popen(*popenargs, **kwargs) as process: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:1039: in __init__ + self._execute_child(args, executable, preexec_fn, close_fds, +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:1990: in _execute_child + raise child_exception_type(errno_num, err_msg, err_filename) +E FileNotFoundError: [Errno 2] No such file or directory: 'git' + +The above exception was the direct cause of the following exception: +tests/unit/specfact_code_review/run/test_runtime_vcs.py:666: in test_attached_vcs_git_survives_sanitized_path + selected = vcs_context(attached_vcs_git["repository"]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:83: in vcs_context + selected = _git(root, "rev-parse", "--verify", commit + "^{commit}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:63: in _git + return _git_bytes(root, *arguments).decode("utf-8").strip() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:52: in _git_bytes + result = _git_result(root, *arguments, input_data=input_data, shallow_file=shallow_file) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:46: in _git_result + raise ProjectRuntimeError( +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export +________ test_attached_vcs_rejects_unverified_git_launcher[undeclared] _________ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:698: in test_attached_vcs_rejects_unverified_git_launcher + with pytest.raises(ProjectRuntimeError, match="project_git_attached_tool_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_git_attached_tool_invalid' +E Actual message: 'project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export' +_______ test_attached_vcs_rejects_unverified_git_launcher[wrong-origin] ________ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:698: in test_attached_vcs_rejects_unverified_git_launcher + with pytest.raises(ProjectRuntimeError, match="project_git_attached_tool_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_git_attached_tool_invalid' +E Actual message: 'project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export' +_______ test_attached_vcs_rejects_unverified_git_launcher[wrong-digest] ________ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:698: in test_attached_vcs_rejects_unverified_git_launcher + with pytest.raises(ProjectRuntimeError, match="project_git_attached_tool_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_git_attached_tool_invalid' +E Actual message: 'project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export' +__________ test_attached_vcs_rejects_unverified_git_launcher[missing] __________ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:698: in test_attached_vcs_rejects_unverified_git_launcher + with pytest.raises(ProjectRuntimeError, match="project_git_attached_tool_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_git_attached_tool_invalid' +E Actual message: 'project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export' +__________ test_attached_vcs_rejects_unverified_git_launcher[symlink] __________ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:698: in test_attached_vcs_rejects_unverified_git_launcher + with pytest.raises(ProjectRuntimeError, match="project_git_attached_tool_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_git_attached_tool_invalid' +E Actual message: 'project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export' +____________ test_forged_worker_context_cannot_select_attached_git _____________ +tests/unit/specfact_code_review/run/test_runtime_vcs.py:734: in test_forged_worker_context_cannot_select_attached_git + with pytest.raises(ProjectRuntimeError, match="project_git_attached_context_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_git_attached_context_invalid' +E Actual message: 'project_git_snapshot_failed:rev-parse:FileNotFoundError; retry local metadata export' +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_attached_vcs_git_survives_sanitized_path +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_attached_vcs_rejects_unverified_git_launcher[undeclared] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_attached_vcs_rejects_unverified_git_launcher[wrong-origin] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_attached_vcs_rejects_unverified_git_launcher[wrong-digest] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_attached_vcs_rejects_unverified_git_launcher[missing] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_attached_vcs_rejects_unverified_git_launcher[symlink] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_forged_worker_context_cannot_select_attached_git +================== 7 failed, 1 passed, 41 deselected in 0.92s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_CASE_IDENTITIES_EVIDENCE.md b/openspec/changes/code-review-16-portable-project-runtime/PR478_CASE_IDENTITIES_EVIDENCE.md new file mode 100644 index 00000000..45dd70c9 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_CASE_IDENTITIES_EVIDENCE.md @@ -0,0 +1,20 @@ +# Case-sensitive snapshot test coverage + +Native run [35073276747](https://github.com/nold-ai/specfact-cli-modules/actions/runs/35073276747) recorded two skipped case-collision tests on Linux. These tests previously exercised only case-insensitive filesystems. + +The test now asserts the actual filesystem contract on both platforms: reject case aliases when the filesystem conflates them; otherwise materialize both exact Git blob contents, require distinct file identities (and distinct parent directories in the directory case), and verify the complete resulting tree. No runtime implementation or review failure policy changed, and the original rejection assertion remains. + +Local validation on 2026-09-16 (Europe/Berlin), macOS / Python 3.14.7: + +```text +hatch run python -m pytest tests/unit/specfact_code_review/run/test_runner.py -k materialization_preserves_case_identities -q +2 passed, 406 deselected in 0.41s +hatch run python -m ruff check tests/unit/specfact_code_review/run/test_runner.py +All checks passed! +hatch run python -m ruff format --check tests/unit/specfact_code_review/run/test_runner.py +1 file already formatted +``` + +Linux capsule execution of the case-sensitive branch remains pending the next native run. This test-only expansion is not evidence of new production behavior or a waiver for skipped required analysis. + +Independent review found no functional findings. The new assertions were extracted into a named helper after review identified added test complexity; final Radon complexities are 6 for the test and 7 for its helper. All assertions remain. diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_CONTROLLED_PACKAGE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_CONTROLLED_PACKAGE_RED.txt new file mode 100644 index 00000000..395e0382 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_CONTROLLED_PACKAGE_RED.txt @@ -0,0 +1,569 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 47 items + +tests/unit/test_external_capsule_corpus.py ..........FFFFFFFFFFFFF...... [ 61%] +.................. [100%] + +=================================== FAILURES =================================== +_ test_controlled_fixture_executes_inside_unchanged_native_measurement[source_pkgs] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107032e70> +scope = 'source_pkgs' + + @pytest.mark.parametrize("scope", ["source_pkgs", "omit_only"]) + def test_controlled_fixture_executes_inside_unchanged_native_measurement(tmp_path, monkeypatch, scope) -> None: + """The generated test must really call the bad function under native coverage filters.""" + module = _load() + root = tmp_path / "upstream" + (root / "tests").mkdir(parents=True) + (root / "src/customer").mkdir(parents=True) + (root / "src/customer/__init__.py").write_text("") + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\npythonpath=["src"]\n') + configured = "source_pkgs = customer, tests" if scope == "source_pkgs" else "omit = customer/unused/*" + (root / ".coveragerc").write_text(f"[run]\n{configured}\n") + original = module.tracked_identity(root) + + def review(copy, _evidence, _config, paths, **_kwargs): + report = tmp_path / "native-coverage.json" + completed = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "pytest_cov", + "--cov", + f"--cov-report=json:{report}", + paths[1], + ], + cwd=copy, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / ".coverage")}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + assert completed.returncode == 1, completed.stdout + completed.stderr + assert "SPECFACT_CONTROLLED_473" in completed.stdout + assert "injected wrong type" in completed.stdout + payload = json.loads(report.read_text()) + selected = "src/customer/specfact_controlled.py" + assert paths == [selected, "tests/test_specfact_controlled.py"] + assert payload["files"][selected]["executed_lines"] == [1, 2] + assert module.tracked_identity(root) == original + assert (copy / ".coveragerc").read_bytes() == (root / ".coveragerc").read_bytes() + return _detected_controlled_findings() + + monkeypatch.setattr(module, "review", review) +> _invoke_controlled(module, root, tmp_path, tmp_path, _controlled_entry()) + +tests/unit/test_external_capsule_corpus.py:231: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/test_external_capsule_corpus.py:177: in _invoke_controlled + module.controlled_defect(root, evidence, workspace / "config.toml", workspace, **options) +scripts/external_capsule_corpus.py:373: in controlled_defect + report = review( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +copy = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut0/controlled/upstream') +_evidence = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut0') +_config = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut0/config.toml') +paths = ['specfact_controlled.py', 'tests/test_specfact_controlled.py'] +_kwargs = {'name': 'controlled-defect'} + + def review(copy, _evidence, _config, paths, **_kwargs): + report = tmp_path / "native-coverage.json" + completed = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "pytest_cov", + "--cov", + f"--cov-report=json:{report}", + paths[1], + ], + cwd=copy, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / ".coverage")}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + assert completed.returncode == 1, completed.stdout + completed.stderr + assert "SPECFACT_CONTROLLED_473" in completed.stdout +> assert "injected wrong type" in completed.stdout +E AssertionError: assert 'injected wrong type' in 'F\nWARNING: Failed to generate report: No data to report.\n\n ...===============\nFAILED tests/test_specfact_controlled.py::test_specfact_controlled_failure - ...\n1 failed in 0.01s\n' +E + where 'F\nWARNING: Failed to generate report: No data to report.\n\n ...===============\nFAILED tests/test_specfact_controlled.py::test_specfact_controlled_failure - ...\n1 failed in 0.01s\n' = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-m', 'pytest', '-q', '-p', 'pytes...tWarning: Failed to generate report: No data to report.\n\n warnings.warn(CovReportWarning(message), stacklevel=1)\n').stdout + +tests/unit/test_external_capsule_corpus.py:221: AssertionError +_ test_controlled_fixture_executes_inside_unchanged_native_measurement[omit_only] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1070218c0> +scope = 'omit_only' + + @pytest.mark.parametrize("scope", ["source_pkgs", "omit_only"]) + def test_controlled_fixture_executes_inside_unchanged_native_measurement(tmp_path, monkeypatch, scope) -> None: + """The generated test must really call the bad function under native coverage filters.""" + module = _load() + root = tmp_path / "upstream" + (root / "tests").mkdir(parents=True) + (root / "src/customer").mkdir(parents=True) + (root / "src/customer/__init__.py").write_text("") + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\npythonpath=["src"]\n') + configured = "source_pkgs = customer, tests" if scope == "source_pkgs" else "omit = customer/unused/*" + (root / ".coveragerc").write_text(f"[run]\n{configured}\n") + original = module.tracked_identity(root) + + def review(copy, _evidence, _config, paths, **_kwargs): + report = tmp_path / "native-coverage.json" + completed = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "pytest_cov", + "--cov", + f"--cov-report=json:{report}", + paths[1], + ], + cwd=copy, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / ".coverage")}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + assert completed.returncode == 1, completed.stdout + completed.stderr + assert "SPECFACT_CONTROLLED_473" in completed.stdout + assert "injected wrong type" in completed.stdout + payload = json.loads(report.read_text()) + selected = "src/customer/specfact_controlled.py" + assert paths == [selected, "tests/test_specfact_controlled.py"] + assert payload["files"][selected]["executed_lines"] == [1, 2] + assert module.tracked_identity(root) == original + assert (copy / ".coveragerc").read_bytes() == (root / ".coveragerc").read_bytes() + return _detected_controlled_findings() + + monkeypatch.setattr(module, "review", review) +> _invoke_controlled(module, root, tmp_path, tmp_path, _controlled_entry()) + +tests/unit/test_external_capsule_corpus.py:231: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +tests/unit/test_external_capsule_corpus.py:177: in _invoke_controlled + module.controlled_defect(root, evidence, workspace / "config.toml", workspace, **options) +scripts/external_capsule_corpus.py:373: in controlled_defect + report = review( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +copy = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut1/controlled/upstream') +_evidence = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut1') +_config = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_execut1/config.toml') +paths = ['specfact_controlled.py', 'tests/test_specfact_controlled.py'] +_kwargs = {'name': 'controlled-defect'} + + def review(copy, _evidence, _config, paths, **_kwargs): + report = tmp_path / "native-coverage.json" + completed = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "pytest_cov", + "--cov", + f"--cov-report=json:{report}", + paths[1], + ], + cwd=copy, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / ".coverage")}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + assert completed.returncode == 1, completed.stdout + completed.stderr + assert "SPECFACT_CONTROLLED_473" in completed.stdout +> assert "injected wrong type" in completed.stdout +E AssertionError: assert 'injected wrong type' in 'F [100%]\n=================================== ...===============\nFAILED tests/test_specfact_controlled.py::test_specfact_controlled_failure - ...\n1 failed in 0.01s\n' +E + where 'F [100%]\n=================================== ...===============\nFAILED tests/test_specfact_controlled.py::test_specfact_controlled_failure - ...\n1 failed in 0.01s\n' = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-m', 'pytest', '-q', '-p', 'pytes...===\nFAILED tests/test_specfact_controlled.py::test_specfact_controlled_failure - ...\n1 failed in 0.01s\n', stderr='').stdout + +tests/unit/test_external_capsule_corpus.py:221: AssertionError +______ test_controlled_fixture_rejects_unsafe_write_targets[parent-link] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107023130> +unsafe = 'parent-link' + + @pytest.mark.parametrize( + "unsafe", ["parent-link", "test-parent-link", "source-exists", "test-exists", "destination-link"] + ) + def test_controlled_fixture_rejects_unsafe_write_targets(tmp_path, monkeypatch, unsafe) -> None: + """Copy injection cannot follow customer links or overwrite existing fixture paths.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + outside = tmp_path / "outside" + outside.mkdir() + targets = {"parent-link": root / "src/customer", "test-parent-link": root / "tests"} + if unsafe in targets: + targets[unsafe].rmdir() + targets[unsafe].symlink_to(outside, target_is_directory=True) + elif unsafe == "destination-link": + (tmp_path / "controlled").symlink_to(outside, target_is_directory=True) + else: + name = ( + "src/customer/specfact_controlled.py" if unsafe == "source-exists" else "tests/test_specfact_controlled.py" + ) + (root / name).write_text("ORIGINAL = True\n") + original = module.tracked_identity(root) + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|destination)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:258: Failed +____ test_controlled_fixture_rejects_unsafe_write_targets[test-parent-link] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10716c350> +unsafe = 'test-parent-link' + + @pytest.mark.parametrize( + "unsafe", ["parent-link", "test-parent-link", "source-exists", "test-exists", "destination-link"] + ) + def test_controlled_fixture_rejects_unsafe_write_targets(tmp_path, monkeypatch, unsafe) -> None: + """Copy injection cannot follow customer links or overwrite existing fixture paths.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + outside = tmp_path / "outside" + outside.mkdir() + targets = {"parent-link": root / "src/customer", "test-parent-link": root / "tests"} + if unsafe in targets: + targets[unsafe].rmdir() + targets[unsafe].symlink_to(outside, target_is_directory=True) + elif unsafe == "destination-link": + (tmp_path / "controlled").symlink_to(outside, target_is_directory=True) + else: + name = ( + "src/customer/specfact_controlled.py" if unsafe == "source-exists" else "tests/test_specfact_controlled.py" + ) + (root / name).write_text("ORIGINAL = True\n") + original = module.tracked_identity(root) + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|destination)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:258: Failed +_____ test_controlled_fixture_rejects_unsafe_write_targets[source-exists] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10709c350> +unsafe = 'source-exists' + + @pytest.mark.parametrize( + "unsafe", ["parent-link", "test-parent-link", "source-exists", "test-exists", "destination-link"] + ) + def test_controlled_fixture_rejects_unsafe_write_targets(tmp_path, monkeypatch, unsafe) -> None: + """Copy injection cannot follow customer links or overwrite existing fixture paths.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + outside = tmp_path / "outside" + outside.mkdir() + targets = {"parent-link": root / "src/customer", "test-parent-link": root / "tests"} + if unsafe in targets: + targets[unsafe].rmdir() + targets[unsafe].symlink_to(outside, target_is_directory=True) + elif unsafe == "destination-link": + (tmp_path / "controlled").symlink_to(outside, target_is_directory=True) + else: + name = ( + "src/customer/specfact_controlled.py" if unsafe == "source-exists" else "tests/test_specfact_controlled.py" + ) + (root / name).write_text("ORIGINAL = True\n") + original = module.tracked_identity(root) + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|destination)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:258: Failed +______ test_controlled_fixture_rejects_unsafe_write_targets[test-exists] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107081400> +unsafe = 'test-exists' + + @pytest.mark.parametrize( + "unsafe", ["parent-link", "test-parent-link", "source-exists", "test-exists", "destination-link"] + ) + def test_controlled_fixture_rejects_unsafe_write_targets(tmp_path, monkeypatch, unsafe) -> None: + """Copy injection cannot follow customer links or overwrite existing fixture paths.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + outside = tmp_path / "outside" + outside.mkdir() + targets = {"parent-link": root / "src/customer", "test-parent-link": root / "tests"} + if unsafe in targets: + targets[unsafe].rmdir() + targets[unsafe].symlink_to(outside, target_is_directory=True) + elif unsafe == "destination-link": + (tmp_path / "controlled").symlink_to(outside, target_is_directory=True) + else: + name = ( + "src/customer/specfact_controlled.py" if unsafe == "source-exists" else "tests/test_specfact_controlled.py" + ) + (root / name).write_text("ORIGINAL = True\n") + original = module.tracked_identity(root) + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|destination)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:258: Failed +____ test_controlled_fixture_rejects_unsafe_write_targets[destination-link] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107082e40> +unsafe = 'destination-link' + + @pytest.mark.parametrize( + "unsafe", ["parent-link", "test-parent-link", "source-exists", "test-exists", "destination-link"] + ) + def test_controlled_fixture_rejects_unsafe_write_targets(tmp_path, monkeypatch, unsafe) -> None: + """Copy injection cannot follow customer links or overwrite existing fixture paths.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + outside = tmp_path / "outside" + outside.mkdir() + targets = {"parent-link": root / "src/customer", "test-parent-link": root / "tests"} + if unsafe in targets: + targets[unsafe].rmdir() + targets[unsafe].symlink_to(outside, target_is_directory=True) + elif unsafe == "destination-link": + (tmp_path / "controlled").symlink_to(outside, target_is_directory=True) + else: + name = ( + "src/customer/specfact_controlled.py" if unsafe == "source-exists" else "tests/test_specfact_controlled.py" + ) + (root / name).write_text("ORIGINAL = True\n") + original = module.tracked_identity(root) + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|destination)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:258: Failed +_ test_controlled_fixture_rejects_invalid_explicit_package_metadata[source-../outside.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject5') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1071a4830> +field = 'source', value = '../outside.py' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("source", "../outside.py"), + ("source", "/outside.py"), + ("import", "customer;raise RuntimeError"), + ("import", "other.specfact_controlled"), + ("import", "customer.class"), + ], + ) + def test_controlled_fixture_rejects_invalid_explicit_package_metadata(tmp_path, monkeypatch, field, value) -> None: + """An explicit corpus path and import must describe the same contained Python module.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + entry = _controlled_entry() + entry["controlled_defect"][field] = value + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|import)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:283: Failed +_ test_controlled_fixture_rejects_invalid_explicit_package_metadata[source-/outside.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject6') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1071a73f0> +field = 'source', value = '/outside.py' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("source", "../outside.py"), + ("source", "/outside.py"), + ("import", "customer;raise RuntimeError"), + ("import", "other.specfact_controlled"), + ("import", "customer.class"), + ], + ) + def test_controlled_fixture_rejects_invalid_explicit_package_metadata(tmp_path, monkeypatch, field, value) -> None: + """An explicit corpus path and import must describe the same contained Python module.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + entry = _controlled_entry() + entry["controlled_defect"][field] = value + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|import)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:283: Failed +_ test_controlled_fixture_rejects_invalid_explicit_package_metadata[import-customer;raise RuntimeError] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject7') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10719eea0> +field = 'import', value = 'customer;raise RuntimeError' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("source", "../outside.py"), + ("source", "/outside.py"), + ("import", "customer;raise RuntimeError"), + ("import", "other.specfact_controlled"), + ("import", "customer.class"), + ], + ) + def test_controlled_fixture_rejects_invalid_explicit_package_metadata(tmp_path, monkeypatch, field, value) -> None: + """An explicit corpus path and import must describe the same contained Python module.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + entry = _controlled_entry() + entry["controlled_defect"][field] = value + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|import)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:283: Failed +_ test_controlled_fixture_rejects_invalid_explicit_package_metadata[import-other.specfact_controlled] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject8') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1071be390> +field = 'import', value = 'other.specfact_controlled' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("source", "../outside.py"), + ("source", "/outside.py"), + ("import", "customer;raise RuntimeError"), + ("import", "other.specfact_controlled"), + ("import", "customer.class"), + ], + ) + def test_controlled_fixture_rejects_invalid_explicit_package_metadata(tmp_path, monkeypatch, field, value) -> None: + """An explicit corpus path and import must describe the same contained Python module.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + entry = _controlled_entry() + entry["controlled_defect"][field] = value + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|import)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:283: Failed +_ test_controlled_fixture_rejects_invalid_explicit_package_metadata[import-customer.class] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-892/test_controlled_fixture_reject9') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x106f9a810> +field = 'import', value = 'customer.class' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("source", "../outside.py"), + ("source", "/outside.py"), + ("import", "customer;raise RuntimeError"), + ("import", "other.specfact_controlled"), + ("import", "customer.class"), + ], + ) + def test_controlled_fixture_rejects_invalid_explicit_package_metadata(tmp_path, monkeypatch, field, value) -> None: + """An explicit corpus path and import must describe the same contained Python module.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + entry = _controlled_entry() + entry["controlled_defect"][field] = value + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) +> with pytest.raises(ValueError, match="controlled.*(path|import)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:283: Failed +________ test_pinned_corpus_records_explicit_controlled_package_context ________ + + def test_pinned_corpus_records_explicit_controlled_package_context() -> None: + """Each frozen upstream and reconstruction has its own reviewed package destination.""" + module = _load() + manifest = json.loads(module.MANIFEST.read_text()) + entries = [*manifest["repositories"], *manifest["reconstructions"]] + expected = { + "requests": "requests", + "hatch": "hatch", + "flask": "flask", + "poetry": "poetry", + "hatch-detached": "portable_customer", + } + for entry in entries: + package = expected[entry["name"]] +> assert entry.get("controlled_defect") == { + "source": f"src/{package}/specfact_controlled.py", + "import": f"{package}.specfact_controlled", + } +E AssertionError: assert None == {'source': 'src/requests/specfact_controlled.py', 'import': 'requests.specfact_controlled'} +E + where None = ('controlled_defect') +E + where = {'name': 'requests', 'url': 'https://github.com/psf/requests.git', 'commit': 'dae7ef63b4df6eded86637f251fc4e3a06c3b479', 'manager': 'pip', ...}.get + +tests/unit/test_external_capsule_corpus.py:301: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_executes_inside_unchanged_native_measurement[source_pkgs] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_executes_inside_unchanged_native_measurement[omit_only] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_unsafe_write_targets[parent-link] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_unsafe_write_targets[test-parent-link] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_unsafe_write_targets[source-exists] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_unsafe_write_targets[test-exists] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_unsafe_write_targets[destination-link] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_invalid_explicit_package_metadata[source-../outside.py] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_invalid_explicit_package_metadata[source-/outside.py] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_invalid_explicit_package_metadata[import-customer;raise RuntimeError] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_invalid_explicit_package_metadata[import-other.specfact_controlled] +FAILED tests/unit/test_external_capsule_corpus.py::test_controlled_fixture_rejects_invalid_explicit_package_metadata[import-customer.class] +FAILED tests/unit/test_external_capsule_corpus.py::test_pinned_corpus_records_explicit_controlled_package_context +======================== 13 failed, 34 passed in 0.41s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_CORE_DECLARATION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_CORE_DECLARATION_RED.txt new file mode 100644 index 00000000..b7de295d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_CORE_DECLARATION_RED.txt @@ -0,0 +1,26 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 11 items / 10 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py F [100%] + +=================================== FAILURES =================================== +___________ test_declared_core_dependency_supplies_registry_handoff ____________ + + def test_declared_core_dependency_supplies_registry_handoff() -> None: + """Hermetic Hatch inputs must include the real registry API used by review.""" + root = Path(__file__).resolve().parents[4] + declared = _declared_development_dependencies(root) + requirement = declared.get("specfact-cli") +> assert requirement is not None, "Core installed by developer bootstrap is not a declared target dependency" +E AssertionError: Core installed by developer bootstrap is not a declared target dependency +E assert None is not None + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:194: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_declared_core_dependency_supplies_registry_handoff +======================= 1 failed, 10 deselected in 0.10s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_CORE_NATIVE_RED.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_CORE_NATIVE_RED.json new file mode 100644 index 00000000..d8182f0e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_CORE_NATIVE_RED.json @@ -0,0 +1,43 @@ +{ + "run": 35081982712, + "scope": "actual native BasedPyright missing import, not a parser or environment waiver", + "findings": [ + { + "category": "type_safety", + "severity": "error", + "tool": "basedpyright", + "rule": "reportMissingImports", + "file": "packages/specfact-code-review/src/specfact_code_review/run/runner.py", + "line": 484, + "message": "Import \"specfact_cli.registry\" could not be resolved", + "fixable": false, + "status": "open", + "differential_state": null, + "autofix_available": false, + "blocking": true, + "waiver_reference": null, + "execution_state": null, + "evidence_outcome": null, + "evidence_refs": null, + "confidence": null, + "rewrite_hint": null, + "canonical_pattern": null, + "intent_key": null, + "estimated_deletion_lines": null, + "related_locations": null, + "guidance_kind": null, + "recommended_action": null, + "clean_code_principle": null, + "rationale": null, + "safety_checks": null, + "preserve_reason": null, + "action_status": null, + "before_ref": null, + "after_ref": null, + "improvement": null, + "signal_trace": null, + "preserve_reasons": null, + "remediation_packet": null + } + ] +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_CORPUS_CONTROL_RED.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_CORPUS_CONTROL_RED.json new file mode 100644 index 00000000..5f972aba --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_CORPUS_CONTROL_RED.json @@ -0,0 +1,148 @@ +{ + "status": "FAIL", + "authority": "retained CI regression evidence, not acceptance", + "run": "https://github.com/nold-ai/specfact-cli-modules/actions/runs/35124077143", + "head": "38402b964f8544b48d9501e4847eca3a8be4a8ec", + "injector_sha256": "e433b51e1de4a0b89f1779cd2acc202183fb7e2455651b505ffd4ee76afbb703", + "cold_warm_executions": 30, + "cold_warm_unknown": 0, + "cold_warm_pytest_pass": 30, + "controlled_unknown": [ + { + "abi": "311", + "repository": "requests", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "311", + "repository": "hatch", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "311", + "repository": "flask", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "312", + "repository": "requests", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "312", + "repository": "hatch", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "312", + "repository": "flask", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "313", + "repository": "requests", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "313", + "repository": "hatch", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + }, + { + "abi": "313", + "repository": "flask", + "mode": "controlled-defect", + "verdict": "FAIL", + "unknown": true, + "pytest_outcome": "UNKNOWN", + "exit": 1, + "call_outcomes": { + "failed": 1 + }, + "errors": [ + "Coverage data missing for /opt/specfact/snapshot/specfact_controlled.py" + ] + } + ], + "limitation": "Complete native cold/warm execution does not make the overall corpus pass; required controlled-defect evidence is incomplete. Configured source restrictions remain authoritative and the injected root function was never called." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_BLOCKED_PRELOAD_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_BLOCKED_PRELOAD_RED.txt new file mode 100644 index 00000000..5cfb059d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_BLOCKED_PRELOAD_RED.txt @@ -0,0 +1,140 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 19 items + +tests/unit/specfact_code_review/run/test_target_coverage.py ............ [ 63%] +....F.F [100%] + +=================================== FAILURES =================================== +______________ test_explicit_blocked_cov_plugin_stays_incomplete _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-809/test_explicit_blocked_cov_plug0') + + def test_explicit_blocked_cov_plugin_stays_incomplete(tmp_path): + completed, receipt = _run(_project(tmp_path), tmp_path, ["-p", "no:pytest_cov"]) + assert completed.returncode == 0 +> assert receipt["coverage"] == {} +E AssertionError: assert {'meta': {'fo...'75.00', ...}} == {} +E +E Left contains 3 more items: +E {'files': {'app.py': {'classes': {'': {'excluded_lines': [], +E 'executed_lines': [1, 2, 3], +E 'missing_lines': [4], +E 'start_line': 1, +E 'summary': {'covered_lines': 3,... +E +E ...Full output truncated (68 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_target_coverage.py:267: AssertionError +______ test_verified_preload_does_not_break_strict_rewrite_warning_policy ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-809/test_verified_preload_does_not0') + + def test_verified_preload_does_not_break_strict_rewrite_warning_policy(tmp_path): +> completed, receipt = _run( + _project(tmp_path), tmp_path, ['-W', 'error::pytest.PytestAssertRewriteWarning'] + ) + +tests/unit/specfact_code_review/run/test_target_coverage.py:282: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +source = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-809/test_verified_preload_does_not0/project') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-809/test_verified_preload_does_not0') +extra = ['-W', 'error::pytest.PytestAssertRewriteWarning'], startup = None + + def _run(source, tmp_path, extra=(), *, startup=None): + output = tmp_path / "private.json" + arguments = tmp_path / "arguments.json" + arguments.write_text(json.dumps(extra)) + completed = subprocess.run( + [sys.executable, "-c", _SCRIPT, str(_HELPER), str(source), str(output), str(arguments)], + cwd=source, + env={ + **os.environ, + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "COVERAGE_FILE": str(tmp_path / ".coverage"), + **({"PYTHONPATH": str(startup)} if startup else {}), + }, + text=True, + capture_output=True, + check=False, + timeout=60, + ) + receipt = output.with_suffix(".receipt") +> assert receipt.is_file(), completed.stdout + completed.stderr +E AssertionError: Traceback (most recent call last): +E File "", line 14, in +E code = pytest.main(args) +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 201, in main +E return _main(args=args, plugins=plugins, prog="pytest.main()") +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 223, in _main +E config = _prepareconfig(new_args, plugins, prog=prog) +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 410, in _prepareconfig +E config: Config = pluginmanager.hook.pytest_cmdline_parse( +E ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ +E pluginmanager=pluginmanager, args=args +E ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ) +E ^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__ +E return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult) +E ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec +E return self._inner_hookexec(hook_name, methods, kwargs, firstresult) +E ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall +E raise exception +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall +E teardown.throw(exception) +E ~~~~~~~~~~~~~~^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/helpconfig.py", line 124, in pytest_cmdline_parse +E config = yield +E ^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall +E res = hook_impl.function(*args) +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 1232, in pytest_cmdline_parse +E self.parse(args) +E ~~~~~~~~~~^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 1576, in parse +E self.pluginmanager.consider_preparse(args, exclude_only=False) +E ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 835, in consider_preparse +E self.consider_pluginarg(parg) +E ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 864, in consider_pluginarg +E self.import_plugin(arg, consider_entry_points=True) +E ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 904, in import_plugin +E self.rewrite_hook.mark_rewrite(importspec) +E ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/assertion/rewrite.py", line 277, in mark_rewrite +E self._warn_already_imported(name) +E ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/assertion/rewrite.py", line 284, in _warn_already_imported +E self.config.issue_config_time_warning( +E ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ +E PytestAssertRewriteWarning( +E ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ...<2 lines>... +E stacklevel=5, +E ^^^^^^^^^^^^^ +E ) +E ^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py", line 1654, in issue_config_time_warning +E warnings.warn(warning, stacklevel=stacklevel) +E ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E pytest.PytestAssertRewriteWarning: Module already imported so cannot be rewritten; _specfact_target_coverage +E +E assert False +E + where False = is_file() +E + where is_file = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-809/test_verified_preload_does_not0/private.receipt').is_file + +tests/unit/specfact_code_review/run/test_target_coverage.py:80: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_explicit_blocked_cov_plugin_stays_incomplete +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_verified_preload_does_not_break_strict_rewrite_warning_policy +========================= 2 failed, 17 passed in 3.22s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_BOOTSTRAP_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_BOOTSTRAP_RED.txt new file mode 100644 index 00000000..f4fa02ae --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_BOOTSTRAP_RED.txt @@ -0,0 +1,183 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 8 items + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py FF [ 25%] +.FFFFF [100%] + +=================================== FAILURES =================================== +____ test_pytest_domain_loads_fixed_helper_in_actual_startup_routes[first] _____ + +coverage_runtime = {'PROJECT': PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_pytest_d...ar/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_pytest_domain_loads_fixed0/builtin'), ...} +route = 'first' + + @pytest.mark.parametrize("route", ["first", "nested"]) + def test_pytest_domain_loads_fixed_helper_in_actual_startup_routes(coverage_runtime, route): + if route == "first": + script = "sys.argv=['bootstrap','pytest-observe']; b['main']()" + else: + script = ( + "import os;os.environ['SPECFACT_PROJECT_PYTHON']='1';" + f"runpy.run_path({str(coverage_runtime['HELPERS'] / 'sitecustomize.py')!r});" + "import _specfact_target_coverage as collector; print(collector.OWNER)" + ) + result = _probe(coverage_runtime, script) + assert result.returncode == 0, result.stderr +> assert result.stdout.strip() == "verified-builtin" +E AssertionError: assert 'customer-collision' == 'verified-builtin' +E +E - verified-builtin +E + customer-collision + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:74: AssertionError +____ test_pytest_domain_loads_fixed_helper_in_actual_startup_routes[nested] ____ + +coverage_runtime = {'PROJECT': PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_pytest_d...ar/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_pytest_domain_loads_fixed1/builtin'), ...} +route = 'nested' + + @pytest.mark.parametrize("route", ["first", "nested"]) + def test_pytest_domain_loads_fixed_helper_in_actual_startup_routes(coverage_runtime, route): + if route == "first": + script = "sys.argv=['bootstrap','pytest-observe']; b['main']()" + else: + script = ( + "import os;os.environ['SPECFACT_PROJECT_PYTHON']='1';" + f"runpy.run_path({str(coverage_runtime['HELPERS'] / 'sitecustomize.py')!r});" + "import _specfact_target_coverage as collector; print(collector.OWNER)" + ) + result = _probe(coverage_runtime, script) + assert result.returncode == 0, result.stderr +> assert result.stdout.strip() == "verified-builtin" +E AssertionError: assert 'customer-collision' == 'verified-builtin' +E +E - verified-builtin +E + customer-collision + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:74: AssertionError +___________________ test_loader_reuses_only_its_owned_helper ___________________ + +coverage_runtime = {'PROJECT': PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_loader_r...ar/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_loader_reuses_only_its_ow0/builtin'), ...} + + def test_loader_reuses_only_its_owned_helper(coverage_runtime): + result = _probe(coverage_runtime, "load=b['_load_pytest_coverage']; print(load() is load())") +> assert result.returncode == 0, result.stderr +E AssertionError: Traceback (most recent call last): +E File "", line 1, in +E import runpy,sys; from pathlib import Path; b=runpy.run_path('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_loader_reuses_only_its_ow0/builtin/specfact_code_review/run/target_bootstrap.py'); load=b['_load_pytest_coverage']; print(load() is load()) +E ~^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: '_load_pytest_coverage' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-I', '-S', '-c', "import runpy,sy... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:85: AssertionError +___ test_preloaded_coverage_alias_cannot_claim_builtin_ownership[different] ____ + +coverage_runtime = {'PROJECT': PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_preloade...ar/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_preloaded_coverage_alias_0/builtin'), ...} +forged_origin = 'different' + + @pytest.mark.parametrize("forged_origin", ["different", "same"]) + def test_preloaded_coverage_alias_cannot_claim_builtin_ownership(coverage_runtime, forged_origin): + path = coverage_runtime["HELPERS"] / "target_coverage.py" if forged_origin == "same" else Path("/customer/collector.py") + script = ( + "import types,importlib.util; alias='_specfact_target_coverage'; fake=types.ModuleType(alias);" + f"fake.__file__={str(path)!r};fake.__spec__=importlib.util.spec_from_file_location(alias,{str(path)!r});" + "sys.modules[alias]=fake; b['_load_pytest_coverage']()" + ) + result = _probe(coverage_runtime, script) + assert result.returncode != 0 +> assert "project_pytest_coverage_helper_origin_mismatch" in result.stderr +E assert 'project_pytest_coverage_helper_origin_mismatch' in 'Traceback (most recent call last):\n File "", line 1, in \n import runpy,sys; from pathlib import... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n' +E + where 'Traceback (most recent call last):\n File "", line 1, in \n import runpy,sys; from pathlib import... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n' = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-I', '-S', '-c', "import runpy,sy... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n').stderr + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:99: AssertionError +______ test_preloaded_coverage_alias_cannot_claim_builtin_ownership[same] ______ + +coverage_runtime = {'PROJECT': PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_preloade...ar/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_preloaded_coverage_alias_1/builtin'), ...} +forged_origin = 'same' + + @pytest.mark.parametrize("forged_origin", ["different", "same"]) + def test_preloaded_coverage_alias_cannot_claim_builtin_ownership(coverage_runtime, forged_origin): + path = coverage_runtime["HELPERS"] / "target_coverage.py" if forged_origin == "same" else Path("/customer/collector.py") + script = ( + "import types,importlib.util; alias='_specfact_target_coverage'; fake=types.ModuleType(alias);" + f"fake.__file__={str(path)!r};fake.__spec__=importlib.util.spec_from_file_location(alias,{str(path)!r});" + "sys.modules[alias]=fake; b['_load_pytest_coverage']()" + ) + result = _probe(coverage_runtime, script) + assert result.returncode != 0 +> assert "project_pytest_coverage_helper_origin_mismatch" in result.stderr +E assert 'project_pytest_coverage_helper_origin_mismatch' in 'Traceback (most recent call last):\n File "", line 1, in \n import runpy,sys; from pathlib import... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n' +E + where 'Traceback (most recent call last):\n File "", line 1, in \n import runpy,sys; from pathlib import... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n' = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-I', '-S', '-c', "import runpy,sy... ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n').stderr + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:99: AssertionError +__________ test_failed_helper_load_does_not_leave_authenticated_alias __________ + +coverage_runtime = {'PROJECT': PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_failed_h...ar/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_failed_helper_load_does_n0/builtin'), ...} + + def test_failed_helper_load_does_not_leave_authenticated_alias(coverage_runtime): + (coverage_runtime["HELPERS"] / "target_coverage.py").write_text("raise RuntimeError('fixture failure')\n") + script = "\ntry:\n b['_load_pytest_coverage']()\nexcept RuntimeError:\n print('_specfact_target_coverage' in sys.modules)\n" + result = _probe(coverage_runtime, script) +> assert result.returncode == 0, result.stderr +E AssertionError: Traceback (most recent call last): +E File "", line 3, in +E b['_load_pytest_coverage']() +E ~^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: '_load_pytest_coverage' +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-I', '-S', '-c', "import runpy,sy...n \n b[\'_load_pytest_coverage\']()\n ~^^^^^^^^^^^^^^^^^^^^^^^^^\nKeyError: \'_load_pytest_coverage\'\n').returncode + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:106: AssertionError +____________ test_changed_collector_bytes_invalidate_runtime_cache _____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-770/test_changed_collector_bytes_i0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109c56fd0> + + def test_changed_collector_bytes_invalidate_runtime_cache(tmp_path, monkeypatch): + helpers = tmp_path / "helpers" + helpers.mkdir() + for path in Path(runtime_builder.__file__).parent.glob("*.py"): + (helpers / path.name).write_bytes(path.read_bytes()) + helper = helpers / "target_coverage.py" + helper.write_text("POLICY = 1\n") + lock = Path("resources/contracts/pr-range-v1-toolchain-lock.json") + (tmp_path / lock).parent.mkdir(parents=True) + (tmp_path / lock).write_bytes((Path(runtime_builder.__file__).parents[1] / lock).read_bytes()) + monkeypatch.setattr(runtime_builder, "__file__", str(helpers / "runtime_builder.py")) + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"fixture trust") + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "fixed-git") + plan = ProjectPlan(tmp_path, manager="pip") + worker = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + identities = [] + digest = runtime_builder.document_digest + + def record_identity(value): + identities.append(value) + return digest(value) + + monkeypatch.setattr(runtime_builder, "document_digest", record_identity) + for content in ("POLICY = 1\n", "POLICY = 2\n"): + helper.write_text(content) + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + runtime_builder.prepare_runtime(plan, runtime=worker, cache_root=tmp_path / "cache", offline=True) +> assert digest(identities[0]) != digest(identities[1]) +E AssertionError: assert 'sha256:41cad88a938b82ca4ef5e5b270a58f68d3b6946ad4911857033a7a206c64fb10' != 'sha256:41cad88a938b82ca4ef5e5b270a58f68d3b6946ad4911857033a7a206c64fb10' +E + where 'sha256:41cad88a938b82ca4ef5e5b270a58f68d3b6946ad4911857033a7a206c64fb10' = ({'project': 'sha256:dca79eb236050d6d0426f04822b5df9f672a9688d2e983ea0717134af043b89c', 'environment': 'linux-x86_64-cp... 'worker': 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'manager': ('pip==26.2.1',), ...}) +E + and 'sha256:41cad88a938b82ca4ef5e5b270a58f68d3b6946ad4911857033a7a206c64fb10' = ({'project': 'sha256:dca79eb236050d6d0426f04822b5df9f672a9688d2e983ea0717134af043b89c', 'environment': 'linux-x86_64-cp... 'worker': 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'manager': ('pip==26.2.1',), ...}) + +tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py:137: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_pytest_domain_loads_fixed_helper_in_actual_startup_routes[first] +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_pytest_domain_loads_fixed_helper_in_actual_startup_routes[nested] +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_loader_reuses_only_its_owned_helper +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_preloaded_coverage_alias_cannot_claim_builtin_ownership[different] +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_preloaded_coverage_alias_cannot_claim_builtin_ownership[same] +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_failed_helper_load_does_not_leave_authenticated_alias +FAILED tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py::test_changed_collector_bytes_invalidate_runtime_cache +========================= 7 failed, 1 passed in 0.65s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_EFFECTIVE_SCOPE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_EFFECTIVE_SCOPE_RED.txt new file mode 100644 index 00000000..0c78e90d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_EFFECTIVE_SCOPE_RED.txt @@ -0,0 +1,29 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/specfact_code_review/run/test_target_coverage.py F [100%] + +=================================== FAILURES =================================== +__________ test_effective_scope_receipt_reflects_native_cli_overrides __________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-817/test_effective_scope_receipt_r0') + + def test_effective_scope_receipt_reflects_native_cli_overrides(tmp_path): + source = _project(tmp_path, native='--cov=unrelated --cov-branch --cov-fail-under=0 --cov-precision=4') + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 + configuration = receipt['coverage_configuration'] + assert configuration['run']['source'] == ['app'] +> assert configuration['effective']['run']['source'] == ['unrelated'] + ^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'effective' + +tests/unit/specfact_code_review/run/test_target_coverage.py:293: KeyError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_effective_scope_receipt_reflects_native_cli_overrides +============================== 1 failed in 0.26s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_LIFECYCLE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_LIFECYCLE_RED.txt new file mode 100644 index 00000000..8253df8e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_LIFECYCLE_RED.txt @@ -0,0 +1,96 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 12 items + +tests/unit/specfact_code_review/run/test_target_coverage.py .......FF... [100%] + +=================================== FAILURES =================================== +______ test_excluded_measurement_preserves_success_and_incomplete_reason _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-781/test_excluded_measurement_pres0') + + def test_excluded_measurement_preserves_success_and_incomplete_reason(tmp_path): + source = _project(tmp_path) + config = source / 'pyproject.toml' + config.write_text(config.read_text().replace('source=["app"]', 'source=["not_installed_anywhere"]')) +> completed, receipt = _run(source, tmp_path) + ^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_target_coverage.py:161: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +source = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-781/test_excluded_measurement_pres0/project') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-781/test_excluded_measurement_pres0') +extra = (), startup = None + + def _run(source, tmp_path, extra=(), *, startup=None): + output = tmp_path / "private.json" + arguments = tmp_path / "arguments.json" + arguments.write_text(json.dumps(extra)) + completed = subprocess.run( + [sys.executable, "-c", _SCRIPT, str(_HELPER), str(source), str(output), str(arguments)], + cwd=source, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / ".coverage"), + **({"PYTHONPATH": str(startup)} if startup else {})}, + text=True, + capture_output=True, + check=False, + timeout=60, + ) + receipt = output.with_suffix(".receipt") +> assert receipt.is_file(), completed.stdout + completed.stderr +E AssertionError: . [100%] +E =============================== warnings summary =============================== +E ../../../../../../../../../tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py:864 +E /private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/_pytest/config/__init__.py:864: PytestAssertRewriteWarning: Module already imported so cannot be rewritten; _specfact_target_coverage +E self.import_plugin(arg, consider_entry_points=True) +E +E -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +E 1 passed, 1 warning in 0.01s +E /private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/coverage/inorout.py:558: CoverageWarning: Module not_installed_anywhere was never imported. (module-not-imported); see https://coverage.readthedocs.io/en/7.16.1/messages.html#warning-module-not-imported +E self.warn(f"Module {pkg} was never imported.", slug="module-not-imported") +E /private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/coverage/control.py:967: CoverageWarning: No data was collected. (no-data-collected); see https://coverage.readthedocs.io/en/7.16.1/messages.html#warning-no-data-collected +E self._warn("No data was collected.", slug="no-data-collected") +E Traceback (most recent call last): +E File "", line 15, in +E evidence = module.evidence() +E File "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", line 163, in evidence +E payload = _export() +E File "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", line 150, in _export +E total = controller.cov.json_report(outfile=str(output)) +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/coverage/control.py", line 1336, in json_report +E return render_report(self.config.json_output, JsonReporter(self), morfs, self._message) +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/coverage/report_core.py", line 59, in render_report +E ret = reporter.report(morfs, outfile=outfile) +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/coverage/jsonreport.py", line 89, in report +E for file_reporter, analysis in get_analysis_to_report(self.coverage, morfs): +E ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^ +E File "/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/coverage/report_core.py", line 94, in get_analysis_to_report +E raise NoDataError("No data to report.") +E coverage.exceptions.NoDataError: No data to report. +E +E assert False +E + where False = is_file() +E + where is_file = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-781/test_excluded_measurement_pres0/private.receipt').is_file + +tests/unit/specfact_code_review/run/test_target_coverage.py:75: AssertionError +__________ test_native_collect_only_does_not_claim_aggregate_failure ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-781/test_native_collect_only_does_0') + + def test_native_collect_only_does_not_claim_aggregate_failure(tmp_path): + source = _project(tmp_path, native='--cov --collect-only') + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 +> assert receipt['coverage_policy']['native_threshold_failed'] is False +E assert True is False + +tests/unit/specfact_code_review/run/test_target_coverage.py:171: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_excluded_measurement_preserves_success_and_incomplete_reason +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_native_collect_only_does_not_claim_aggregate_failure +========================= 2 failed, 10 passed in 2.13s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_PLUGIN_COMPATIBILITY.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_PLUGIN_COMPATIBILITY.json new file mode 100644 index 00000000..ad7b79e2 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_COVERAGE_PLUGIN_COMPATIBILITY.json @@ -0,0 +1,142 @@ +{ + "helper_sha256": "8a5e9484fe2568bc9c0ef6c074cdb456f5f6174cd113dfd00a4c43464ad4155e", + "harness": "tests/unit/specfact_code_review/run/test_target_coverage.py existing assertions invoked unchanged", + "versions": "coverage==7.16.1\nexecnet==2.1.2\niniconfig==2.3.0\npackaging==26.3\npluggy==1.6.0\nPygments==2.21.0\npytest==9.1.1\npytest-cov==7.0.0\npytest-xdist==3.8.0\n", + "results": [ + { + "case": "reviewer_threshold", + "passed": true + }, + { + "case": "native_threshold_and_report", + "passed": true + }, + { + "case": "native_no_report", + "passed": true + }, + { + "case": "reviewer_xdist", + "passed": true + }, + { + "case": "native_xdist", + "passed": true + }, + { + "case": "missing_worker_receipt", + "passed": true + }, + { + "case": "subprocess_coverage", + "passed": true + } + ], + "commands": [ + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/reviewer_threshold/project", + "/private/tmp/specfact-pytest-cov700-evidence/reviewer_threshold/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/reviewer_threshold/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/reviewer_threshold/project", + "exit_code": 0, + "stdout": ". [100%]\n1 passed in 0.01s\n", + "stderr": "" + }, + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/native_threshold_and_report/project", + "/private/tmp/specfact-pytest-cov700-evidence/native_threshold_and_report/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/native_threshold_and_report/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/native_threshold_and_report/project", + "exit_code": 1, + "stdout": ".\nERROR: Coverage failure: total of 75.00 is less than fail-under=95.00\n [100%]\n================================ tests coverage ================================\n_______________ coverage: platform darwin, python 3.14.7-final-0 _______________\n\nCoverage JSON written to file customer.json\nFAIL Required test coverage of 95.0% not reached. Total coverage: 75.00%\n1 passed in 0.01s\n", + "stderr": "" + }, + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/native_no_report/project", + "/private/tmp/specfact-pytest-cov700-evidence/native_no_report/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/native_no_report/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/native_no_report/project", + "exit_code": 0, + "stdout": ". [100%]\n1 passed in 0.01s\n", + "stderr": "" + }, + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/reviewer_xdist/project", + "/private/tmp/specfact-pytest-cov700-evidence/reviewer_xdist/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/reviewer_xdist/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/reviewer_xdist/project", + "exit_code": 0, + "stdout": "bringing up nodes...\nbringing up nodes...\n\n.. [100%]\n2 passed in 0.21s\n", + "stderr": "" + }, + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/native_xdist/project", + "/private/tmp/specfact-pytest-cov700-evidence/native_xdist/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/native_xdist/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/native_xdist/project", + "exit_code": 0, + "stdout": "bringing up nodes...\nbringing up nodes...\n\n.. [100%]\n================================ tests coverage ================================\n_______________ coverage: platform darwin, python 3.14.7-final-0 _______________\n\nName Stmts Miss Cover\n------------------------------\napp.py 4 0 100.00%\n------------------------------\nTOTAL 4 0 100.00%\n2 passed in 0.21s\n", + "stderr": "" + }, + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/missing_worker_receipt/project", + "/private/tmp/specfact-pytest-cov700-evidence/missing_worker_receipt/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/missing_worker_receipt/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/missing_worker_receipt/project", + "exit_code": 0, + "stdout": "bringing up nodes...\nbringing up nodes...\n\n. [100%]\n1 passed in 0.21s\n", + "stderr": "" + }, + { + "command": [ + "/private/tmp/specfact-pytest-cov700-validation/bin/python", + "-c", + "\nimport importlib.util, json, os, sys\nfrom pathlib import Path\nimport pytest\nhelper, snapshot, output, extra = map(Path, sys.argv[1:])\nargs = [\"-q\", \"-p\", \"pytest_cov\", *json.loads(extra.read_text()), \"tests\"]\nif helper.is_file():\n spec = importlib.util.spec_from_file_location(\"_specfact_target_coverage\", helper)\n module = importlib.util.module_from_spec(spec)\n sys.modules[spec.name] = module\n spec.loader.exec_module(module)\n module.configure(snapshot=snapshot, output=output, directories=[])\n args[0:0] = [\"-p\", spec.name]\n code = pytest.main(args)\n evidence = module.evidence()\nelse:\n # Exact pre-change injection: a valued snapshot scope and replacing JSON report.\n args[-1:-1] = [f\"--cov={snapshot}\", f\"--cov-report=json:{output}\"]\n code = pytest.main(args)\n evidence = {\"coverage\": json.loads(output.read_text()) if output.exists() else {},\n \"coverage_policy\": {\"mode\": \"legacy-forced\", \"native_threshold_failed\": False}}\noutput.with_suffix(\".receipt\").write_text(json.dumps({\"exit_code\": int(code), **evidence}))\nraise SystemExit(int(code))\n", + "/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py", + "/private/tmp/specfact-pytest-cov700-evidence/subprocess_coverage/project", + "/private/tmp/specfact-pytest-cov700-evidence/subprocess_coverage/private.json", + "/private/tmp/specfact-pytest-cov700-evidence/subprocess_coverage/arguments.json" + ], + "cwd": "/private/tmp/specfact-pytest-cov700-evidence/subprocess_coverage/project", + "exit_code": 0, + "stdout": ". [100%]\n1 passed in 0.06s\n", + "stderr": "/private/tmp/specfact-pytest-cov700-validation/lib/python3.14/site-packages/coverage/inorout.py:558: CoverageWarning: Module app was never imported. (module-not-imported); see https://coverage.readthedocs.io/en/7.16.1/messages.html#warning-module-not-imported\n self.warn(f\"Module {pkg} was never imported.\", slug=\"module-not-imported\")\n/private/tmp/specfact-pytest-cov700-validation/lib/python3.14/site-packages/coverage/control.py:967: CoverageWarning: No data was collected. (no-data-collected); see https://coverage.readthedocs.io/en/7.16.1/messages.html#warning-no-data-collected\n self._warn(\"No data was collected.\", slug=\"no-data-collected\")\n" + } + ] +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_DEV_ALIGNMENT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_DEV_ALIGNMENT_RED.txt new file mode 100644 index 00000000..f8f78ff9 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_DEV_ALIGNMENT_RED.txt @@ -0,0 +1,145 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py FFF [100%] + +=================================== FAILURES =================================== +______ test_declared_development_tools_admit_compatible_signed_entries[0] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-635/test_declared_development_tool0') +environment_index = 0 + + @pytest.mark.parametrize("environment_index", [0, 1, 2]) + def test_declared_development_tools_admit_compatible_signed_entries(tmp_path: Path, environment_index: int) -> None: + """Declared dev ranges must exclude the real incompatible native resolution.""" + import json + import tomllib + + from packaging.requirements import Requirement + from packaging.utils import canonicalize_name + from packaging.version import Version + + root = Path(__file__).resolve().parents[4] + contract = root / "packages/specfact-code-review/src/specfact_code_review/resources/contracts" + environment = json.loads((contract / "pr-range-v1-toolchain-lock.json").read_text())["environments"][environment_index] + declared = { + str(canonicalize_name(requirement.name)): requirement + for value in tomllib.loads((root / "pyproject.toml").read_text())["tool"]["hatch"]["envs"]["default"]["dependencies"] + for requirement in (Requirement(value),) + } + observed_path = root / "openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_PACKAGES.txt" + observed = dict(line.split("==", 1) for line in observed_path.read_text().splitlines() if "==" in line) + inventory = {"installed": []} + for component in environment["components"]: + name = component["normalized_name"] + if name not in {"pylint", "basedpyright"}: + continue + pinned = Version(component["version"]) + assert declared[name].specifier.contains(pinned), f"Declared {name} excludes signed entry {pinned}" + candidates = (pinned, Version(observed[name])) + selected = max(version for version in candidates if declared[name].specifier.contains(version)) + metadata = tmp_path / f"{name}-{pinned}.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text(f"Metadata-Version: 2.1\nName: {name}\nVersion: {pinned}\n") + inventory["installed"].append({"metadata": {"name": name, "version": str(selected)}}) +> assert not analyzer_dependency_conflicts(inventory, tmp_path) +E AssertionError: assert not {'pylint': 'project_analyzer_dependency_incompatible:pylint: target=4.0.8, sealed entry=4.0.7', 'basedpyright': 'project_analyzer_dependency_incompatible:basedpyright: target=1.40.1, sealed entry=1.39.10'} +E + where {'pylint': 'project_analyzer_dependency_incompatible:pylint: target=4.0.8, sealed entry=4.0.7', 'basedpyright': 'project_analyzer_dependency_incompatible:basedpyright: target=1.40.1, sealed entry=1.39.10'} = analyzer_dependency_conflicts({'installed': [{'metadata': {'name': 'basedpyright', 'version': '1.40.1'}}, {'metadata': {'name': 'pylint', 'version': '4.0.8'}}]}, PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-635/test_declared_development_tool0')) + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:77: AssertionError +______ test_declared_development_tools_admit_compatible_signed_entries[1] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-635/test_declared_development_tool1') +environment_index = 1 + + @pytest.mark.parametrize("environment_index", [0, 1, 2]) + def test_declared_development_tools_admit_compatible_signed_entries(tmp_path: Path, environment_index: int) -> None: + """Declared dev ranges must exclude the real incompatible native resolution.""" + import json + import tomllib + + from packaging.requirements import Requirement + from packaging.utils import canonicalize_name + from packaging.version import Version + + root = Path(__file__).resolve().parents[4] + contract = root / "packages/specfact-code-review/src/specfact_code_review/resources/contracts" + environment = json.loads((contract / "pr-range-v1-toolchain-lock.json").read_text())["environments"][environment_index] + declared = { + str(canonicalize_name(requirement.name)): requirement + for value in tomllib.loads((root / "pyproject.toml").read_text())["tool"]["hatch"]["envs"]["default"]["dependencies"] + for requirement in (Requirement(value),) + } + observed_path = root / "openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_PACKAGES.txt" + observed = dict(line.split("==", 1) for line in observed_path.read_text().splitlines() if "==" in line) + inventory = {"installed": []} + for component in environment["components"]: + name = component["normalized_name"] + if name not in {"pylint", "basedpyright"}: + continue + pinned = Version(component["version"]) + assert declared[name].specifier.contains(pinned), f"Declared {name} excludes signed entry {pinned}" + candidates = (pinned, Version(observed[name])) + selected = max(version for version in candidates if declared[name].specifier.contains(version)) + metadata = tmp_path / f"{name}-{pinned}.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text(f"Metadata-Version: 2.1\nName: {name}\nVersion: {pinned}\n") + inventory["installed"].append({"metadata": {"name": name, "version": str(selected)}}) +> assert not analyzer_dependency_conflicts(inventory, tmp_path) +E AssertionError: assert not {'pylint': 'project_analyzer_dependency_incompatible:pylint: target=4.0.8, sealed entry=4.0.7', 'basedpyright': 'project_analyzer_dependency_incompatible:basedpyright: target=1.40.1, sealed entry=1.39.10'} +E + where {'pylint': 'project_analyzer_dependency_incompatible:pylint: target=4.0.8, sealed entry=4.0.7', 'basedpyright': 'project_analyzer_dependency_incompatible:basedpyright: target=1.40.1, sealed entry=1.39.10'} = analyzer_dependency_conflicts({'installed': [{'metadata': {'name': 'basedpyright', 'version': '1.40.1'}}, {'metadata': {'name': 'pylint', 'version': '4.0.8'}}]}, PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-635/test_declared_development_tool1')) + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:77: AssertionError +______ test_declared_development_tools_admit_compatible_signed_entries[2] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-635/test_declared_development_tool2') +environment_index = 2 + + @pytest.mark.parametrize("environment_index", [0, 1, 2]) + def test_declared_development_tools_admit_compatible_signed_entries(tmp_path: Path, environment_index: int) -> None: + """Declared dev ranges must exclude the real incompatible native resolution.""" + import json + import tomllib + + from packaging.requirements import Requirement + from packaging.utils import canonicalize_name + from packaging.version import Version + + root = Path(__file__).resolve().parents[4] + contract = root / "packages/specfact-code-review/src/specfact_code_review/resources/contracts" + environment = json.loads((contract / "pr-range-v1-toolchain-lock.json").read_text())["environments"][environment_index] + declared = { + str(canonicalize_name(requirement.name)): requirement + for value in tomllib.loads((root / "pyproject.toml").read_text())["tool"]["hatch"]["envs"]["default"]["dependencies"] + for requirement in (Requirement(value),) + } + observed_path = root / "openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_PACKAGES.txt" + observed = dict(line.split("==", 1) for line in observed_path.read_text().splitlines() if "==" in line) + inventory = {"installed": []} + for component in environment["components"]: + name = component["normalized_name"] + if name not in {"pylint", "basedpyright"}: + continue + pinned = Version(component["version"]) + assert declared[name].specifier.contains(pinned), f"Declared {name} excludes signed entry {pinned}" + candidates = (pinned, Version(observed[name])) + selected = max(version for version in candidates if declared[name].specifier.contains(version)) + metadata = tmp_path / f"{name}-{pinned}.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text(f"Metadata-Version: 2.1\nName: {name}\nVersion: {pinned}\n") + inventory["installed"].append({"metadata": {"name": name, "version": str(selected)}}) +> assert not analyzer_dependency_conflicts(inventory, tmp_path) +E AssertionError: assert not {'pylint': 'project_analyzer_dependency_incompatible:pylint: target=4.0.8, sealed entry=4.0.7', 'basedpyright': 'project_analyzer_dependency_incompatible:basedpyright: target=1.40.1, sealed entry=1.39.10'} +E + where {'pylint': 'project_analyzer_dependency_incompatible:pylint: target=4.0.8, sealed entry=4.0.7', 'basedpyright': 'project_analyzer_dependency_incompatible:basedpyright: target=1.40.1, sealed entry=1.39.10'} = analyzer_dependency_conflicts({'installed': [{'metadata': {'name': 'basedpyright', 'version': '1.40.1'}}, {'metadata': {'name': 'pylint', 'version': '4.0.8'}}]}, PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-635/test_declared_development_tool2')) + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:77: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_declared_development_tools_admit_compatible_signed_entries[0] +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_declared_development_tools_admit_compatible_signed_entries[1] +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_declared_development_tools_admit_compatible_signed_entries[2] +============================== 3 failed in 0.22s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_IMPLICIT_HATCH_DEFAULT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_IMPLICIT_HATCH_DEFAULT_RED.txt new file mode 100644 index 00000000..1d6c33fb --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_IMPLICIT_HATCH_DEFAULT_RED.txt @@ -0,0 +1,176 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_snapshot_activation.py FFF [100%] + +=================================== FAILURES =================================== +______ test_index_activation_keeps_snapshot_dependency_identity[default] _______ + +activated_repository = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_index_activation_keeps_sn0/project') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_index_activation_keeps_sn0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b020050> +active = 'default' + + @pytest.mark.parametrize("active", ["review", "default"]) + def test_index_activation_keeps_snapshot_dependency_identity( + activated_repository: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, active: str + ) -> None: + """The actual cached review path selects activation without importing live inputs.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", active) + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([Path("app.py")], tmp_path / "snapshot") + assert snapshot is not None +> plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_snapshot_activation.py:73: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:52: in discover_snapshot + plan = discover_project(root, config_path=config_path, activation_root=repository) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:399: in discover_project + selection = _project_selection(root, project, hatch, values, activation_root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_index_activation_keeps_sn0/snapshot') +project = {'project': {'name': 'customer'}, 'tool': {'hatch': {'envs': {'review': {'dependencies': ['requests==2.32.4']}, 'other': {}}}}} +hatch = {}, values = {} +activation_root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_index_activation_keeps_sn0/project') + + def _project_selection( + root: Path, project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], activation_root: Path | None + ) -> dict[str, Any]: + """Combine explicit selection with independently verified snapshot activation.""" + activation_root = root if activation_root is None else activation_root.resolve() + selection = ( + {**_active_selection(activation_root, project, hatch, snapshot=activation_root != root), **values} + if "manager" not in values + else values + ) + if activation_root != root and selection.get("manager") == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + active = selection.get("environment") + if active and active not in environments: +> raise ProjectRuntimeError( + f"project_active_environment_missing:{active}; select an environment in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_active_environment_missing:default; select an environment in --project-config + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:379: ProjectRuntimeError +______ test_revision_activation_binds_its_own_dependency_inputs[default] _______ + +activated_repository = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_revision_activation_binds0/project') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b020770> +active = 'default' + + @pytest.mark.parametrize("active", ["review", "default"]) + def test_revision_activation_binds_its_own_dependency_inputs( + activated_repository: Path, monkeypatch: pytest.MonkeyPatch, active: str + ) -> None: + """A revision side reuses only verified selection, never another side's dependency bytes.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", active) + commit = subprocess.check_output(["git", "-C", str(activated_repository), "rev-parse", "HEAD"], text=True).strip() + snapshot = scope._materialize_commit(activated_repository, commit) + try: +> plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_snapshot_activation.py:148: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:52: in discover_snapshot + plan = discover_project(root, config_path=config_path, activation_root=repository) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:399: in discover_project + selection = _project_selection(root, project, hatch, values, activation_root) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-review-6da7ac86795e-69etvars') +project = {'project': {'name': 'customer'}, 'tool': {'hatch': {'envs': {'review': {'dependencies': ['requests==2.32.3']}, 'other': {}}}}} +hatch = {}, values = {} +activation_root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_revision_activation_binds0/project') + + def _project_selection( + root: Path, project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], activation_root: Path | None + ) -> dict[str, Any]: + """Combine explicit selection with independently verified snapshot activation.""" + activation_root = root if activation_root is None else activation_root.resolve() + selection = ( + {**_active_selection(activation_root, project, hatch, snapshot=activation_root != root), **values} + if "manager" not in values + else values + ) + if activation_root != root and selection.get("manager") == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + active = selection.get("environment") + if active and active not in environments: +> raise ProjectRuntimeError( + f"project_active_environment_missing:{active}; select an environment in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_active_environment_missing:default; select an environment in --project-config + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:379: ProjectRuntimeError +____________ test_live_activation_recognizes_implicit_hatch_default ____________ + +activated_repository = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-678/test_live_activation_recognize0/project') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b02cdd0> + + def test_live_activation_recognizes_implicit_hatch_default( + activated_repository: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Hatch defines default even when only custom environment tables exist.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", "default") +> plan = runtime_discovery.discover_project(activated_repository) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_snapshot_activation.py:163: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:413: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'project': {'name': 'customer'}, 'tool': {'hatch': {'envs': {'review': {'dependencies': ['requests==2.32.5']}, 'other': {}}}}} +hatch = {}, values = {}, manager = 'hatch' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: +> raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_environment_ambiguous:other,review; select environment in --project-config + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:296: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_snapshot_activation.py::test_index_activation_keeps_snapshot_dependency_identity[default] +FAILED tests/unit/specfact_code_review/run/test_snapshot_activation.py::test_revision_activation_binds_its_own_dependency_inputs[default] +FAILED tests/unit/specfact_code_review/run/test_snapshot_activation.py::test_live_activation_recognizes_implicit_hatch_default +============================== 3 failed in 0.48s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_INDEX_ACTIVATION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_INDEX_ACTIVATION_RED.txt new file mode 100644 index 00000000..834b7e8c --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_INDEX_ACTIVATION_RED.txt @@ -0,0 +1,81 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 5 items + +tests/unit/specfact_code_review/run/test_snapshot_activation.py F...F [100%] + +=================================== FAILURES =================================== +___________ test_index_activation_keeps_snapshot_dependency_identity ___________ + +activated_repository = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-648/test_index_activation_keeps_sn0/project') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-648/test_index_activation_keeps_sn0') + + def test_index_activation_keeps_snapshot_dependency_identity(activated_repository: Path, tmp_path: Path) -> None: + """The actual cached review path selects activation without importing live inputs.""" + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([Path("app.py")], tmp_path / "snapshot") + assert snapshot is not None +> plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_snapshot_activation.py:51: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:51: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:389: in discover_project + environment, groups = _selection(project, hatch, selection, manager) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +project = {'project': {'name': 'customer'}, 'tool': {'hatch': {'envs': {'review': {'dependencies': ['requests==2.32.4']}, 'other': {}}}}} +hatch = {}, values = {}, manager = 'hatch' + + def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str + ) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: +> raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_environment_ambiguous:other,review; select environment in --project-config + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:294: ProjectRuntimeError +______________ test_removed_active_environment_is_not_substituted ______________ + +activated_repository = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-648/test_removed_active_environmen0/project') +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-648/test_removed_active_environmen0') + + def test_removed_active_environment_is_not_substituted(activated_repository: Path, tmp_path: Path) -> None: + """Each selected revision must contain the verified active environment itself.""" + metadata = activated_repository / "pyproject.toml" + metadata.write_text('[project]\nname="customer"\n[tool.hatch.envs.other]\n') + _git(activated_repository, "add", "pyproject.toml") + metadata.write_text('[project]\nname="customer"\n[tool.hatch.envs.review]\n[tool.hatch.envs.other]\n') + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([activated_repository / "app.py"], tmp_path / "snapshot") + assert snapshot is not None +> with pytest.raises(ProjectRuntimeError, match="project_active_environment_missing:review"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_snapshot_activation.py:97: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_snapshot_activation.py::test_index_activation_keeps_snapshot_dependency_identity +FAILED tests/unit/specfact_code_review/run/test_snapshot_activation.py::test_removed_active_environment_is_not_substituted +========================= 2 failed, 3 passed in 0.68s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_ALIAS_CWD_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_ALIAS_CWD_RED.txt new file mode 100644 index 00000000..96c2e6df --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_ALIAS_CWD_RED.txt @@ -0,0 +1,36 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 32 items / 31 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_installed_coverage.py F [100%] + +=================================== FAILURES =================================== +_______ test_native_alias_uses_coverage_topdir_and_restores_customer_cwd _______ + +aliased_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_n...plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/good.py')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10936e3f0> + + def test_native_alias_uses_coverage_topdir_and_restores_customer_cwd(aliased_project, tmp_path, monkeypatch): + project = aliased_project + elsewhere = tmp_path / "customer-cwd" + elsewhere.mkdir() + monkeypatch.chdir(elsewhere) + relocations = _native_relocations(project) +> assert Path(relocations[str(project.installed)]).resolve() == project.source.resolve() +E AssertionError: assert PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/math.py') == PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/snapshot/src/capsule_owned/math.py') +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/math.py') = resolve() +E + where resolve = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/math.py').resolve +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/math.py') = Path('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/math.py') +E + and PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/snapshot/src/capsule_owned/math.py') = resolve() +E + where resolve = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/snapshot/src/capsule_owned/math.py').resolve +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-593/test_n...plbft6h0000gn/T/pytest-of-dom/pytest-593/test_native_alias_uses_coverag0/runtime/site-packages/capsule_owned/good.py')).source + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_installed_coverage.py:476: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_native_alias_uses_coverage_topdir_and_restores_customer_cwd +======================= 1 failed, 31 deselected in 0.23s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_ALIAS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_ALIAS_RED.txt new file mode 100644 index 00000000..6c9c5416 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_ALIAS_RED.txt @@ -0,0 +1,55 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 25 items / 24 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_installed_coverage.py F [100%] + +=================================== FAILURES =================================== +_________ test_native_coverage_alias_cannot_credit_transformed_source __________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_n...h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0/runtime/site-packages/capsule-owned-1.0.dist-info')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x107d3b490> + + def test_native_coverage_alias_cannot_credit_transformed_source(installed_project, tmp_path, monkeypatch): + import coverage + + project = installed_project + monkeypatch.chdir(project.snapshot) + project.source.write_text("def add(left, right):\n return left - right\n") + good = project.source.with_name("good.py") + installed_good = project.installed.with_name("good.py") + good.write_text("VALUE = 7\n") + installed_good.write_bytes(good.read_bytes()) + with (project.metadata / "RECORD").open("a") as stream: + stream.write(f"capsule_owned/good.py,{_record_hash(installed_good)},10\n") + (project.snapshot / "pyproject.toml").write_text('[tool.coverage.paths]\nsource=["src", "*/site-packages"]\n') + test = project.snapshot / "tests/test_math.py" + test.write_text(test.read_text() + "from capsule_owned.good import VALUE\ndef test_good():\n assert VALUE == 7\n") + bridge = installed_coverage.plan_installed_coverage([project.source, good], snapshot=project.snapshot, site_packages=project.site) + assert bridge.diagnostics[str(project.source)] == "installed_source_differs" + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + assert raw["files"]["src/capsule_owned/math.py"]["executed_lines"] + native = coverage.Coverage(config_file=str(project.snapshot / "pyproject.toml")) + aliases = native._make_aliases() + relocations = {str(path): aliases.map(str(path)) for path in (project.installed, installed_good)} + # Use the former interface on RED; the final controller consumes native relocation evidence. + arguments = {"snapshot": project.snapshot} + if "relocations" in __import__("inspect").signature(installed_coverage.attribute_installed_coverage).parameters: + arguments["relocations"] = relocations + attributed, diagnostics = installed_coverage.attribute_installed_coverage(bridge, raw, **arguments) +> assert str(project.source) in diagnostics, "native aliases erased transformed-source origin uncertainty" +E AssertionError: native aliases erased transformed-source origin uncertainty +E assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-587/test_n...h0000gn/T/pytest-of-dom/pytest-587/test_native_coverage_alias_can0/runtime/site-packages/capsule-owned-1.0.dist-info')).source + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_installed_coverage.py:376: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_native_coverage_alias_cannot_credit_transformed_source +======================= 1 failed, 24 deselected in 0.36s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_COVERAGE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_COVERAGE_RED.txt new file mode 100644 index 00000000..dbfc1e5a --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_COVERAGE_RED.txt @@ -0,0 +1,313 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 13 items + +tests/unit/specfact_code_review/run/test_installed_coverage.py FFFFFFFF. [ 69%] +FFF. [100%] + +=================================== FAILURES =================================== +_ test_native_installed_package_coverage_is_attributed_without_changing_imports _ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_n...h0000gn/T/pytest-of-dom/pytest-571/test_native_installed_package_0/runtime/site-packages/capsule-owned-1.0.dist-info')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_native_installed_package_0') + + def test_native_installed_package_coverage_is_attributed_without_changing_imports(installed_project, tmp_path): + project = installed_project + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + before = json.dumps(raw, sort_keys=True) + attributed, diagnostics = _normalize(project, bridge, raw) + row = attributed["files"].get(str(project.source)) +> assert row is not None, "native installed production source is absent from snapshot-only coverage" +E AssertionError: native installed production source is absent from snapshot-only coverage +E assert None is not None + +tests/unit/specfact_code_review/run/test_installed_coverage.py:97: AssertionError +_________ test_actual_defect_stays_failed_after_installed_attribution __________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_a...h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0/runtime/site-packages/capsule-owned-1.0.dist-info')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0') + + def test_actual_defect_stays_failed_after_installed_attribution(installed_project, tmp_path): + project = installed_project + (project.snapshot / "tests/test_math.py").write_text( + "from capsule_owned.math import add\ndef test_wrong():\n assert add(2, 3) == 6\n" + ) + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path) + attributed, diagnostics = _normalize(project, bridge, raw) + assert result.returncode == 1 +> assert str(project.source) in attributed["files"] +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0/snapshot/src/capsule_owned/math.py' in {'tests/test_math.py': {'executed_lines': [1, 2, 3], 'summary': {'covered_lines': 3, 'num_statements': 3, 'percent_covered': 100.0, 'percent_covered_display': '100', ...}, 'missing_lines': [], 'excluded_lines': [], ...}} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_a...h0000gn/T/pytest-of-dom/pytest-571/test_actual_defect_stays_faile0/runtime/site-packages/capsule-owned-1.0.dist-info')).source + +tests/unit/specfact_code_review/run/test_installed_coverage.py:113: AssertionError +___________ test_unsafe_installed_attribution_is_incomplete[changed] ___________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut0/runtime/site-packages/capsule-owned-1.0.dist-info')) +case = 'changed' + + @pytest.mark.parametrize("case", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"]) + def test_unsafe_installed_attribution_is_incomplete(installed_project, case): + project = installed_project + if case == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + elif case == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + elif case == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + elif case == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + elif case == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + else: + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + bridge = _bridge(project) + assert not bridge.mappings +> assert str(project.source) in bridge.diagnostics +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut0/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut0/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut0/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut0/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut0/runtime/site-packages/capsule-owned-1.0.dist-info')).source +E + and {} = namespace(directories=(), mappings=(), diagnostics={}).diagnostics + +tests/unit/specfact_code_review/run/test_installed_coverage.py:140: AssertionError +______ test_unsafe_installed_attribution_is_incomplete[duplicate-source] _______ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut1/runtime/site-packages/capsule-owned-1.0.dist-info')) +case = 'duplicate-source' + + @pytest.mark.parametrize("case", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"]) + def test_unsafe_installed_attribution_is_incomplete(installed_project, case): + project = installed_project + if case == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + elif case == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + elif case == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + elif case == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + elif case == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + else: + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + bridge = _bridge(project) + assert not bridge.mappings +> assert str(project.source) in bridge.diagnostics +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut1/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut1/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut1/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut1/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut1/runtime/site-packages/capsule-owned-1.0.dist-info')).source +E + and {} = namespace(directories=(), mappings=(), diagnostics={}).diagnostics + +tests/unit/specfact_code_review/run/test_installed_coverage.py:140: AssertionError +_______ test_unsafe_installed_attribution_is_incomplete[duplicate-owner] _______ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut2/runtime/site-packages/capsule-owned-1.0.dist-info')) +case = 'duplicate-owner' + + @pytest.mark.parametrize("case", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"]) + def test_unsafe_installed_attribution_is_incomplete(installed_project, case): + project = installed_project + if case == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + elif case == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + elif case == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + elif case == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + elif case == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + else: + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + bridge = _bridge(project) + assert not bridge.mappings +> assert str(project.source) in bridge.diagnostics +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut2/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut2/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut2/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut2/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut2/runtime/site-packages/capsule-owned-1.0.dist-info')).source +E + and {} = namespace(directories=(), mappings=(), diagnostics={}).diagnostics + +tests/unit/specfact_code_review/run/test_installed_coverage.py:140: AssertionError +_________ test_unsafe_installed_attribution_is_incomplete[record-hash] _________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut3/runtime/site-packages/capsule-owned-1.0.dist-info')) +case = 'record-hash' + + @pytest.mark.parametrize("case", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"]) + def test_unsafe_installed_attribution_is_incomplete(installed_project, case): + project = installed_project + if case == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + elif case == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + elif case == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + elif case == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + elif case == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + else: + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + bridge = _bridge(project) + assert not bridge.mappings +> assert str(project.source) in bridge.diagnostics +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut3/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut3/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut3/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut3/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut3/runtime/site-packages/capsule-owned-1.0.dist-info')).source +E + and {} = namespace(directories=(), mappings=(), diagnostics={}).diagnostics + +tests/unit/specfact_code_review/run/test_installed_coverage.py:140: AssertionError +________ test_unsafe_installed_attribution_is_incomplete[record-escape] ________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut4/runtime/site-packages/capsule-owned-1.0.dist-info')) +case = 'record-escape' + + @pytest.mark.parametrize("case", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"]) + def test_unsafe_installed_attribution_is_incomplete(installed_project, case): + project = installed_project + if case == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + elif case == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + elif case == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + elif case == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + elif case == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + else: + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + bridge = _bridge(project) + assert not bridge.mappings +> assert str(project.source) in bridge.diagnostics +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut4/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut4/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut4/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut4/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut4/runtime/site-packages/capsule-owned-1.0.dist-info')).source +E + and {} = namespace(directories=(), mappings=(), diagnostics={}).diagnostics + +tests/unit/specfact_code_review/run/test_installed_coverage.py:140: AssertionError +______ test_unsafe_installed_attribution_is_incomplete[foreign-namespace] ______ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut5/runtime/site-packages/capsule-owned-1.0.dist-info')) +case = 'foreign-namespace' + + @pytest.mark.parametrize("case", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"]) + def test_unsafe_installed_attribution_is_incomplete(installed_project, case): + project = installed_project + if case == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + elif case == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + elif case == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + elif case == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + elif case == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + else: + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + bridge = _bridge(project) + assert not bridge.mappings +> assert str(project.source) in bridge.diagnostics +E AssertionError: assert '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut5/snapshot/src/capsule_owned/math.py' in {} +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut5/snapshot/src/capsule_owned/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut5/snapshot/src/capsule_owned/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut5/snapshot/src/capsule_owned/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_u...h0000gn/T/pytest-of-dom/pytest-571/test_unsafe_installed_attribut5/runtime/site-packages/capsule-owned-1.0.dist-info')).source +E + and {} = namespace(directories=(), mappings=(), diagnostics={}).diagnostics + +tests/unit/specfact_code_review/run/test_installed_coverage.py:140: AssertionError +_______ test_content_change_after_planning_invalidates_coverage[source] ________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_c...h0000gn/T/pytest-of-dom/pytest-571/test_content_change_after_plan0/runtime/site-packages/capsule-owned-1.0.dist-info')) +changed = 'source' + + @pytest.mark.parametrize("changed", ["source", "installed"]) + def test_content_change_after_planning_invalidates_coverage(installed_project, changed): + project = installed_project + bridge = _bridge(project) +> assert bridge.mappings +E assert () +E + where () = namespace(directories=(), mappings=(), diagnostics={}).mappings + +tests/unit/specfact_code_review/run/test_installed_coverage.py:153: AssertionError +______ test_content_change_after_planning_invalidates_coverage[installed] ______ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_c...h0000gn/T/pytest-of-dom/pytest-571/test_content_change_after_plan1/runtime/site-packages/capsule-owned-1.0.dist-info')) +changed = 'installed' + + @pytest.mark.parametrize("changed", ["source", "installed"]) + def test_content_change_after_planning_invalidates_coverage(installed_project, changed): + project = installed_project + bridge = _bridge(project) +> assert bridge.mappings +E assert () +E + where () = namespace(directories=(), mappings=(), diagnostics={}).mappings + +tests/unit/specfact_code_review/run/test_installed_coverage.py:153: AssertionError +_______________ test_missing_measured_row_cannot_be_manufactured _______________ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-571/test_m...h0000gn/T/pytest-of-dom/pytest-571/test_missing_measured_row_cann0/runtime/site-packages/capsule-owned-1.0.dist-info')) + + def test_missing_measured_row_cannot_be_manufactured(installed_project): + project = installed_project + bridge = _bridge(project) +> assert bridge.mappings +E assert () +E + where () = namespace(directories=(), mappings=(), diagnostics={}).mappings + +tests/unit/specfact_code_review/run/test_installed_coverage.py:164: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_native_installed_package_coverage_is_attributed_without_changing_imports +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_actual_defect_stays_failed_after_installed_attribution +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unsafe_installed_attribution_is_incomplete[changed] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unsafe_installed_attribution_is_incomplete[duplicate-source] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unsafe_installed_attribution_is_incomplete[duplicate-owner] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unsafe_installed_attribution_is_incomplete[record-hash] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unsafe_installed_attribution_is_incomplete[record-escape] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unsafe_installed_attribution_is_incomplete[foreign-namespace] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_content_change_after_planning_invalidates_coverage[source] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_content_change_after_planning_invalidates_coverage[installed] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_missing_measured_row_cannot_be_manufactured +========================= 11 failed, 2 passed in 1.45s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_FLAT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_FLAT_RED.txt new file mode 100644 index 00000000..c7e0e568 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_FLAT_RED.txt @@ -0,0 +1,37 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 24 items / 23 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_installed_coverage.py F [100%] + +=================================== FAILURES =================================== +___ test_unexecuted_flat_snapshot_row_does_not_mask_real_installed_coverage ____ + +installed_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-576/test_u...h0000gn/T/pytest-of-dom/pytest-576/test_unexecuted_flat_snapshot_0/runtime/site-packages/capsule-owned-1.0.dist-info')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-576/test_unexecuted_flat_snapshot_0') + + def test_unexecuted_flat_snapshot_row_does_not_mask_real_installed_coverage(installed_project, tmp_path): + project = installed_project + flat = project.snapshot / "capsule_owned" + project.source.parent.rename(flat) + project.source = flat / "math.py" + (flat / "__init__.py").write_bytes((project.installed.parent / "__init__.py").read_bytes()) + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path, import_mode="importlib") + assert result.returncode == 0, result.stdout + result.stderr + snapshot_row = raw["files"]["capsule_owned/math.py"] + assert snapshot_row["executed_lines"] == [] + assert snapshot_row["summary"]["percent_covered"] == 0 + attributed, diagnostics = _normalize(project, bridge, raw) +> assert attributed["files"][str(project.source)]["summary"]["percent_covered"] == 100 + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-576/test_unexecuted_flat_snapshot_0/snapshot/capsule_owned/math.py' + +tests/unit/specfact_code_review/run/test_installed_coverage.py:336: KeyError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_unexecuted_flat_snapshot_row_does_not_mask_real_installed_coverage +======================= 1 failed, 23 deselected in 0.26s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_RENAMED_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_RENAMED_RED.txt new file mode 100644 index 00000000..032326bc --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_INSTALLED_RENAMED_RED.txt @@ -0,0 +1,46 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 33 items / 32 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_installed_coverage.py F [100%] + +=================================== FAILURES =================================== +___________ test_native_alias_cannot_credit_renamed_unmatched_source ___________ + +aliased_project = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_n...plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0/runtime/site-packages/capsule_owned/good.py')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b0fba80> + + def test_native_alias_cannot_credit_renamed_unmatched_source(aliased_project, tmp_path, monkeypatch): + project = aliased_project + destination = project.snapshot / "src/original/math.py" + destination.parent.mkdir() + project.source.rename(destination) + project.source = destination + project.source.write_text("def add(left, right):\n return left - right\n") + (project.snapshot / "pyproject.toml").write_text( + '[tool.coverage.paths]\nsource=["src/original", "*/site-packages/capsule_owned"]\n' + ) + bridge = installed_coverage.plan_installed_coverage( + [project.source, project.good], snapshot=project.snapshot, site_packages=project.site + ) + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + attributed, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations=_native_relocations(project) + ) +> assert "coverage_alias_origin_unverified" in diagnostics.get(str(project.source), "") +E AssertionError: assert 'coverage_alias_origin_unverified' in '' +E + where '' = ('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0/snapshot/src/original/math.py', '') +E + where = {}.get +E + and '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0/snapshot/src/original/math.py' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0/snapshot/src/original/math.py')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0/snapshot/src/original/math.py') = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-599/test_n...plbft6h0000gn/T/pytest-of-dom/pytest-599/test_native_alias_cannot_credi0/runtime/site-packages/capsule_owned/good.py')).source + +tests/unit/specfact_code_review/run/test_installed_coverage.py:525: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_native_alias_cannot_credit_renamed_unmatched_source +======================= 1 failed, 32 deselected in 1.50s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_LOGGING_PARSER_EVIDENCE.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_LOGGING_PARSER_EVIDENCE.json new file mode 100644 index 00000000..f85521d5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_LOGGING_PARSER_EVIDENCE.json @@ -0,0 +1,102 @@ +{ + "schema": "semgrep-logging-fixture-parser-evidence-v1", + "recorded_at": "2026-09-16T11:48:48.876475+00:00", + "scope": "Local exact-engine parser reproduction and equivalent test fixture rewrite; not full Linux capsule acceptance.", + "engine": "Semgrep 1.144.0 semgrep-core from official analyzer runtime wheelhouse", + "image": "ghcr.io/nold-ai/specfact-code-review-analyzer-runtime:customer-466-cp312-v2", + "execution": "docker run --rm --network none --platform linux/amd64; read-only probe mount; macOS Linux emulation", + "native_red_run": 35090737789, + "native_error": "tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:61:26 comma in keyword-only lambda", + "assertion_and_signature_equivalence": "AST comparison: every assert unchanged; exact lambda/function arguments and returned argv expression identical. Unused executable explicitly discarded; host Python remains intentional.", + "input_sha256": { + "logging-lambda.py": "e5fb78295ef914765fa4d3d09740fcfed74107433805cbbb5f258f83e132a9ce", + "logging-red.py": "3391f961517593e2c61ca446268dc12147bcc9e7f2278f3ab069299646c840a3", + "logging-green.py": "663db6ca9a7e9d243ff7219ffd0ce7b6a10da6f7608895d9447553c90c3afd81" + }, + "parser_contract": "Read global.error_file_count, not process exit: parsing_stats exits zero even on syntax failure.", + "red": [ + { + "file": "logging-lambda.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 32,\n \"untranslated_node_count\": 1\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-lambda.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 32,\n \"untranslated_node_count\": 1\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-lambda.py\n000.2s: [Python] done parsing /probe/logging-lambda.py\n\nTODO statistics:\n\n" + }, + { + "file": "logging-lambda.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 32,\n \"untranslated_node_count\": 1\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-lambda.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 32,\n \"untranslated_node_count\": 1\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-lambda.py\n000.2s: [Python] done parsing /probe/logging-lambda.py\n\nTODO statistics:\n\n" + }, + { + "file": "logging-lambda.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 1,\n \"error_line_count\": 1,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-lambda.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 1,\n \"error_line_count\": 1,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-lambda.py\n/probe/logging-lambda.py: exn = Parsing_error.Syntax_error (/probe/logging-lambda.py:1:28 \",\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/logging-lambda.py\n000.2s: [Python] done parsing /probe/logging-lambda.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + }, + { + "file": "logging-red.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 379,\n \"error_line_count\": 379,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-red.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 379,\n \"error_line_count\": 379,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-red.py\n/probe/logging-red.py: exn = Parsing_error.Syntax_error (/probe/logging-red.py:61:26 \",\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/logging-red.py\n000.2s: [Python] done parsing /probe/logging-red.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + }, + { + "file": "logging-red.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 379,\n \"error_line_count\": 379,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-red.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 379,\n \"error_line_count\": 379,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-red.py\n/probe/logging-red.py: exn = Common.Impossible\nRaised at Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1119, characters 15-38\nCalled from Parse_python_tree_sitter.map_anon_choice_exp_aad5b2d in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 201, characters 14-34\nCalled from Parse_python_tree_sitter.map_argument_list in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 229, characters 18-52\nCalled from Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1160, characters 25-48\nCalled from Parse_python_tree_sitter.map_expression_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1595, characters 23-45\nCalled from Parse_python_tree_sitter.map_simple_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1715, characters 21-51\nCalled from Parse_python_tree_sitter.map_simple_statements in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1816, characters 11-38\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_python_tree_sitter.map_block in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1834, characters 11-29\nCalled from Parse_python_tree_sitter.map_function_definition in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2122, characters 13-29\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1962, characters 16-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_tree_sitter_helpers.wrap_parser in file \"OSS/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml\", line 131, characters 13-40\nCalled from Pfff_or_tree_sitter.run_parser in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 161, characters 18-24\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/logging-red.py\n000.2s: [Python] done parsing /probe/logging-red.py\n\nTODO statistics:\n\nParse_python_tree_sitter.map_anon_choice_exp_aad5b2d:201, -> 1\n" + }, + { + "file": "logging-red.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 379,\n \"error_line_count\": 379,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-red.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 379,\n \"error_line_count\": 379,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-red.py\n/probe/logging-red.py: exn = Parsing_error.Syntax_error (/probe/logging-red.py:61:26 \",\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/logging-red.py\n000.2s: [Python] done parsing /probe/logging-red.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + } + ], + "green": [ + { + "file": "logging-green.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 381,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 7870,\n \"untranslated_node_count\": 9\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-green.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 381,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 7870,\n \"untranslated_node_count\": 9\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-green.py\n000.2s: [Python] done parsing /probe/logging-green.py\n\nTODO statistics:\n\n" + }, + { + "file": "logging-green.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 381,\n \"error_line_count\": 381,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-green.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 381,\n \"error_line_count\": 381,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-green.py\n/probe/logging-green.py: exn = Common.Impossible\nRaised at Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1119, characters 15-38\nCalled from Parse_python_tree_sitter.map_anon_choice_exp_aad5b2d in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 201, characters 14-34\nCalled from Parse_python_tree_sitter.map_argument_list in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 229, characters 18-52\nCalled from Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1160, characters 25-48\nCalled from Parse_python_tree_sitter.map_expression_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1595, characters 23-45\nCalled from Parse_python_tree_sitter.map_simple_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1715, characters 21-51\nCalled from Parse_python_tree_sitter.map_simple_statements in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1816, characters 11-38\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_python_tree_sitter.map_block in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1834, characters 11-29\nCalled from Parse_python_tree_sitter.map_function_definition in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2122, characters 13-29\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1962, characters 16-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_tree_sitter_helpers.wrap_parser in file \"OSS/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml\", line 131, characters 13-40\nCalled from Pfff_or_tree_sitter.run_parser in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 161, characters 18-24\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/logging-green.py\n000.2s: [Python] done parsing /probe/logging-green.py\n\nTODO statistics:\n\nParse_python_tree_sitter.map_anon_choice_exp_aad5b2d:201, -> 1\n" + }, + { + "file": "logging-green.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 381,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 7870,\n \"untranslated_node_count\": 9\n },\n \"projects\": [\n {\n \"name\": \"/probe/logging-green.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 381,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 7870,\n \"untranslated_node_count\": 9\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/logging-green.py\n000.2s: [Python] done parsing /probe/logging-green.py\n\nTODO statistics:\n\n" + } + ], + "validation": { + "pytest": "27 passed, 1 skipped (Linux proc descriptor contract) on macOS Python 3.14.7", + "ruff": "passed", + "basedpyright": "0 errors, 0 warnings", + "limits": "Default and Pfff parse the final full file. Tree-sitter still raises Common.Impossible. Lambda alone parses with default/tree-sitter. This fixture change does not fix arbitrary customer combinations or suppress parser errors." + } +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_FAILURE.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_FAILURE.json new file mode 100644 index 00000000..2c85cad1 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_FAILURE.json @@ -0,0 +1,81 @@ +{ + "authority": "retained-native-regression", + "acceptance": false, + "run_url": "https://github.com/nold-ai/specfact-cli-modules/actions/runs/35093180778", + "artifact_id": 10445028712, + "artifact_zip_sha256": "5d3fb99ce33acf857261f0162cc638e9ec5c83e0148769dd3924ed3c87fc82ae", + "base": "efe7303efb32ac31379906a38e3f960ec4e7a744", + "tree": "459e5ba6f480f9ef022768b75b68499b53c211a6", + "patch_sha256": "419ee486eb56f0b4f573ebb4f17556cc465596c5004264c37207c7bdc2bba4ea", + "report_sha256": "81be65bd5da711d8b3894d09210352be9137681e664321dbfd326d18cae5bc57", + "native_hook_exit": 1, + "pytest_exit": 1, + "collected": 1012, + "record_outcomes": { + "setup:passed": 1012, + "call:passed": 1012, + "teardown:passed": 1012 + }, + "collection_errors": [], + "internal_errors": [], + "argv": [ + "-ra", + "-v", + "--import-mode=importlib", + "-p", + "anyio", + "-p", + "pytest_cov", + "-p", + "pytest_mock", + "-p", + "asyncio", + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + "--cov=/opt/specfact/snapshot", + "--cov-report=json:/opt/specfact/tmp/coverage.json", + "tests/unit/specfact_code_review/run/test_installed_coverage.py", + "tests/unit/specfact_code_review/run/test_portable_snapshot.py", + "tests/unit/specfact_code_review/run/test_portable_worker.py", + "tests/unit/specfact_code_review/run/test_runner.py", + "tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py", + "tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py", + "tests/unit/specfact_code_review/run/test_runtime_builder.py", + "tests/unit/specfact_code_review/run/test_runtime_builder_logging.py", + "tests/unit/specfact_code_review/run/test_runtime_compatibility.py", + "tests/unit/specfact_code_review/run/test_runtime_discovery.py", + "tests/unit/specfact_code_review/run/test_runtime_models.py", + "tests/unit/specfact_code_review/run/test_runtime_tools.py", + "tests/unit/specfact_code_review/run/test_runtime_trust.py", + "tests/unit/specfact_code_review/run/test_runtime_vcs.py", + "tests/unit/specfact_code_review/run/test_snapshot_activation.py", + "tests/unit/specfact_code_review/run/test_target_pytest.py", + "tests/unit/specfact_code_review/tools/test_semgrep_runner.py" + ], + "configured_addopts": "-ra -v --import-mode=importlib", + "coverage_totals": { + "covered_branches": 2132, + "covered_lines": 8515, + "excluded_lines": 105, + "missing_branches": 3646, + "missing_lines": 8417, + "num_branches": 5778, + "num_partial_branches": 602, + "num_statements": 16932, + "percent_branches_covered": 36.89858082381447, + "percent_branches_covered_display": "36.90", + "percent_covered": 46.88243064729194, + "percent_covered_display": "46.88", + "percent_statements_covered": 50.28939286557997, + "percent_statements_covered_display": "50.29" + }, + "coverage_files": 64, + "versions": { + "pytest_version": "9.1.1", + "pytest_cov_version": "7.1.0", + "coverage_version": "7.16.1" + }, + "diagnostic": "analyzer_reported_incomplete_execution", + "limitation": "Executed immutable signed0.50.0 observer; final candidate observer and signed release acceptance remain pending." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_POLICY_EVIDENCE.md b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_POLICY_EVIDENCE.md new file mode 100644 index 00000000..72cf76c9 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_POLICY_EVIDENCE.md @@ -0,0 +1,31 @@ +# Native aggregate coverage policy controller evidence + +Date: 2026-09-16 (Europe/Berlin). + +The three ordinary-customer pytest execution scenarios were added to the canonical portable-runtime specification before the regression tests. The controller tests serialize a target observation and provide the actual subprocess exit through a controlled subprocess boundary; they exercise the real controller, observation validator, and coverage evaluator. These are controller regressions, not a Linux capsule acceptance run. + +## Recorded failure + +Command: `source /private/tmp/specfact-475-module-env.sh` then `.venv/bin/pytest tests/unit/specfact_code_review/run/test_portable_worker.py -q`. + +Before implementation: **21 failed, 50 passed**, 1.44 seconds. Full assertion transcript: [PR478_NATIVE_COVERAGE_POLICY_RED.txt](PR478_NATIVE_COVERAGE_POLICY_RED.txt). + +Failing cases cover native aggregate policy failures despite passing tests, coexistence with failed test outcomes and incomplete collection (including no records), malformed scalar fields and missing receipt fields, contradictory native modes and exit zero. Passing controls preserve older observations without the new field and non-failing native, reviewer, and disabled receipts for both zero and unexplained nonzero exits. + +## Passing evidence + +After implementation, the same focused command: **71 passed**, 0.23 seconds (Python 3.14.7, pytest 9.1.1). Local full output: `/private/tmp/specfact-478-native-coverage-policy-green.txt`. + +Ruff formatting/checks and focused BasedPyright: PASS (zero type errors or warnings). New private helper complexity is at most 9. Focused Pylint retains only the preexisting deferred runner import and native pytest `_ensure_unconfigure` protected-access warnings; the relevant import AST and test function AST are unchanged from HEAD. Existing `run_portable_pytest` and `validate_observation` complexities remain 11 and 12 respectively. No new warning exception was added. + +## Behavior and limits + +`TEST_COVERAGE_POLICY_FAILED` is a blocking testing finding only when a validated native receipt records a measured total, a native threshold failure, and actual pytest exit 1. The message retains the actual total, threshold, and precision. Existing test findings and incomplete execution diagnostics remain visible. Invalid receipts produce `project_pytest_coverage_policy_invalid`; absent receipts retain the existing reader behavior. The controller validates bounded scalar types but does not recreate Coverage rounding or infer a failure from an arbitrary exit 1. Per-file policy and native configured thresholds are unchanged. + +The target producer and native Coverage lifecycle are validated separately by the integration owner. Linux signed-capsule confirmation and combined source gates remain pending; this evidence does not claim those passed. + +## Specific target coverage diagnostic follow-up + +The existing scenarios require exact disabled, unsupported, and missing-worker evidence reasons. Before the follow-up production edit, new controller regressions produced **8 failed, 75 passed** (0.29 seconds); their transcript is appended to the same RED artifact. After implementation, the focused file has **83 passed** (0.26 seconds). Ruff format/check and focused BasedPyright passed; Pylint retains only the same two baseline warnings. + +When coverage files are absent, the controller now retains a nonempty target `coverage_diagnostic` exactly. A present non-string diagnostic yields `project_pytest_coverage_diagnostic_invalid`; absent/empty diagnostics preserve the existing `project_pytest_coverage_missing` remedy. Collection/internal errors keep precedence, and actual failed test records plus native aggregate policy findings remain visible. This does not change collection, execution, source coverage thresholds, or native outcomes. diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_POLICY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_POLICY_RED.txt new file mode 100644 index 00000000..bf632221 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_COVERAGE_POLICY_RED.txt @@ -0,0 +1,781 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 71 items + +tests/unit/specfact_code_review/run/test_portable_worker.py ............ [ 16%] +..............................FFFFFFFFFFFFFFFFFFFFF........ [100%] + +=================================== FAILURES =================================== +_____ test_native_aggregate_coverage_failure_has_explicit_blocking_finding _____ + +observe_coverage_policy = .observe at 0x1089c0bf0> + + def test_native_aggregate_coverage_failure_has_explicit_blocking_finding(observe_coverage_policy) -> None: + """A real native aggregate failure remains a failure even when every test passed.""" + findings = observe_coverage_policy(_coverage_observation(), 1) + assert len(findings) == 1 + finding = findings[0] +> assert (finding.rule, finding.category, finding.severity) == ("TEST_COVERAGE_POLICY_FAILED", "testing", "error") +E AssertionError: assert ('tool_error'...ror', 'error') == ('TEST_COVERA...ing', 'error') +E +E At index 0 diff: 'tool_error' != 'TEST_COVERAGE_POLICY_FAILED' +E Use -v to get more diff + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:309: AssertionError +_ test_native_coverage_failure_retains_incomplete_and_failed_test_evidence[False] _ + +observe_coverage_policy = .observe at 0x10897f8a0> +empty_records = False + + @pytest.mark.parametrize("empty_records", [False, True]) + def test_native_coverage_failure_retains_incomplete_and_failed_test_evidence( + observe_coverage_policy, empty_records: bool + ) -> None: + """The aggregate finding supplements, rather than replaces, partial test evidence.""" + observation = _coverage_observation() + observation["collection_errors"] = ["controlled collection failure"] + observation["records"] = ( + [] if empty_records else [{"nodeid": "test_app.py::test_app", "phase": "call", "outcome": "failed"}] + ) + findings = observe_coverage_policy(observation, 1) +> assert "TEST_COVERAGE_POLICY_FAILED" in {row.rule for row in findings} +E AssertionError: assert 'TEST_COVERAGE_POLICY_FAILED' in {'TEST_OUTCOME_NOT_PASS', 'tool_error'} + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:324: AssertionError +_ test_native_coverage_failure_retains_incomplete_and_failed_test_evidence[True] _ + +observe_coverage_policy = .observe at 0x10897da60> +empty_records = True + + @pytest.mark.parametrize("empty_records", [False, True]) + def test_native_coverage_failure_retains_incomplete_and_failed_test_evidence( + observe_coverage_policy, empty_records: bool + ) -> None: + """The aggregate finding supplements, rather than replaces, partial test evidence.""" + observation = _coverage_observation() + observation["collection_errors"] = ["controlled collection failure"] + observation["records"] = ( + [] if empty_records else [{"nodeid": "test_app.py::test_app", "phase": "call", "outcome": "failed"}] + ) + findings = observe_coverage_policy(observation, 1) +> assert "TEST_COVERAGE_POLICY_FAILED" in {row.rule for row in findings} +E AssertionError: assert 'TEST_COVERAGE_POLICY_FAILED' in {'tool_error'} + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:324: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[mode-unknown] __ + +observe_coverage_policy = .observe at 0x10897f060> +field = 'mode', value = 'unknown' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108960d40>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[threshold-True] _ + +observe_coverage_policy = .observe at 0x10897ee50> +field = 'threshold', value = True + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108960ad0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[threshold-101] _ + +observe_coverage_policy = .observe at 0x10897f110> +field = 'threshold', value = 101 + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x1089605f0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[threshold-nan] _ + +observe_coverage_policy = .observe at 0x10897f270> +field = 'threshold', value = nan + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x10899b1d0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[precision-True] _ + +observe_coverage_policy = .observe at 0x10897f740> +field = 'precision', value = True + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x10899acf0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[precision--1] __ + +observe_coverage_policy = .observe at 0x10897d6f0> +field = 'precision', value = -1 + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x10899b780>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[precision-10] __ + +observe_coverage_policy = .observe at 0x1089e1b10> +field = 'precision', value = 10 + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108998110>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[measured_total-46.88] _ + +observe_coverage_policy = .observe at 0x1089e1dd0> +field = 'measured_total', value = '46.88' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108999080>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[measured_total-inf] _ + +observe_coverage_policy = .observe at 0x1089e2560> +field = 'measured_total', value = inf + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x10899aa80>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[measured_total-None] _ + +observe_coverage_policy = .observe at 0x1089e2980> +field = 'measured_total', value = None + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x10899bd30>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[native_threshold_failed-1] _ + +observe_coverage_policy = .observe at 0x10897e770> +field = 'native_threshold_failed', value = 1 + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa1ff0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[mode-reviewer] _ + +observe_coverage_policy = .observe at 0x10897d2d0> +field = 'mode', value = 'reviewer' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa18a0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +_ test_malformed_or_contradictory_coverage_policy_is_actionable[mode-disabled] _ + +observe_coverage_policy = .observe at 0x10897e090> +field = 'mode', value = 'disabled' + + @pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], + ) + def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa1560>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:352: AssertionError +__________ test_incomplete_coverage_policy_shape_is_actionable[None] ___________ + +observe_coverage_policy = .observe at 0x10897f7f0> +policy = None + + @pytest.mark.parametrize("policy", [None, [], {}, {"mode": "native"}]) + def test_incomplete_coverage_policy_shape_is_actionable(observe_coverage_policy, policy) -> None: + """A present but incomplete receipt cannot use the legacy missing-receipt path.""" + observation = _coverage_observation() + observation["coverage_policy"] = policy + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa3100>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:362: AssertionError +_________ test_incomplete_coverage_policy_shape_is_actionable[policy1] _________ + +observe_coverage_policy = .observe at 0x1089e26c0> +policy = [] + + @pytest.mark.parametrize("policy", [None, [], {}, {"mode": "native"}]) + def test_incomplete_coverage_policy_shape_is_actionable(observe_coverage_policy, policy) -> None: + """A present but incomplete receipt cannot use the legacy missing-receipt path.""" + observation = _coverage_observation() + observation["coverage_policy"] = policy + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa3c60>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:362: AssertionError +_________ test_incomplete_coverage_policy_shape_is_actionable[policy2] _________ + +observe_coverage_policy = .observe at 0x1089e2770> +policy = {} + + @pytest.mark.parametrize("policy", [None, [], {}, {"mode": "native"}]) + def test_incomplete_coverage_policy_shape_is_actionable(observe_coverage_policy, policy) -> None: + """A present but incomplete receipt cannot use the legacy missing-receipt path.""" + observation = _coverage_observation() + observation["coverage_policy"] = policy + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa0ad0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:362: AssertionError +_________ test_incomplete_coverage_policy_shape_is_actionable[policy3] _________ + +observe_coverage_policy = .observe at 0x1089e2cf0> +policy = {'mode': 'native'} + + @pytest.mark.parametrize("policy", [None, [], {}, {"mode": "native"}]) + def test_incomplete_coverage_policy_shape_is_actionable(observe_coverage_policy, policy) -> None: + """A present but incomplete receipt cannot use the legacy missing-receipt path.""" + observation = _coverage_observation() + observation["coverage_policy"] = policy + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa0450>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:362: AssertionError +________ test_native_coverage_failure_cannot_contradict_successful_exit ________ + +observe_coverage_policy = .observe at 0x1089e35e0> + + def test_native_coverage_failure_cannot_contradict_successful_exit(observe_coverage_policy) -> None: + """A failed-policy receipt paired with exit zero must remain incomplete evidence.""" + findings = observe_coverage_policy(_coverage_observation(0), 0) +> assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x108aa3030>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:368: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_native_aggregate_coverage_failure_has_explicit_blocking_finding +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_native_coverage_failure_retains_incomplete_and_failed_test_evidence[False] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_native_coverage_failure_retains_incomplete_and_failed_test_evidence[True] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[mode-unknown] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[threshold-True] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[threshold-101] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[threshold-nan] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[precision-True] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[precision--1] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[precision-10] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[measured_total-46.88] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[measured_total-inf] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[measured_total-None] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[native_threshold_failed-1] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[mode-reviewer] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_or_contradictory_coverage_policy_is_actionable[mode-disabled] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_incomplete_coverage_policy_shape_is_actionable[None] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_incomplete_coverage_policy_shape_is_actionable[policy1] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_incomplete_coverage_policy_shape_is_actionable[policy2] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_incomplete_coverage_policy_shape_is_actionable[policy3] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_native_coverage_failure_cannot_contradict_successful_exit +======================== 21 failed, 50 passed in 1.44s ========================= + + +FOLLOW-UP: specific coverage diagnostic, before implementation +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 83 items + +tests/unit/specfact_code_review/run/test_portable_worker.py ............ [ 14%] +...........................................................FFFFFFFF.... [100%] + +=================================== FAILURES =================================== +_ test_specific_missing_coverage_diagnostic_preserves_other_failures[project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable] _ + +observe_coverage_policy = .observe at 0x1096d1c70> +diagnostic = 'project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable' + + @pytest.mark.parametrize( + "diagnostic", + [ + "project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable", + "project_pytest_coverage_xdist_unsupported:missing worker coverage", + "project_pytest_coverage_export_failed:configured report suppression", + ], + ) + def test_specific_missing_coverage_diagnostic_preserves_other_failures(observe_coverage_policy, diagnostic) -> None: + """Keep the exact target remedy together with native policy and actual test failures.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + observation["records"][0]["outcome"] = "failed" + findings = observe_coverage_policy(observation, 1) +> assert any(row.message == diagnostic for row in findings) +E assert False +E + where False = any(. at 0x1096497e0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:401: AssertionError +_ test_specific_missing_coverage_diagnostic_preserves_other_failures[project_pytest_coverage_xdist_unsupported:missing worker coverage] _ + +observe_coverage_policy = .observe at 0x1096a19b0> +diagnostic = 'project_pytest_coverage_xdist_unsupported:missing worker coverage' + + @pytest.mark.parametrize( + "diagnostic", + [ + "project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable", + "project_pytest_coverage_xdist_unsupported:missing worker coverage", + "project_pytest_coverage_export_failed:configured report suppression", + ], + ) + def test_specific_missing_coverage_diagnostic_preserves_other_failures(observe_coverage_policy, diagnostic) -> None: + """Keep the exact target remedy together with native policy and actual test failures.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + observation["records"][0]["outcome"] = "failed" + findings = observe_coverage_policy(observation, 1) +> assert any(row.message == diagnostic for row in findings) +E assert False +E + where False = any(. at 0x1096499a0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:401: AssertionError +_ test_specific_missing_coverage_diagnostic_preserves_other_failures[project_pytest_coverage_export_failed:configured report suppression] _ + +observe_coverage_policy = .observe at 0x1096f3b60> +diagnostic = 'project_pytest_coverage_export_failed:configured report suppression' + + @pytest.mark.parametrize( + "diagnostic", + [ + "project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable", + "project_pytest_coverage_xdist_unsupported:missing worker coverage", + "project_pytest_coverage_export_failed:configured report suppression", + ], + ) + def test_specific_missing_coverage_diagnostic_preserves_other_failures(observe_coverage_policy, diagnostic) -> None: + """Keep the exact target remedy together with native policy and actual test failures.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + observation["records"][0]["outcome"] = "failed" + findings = observe_coverage_policy(observation, 1) +> assert any(row.message == diagnostic for row in findings) +E assert False +E + where False = any(. at 0x10964a260>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:401: AssertionError +_____ test_malformed_missing_coverage_diagnostic_is_not_stringified[None] ______ + +observe_coverage_policy = .observe at 0x1097101a0> +diagnostic = None + + @pytest.mark.parametrize("diagnostic", [None, False, 7, [], {"reason": "untrusted"}]) + def test_malformed_missing_coverage_diagnostic_is_not_stringified(observe_coverage_policy, diagnostic) -> None: + """Reject a malformed target diagnostic instead of formatting an arbitrary object.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_diagnostic_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x1097e9150>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:413: AssertionError +_____ test_malformed_missing_coverage_diagnostic_is_not_stringified[False] _____ + +observe_coverage_policy = .observe at 0x1097105c0> +diagnostic = False + + @pytest.mark.parametrize("diagnostic", [None, False, 7, [], {"reason": "untrusted"}]) + def test_malformed_missing_coverage_diagnostic_is_not_stringified(observe_coverage_policy, diagnostic) -> None: + """Reject a malformed target diagnostic instead of formatting an arbitrary object.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_diagnostic_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x1097eb510>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:413: AssertionError +_______ test_malformed_missing_coverage_diagnostic_is_not_stringified[7] _______ + +observe_coverage_policy = .observe at 0x1097109e0> +diagnostic = 7 + + @pytest.mark.parametrize("diagnostic", [None, False, 7, [], {"reason": "untrusted"}]) + def test_malformed_missing_coverage_diagnostic_is_not_stringified(observe_coverage_policy, diagnostic) -> None: + """Reject a malformed target diagnostic instead of formatting an arbitrary object.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_diagnostic_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x1097e81e0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:413: AssertionError +__ test_malformed_missing_coverage_diagnostic_is_not_stringified[diagnostic3] __ + +observe_coverage_policy = .observe at 0x109710eb0> +diagnostic = [] + + @pytest.mark.parametrize("diagnostic", [None, False, 7, [], {"reason": "untrusted"}]) + def test_malformed_missing_coverage_diagnostic_is_not_stringified(observe_coverage_policy, diagnostic) -> None: + """Reject a malformed target diagnostic instead of formatting an arbitrary object.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_diagnostic_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x1097fe5a0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:413: AssertionError +__ test_malformed_missing_coverage_diagnostic_is_not_stringified[diagnostic4] __ + +observe_coverage_policy = .observe at 0x109711380> +diagnostic = {'reason': 'untrusted'} + + @pytest.mark.parametrize("diagnostic", [None, False, 7, [], {"reason": "untrusted"}]) + def test_malformed_missing_coverage_diagnostic_is_not_stringified(observe_coverage_policy, diagnostic) -> None: + """Reject a malformed target diagnostic instead of formatting an arbitrary object.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) +> assert any("project_pytest_coverage_diagnostic_invalid" in row.message for row in findings) +E assert False +E + where False = any(. at 0x1097fca00>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_worker.py:413: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_specific_missing_coverage_diagnostic_preserves_other_failures[project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_specific_missing_coverage_diagnostic_preserves_other_failures[project_pytest_coverage_xdist_unsupported:missing worker coverage] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_specific_missing_coverage_diagnostic_preserves_other_failures[project_pytest_coverage_export_failed:configured report suppression] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_missing_coverage_diagnostic_is_not_stringified[None] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_missing_coverage_diagnostic_is_not_stringified[False] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_missing_coverage_diagnostic_is_not_stringified[7] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_missing_coverage_diagnostic_is_not_stringified[diagnostic3] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_malformed_missing_coverage_diagnostic_is_not_stringified[diagnostic4] +========================= 8 failed, 75 passed in 0.29s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_FAILURE.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_FAILURE.txt new file mode 100644 index 00000000..8b8affde --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_FAILURE.txt @@ -0,0 +1,4 @@ +Checking dependencies + + +Error: Invalid value: project_analyzer_dependency_incompatible:pylint:isort; selected=9.0.1, required=!=5.13,<9,>=5; private log: /home/runner/work/_temp/native-hooks/home/.cache/specfact/code-review/project-runtimes-v2/failed-cd0f3af355b55f461cbd7f0778745d9b0d3d184a5fda875998a1508f432760d7-ippyozod.log diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_PACKAGES.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_PACKAGES.txt new file mode 100644 index 00000000..535546b5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_ENVIRONMENT_PACKAGES.txt @@ -0,0 +1,123 @@ +annotated-doc==0.0.5 +annotated-types==0.8.0 +anyio==4.15.1 +astroid==4.0.4 +asttokens==2.4.1 +attrs==26.1.0 +azure-core==1.41.0 +azure-identity==1.25.3 +basedpyright==1.40.1 +beartype==0.22.9 +boltons==21.0.0 +bracex==3.0.1 +certifi==2026.7.22 +cffi==2.1.1 +cfgv==3.5.0 +charset-normalizer==3.5.1 +click==8.4.2 +click-option-group==0.5.9 +colorama==0.4.6 +commentjson==0.9.0 +coverage==7.16.1 +cryptography==50.0.1 +dill==0.4.1 +distlib==0.4.3 +exceptiongroup==1.2.2 +face==26.0.1 +filelock==3.32.7 +gitdb==4.0.12 +GitPython==3.1.62 +glom==25.12.0 +googleapis-common-protos==1.75.3 +graphviz==0.21 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.3 +icontract==2.7.3 +identify==2.6.19 +idna==3.19 +importlib_metadata==8.7.1 +iniconfig==2.3.0 +isort==9.0.1 +Jinja2==3.1.6 +json5==0.15.0 +jsonschema==4.25.1 +jsonschema-specifications==2025.9.1 +lark-parser==0.7.8 +lz4==4.4.5 +mando==0.7.1 +markdown-it-py==4.2.0 +MarkupSafe==3.0.3 +mccabe==0.7.0 +mcp==1.29.0 +mdurl==0.1.2 +msal==1.38.0 +msal-extensions==1.3.1 +mypy_extensions==1.1.0 +networkx==3.6.1 +nodeenv==1.10.0 +nodejs-wheel-binaries==24.19.0 +opentelemetry-api==1.37.0 +opentelemetry-exporter-otlp-proto-common==1.37.0 +opentelemetry-exporter-otlp-proto-http==1.37.0 +opentelemetry-instrumentation==0.58b0 +opentelemetry-instrumentation-requests==0.58b0 +opentelemetry-instrumentation-threading==0.58b0 +opentelemetry-proto==1.37.0 +opentelemetry-sdk==1.37.0 +opentelemetry-semantic-conventions==0.58b0 +opentelemetry-util-http==0.58b0 +packaging==26.3 +peewee==3.19.0 +platformdirs==4.11.8 +pluggy==1.6.0 +pre_commit==4.6.2 +prompt_toolkit==3.0.53 +protobuf==6.33.6 +pycparser==2.23 +pydantic==2.13.5 +pydantic-settings==2.15.0 +pydantic_core==2.46.5 +Pygments==2.21.0 +PyJWT==2.13.0 +pylint==4.0.8 +pytest==9.1.1 +pytest-asyncio==1.4.0 +pytest-cov==7.1.0 +pytest-mock==3.15.1 +python-discovery==1.6.0 +python-dotenv==1.2.3 +python-multipart==0.0.32 +PyYAML==6.0.3 +questionary==2.1.1 +radon==6.0.1 +referencing==0.37.0 +requests==2.34.2 +rich==15.0.0 +rpds-py==2026.6.3 +ruamel.yaml==0.19.1 +ruamel.yaml.clib==0.2.15 +ruff==0.16.7 +semantic-version==2.10.0 +semgrep==1.177.0 +shellingham==1.5.4 +six==1.17.0 +smmap==5.0.3 +-e git+https://github.com/nold-ai/specfact-cli@2f9567e28e98b082801b13ff02e863933ce2f0f2#egg=specfact_cli +-e git+https://github.com/nold-ai/specfact-cli-modules@efe7303efb32ac31379906a38e3f960ec4e7a744#egg=specfact_cli_modules +sse-starlette==3.4.11 +starlette==1.6.0 +tomli==2.4.1 +tomlkit==0.15.1 +typer==0.27.2 +typing-inspection==0.4.4 +typing_extensions==4.16.0 +urllib3==2.8.0 +uvicorn==0.53.0 +virtualenv==21.7.10 +watchdog==6.0.0 +wcmatch==8.5.2 +wcwidth==0.8.3 +wrapt==1.17.3 +zipp==4.1.0 diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_FIXTURE_FAILURES.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_FIXTURE_FAILURES.json new file mode 100644 index 00000000..71cbc9fe --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_FIXTURE_FAILURES.json @@ -0,0 +1,30 @@ +{ + "source": "GitHub Actions run 35090737789, original unchanged hooks", + "authority": "failed-native-hook-evidence", + "acceptance": false, + "artifact_id": 10444661483, + "artifact_sha256": "aba4fcb81b9756574b0a0ac9d06f0e0a7bf9efc596f75fedfb86469bc34f714b", + "source_tree": "6785a630a52241acf70f97e856aeec89e75dc35b", + "block2_seconds": 284.0, + "collected": 1011, + "passing_calls": 1010, + "failed_records": [ + { + "detail": "tmp_path = PosixPath('/opt/specfact/tmp/pytest/test_unbound_grafts_cannot_rem0')\n\n def test_unbound_grafts_cannot_remove_selected_ancestry(tmp_path: Path) -> None:\n root = tmp_path / \"source\"\n _repository(root)\n (root / \"app.py\").write_text(\"VALUE = 2\\n\")\n selected = _commit_fixture(root, \"selected\")\n before = vcs_context(root)\n> (root / \".git/info/grafts\").write_text(selected + \"\\n\")\n\ntests/unit/specfact_code_review/run/test_runtime_vcs.py:474: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n../python/lib/python3.12/pathlib.py:1047: in write_text\n with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = PosixPath('/opt/specfact/tmp/pytest/test_unbound_grafts_cannot_rem0/source/.git/info/grafts')\nmode = 'w', buffering = -1, encoding = 'utf-8', errors = None, newline = None\n\n def open(self, mode='r', buffering=-1, encoding=None,\n errors=None, newline=None):\n \"\"\"\n Open the file pointed to by this path and return a file object, as\n the built-in open() function does.\n \"\"\"\n if \"b\" not in mode:\n encoding = io.text_encoding(encoding)\n> return io.open(self, mode, buffering, encoding, errors, newline)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nE FileNotFoundError: [Errno 2] No such file or directory: '/opt/specfact/tmp/pytest/test_unbound_grafts_cannot_rem0/source/.git/info/grafts'\n\n../python/lib/python3.12/pathlib.py:1013: FileNotFoundError", + "nodeid": "tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unbound_grafts_cannot_remove_selected_ancestry", + "outcome": "failed", + "phase": "call", + "wasxfail": "" + } + ], + "semgrep_replay_errors": [ + { + "code": 3, + "level": "warn", + "type": "Syntax error", + "message": "Syntax error at line /opt/specfact/snapshot/tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:61:\n `,` was unexpected", + "path": "/opt/specfact/snapshot/tests/unit/specfact_code_review/run/test_runtime_builder_logging.py" + } + ], + "timing_diagnostic": "Not run: the original review completed and produced a report; no timeout budget change is justified." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_CLOSURE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_CLOSURE_RED.txt new file mode 100644 index 00000000..1aea69ab --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_CLOSURE_RED.txt @@ -0,0 +1,31 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 12 items + +tests/unit/specfact_code_review/run/test_runtime_tools.py ..........F. [100%] + +=================================== FAILURES =================================== +______________ test_git_helper_transitive_libraries_are_captured _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-690/test_git_helper_transitive_lib0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a3a5950> + + def test_git_helper_transitive_libraries_are_captured(tmp_path: Path, monkeypatch) -> None: + tools, system = _native_fixture(tmp_path, monkeypatch) + (system / 'libhelper.so.1').write_bytes((system / 'git').read_bytes()) + (system / 'libtransitive.so.1').write_bytes((system / 'git').read_bytes()) + _elf_needing(system / 'git-remote-http', 'libhelper.so.1') + _elf_needing(system / 'libhelper.so.1', 'libtransitive.so.1') + capture = tools.capture_native_tools(('git',), capsule_root=_sealed_shell(tmp_path)) +> assert 'native-tools/native/libhelper.so.1' in capture.files +E AssertionError: assert 'native-tools/native/libhelper.so.1' in {'native-tools/native/ld-linux-x86-64.so.2': b'\x7fELF\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0...\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ...} +E + where {'native-tools/native/ld-linux-x86-64.so.2': b'\x7fELF\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0...\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ...} = CapturedTools(files={'native-tools/native/ld-linux-x86-64.so.2': b'\x7fELF\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00...e/bin/sh': {'source': '/bin/sh', 'sha256': 'sha256:ffd94ee4a76de5639b2119d943bf39759049075dae35a1db573d27ee6365068e'}}).files + +tests/unit/specfact_code_review/run/test_runtime_tools.py:126: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_git_helper_transitive_libraries_are_captured +========================= 1 failed, 11 passed in 0.14s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_COLLISION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_COLLISION_RED.txt new file mode 100644 index 00000000..e5118e93 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_COLLISION_RED.txt @@ -0,0 +1,52 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 18 items / 15 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_runtime_tools.py .F. [100%] + +=================================== FAILURES =================================== +__________ test_builder_tool_collisions_fail_before_any_install[bin] ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-696/test_builder_tool_collisions_f1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10bad0510> +collision = 'bin' + + @pytest.mark.parametrize("collision", ["bin/uname", "bin", "native-tools/forged"]) + def test_builder_tool_collisions_fail_before_any_install(tmp_path: Path, monkeypatch, collision: str) -> None: + tools, _ = _native_fixture(tmp_path, monkeypatch) + capture = tools.capture_native_tools(("uname",)) + artifact = tmp_path / "artifact" + target = artifact / collision + target.parent.mkdir(parents=True) + target.write_text("builder controlled") + with pytest.raises(ProjectRuntimeError, match="native_tool_collision"): +> tools.install_native_tools(capture, artifact) + +tests/unit/specfact_code_review/run/test_runtime_tools.py:83: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_tools.py:148: in install_native_tools + target.parent.mkdir(parents=True, exist_ok=True) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-696/test_builder_tool_collisions_f1/artifact/bin') +mode = 511, parents = True, exist_ok = True + + def mkdir(self, mode=0o777, parents=False, exist_ok=False): + """ + Create a new directory at this given path. + """ + try: +> os.mkdir(self, mode) +E FileExistsError: [Errno 17] File exists: '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-696/test_builder_tool_collisions_f1/artifact/bin' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:1011: FileExistsError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_builder_tool_collisions_fail_before_any_install[bin] +================== 1 failed, 2 passed, 15 deselected in 0.25s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_GIT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_GIT_RED.txt new file mode 100644 index 00000000..b7cfe1ad --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_GIT_RED.txt @@ -0,0 +1,28 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 17 items / 16 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_tools.py F [100%] + +=================================== FAILURES =================================== +_____________ test_declared_git_captures_local_transport_services ______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-694/test_declared_git_captures_loc0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1080c2650> + + def test_declared_git_captures_local_transport_services(tmp_path: Path, monkeypatch) -> None: + tools, _ = _native_fixture(tmp_path, monkeypatch) + capture = tools.capture_native_tools(("git",), capsule_root=_sealed_shell(tmp_path)) + for name in ("git-upload-pack", "git-receive-pack", "git-upload-archive"): +> assert f"native-tools/git-core/{name}" in capture.files +E AssertionError: assert 'native-tools/git-core/git-upload-pack' in {'native-tools/native/ld-linux-x86-64.so.2': b'\x7fELF\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0...\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ...} +E + where {'native-tools/native/ld-linux-x86-64.so.2': b'\x7fELF\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x0...\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ...} = CapturedTools(files={'native-tools/native/ld-linux-x86-64.so.2': b'\x7fELF\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00...e/bin/sh': {'source': '/bin/sh', 'sha256': 'sha256:ffd94ee4a76de5639b2119d943bf39759049075dae35a1db573d27ee6365068e'}}).files + +tests/unit/specfact_code_review/run/test_runtime_tools.py:213: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_declared_git_captures_local_transport_services +======================= 1 failed, 16 deselected in 0.15s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_LINUX_SMOKE.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_LINUX_SMOKE.json new file mode 100644 index 00000000..0668b461 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_LINUX_SMOKE.json @@ -0,0 +1,318 @@ +{ + "authority": "local-container-smoke-not-capsule-acceptance", + "payload_bytes": 38128554, + "payload_files": 50, + "identity": "sha256:d56dcce0a11a61cd9844d0c6a9c4740bc73063df5e794ed8a31c380c3e7fefe8", + "inventory": { + "native-tools/executables/git": { + "source": "/usr/bin/git", + "sha256": "sha256:2a8c18fbf43da9f692d75474c72bea9dfd796c260b0f3dfe456376abc3bbd668" + }, + "native-tools/executables/sed": { + "source": "/usr/bin/sed", + "sha256": "sha256:23f70dd6496d75419823039e1cebf95a2fd0508c6dfec72ac2352f7e08391dd5" + }, + "native-tools/executables/uname": { + "source": "/usr/bin/uname", + "sha256": "sha256:c6e023b172383fae5318c600e6f2f3a153513df62bf06ad6c9269296d2a78e89" + }, + "native-tools/executables/git-core/git-remote-http": { + "source": "/usr/lib/git-core/git-remote-http", + "sha256": "sha256:8353a5124ddb2281838bc54bd97c21cbecfe900615fdb60b281198c5058ddb5b" + }, + "native-tools/executables/git-core/git-remote-https": { + "source": "/usr/lib/git-core/git-remote-http", + "sha256": "sha256:8353a5124ddb2281838bc54bd97c21cbecfe900615fdb60b281198c5058ddb5b" + }, + "native-tools/executables/git-core/git-upload-pack": { + "source": "/usr/lib/git-core/git", + "sha256": "sha256:2a8c18fbf43da9f692d75474c72bea9dfd796c260b0f3dfe456376abc3bbd668" + }, + "native-tools/executables/git-core/git-receive-pack": { + "source": "/usr/lib/git-core/git", + "sha256": "sha256:2a8c18fbf43da9f692d75474c72bea9dfd796c260b0f3dfe456376abc3bbd668" + }, + "native-tools/executables/git-core/git-upload-archive": { + "source": "/usr/lib/git-core/git", + "sha256": "sha256:2a8c18fbf43da9f692d75474c72bea9dfd796c260b0f3dfe456376abc3bbd668" + }, + "native-tools/native/ld-linux-x86-64.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + "sha256": "sha256:cd4df4f3c7b83673d61189bf2eaebd33ca4f2853ab9772b8a25e025ef99b1e81" + }, + "native-tools/native/libacl.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libacl.so.1.1.2302", + "sha256": "sha256:1f5247285409ec89990e9fe384d78dfb2ae6ec8e210a2657073babc8d6b7e54f" + }, + "native-tools/native/libc.so.6": { + "source": "/usr/lib/x86_64-linux-gnu/libc.so.6", + "sha256": "sha256:8db37cf3f2169f59a0f07ef1fea308c35656668c64c8ff294e1860f4121eb161" + }, + "native-tools/native/libcurl-gnutls.so.4": { + "source": "/usr/lib/x86_64-linux-gnu/libcurl-gnutls.so.4.8.0", + "sha256": "sha256:78e00eca595180e14691e9d5b59aaeecb48ee3f34dab0c9858656bc87c37ad4f" + }, + "native-tools/native/libbrotlidec.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libbrotlidec.so.1.1.0", + "sha256": "sha256:64d8a5019d4c294b89fde1193343ea324bbd8603652554e5545f0a01595fa2c5" + }, + "native-tools/native/libbrotlicommon.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libbrotlicommon.so.1.1.0", + "sha256": "sha256:a91ead095d2c80520c55a89057bbe10b031a075340442e63f44b310f93883a1b" + }, + "native-tools/native/libgnutls.so.30": { + "source": "/usr/lib/x86_64-linux-gnu/libgnutls.so.30.37.1", + "sha256": "sha256:ddbae2995750875c07bc218d12a062d73f8250678f54dedda7e9be5068781c98" + }, + "native-tools/native/libgmp.so.10": { + "source": "/usr/lib/x86_64-linux-gnu/libgmp.so.10.5.0", + "sha256": "sha256:0ccdfb6d6f5c039465f6d002cf7e4c072d48ac6a2cffc8dd6c748dec31592804" + }, + "native-tools/native/libgssapi_krb5.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2.2", + "sha256": "sha256:6c1b81696044d79a47d6f0f494ee60aaf641c525c46a11da0c6e4a041a782d3c" + }, + "native-tools/native/libcom_err.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/libcom_err.so.2.1", + "sha256": "sha256:022943b3b11c860b049bce41342f1c2594941b7b401d95dfdf235521099fee08" + }, + "native-tools/native/libhogweed.so.6": { + "source": "/usr/lib/x86_64-linux-gnu/libhogweed.so.6.8", + "sha256": "sha256:c3c606139d4f7776efa4dbd4a6b62e30c916b07a0cafd8c3ef173d6f093607e3" + }, + "native-tools/native/libidn2.so.0": { + "source": "/usr/lib/x86_64-linux-gnu/libidn2.so.0.4.0", + "sha256": "sha256:6e632fd9a3125db60bbc623719bbc17b0470330321c5b1c57f1ff7190e5551ad" + }, + "native-tools/native/libk5crypto.so.3": { + "source": "/usr/lib/x86_64-linux-gnu/libk5crypto.so.3.1", + "sha256": "sha256:73bc9d72c0c684d6149a3c38f96aab891d178375415c05954fdda587685936f5" + }, + "native-tools/native/libkrb5.so.3": { + "source": "/usr/lib/x86_64-linux-gnu/libkrb5.so.3.3", + "sha256": "sha256:9615a2841f0783c410eec7fae005a951282551cfedd377737ad8431c8c8cde64" + }, + "native-tools/native/libkeyutils.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libkeyutils.so.1.10", + "sha256": "sha256:f48214417757f18793ed6e180cc14ee1d6f04252a518fc7270e8ca1d0b4260fe" + }, + "native-tools/native/libkrb5support.so.0": { + "source": "/usr/lib/x86_64-linux-gnu/libkrb5support.so.0.1", + "sha256": "sha256:0aa43578471faecbd642ed2ee6ab92b6682a2b676644193e956fc59d18ca24bb" + }, + "native-tools/native/liblber.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/liblber.so.2.0.200", + "sha256": "sha256:ddf14275b1fb70ba06510094071adcc1462e675f15ae6cdcadbf40c1dabe5a03" + }, + "native-tools/native/libldap.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/libldap.so.2.0.200", + "sha256": "sha256:a9829b999c95452eb0a34cc7b5349427b76dbf3e5d5c8672a5ca69e454c1b151" + }, + "native-tools/native/libnettle.so.8": { + "source": "/usr/lib/x86_64-linux-gnu/libnettle.so.8.8", + "sha256": "sha256:a245ed916229583d6fe0b07657b3ed945bc148c28cc08b5266295f620d2b91cc" + }, + "native-tools/native/libnghttp2.so.14": { + "source": "/usr/lib/x86_64-linux-gnu/libnghttp2.so.14.26.0", + "sha256": "sha256:46764ab5b6ca7e353a322054f0bee45d9158a27f3e6a82a2fa4447f2ba9ba268" + }, + "native-tools/native/libp11-kit.so.0": { + "source": "/usr/lib/x86_64-linux-gnu/libp11-kit.so.0.3.1", + "sha256": "sha256:a59efd4792d3343dedea1d26a28f5597abfe3369082deb504c4802a31b014897" + }, + "native-tools/native/libffi.so.8": { + "source": "/usr/lib/x86_64-linux-gnu/libffi.so.8.1.4", + "sha256": "sha256:00f593fe192f2851b8ce23b25cec2488d769beb5a8f63e8c9e563071e1075153" + }, + "native-tools/native/libpcre2-8.so.0": { + "source": "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0.11.2", + "sha256": "sha256:e00576d71d81d3ba0cfa4903c835a44a8723aac96f72f79ff75200b4cff9071b" + }, + "native-tools/native/libpsl.so.5": { + "source": "/usr/lib/x86_64-linux-gnu/libpsl.so.5.3.4", + "sha256": "sha256:ae6c5a9aecc249fc66e03c16ec6c4385d09d7e12e0ea7694bb831ab4fee8133c" + }, + "native-tools/native/libresolv.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/libresolv.so.2", + "sha256": "sha256:4086b12d3b95ef6f02373b30b88444e7d79fb8693ed1cfeaa5162e6959878912" + }, + "native-tools/native/librtmp.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/librtmp.so.1", + "sha256": "sha256:15a2a75e8c437b9c99971b5ee10278ab110beef1a56d1d9e5d6759ea140e98ac" + }, + "native-tools/native/libsasl2.so.2": { + "source": "/usr/lib/x86_64-linux-gnu/libsasl2.so.2.0.25", + "sha256": "sha256:a0f410ad093236856eee59c4ff2838dc04d932ea0993400f23ed77d67d85af34" + }, + "native-tools/native/libcrypto.so.3": { + "source": "/usr/lib/x86_64-linux-gnu/libcrypto.so.3", + "sha256": "sha256:6a66c3ba6b3749aacc9497973fff00f6ba61c703ba7015d0d7ab3fd9510974b6" + }, + "native-tools/native/libselinux.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libselinux.so.1", + "sha256": "sha256:0ec0843463f90e227b0e138e1f379774f50b5514bd7f5dbd4c8a87aca0aa6670" + }, + "native-tools/native/libssh.so.4": { + "source": "/usr/lib/x86_64-linux-gnu/libssh.so.4.9.6", + "sha256": "sha256:b82bd54edb908605b8142be1a6d242df65501af0e9bb7581cc36ebf4d8a5ecfd" + }, + "native-tools/native/libtasn1.so.6": { + "source": "/usr/lib/x86_64-linux-gnu/libtasn1.so.6.6.3", + "sha256": "sha256:59994414d0cdec4f678ef91eaaafc44b26b00bca507e4cc7978caaf79f584173" + }, + "native-tools/native/libunistring.so.5": { + "source": "/usr/lib/x86_64-linux-gnu/libunistring.so.5.0.0", + "sha256": "sha256:7bcf825cd449892db5105ad2aed91dcf94a27b796426600c765720c584c54dd1" + }, + "native-tools/native/libz.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libz.so.1.3", + "sha256": "sha256:86200da370f20476a2507e9097a789b5ef97269b4ca8d5e164ad82dab9d99892" + }, + "native-tools/native/libzstd.so.1": { + "source": "/usr/lib/x86_64-linux-gnu/libzstd.so.1.5.5", + "sha256": "sha256:0a2128bc10841fb29e76d08d945864dfb0b6a66da5df6df5d8299197439e54bb" + }, + "bin/git": { + "source": "generated-controller-launcher", + "sha256": "sha256:719aed1ab68bbc0f0888bd0f304351edf977ce52cfbc74172d5c5575596ece0c" + }, + "bin/sed": { + "source": "generated-controller-launcher", + "sha256": "sha256:804c4a9dd336b8ad8508b762157cffc69b30e84bd3df75e1afe7d50631fa99b1" + }, + "bin/uname": { + "source": "generated-controller-launcher", + "sha256": "sha256:2b956649fc11c040b7a6a5abf347316b59aa49a197544bf27c3d9a4ecba23a52" + }, + "native-tools/git-core/git-remote-http": { + "source": "generated-controller-launcher", + "sha256": "sha256:500e637f5b05948d09f9db427b752aaefe6cd073a9702637a45ab0a17adebeda" + }, + "native-tools/git-core/git-remote-https": { + "source": "generated-controller-launcher", + "sha256": "sha256:d9a57e59ad8f81e0a82091bba3549f263513159fcc84e1f07e0e1e05e4ee689d" + }, + "native-tools/git-core/git-upload-pack": { + "source": "generated-controller-launcher", + "sha256": "sha256:af325af3eb44720eeccf865daf7a71bb11bdc358f03d1fbea3c79ba94939ac6b" + }, + "native-tools/git-core/git-receive-pack": { + "source": "generated-controller-launcher", + "sha256": "sha256:c6e694c397bd198232a623802a52751bc8c1eecc45d74683d4fa6981c78fcc8f" + }, + "native-tools/git-core/git-upload-archive": { + "source": "generated-controller-launcher", + "sha256": "sha256:ee01fa082b248246a96af60831f68594d69c7b557ad574f4b6195c2fa2c98e85" + }, + "@capsule/bin/sh": { + "source": "/bin/sh", + "sha256": "sha256:86d31f6fb799e91fa21bad341484564510ca287703a16e9e46c53338776f4f42" + } + }, + "commands": [ + { + "argv": [ + "uname", + "-s" + ], + "exit": 0, + "stdout": "Linux\n", + "stderr": "" + }, + { + "argv": [ + "sed", + "--version" + ], + "exit": 0, + "stdout": "/opt/specfact/project-runtime/native-tools/executables/sed (GNU sed) 4.9\nPackaged by Debian\nCopyright (C) 2022 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later .\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.\n\nWritten by Jay Fenlason, Tom Lord, Ken Pizzini,\nPaolo Bonzini, Jim Meyering, and Assaf Gordon.\n\nThis sed program was built with SELinux support.\nSELinux is disabled on this system.\n\nGNU sed home page: .\nGeneral help using GNU software: .\nE-mail bug reports to: .\n", + "stderr": "" + }, + { + "argv": [ + "git", + "init", + "/tmp/tmp5q42o2vh/repository" + ], + "exit": 0, + "stdout": "Initialized empty Git repository in /tmp/tmp5q42o2vh/repository/.git/\n", + "stderr": "hint: Using 'master' as the name for the initial branch. This default branch name\nhint: is subject to change. To configure the initial branch name to use in all\nhint: of your new repositories, which will suppress this warning, call:\nhint: \nhint: \tgit config --global init.defaultBranch \nhint: \nhint: Names commonly chosen instead of 'master' are 'main', 'trunk' and\nhint: 'development'. The just-created branch can be renamed via this command:\nhint: \nhint: \tgit branch -m \n" + }, + { + "argv": [ + "git", + "config", + "user.email", + "fixture@example.invalid" + ], + "exit": 0, + "stdout": "", + "stderr": "" + }, + { + "argv": [ + "git", + "config", + "user.name", + "Native fixture" + ], + "exit": 0, + "stdout": "", + "stderr": "" + }, + { + "argv": [ + "git", + "config", + "filter.fixture.clean", + "sed s/SAFE/BLOCKED/" + ], + "exit": 0, + "stdout": "", + "stderr": "" + }, + { + "argv": [ + "git", + "add", + "." + ], + "exit": 0, + "stdout": "", + "stderr": "" + }, + { + "argv": [ + "git", + "show", + ":data.txt" + ], + "exit": 0, + "stdout": "BLOCKED\n", + "stderr": "" + }, + { + "argv": [ + "git", + "commit", + "-m", + "fixture" + ], + "exit": 0, + "stdout": "[master (root-commit) 47cffa7] fixture\n 2 files changed, 2 insertions(+)\n create mode 100644 .gitattributes\n create mode 100644 data.txt\n", + "stderr": "" + }, + { + "argv": [ + "git", + "clone", + "--depth=1", + "file:///tmp/tmp5q42o2vh/repository", + "/tmp/tmp5q42o2vh/clone" + ], + "exit": 0, + "stdout": "", + "stderr": "Cloning into '/tmp/tmp5q42o2vh/clone'...\n" + } + ] +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_RED.txt new file mode 100644 index 00000000..cbba1fec --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NATIVE_TOOLS_RED.txt @@ -0,0 +1,226 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 10 items + +tests/unit/specfact_code_review/run/test_runtime_tools.py FFFFFFFFFF [100%] + +=================================== FAILURES =================================== +_______ test_repository_declares_native_tools_without_manager_execution ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_repository_declares_nativ0') + + def test_repository_declares_native_tools_without_manager_execution(tmp_path: Path) -> None: + (tmp_path / 'pyproject.toml').write_text( + '[project]\nname="sample"\nversion="1"\n' + '[tool.specfact.code-review]\nnative_tools=["git", "uname", "sed"]\n' + ) + plan = discover_project(tmp_path) +> assert plan.native_tools == ('git', 'sed', 'uname') + ^^^^^^^^^^^^^^^^^ +E AttributeError: 'ProjectPlan' object has no attribute 'native_tools' + +tests/unit/specfact_code_review/run/test_runtime_tools.py:17: AttributeError +__________ test_explicit_native_tools_override_repository_declaration __________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_explicit_native_tools_ove0') + + def test_explicit_native_tools_override_repository_declaration(tmp_path: Path) -> None: + (tmp_path / 'pyproject.toml').write_text('[tool.specfact.code-review]\nnative_tools=["git"]\n') + config = tmp_path / 'review.toml' + config.write_text('native_tools=["uname"]\n') +> assert discover_project(tmp_path, config_path=config).native_tools == ('uname',) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_tools.py:25: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:396: in discover_project + values = _config(config_path) + ^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_explicit_native_tools_ove0/review.toml') + + def _config(path: Path | None) -> dict[str, Any]: + if path is None: + return {} + if path.is_symlink() or not path.is_file(): + raise ProjectRuntimeError("project_config_invalid: expected a regular TOML file") + values = _toml(path) + unknown = sorted(set(values) - _CONFIG_FIELDS) + if unknown: +> raise ProjectRuntimeError(f"project_config_unknown_fields:{','.join(unknown)}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_config_unknown_fields:native_tools + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:124: ProjectRuntimeError +_______ test_unknown_native_capabilities_are_not_path_guesses[["curl"]] ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_unknown_native_capabiliti0') +declaration = '["curl"]' + + @pytest.mark.parametrize('declaration', ['["curl"]', '["/usr/bin/git"]', '"git"']) + def test_unknown_native_capabilities_are_not_path_guesses(tmp_path: Path, declaration: str) -> None: + (tmp_path / 'pyproject.toml').write_text(f'[tool.specfact.code-review]\nnative_tools={declaration}\n') +> with pytest.raises(ProjectRuntimeError, match='native_tools|native_tool_unsupported'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_tools.py:31: Failed +___ test_unknown_native_capabilities_are_not_path_guesses[["/usr/bin/git"]] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_unknown_native_capabiliti1') +declaration = '["/usr/bin/git"]' + + @pytest.mark.parametrize('declaration', ['["curl"]', '["/usr/bin/git"]', '"git"']) + def test_unknown_native_capabilities_are_not_path_guesses(tmp_path: Path, declaration: str) -> None: + (tmp_path / 'pyproject.toml').write_text(f'[tool.specfact.code-review]\nnative_tools={declaration}\n') +> with pytest.raises(ProjectRuntimeError, match='native_tools|native_tool_unsupported'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_tools.py:31: Failed +_________ test_unknown_native_capabilities_are_not_path_guesses["git"] _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_unknown_native_capabiliti2') +declaration = '"git"' + + @pytest.mark.parametrize('declaration', ['["curl"]', '["/usr/bin/git"]', '"git"']) + def test_unknown_native_capabilities_are_not_path_guesses(tmp_path: Path, declaration: str) -> None: + (tmp_path / 'pyproject.toml').write_text(f'[tool.specfact.code-review]\nnative_tools={declaration}\n') +> with pytest.raises(ProjectRuntimeError, match='native_tools|native_tool_unsupported'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_tools.py:31: Failed +_____________ test_capture_binds_launchers_executables_and_loader ______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_capture_binds_launchers_e0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a946650> + + def test_capture_binds_launchers_executables_and_loader(tmp_path: Path, monkeypatch) -> None: +> tools, system = _native_fixture(tmp_path, monkeypatch) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_tools.py:54: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_capture_binds_launchers_e0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a946650> + + def _native_fixture(tmp_path: Path, monkeypatch): + """Use ELF fixtures without executing native code on the host.""" + import struct +> from specfact_code_review.run import runtime_tools +E ImportError: cannot import name 'runtime_tools' from 'specfact_code_review.run' (/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/__init__.py) + +tests/unit/specfact_code_review/run/test_runtime_tools.py:38: ImportError +_______ test_builder_tool_collisions_fail_before_any_install[bin/uname] ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_builder_tool_collisions_f0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a9469e0> +collision = 'bin/uname' + + @pytest.mark.parametrize('collision', ['bin/uname', 'native-tools/forged']) + def test_builder_tool_collisions_fail_before_any_install(tmp_path: Path, monkeypatch, collision: str) -> None: +> tools, _ = _native_fixture(tmp_path, monkeypatch) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_tools.py:70: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_builder_tool_collisions_f0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a9469e0> + + def _native_fixture(tmp_path: Path, monkeypatch): + """Use ELF fixtures without executing native code on the host.""" + import struct +> from specfact_code_review.run import runtime_tools +E ImportError: cannot import name 'runtime_tools' from 'specfact_code_review.run' (/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/__init__.py) + +tests/unit/specfact_code_review/run/test_runtime_tools.py:38: ImportError +__ test_builder_tool_collisions_fail_before_any_install[native-tools/forged] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_builder_tool_collisions_f1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a8cbbf0> +collision = 'native-tools/forged' + + @pytest.mark.parametrize('collision', ['bin/uname', 'native-tools/forged']) + def test_builder_tool_collisions_fail_before_any_install(tmp_path: Path, monkeypatch, collision: str) -> None: +> tools, _ = _native_fixture(tmp_path, monkeypatch) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_tools.py:70: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_builder_tool_collisions_f1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a8cbbf0> + + def _native_fixture(tmp_path: Path, monkeypatch): + """Use ELF fixtures without executing native code on the host.""" + import struct +> from specfact_code_review.run import runtime_tools +E ImportError: cannot import name 'runtime_tools' from 'specfact_code_review.run' (/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/__init__.py) + +tests/unit/specfact_code_review/run/test_runtime_tools.py:38: ImportError +________ test_missing_git_helper_is_actionable_not_an_ambient_fallback _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_missing_git_helper_is_act0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a9e86b0> + + def test_missing_git_helper_is_actionable_not_an_ambient_fallback(tmp_path: Path, monkeypatch) -> None: +> tools, system = _native_fixture(tmp_path, monkeypatch) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_tools.py:83: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_missing_git_helper_is_act0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a9e86b0> + + def _native_fixture(tmp_path: Path, monkeypatch): + """Use ELF fixtures without executing native code on the host.""" + import struct +> from specfact_code_review.run import runtime_tools +E ImportError: cannot import name 'runtime_tools' from 'specfact_code_review.run' (/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/__init__.py) + +tests/unit/specfact_code_review/run/test_runtime_tools.py:38: ImportError +_________________ test_empty_capture_reads_no_native_programs __________________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_empty_capture_reads_no_na0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a9e97b0> + + def test_empty_capture_reads_no_native_programs(tmp_path: Path, monkeypatch) -> None: +> tools, _ = _native_fixture(tmp_path, monkeypatch) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_tools.py:90: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-687/test_empty_capture_reads_no_na0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a9e97b0> + + def _native_fixture(tmp_path: Path, monkeypatch): + """Use ELF fixtures without executing native code on the host.""" + import struct +> from specfact_code_review.run import runtime_tools +E ImportError: cannot import name 'runtime_tools' from 'specfact_code_review.run' (/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/__init__.py) + +tests/unit/specfact_code_review/run/test_runtime_tools.py:38: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_repository_declares_native_tools_without_manager_execution +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_explicit_native_tools_override_repository_declaration +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_unknown_native_capabilities_are_not_path_guesses[["curl"]] +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_unknown_native_capabilities_are_not_path_guesses[["/usr/bin/git"]] +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_unknown_native_capabilities_are_not_path_guesses["git"] +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_capture_binds_launchers_executables_and_loader +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_builder_tool_collisions_fail_before_any_install[bin/uname] +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_builder_tool_collisions_fail_before_any_install[native-tools/forged] +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_missing_git_helper_is_actionable_not_an_ambient_fallback +FAILED tests/unit/specfact_code_review/run/test_runtime_tools.py::test_empty_capture_reads_no_native_programs +============================== 10 failed in 0.13s ============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_DOMAIN_EVIDENCE.md b/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_DOMAIN_EVIDENCE.md new file mode 100644 index 00000000..be802f55 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_DOMAIN_EVIDENCE.md @@ -0,0 +1,49 @@ +# PR #478: BasedPyright Node distribution ownership + +On 2026-09-16, the canonical scenario `BasedPyright preserves its actual sealed +Node distribution` preceded the regression tests and production correction. + +## Dependency evidence + +BasedPyright 1.39.10's actual wheel metadata declares +`Requires-Dist: nodejs-wheel-binaries>=20.13.1`; its `run_node.py` imports +`nodejs_wheel.executable`. The exact wheel was read from the +[PyPI release metadata](https://pypi.org/pypi/basedpyright/1.39.10/json) +(accessed 2026-09-16), SHA-256 +`cbd75d83c0be841329bcfef2d2f1182f152a6d975b8eb199e75cf5b8e9a3de78`. +The signed toolchain selects `nodejs-wheel-binaries==24.16.0`; the observed +project inventory selected 24.19.0. + +An isolated host reproduction used the real wheel metadata and import code with +production dependency-graph, mount-selection, and verified-finder APIs. Before +the correction, the graph chose project ownership, omitted `nodejs_wheel` from +the member mount, and the actual import raised +`project_worker_analyzer_origin_mismatch:nodejs_wheel`. This independently proves +the domain defect; it does not prove the cause of a prior native run without +that run's raw diagnostic. It does not claim Linux namespace execution. + +## Failing-first and passing evidence + +The regression command was: + +```sh +SPECFACT_CLI_REPO=/private/tmp/specfact-473-core .venv/bin/python -m pytest \ + tests/unit/specfact_code_review/run/test_runtime_compatibility.py \ + -k 'node_' -q --tb=short +``` + +Before the production edit, `PR478_NODE_DOMAIN_RED.txt` records two genuine +failures: the missing explicit version-conflict diagnostic and the matching +version's failed verified import. Two preservation controls passed: no project +Node and an unrelated project distribution named `nodejs-wheel`. + +Only the member distribution name changed in production, from `nodejs-wheel` +to `nodejs-wheel-binaries`. A different project version now gets an explicit +member incompatibility; no project version is silently substituted. Matching +and absent project versions retain the sealed package and metadata mount. + +The compatibility, runtime-domain, and target-launch test files subsequently +passed together: **87 passed in 11.66 seconds**, macOS, Python 3.14.7, +pytest 9.1.1. Ruff check and format check passed. New test/helper complexity is +at most nine. The real Linux native review remains an integration gate owned +by the parent task; these focused checks are not a capsule acceptance claim. diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_DOMAIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_DOMAIN_RED.txt new file mode 100644 index 00000000..3ed3270a --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_DOMAIN_RED.txt @@ -0,0 +1,34 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 10 items / 6 deselected / 4 selected + +tests/unit/specfact_code_review/run/test_runtime_compatibility.py F.F. [100%] + +=================================== FAILURES =================================== +_____________ test_basedpyright_node_version_conflict_is_explicit ______________ +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:118: in test_basedpyright_node_version_conflict_is_explicit + assert analyzer_dependency_conflicts(inventory, node_analyzers) == { +E AssertionError: assert {} == {'basedpyrigh...ntry=24.16.0'} +E +E Right contains 1 more item: +E {'basedpyright': 'project_analyzer_dependency_incompatible:nodejs-wheel-binaries: ' +E 'target=24.19.0, sealed entry=24.16.0'} +E Use -v to get more diff +________ test_basedpyright_node_closure_keeps_verified_import[matching] ________ +tests/unit/specfact_code_review/run/test_runtime_compatibility.py:156: in test_basedpyright_node_closure_keeps_verified_import + spec = finder.find_spec("nodejs_wheel") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py:56: in find_spec + return _verified_analyzer_spec(fullname, spec) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py:41: in _verified_analyzer_spec + raise ImportError(f"project_worker_analyzer_origin_mismatch:{fullname}") +E ImportError: project_worker_analyzer_origin_mismatch:nodejs_wheel +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_basedpyright_node_version_conflict_is_explicit +FAILED tests/unit/specfact_code_review/run/test_runtime_compatibility.py::test_basedpyright_node_closure_keeps_verified_import[matching] +================== 2 failed, 2 passed, 6 deselected in 0.12s =================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_POLICY_CACHE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_POLICY_CACHE_RED.txt new file mode 100644 index 00000000..5ee5a74b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_NODE_POLICY_CACHE_RED.txt @@ -0,0 +1,71 @@ +Checking dependencies +Syncing dependencies +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 22 items / 21 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_builder.py F [100%] + +=================================== FAILURES =================================== +______ test_member_compatibility_policy_invalidates_offline_runtime_reuse ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-705/test_member_compatibility_poli0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108611ba0> + + def test_member_compatibility_policy_invalidates_offline_runtime_reuse(tmp_path: Path, monkeypatch) -> None: +> _assert_policy_change_invalidates_reuse(tmp_path, monkeypatch, "runtime_compatibility.py") + +tests/unit/specfact_code_review/run/test_runtime_builder.py:248: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-705/test_member_compatibility_poli0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108611ba0> +helper_name = 'runtime_compatibility.py' + + def _assert_policy_change_invalidates_reuse(tmp_path: Path, monkeypatch, helper_name: str) -> None: + helpers = tmp_path / "helpers" + helpers.mkdir() + for helper in Path(runtime_builder.__file__).parent.glob("*.py"): + (helpers / helper.name).write_bytes(helper.read_bytes()) + lock = Path("resources/contracts/pr-range-v1-toolchain-lock.json") + (tmp_path / lock).parent.mkdir(parents=True) + (tmp_path / lock).write_bytes((Path(runtime_builder.__file__).parents[1] / lock).read_bytes()) + monkeypatch.setattr(runtime_builder, "__file__", str(helpers / "runtime_builder.py")) + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "fixed-git-identity") + plan = ProjectPlan(tmp_path, manager="pip") + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + cache = tmp_path / "cache" + identities = [] + digest = runtime_builder.document_digest + + def record_identity(values): + identities.append(values) + return digest(values) + + monkeypatch.setattr(runtime_builder, "document_digest", record_identity) + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + cached_path = cache / digest(identities[-1])[7:] + cached_path.mkdir() + loaded = [] + + def load_cached(path, **_kwargs): + loaded.append(path) + return SimpleNamespace(descriptor={"project_identity": plan.identity}) + + monkeypatch.setattr(runtime_builder, "load_runtime", load_cached) + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + helper = helpers / helper_name + helper.write_bytes(helper.read_bytes() + b"\n# changed dispatcher fixture\n") +> with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:236: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_member_compatibility_policy_invalidates_offline_runtime_reuse +======================= 1 failed, 21 deselected in 0.12s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLIC_TRUST_NATIVE_PROBE.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLIC_TRUST_NATIVE_PROBE.json new file mode 100644 index 00000000..9711c607 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLIC_TRUST_NATIVE_PROBE.json @@ -0,0 +1,33 @@ +{ + "scope": "Official Semgrep1.144.0 initialization in offline disposable Linux; diagnostic-only, not updated capsule acceptance", + "records": [ + { + "uname": "/usr/bin/uname", + "cert_bytes": 240216 + }, + { + "case": "missing-explicit", + "exit": 2, + "stdout": "", + "stderr": "Fatal error: exception Failure(\"Failed to create system store X509 authenticator: ca-certs: no trust anchor file found, looked into /tmp/absent.pem.\\nPlease report an issue at https://github.com/mirage/ca-certs, including:\\n- the output of uname -s\\n- the distribution you use\\n- the location of default trust anchors (if known)\\n\")\nRaised at Stdlib.failwith in file \"stdlib.ml\", line 29, characters 17-33\nCalled from Opentelemetry_client_cohttp_eio.Httpc.authenticator in file \"src/client-cohttp-eio/opentelemetry_client_cohttp_eio.ml\", line 119, characters 6-75\n" + }, + { + "case": "valid-explicit", + "exit": 0, + "stdout": "1.144.0\n", + "stderr": "" + }, + { + "case": "default-before", + "exit": 2, + "stdout": "", + "stderr": "Fatal error: exception Failure(\"Failed to create system store X509 authenticator: ca-certs: no trust anchor file found, looked into /etc/ssl/certs/ca-certificates.crt, /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem, /etc/ssl/ca-bundle.pem.\\nPlease report an issue at https://github.com/mirage/ca-certs, including:\\n- the output of uname -s\\n- the distribution you use\\n- the location of default trust anchors (if known)\\n\")\nRaised at Stdlib.failwith in file \"stdlib.ml\", line 29, characters 17-33\nCalled from Opentelemetry_client_cohttp_eio.Httpc.authenticator in file \"src/client-cohttp-eio/opentelemetry_client_cohttp_eio.ml\", line 119, characters 6-75\n" + }, + { + "case": "default-after", + "exit": 0, + "stdout": "1.144.0\n", + "stderr": "" + } + ] +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLIC_TRUST_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLIC_TRUST_RED.txt new file mode 100644 index 00000000..bfbc6096 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLIC_TRUST_RED.txt @@ -0,0 +1,209 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 9 items + +tests/unit/specfact_code_review/run/test_runtime_trust.py FFFFFFFFF [100%] + +=================================== FAILURES =================================== +_____________ test_signed_public_trust_survives_offline_attachment _____________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_sig...t-733/test_signed_public_trust_survi0/cache'), mutation=. at 0x10a3f2980>) + + def test_signed_public_trust_survives_offline_attachment(trust_case: SimpleNamespace) -> None: + prepared = _prepare(trust_case) + target = prepared.root / DESTINATION +> assert target.is_file(), "attached worker /etc lacks the signed public certificate bundle" +E AssertionError: attached worker /etc lacks the signed public certificate bundle +E assert False +E + where False = is_file() +E + where is_file = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_signed_public_trust_...i0/cache/0d0e1c1b662482ebc488d0fa4cde4772748d1ec4426cf8775746fcdf4dd76e55/worker-config/ssl/certs/ca-certificates.crt').is_file + +tests/unit/specfact_code_review/run/test_runtime_trust.py:58: AssertionError +_______________ test_public_trust_change_invalidates_warm_cache ________________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_pub...t-733/test_public_trust_change_inval0/cache'), mutation=. at 0x10a3f2560>) + + def test_public_trust_change_invalidates_warm_cache(trust_case: SimpleNamespace) -> None: + _prepare(trust_case) + trust_case.source.write_bytes(trust_case.payload + b"\n# changed signed fixture bundle\n") +> with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:76: Failed +______________ test_builder_cannot_replace_captured_public_trust _______________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_bui...ce_ca0/cache'), mutation=. at 0x10a418720>) + + def test_builder_cannot_replace_captured_public_trust(trust_case: SimpleNamespace) -> None: + trust_case.mutation = lambda _artifact: trust_case.source.write_bytes(b"builder modified fixture source") + prepared = _prepare(trust_case) +> assert (prepared.root / DESTINATION).is_file(), "public trust must be captured before building" +E AssertionError: public trust must be captured before building +E assert False +E + where False = is_file() +E + where is_file = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_builder_cannot_replace_ca0/cache/0d0e1c1b662482ebc488d0fa4cde4772748d1ec4426cf8775746fcdf4dd76e55') / 'worker-config/ssl/certs/ca-certificates.crt').is_file +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_builder_cannot_replace_ca0/cache/0d0e1c1b662482ebc488d0fa4cde4772748d1ec4426cf8775746fcdf4dd76e55') = PreparedRuntime(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test...oject-runtime-layer-v2', 'worker_identity': 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'}).root + +tests/unit/specfact_code_review/run/test_runtime_trust.py:83: AssertionError +______________ test_unverified_public_trust_is_rejected[missing] _______________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_unv...t-733/test_unverified_public_trust_i0/cache'), mutation=. at 0x10a4617a0>) +violation = 'missing' + + @pytest.mark.parametrize("violation", ["missing", "symlink", "private-key", "malformed", "oversized"]) + def test_unverified_public_trust_is_rejected(trust_case: SimpleNamespace, violation: str) -> None: + source = trust_case.source + if violation == "missing": + source.unlink() + elif violation == "symlink": + destination = source.parent / "elsewhere.pem" + source.rename(destination) + source.symlink_to(destination) + else: + content = { + "private-key": trust_case.payload + b"\n-----BEGIN PRIVATE KEY-----\nsecret\n", + "malformed": b"not a certificate", + "oversized": b"x" * 2_000_001, + }[violation] + source.write_bytes(content) +> with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:103: Failed +______________ test_unverified_public_trust_is_rejected[symlink] _______________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_unv...t-733/test_unverified_public_trust_i1/cache'), mutation=. at 0x10a4622a0>) +violation = 'symlink' + + @pytest.mark.parametrize("violation", ["missing", "symlink", "private-key", "malformed", "oversized"]) + def test_unverified_public_trust_is_rejected(trust_case: SimpleNamespace, violation: str) -> None: + source = trust_case.source + if violation == "missing": + source.unlink() + elif violation == "symlink": + destination = source.parent / "elsewhere.pem" + source.rename(destination) + source.symlink_to(destination) + else: + content = { + "private-key": trust_case.payload + b"\n-----BEGIN PRIVATE KEY-----\nsecret\n", + "malformed": b"not a certificate", + "oversized": b"x" * 2_000_001, + }[violation] + source.write_bytes(content) +> with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:103: Failed +____________ test_unverified_public_trust_is_rejected[private-key] _____________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_unv...t-733/test_unverified_public_trust_i2/cache'), mutation=. at 0x10a462610>) +violation = 'private-key' + + @pytest.mark.parametrize("violation", ["missing", "symlink", "private-key", "malformed", "oversized"]) + def test_unverified_public_trust_is_rejected(trust_case: SimpleNamespace, violation: str) -> None: + source = trust_case.source + if violation == "missing": + source.unlink() + elif violation == "symlink": + destination = source.parent / "elsewhere.pem" + source.rename(destination) + source.symlink_to(destination) + else: + content = { + "private-key": trust_case.payload + b"\n-----BEGIN PRIVATE KEY-----\nsecret\n", + "malformed": b"not a certificate", + "oversized": b"x" * 2_000_001, + }[violation] + source.write_bytes(content) +> with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:103: Failed +_____________ test_unverified_public_trust_is_rejected[malformed] ______________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_unv...t-733/test_unverified_public_trust_i3/cache'), mutation=. at 0x10a462a30>) +violation = 'malformed' + + @pytest.mark.parametrize("violation", ["missing", "symlink", "private-key", "malformed", "oversized"]) + def test_unverified_public_trust_is_rejected(trust_case: SimpleNamespace, violation: str) -> None: + source = trust_case.source + if violation == "missing": + source.unlink() + elif violation == "symlink": + destination = source.parent / "elsewhere.pem" + source.rename(destination) + source.symlink_to(destination) + else: + content = { + "private-key": trust_case.payload + b"\n-----BEGIN PRIVATE KEY-----\nsecret\n", + "malformed": b"not a certificate", + "oversized": b"x" * 2_000_001, + }[violation] + source.write_bytes(content) +> with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:103: Failed +_____________ test_unverified_public_trust_is_rejected[oversized] ______________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_unv...t-733/test_unverified_public_trust_i4/cache'), mutation=. at 0x10a461e80>) +violation = 'oversized' + + @pytest.mark.parametrize("violation", ["missing", "symlink", "private-key", "malformed", "oversized"]) + def test_unverified_public_trust_is_rejected(trust_case: SimpleNamespace, violation: str) -> None: + source = trust_case.source + if violation == "missing": + source.unlink() + elif violation == "symlink": + destination = source.parent / "elsewhere.pem" + source.rename(destination) + source.symlink_to(destination) + else: + content = { + "private-key": trust_case.payload + b"\n-----BEGIN PRIVATE KEY-----\nsecret\n", + "malformed": b"not a certificate", + "oversized": b"x" * 2_000_001, + }[violation] + source.write_bytes(content) +> with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:103: Failed +_________ test_builder_certificate_destination_collision_fails_closed __________ + +trust_case = namespace(source=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-733/test_bui...e'), mutation=.collision at 0x10a461850>) + + def test_builder_certificate_destination_collision_fails_closed(trust_case: SimpleNamespace) -> None: + def collision(artifact): + destination = artifact / DESTINATION + destination.parent.mkdir(parents=True) + destination.write_bytes(b"builder-selected trust") + + trust_case.mutation = collision +> with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust_collision"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_trust.py:115: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_signed_public_trust_survives_offline_attachment +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_public_trust_change_invalidates_warm_cache +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_builder_cannot_replace_captured_public_trust +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_unverified_public_trust_is_rejected[missing] +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_unverified_public_trust_is_rejected[symlink] +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_unverified_public_trust_is_rejected[private-key] +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_unverified_public_trust_is_rejected[malformed] +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_unverified_public_trust_is_rejected[oversized] +FAILED tests/unit/specfact_code_review/run/test_runtime_trust.py::test_builder_certificate_destination_collision_fails_closed +============================== 9 failed in 0.33s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLISHED_CORE_API.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLISHED_CORE_API.json new file mode 100644 index 00000000..6c747b83 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_PUBLISHED_CORE_API.json @@ -0,0 +1,19 @@ +{ + "package": "specfact-cli==0.55.4", + "wheel": "specfact_cli-0.55.4-py3-none-any.whl", + "wheel_sha256": "aac27b5b1d9d96990510f22797316909ed78e38590ab77bc72eff9d51b65f4b5", + "wheel_bytes": 788730, + "acquisition": "python -m pip download --no-deps --only-binary=:all: specfact-cli==0.55.4", + "validation": "isolated -I Python import from the extracted wheel, using existing developer dependencies", + "registry_imports": [ + "specfact_cli.registry.module_discovery", + "specfact_cli.registry.module_installer" + ], + "verified_contracts": [ + "discover_all_modules_for_project_with_shadowed is callable", + "USER_MODULES_ROOT and MARKETPLACE_MODULES_ROOT are Path values", + "module_installer._bundled_public_key_path() is an existing wheel-owned public-key file" + ], + "result": "PASS", + "limits": "This verifies the published Python registry interface; updated hermetic native preparation and review remain separate required gates." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_PYTEST_PATH_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_PYTEST_PATH_RED.txt new file mode 100644 index 00000000..96ec6795 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_PYTEST_PATH_RED.txt @@ -0,0 +1,99 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 29 items / 25 deselected / 4 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py FFF [ 75%] +F [100%] + +=================================== FAILURES =================================== +_______ test_nested_nonpass_finding_uses_snapshot_relative_path[checks] ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-552/test_nested_nonpass_finding_us0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10beff490> +pytest_root = 'checks' + + @pytest.mark.parametrize("pytest_root", ["checks", "absolute"]) + def test_nested_nonpass_finding_uses_snapshot_relative_path(tmp_path, monkeypatch, pytest_root): + nodeid = "check_app.py::test_failure" + observation = _observation(_record(nodeid, "failed")) + observation["exit_code"] = 1 + observation["pytest_root"] = str(tmp_path / "checks") if pytest_root == "absolute" else pytest_root + findings = _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_app.py")) + failed = [finding for finding in findings if finding.rule == "TEST_OUTCOME_NOT_PASS"] + assert len(failed) == 1 +> assert failed[0].file == "checks/check_app.py" +E AssertionError: assert 'check_app.py' == 'checks/check_app.py' +E +E - checks/check_app.py +E ? ------- +E + check_app.py + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:199: AssertionError +______ test_nested_nonpass_finding_uses_snapshot_relative_path[absolute] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-552/test_nested_nonpass_finding_us1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10beff6f0> +pytest_root = 'absolute' + + @pytest.mark.parametrize("pytest_root", ["checks", "absolute"]) + def test_nested_nonpass_finding_uses_snapshot_relative_path(tmp_path, monkeypatch, pytest_root): + nodeid = "check_app.py::test_failure" + observation = _observation(_record(nodeid, "failed")) + observation["exit_code"] = 1 + observation["pytest_root"] = str(tmp_path / "checks") if pytest_root == "absolute" else pytest_root + findings = _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_app.py")) + failed = [finding for finding in findings if finding.rule == "TEST_OUTCOME_NOT_PASS"] + assert len(failed) == 1 +> assert failed[0].file == "checks/check_app.py" +E AssertionError: assert 'check_app.py' == 'checks/check_app.py' +E +E - checks/check_app.py +E ? ------- +E + check_app.py + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:199: AssertionError +_ test_escaping_outcome_path_is_incomplete_without_false_attribution[../outside-check_app.py::test_failure] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-552/test_escaping_outcome_path_is_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d09f530> +pytest_root = '../outside', nodeid = 'check_app.py::test_failure' + + @pytest.mark.parametrize("pytest_root,nodeid", [("../outside", "check_app.py::test_failure"), (".", "../check_app.py::test_failure")]) + def test_escaping_outcome_path_is_incomplete_without_false_attribution(tmp_path, monkeypatch, pytest_root, nodeid): + observation = _observation(_record(nodeid, "failed")) + observation["exit_code"] = 1 + observation["pytest_root"] = pytest_root + findings = _run(tmp_path, monkeypatch, observation) + assert any(finding.rule == "tool_error" for finding in findings) +> assert not any(finding.rule == "TEST_OUTCOME_NOT_PASS" for finding in findings) +E assert not True +E + where True = any(. at 0x10d18f510>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:212: AssertionError +_ test_escaping_outcome_path_is_incomplete_without_false_attribution[.-../check_app.py::test_failure] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-552/test_escaping_outcome_path_is_1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c526250> +pytest_root = '.', nodeid = '../check_app.py::test_failure' + + @pytest.mark.parametrize("pytest_root,nodeid", [("../outside", "check_app.py::test_failure"), (".", "../check_app.py::test_failure")]) + def test_escaping_outcome_path_is_incomplete_without_false_attribution(tmp_path, monkeypatch, pytest_root, nodeid): + observation = _observation(_record(nodeid, "failed")) + observation["exit_code"] = 1 + observation["pytest_root"] = pytest_root + findings = _run(tmp_path, monkeypatch, observation) +> assert any(finding.rule == "tool_error" for finding in findings) +E assert False +E + where False = any(. at 0x10d18e330>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:211: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_nested_nonpass_finding_uses_snapshot_relative_path[checks] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_nested_nonpass_finding_uses_snapshot_relative_path[absolute] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_escaping_outcome_path_is_incomplete_without_false_attribution[../outside-check_app.py::test_failure] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_escaping_outcome_path_is_incomplete_without_false_attribution[.-../check_app.py::test_failure] +======================= 4 failed, 25 deselected in 0.24s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_PYTEST_STARTUP_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_PYTEST_STARTUP_RED.txt new file mode 100644 index 00000000..bed0b714 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_PYTEST_STARTUP_RED.txt @@ -0,0 +1,261 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 35 items / 29 deselected / 6 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py FFF [ 50%] +FFF [100%] + +=================================== FAILURES =================================== +________ test_malformed_observed_root_is_an_actionable_diagnostic[None] ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-561/test_malformed_observed_root_i0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108dcba80> +pytest_root = None + + @pytest.mark.parametrize("pytest_root", [None, "", 12, [], {}]) + def test_malformed_observed_root_is_an_actionable_diagnostic(tmp_path, monkeypatch, pytest_root): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = pytest_root +> findings = _run(tmp_path, monkeypatch, observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:225: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:50: in _run + return portable_worker.run_portable_pytest(files, ("portable-pytest-v2", "{}")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:229: in run_portable_pytest + pytest_root = resolve_portable_pytest_root(observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +observation = {'exit_code': 0, 'collected': ['tests/test_app.py::test_pass'], 'records': [{'nodeid': 'tests/test_app.py::test_pass',...come': 'passed', 'wasxfail': ''}], 'coverage': {'files': {'src/app.py': {'summary': {'percent_covered': 100.0}}}}, ...} + + @ensure(lambda result: result.is_absolute() and result.is_relative_to(Path.cwd().resolve())) + def resolve_portable_pytest_root(observation: dict[str, Any]) -> Path: + """Resolve the native pytest root within the reviewed source snapshot.""" + snapshot = Path.cwd().resolve() +> pytest_root = (snapshot / observation.get("pytest_root", ".")).resolve() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: unsupported operand type(s) for /: 'PosixPath' and 'NoneType' + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runner.py:6143: TypeError +__________ test_malformed_observed_root_is_an_actionable_diagnostic[] __________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-561/test_malformed_observed_root_i1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108dcbe10> +pytest_root = '' + + @pytest.mark.parametrize("pytest_root", [None, "", 12, [], {}]) + def test_malformed_observed_root_is_an_actionable_diagnostic(tmp_path, monkeypatch, pytest_root): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = pytest_root + findings = _run(tmp_path, monkeypatch, observation) +> assert len(findings) == 1 +E assert 0 == 1 +E + where 0 = len([]) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:226: AssertionError +_________ test_malformed_observed_root_is_an_actionable_diagnostic[12] _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-561/test_malformed_observed_root_i2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108d8be30> +pytest_root = 12 + + @pytest.mark.parametrize("pytest_root", [None, "", 12, [], {}]) + def test_malformed_observed_root_is_an_actionable_diagnostic(tmp_path, monkeypatch, pytest_root): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = pytest_root +> findings = _run(tmp_path, monkeypatch, observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:225: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:50: in _run + return portable_worker.run_portable_pytest(files, ("portable-pytest-v2", "{}")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:229: in run_portable_pytest + pytest_root = resolve_portable_pytest_root(observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +observation = {'exit_code': 0, 'collected': ['tests/test_app.py::test_pass'], 'records': [{'nodeid': 'tests/test_app.py::test_pass',...come': 'passed', 'wasxfail': ''}], 'coverage': {'files': {'src/app.py': {'summary': {'percent_covered': 100.0}}}}, ...} + + @ensure(lambda result: result.is_absolute() and result.is_relative_to(Path.cwd().resolve())) + def resolve_portable_pytest_root(observation: dict[str, Any]) -> Path: + """Resolve the native pytest root within the reviewed source snapshot.""" + snapshot = Path.cwd().resolve() +> pytest_root = (snapshot / observation.get("pytest_root", ".")).resolve() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: unsupported operand type(s) for /: 'PosixPath' and 'int' + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runner.py:6143: TypeError +____ test_malformed_observed_root_is_an_actionable_diagnostic[pytest_root3] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-561/test_malformed_observed_root_i3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108cbb240> +pytest_root = [] + + @pytest.mark.parametrize("pytest_root", [None, "", 12, [], {}]) + def test_malformed_observed_root_is_an_actionable_diagnostic(tmp_path, monkeypatch, pytest_root): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = pytest_root +> findings = _run(tmp_path, monkeypatch, observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:225: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:50: in _run + return portable_worker.run_portable_pytest(files, ("portable-pytest-v2", "{}")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:229: in run_portable_pytest + pytest_root = resolve_portable_pytest_root(observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +observation = {'exit_code': 0, 'collected': ['tests/test_app.py::test_pass'], 'records': [{'nodeid': 'tests/test_app.py::test_pass',...come': 'passed', 'wasxfail': ''}], 'coverage': {'files': {'src/app.py': {'summary': {'percent_covered': 100.0}}}}, ...} + + @ensure(lambda result: result.is_absolute() and result.is_relative_to(Path.cwd().resolve())) + def resolve_portable_pytest_root(observation: dict[str, Any]) -> Path: + """Resolve the native pytest root within the reviewed source snapshot.""" + snapshot = Path.cwd().resolve() +> pytest_root = (snapshot / observation.get("pytest_root", ".")).resolve() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: unsupported operand type(s) for /: 'PosixPath' and 'list' + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runner.py:6143: TypeError +____ test_malformed_observed_root_is_an_actionable_diagnostic[pytest_root4] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-561/test_malformed_observed_root_i4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108cbb570> +pytest_root = {} + + @pytest.mark.parametrize("pytest_root", [None, "", 12, [], {}]) + def test_malformed_observed_root_is_an_actionable_diagnostic(tmp_path, monkeypatch, pytest_root): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = pytest_root +> findings = _run(tmp_path, monkeypatch, observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:225: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:50: in _run + return portable_worker.run_portable_pytest(files, ("portable-pytest-v2", "{}")) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:229: in run_portable_pytest + pytest_root = resolve_portable_pytest_root(observation) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/private/tmp/specfact-477-pytest-release/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +observation = {'exit_code': 0, 'collected': ['tests/test_app.py::test_pass'], 'records': [{'nodeid': 'tests/test_app.py::test_pass',...come': 'passed', 'wasxfail': ''}], 'coverage': {'files': {'src/app.py': {'summary': {'percent_covered': 100.0}}}}, ...} + + @ensure(lambda result: result.is_absolute() and result.is_relative_to(Path.cwd().resolve())) + def resolve_portable_pytest_root(observation: dict[str, Any]) -> Path: + """Resolve the native pytest root within the reviewed source snapshot.""" + snapshot = Path.cwd().resolve() +> pytest_root = (snapshot / observation.get("pytest_root", ".")).resolve() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: unsupported operand type(s) for /: 'PosixPath' and 'dict' + +/private/tmp/specfact-477-pytest-release/packages/specfact-code-review/src/specfact_code_review/run/runner.py:6143: TypeError +_____ test_native_usage_error_retains_capsule_member_and_target_execution ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-561/test_native_usage_error_retain0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108dab750> + + def test_native_usage_error_retains_capsule_member_and_target_execution(tmp_path, monkeypatch): + (tmp_path / "pytest.ini").write_text("[pytest]\n") + native_output = tmp_path / "native-observation.json" + program = """ + import importlib.util, json, sys + from pathlib import Path + import pytest + spec = importlib.util.spec_from_file_location('native_observer', sys.argv[1]) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + observer = module.Observer() + code = pytest.main(['--specfact-controlled-invalid-option'], plugins=[observer]) + Path(sys.argv[2]).write_text(json.dumps({ + 'exit_code': int(code), 'pytest_root': observer.pytest_root, + 'records': observer.records, 'collected': sorted(observer.collected), + 'coverage': {}, 'argv': ['--specfact-controlled-invalid-option'], + })) + raise SystemExit(int(code)) + """ + native = subprocess.run( + [sys.executable, "-c", program, str(Path(target_pytest.__file__).resolve()), str(native_output)], + cwd=tmp_path, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "PYTEST_ADDOPTS": ""}, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert native.returncode == 4, native.stdout + native.stderr + observation = json.loads(native_output.read_text()) + assert observation["pytest_root"] is None + assert observation["records"] == [] + assert "--specfact-controlled-invalid-option" in native.stderr + result_path = tmp_path / "result.json" + observed_path = tmp_path / "observation.json" + files = [tmp_path / "src/app.py"] + monkeypatch.setattr( + runner, + "_load_capsule_request", + lambda _path: ("targeted-pytest-coverage", files, False, ("portable-pytest-v2", "{}"), False), + ) + monkeypatch.setattr( + runner, + "_member_findings", + lambda *_args, **_kwargs: _run(tmp_path, monkeypatch, observation), + ) + redirects = { + "/opt/specfact/tmp/pytest-observation.json": observed_path, + "/opt/specfact/output/result.json": result_path, + } + monkeypatch.setattr(runner, "Path", Mock(wraps=Path, side_effect=lambda value: redirects.get(str(value), Path(value)))) + runner._capsule_process_request(tmp_path / "request.json") + response = json.loads(result_path.read_text()) +> assert response["member"] == "targeted-pytest-coverage" +E AssertionError: assert 'unknown' == 'targeted-pytest-coverage' +E +E - targeted-pytest-coverage +E + unknown + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:284: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_malformed_observed_root_is_an_actionable_diagnostic[None] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_malformed_observed_root_is_an_actionable_diagnostic[] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_malformed_observed_root_is_an_actionable_diagnostic[12] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_malformed_observed_root_is_an_actionable_diagnostic[pytest_root3] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_malformed_observed_root_is_an_actionable_diagnostic[pytest_root4] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_native_usage_error_retains_capsule_member_and_target_execution +======================= 6 failed, 29 deselected in 1.52s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_REVIEWER_COVERAGE_RECONSTRUCTED_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_REVIEWER_COVERAGE_RECONSTRUCTED_RED.txt new file mode 100644 index 00000000..22629c52 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_REVIEWER_COVERAGE_RECONSTRUCTED_RED.txt @@ -0,0 +1,118 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 6 items + +tests/unit/specfact_code_review/run/test_target_coverage.py FFFFFF [100%] + +=================================== FAILURES =================================== +___________ test_reviewer_does_not_activate_dormant_aggregate_policy ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_reviewer_does_not_activat0') + + def test_reviewer_does_not_activate_dormant_aggregate_policy(tmp_path): + completed, receipt = _run(_project(tmp_path), tmp_path) +> assert completed.returncode == 0, completed.stdout + completed.stderr +E AssertionError: . +E ERROR: Coverage failure: total of 75.00 is less than fail-under=95.00 +E [100%] +E ================================ tests coverage ================================ +E _______________ coverage: platform darwin, python 3.14.7-final-0 _______________ +E +E Coverage JSON written to file /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_reviewer_does_not_activat0/private.json +E FAIL Required test coverage of 95.0% not reached. Total coverage: 75.00% +E 1 passed in 0.01s +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/private/tmp/specfact-477-pytest-release/.venv/bin/python', '-c', '\nimport importlib.util, js...rivate.json\nFAIL Required test coverage of 95.0% not reached. Total coverage: 75.00%\n1 passed in 0.01s\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_coverage.py:80: AssertionError +_______ test_native_threshold_failure_and_json_destination_remain_native _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_native_threshold_failure_0') + + def test_native_threshold_failure_and_json_destination_remain_native(tmp_path): + source = _project(tmp_path, native="--cov --cov-report=json:customer.json") + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 1 +> assert (source / "customer.json").is_file() +E AssertionError: assert False +E + where False = is_file() +E + where is_file = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_native_threshold_failure_0/project') / 'customer.json').is_file + +tests/unit/specfact_code_review/run/test_target_coverage.py:91: AssertionError +_____ test_explicit_coverage_disable_leaves_evidence_incomplete[--no-cov] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_explicit_coverage_disable0') +options = '--no-cov' + + @pytest.mark.parametrize("options", ["--no-cov", "--cov --no-cov"]) + def test_explicit_coverage_disable_leaves_evidence_incomplete(tmp_path, options): + completed, receipt = _run(_project(tmp_path, native=options), tmp_path) + assert completed.returncode == 0 + assert receipt["coverage"] == {} +> assert receipt["coverage_policy"]["mode"] == "disabled" +E AssertionError: assert 'legacy-forced' == 'disabled' +E +E - disabled +E + legacy-forced + +tests/unit/specfact_code_review/run/test_target_coverage.py:102: AssertionError +__ test_explicit_coverage_disable_leaves_evidence_incomplete[--cov --no-cov] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_explicit_coverage_disable1') +options = '--cov --no-cov' + + @pytest.mark.parametrize("options", ["--no-cov", "--cov --no-cov"]) + def test_explicit_coverage_disable_leaves_evidence_incomplete(tmp_path, options): + completed, receipt = _run(_project(tmp_path, native=options), tmp_path) + assert completed.returncode == 0 + assert receipt["coverage"] == {} +> assert receipt["coverage_policy"]["mode"] == "disabled" +E AssertionError: assert 'legacy-forced' == 'disabled' +E +E - disabled +E + legacy-forced + +tests/unit/specfact_code_review/run/test_target_coverage.py:102: AssertionError +______ test_native_no_report_control_does_not_disable_private_measurement ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_native_no_report_control_0') + + def test_native_no_report_control_does_not_disable_private_measurement(tmp_path): + source = _project(tmp_path, native="--cov --cov-report= --cov-fail-under=0") + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 + assert receipt["coverage"]["files"] +> assert receipt["coverage_policy"]["mode"] == "native" +E AssertionError: assert 'legacy-forced' == 'native' +E +E - native +E + legacy-forced + +tests/unit/specfact_code_review/run/test_target_coverage.py:111: AssertionError +__________ test_native_failure_report_suppression_remains_incomplete ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-773/test_native_failure_report_sup0') + + def test_native_failure_report_suppression_remains_incomplete(tmp_path): + source = _project(tmp_path, native="--cov --no-cov-on-fail", failure=True) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 1 + assert receipt["coverage"] == {} +> assert "--no-cov-on-fail" in receipt["coverage_diagnostic"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'coverage_diagnostic' + +tests/unit/specfact_code_review/run/test_target_coverage.py:120: KeyError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_reviewer_does_not_activate_dormant_aggregate_policy +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_native_threshold_failure_and_json_destination_remain_native +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_explicit_coverage_disable_leaves_evidence_incomplete[--no-cov] +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_explicit_coverage_disable_leaves_evidence_incomplete[--cov --no-cov] +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_native_no_report_control_does_not_disable_private_measurement +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_native_failure_report_suppression_remains_incomplete +============================== 6 failed in 0.88s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_SEALED_SHELL_AUDIT.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_SEALED_SHELL_AUDIT.json new file mode 100644 index 00000000..61532c37 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_SEALED_SHELL_AUDIT.json @@ -0,0 +1,203 @@ +{ + "manifest": "4574e160caedb71f8f47c8918f554678f4eddd4d9af1a4058c449b384f3a4ecd", + "manifest_verified": true, + "layers": [ + { + "layer": "sha256:039e6f9f9752f74a3ff4a6a224f64c7c864da16ed98f882107704328f41b9c42", + "verified": true, + "relevant_entries": [ + { + "path": "bin", + "type": "2", + "target": "usr/bin", + "mode": "0o777", + "sha256": null + }, + { + "path": "lib", + "type": "2", + "target": "usr/lib", + "mode": "0o777", + "sha256": null + }, + { + "path": "lib64", + "type": "2", + "target": "usr/lib64", + "mode": "0o777", + "sha256": null + }, + { + "path": "usr", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + }, + { + "path": "usr/bin", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + }, + { + "path": "usr/bin/dash", + "type": "0", + "target": "", + "mode": "0o755", + "sha256": "f5adb8bf0100ed0f8c7782ca5f92814e9229525a4b4e0d401cf3bea09ac960a6" + }, + { + "path": "usr/bin/sh", + "type": "2", + "target": "dash", + "mode": "0o777", + "sha256": null + }, + { + "path": "usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + "type": "0", + "target": "", + "mode": "0o755", + "sha256": "02bcda52c1a5dfc236f94d9e5255b4a0e26347d8a372a5223b650e31f291ce3c" + }, + { + "path": "usr/lib/x86_64-linux-gnu/libc.so.6", + "type": "0", + "target": "", + "mode": "0o755", + "sha256": "6b4a45352fd0c540a9c7c718f35ce8c8e46a4e482f9d3885a910c32d1a0e1421" + }, + { + "path": "usr/lib64", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + }, + { + "path": "usr/lib64/ld-linux-x86-64.so.2", + "type": "2", + "target": "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", + "mode": "0o777", + "sha256": null + } + ] + }, + { + "layer": "sha256:e8e392f3681c10ad9c2b19ab6614fd158f6d5a03e0f71694aea9429d38813dbd", + "verified": true, + "relevant_entries": [ + { + "path": "usr", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + }, + { + "path": "usr/bin", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + } + ] + }, + { + "layer": "sha256:8e0b0d5a4a0425bc991b95b78af125c6005a0a82a89011f076802a0265d8ff32", + "verified": true, + "relevant_entries": [ + { + "path": "usr", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + } + ] + }, + { + "layer": "sha256:1634f3c69c84a41815c187086e80cf5fdec289c6ce620014ef3ca9b54c019279", + "verified": true, + "relevant_entries": [ + { + "path": "usr", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + } + ] + }, + { + "layer": "sha256:f29564965d59de423c4f75a3ec1030463110fbe3bdef933fa616a465dc2e6f0c", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:d785c0e86650afc22c2fa1d8c0f026d23b180599223ace01702400d7c8b6a2f5", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:41b40f70f9f208de81a3d2439a33b5c514406b4b49629ec65bb6adc246436300", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:6ad545fc3f52bb716eb8a93afc7a974fa0ea3c9b511d0597a727c96eaf926acb", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:e968d380b220fed83583e177bf65d77ec5e8870205596bd9f8eb8528093a0c37", + "verified": true, + "relevant_entries": [ + { + "path": "root/.wh..cache", + "type": "0", + "target": "", + "mode": "0o0", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "tmp/.wh.write_manifest.py", + "type": "0", + "target": "", + "mode": "0o0", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "usr", + "type": "5", + "target": "", + "mode": "0o755", + "sha256": null + } + ] + }, + { + "layer": "sha256:a07b58bafec5371f5aa327bb9bf8a4a28fb9dd57c91fb9e4a88d30f215301946", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:1e81566b30b25ab6282f3e094dfbddc808f2cc7a44113093d6a2332596d9560b", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:6f4e76a343af20fd6cef4ef25b31c854b616a5374c454d97bcc4baa75ef0e51d", + "verified": true, + "relevant_entries": [] + }, + { + "layer": "sha256:45638b56ff23e3aeb6f46940f90fd1dcfefd6368a99bcb65508d608ac9abc627", + "verified": true, + "relevant_entries": [] + } + ], + "note": "Static verified OCI composition evidence only. Does not establish native execution." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_SEMGREP_DIAGNOSTICS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_SEMGREP_DIAGNOSTICS_RED.txt new file mode 100644 index 00000000..cdd83c75 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_SEMGREP_DIAGNOSTICS_RED.txt @@ -0,0 +1,186 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 63 items / 58 deselected / 5 selected + +tests/unit/specfact_code_review/tools/test_semgrep_runner.py FFFFF [100%] + +=================================== FAILURES =================================== +______ test_structured_semgrep_errors_retain_diagnostic_context[0-clean] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-652/test_structured_semgrep_errors0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109d4de00> +bug_pass = False, returncode = 0 + + @pytest.mark.parametrize("bug_pass", [False, True], ids=["clean", "bugs"]) + @pytest.mark.parametrize("returncode", [0, 1]) + def test_structured_semgrep_errors_retain_diagnostic_context( + tmp_path: Path, monkeypatch: MonkeyPatch, bug_pass: bool, returncode: int + ) -> None: + """Both passes retain the native error without accepting partial execution.""" + bundle = tmp_path / "bundle" + (bundle / ".semgrep").mkdir(parents=True) + for name in ("clean_code", "bugs"): + (bundle / f".semgrep/{name}.yaml").write_text("rules: []\n", encoding="utf-8") + target = tmp_path / "target.py" + target.write_text("VALUE = 1\n", encoding="utf-8") + payload = { + "results": [], + "errors": [{"type": "ParseError", "code": 3, "message": "target parse failed", "source": "PRIVATE_SOURCE"}], + } + process = Mock(return_value=completed_process("semgrep", stdout=json.dumps(payload), returncode=returncode)) + monkeypatch.setattr(subprocess, "run", process) + + findings = (run_semgrep_bugs if bug_pass else run_semgrep)([target], bundle_root=bundle) + + assert len(findings) == 1 + assert findings[0].category == "tool_error" + assert findings[0].severity == "error" + assert "count=1" in findings[0].message +> assert '"type":"ParseError"' in findings[0].message +E assert '"type":"ParseError"' in 'Unable to parse Semgrep output: semgrep returned structured errors (count=1)' +E + where 'Unable to parse Semgrep output: semgrep returned structured errors (count=1)' = ReviewFinding(category='tool_error', severity='error', tool='semgrep', rule='tool_error', file='/private/var/folders/l..., before_ref=None, after_ref=None, improvement=None, signal_trace=None, preserve_reasons=None, remediation_packet=None).message + +tests/unit/specfact_code_review/tools/test_semgrep_runner.py:287: AssertionError +_______ test_structured_semgrep_errors_retain_diagnostic_context[0-bugs] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-652/test_structured_semgrep_errors1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109d4e8b0> +bug_pass = True, returncode = 0 + + @pytest.mark.parametrize("bug_pass", [False, True], ids=["clean", "bugs"]) + @pytest.mark.parametrize("returncode", [0, 1]) + def test_structured_semgrep_errors_retain_diagnostic_context( + tmp_path: Path, monkeypatch: MonkeyPatch, bug_pass: bool, returncode: int + ) -> None: + """Both passes retain the native error without accepting partial execution.""" + bundle = tmp_path / "bundle" + (bundle / ".semgrep").mkdir(parents=True) + for name in ("clean_code", "bugs"): + (bundle / f".semgrep/{name}.yaml").write_text("rules: []\n", encoding="utf-8") + target = tmp_path / "target.py" + target.write_text("VALUE = 1\n", encoding="utf-8") + payload = { + "results": [], + "errors": [{"type": "ParseError", "code": 3, "message": "target parse failed", "source": "PRIVATE_SOURCE"}], + } + process = Mock(return_value=completed_process("semgrep", stdout=json.dumps(payload), returncode=returncode)) + monkeypatch.setattr(subprocess, "run", process) + + findings = (run_semgrep_bugs if bug_pass else run_semgrep)([target], bundle_root=bundle) + + assert len(findings) == 1 + assert findings[0].category == "tool_error" + assert findings[0].severity == "error" + assert "count=1" in findings[0].message +> assert '"type":"ParseError"' in findings[0].message +E assert '"type":"ParseError"' in 'Unable to parse Semgrep bugs pass output: semgrep returned structured errors (count=1)' +E + where 'Unable to parse Semgrep bugs pass output: semgrep returned structured errors (count=1)' = ReviewFinding(category='tool_error', severity='error', tool='semgrep', rule='tool_error', file='/private/var/folders/l..., before_ref=None, after_ref=None, improvement=None, signal_trace=None, preserve_reasons=None, remediation_packet=None).message + +tests/unit/specfact_code_review/tools/test_semgrep_runner.py:287: AssertionError +______ test_structured_semgrep_errors_retain_diagnostic_context[1-clean] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-652/test_structured_semgrep_errors2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109e80830> +bug_pass = False, returncode = 1 + + @pytest.mark.parametrize("bug_pass", [False, True], ids=["clean", "bugs"]) + @pytest.mark.parametrize("returncode", [0, 1]) + def test_structured_semgrep_errors_retain_diagnostic_context( + tmp_path: Path, monkeypatch: MonkeyPatch, bug_pass: bool, returncode: int + ) -> None: + """Both passes retain the native error without accepting partial execution.""" + bundle = tmp_path / "bundle" + (bundle / ".semgrep").mkdir(parents=True) + for name in ("clean_code", "bugs"): + (bundle / f".semgrep/{name}.yaml").write_text("rules: []\n", encoding="utf-8") + target = tmp_path / "target.py" + target.write_text("VALUE = 1\n", encoding="utf-8") + payload = { + "results": [], + "errors": [{"type": "ParseError", "code": 3, "message": "target parse failed", "source": "PRIVATE_SOURCE"}], + } + process = Mock(return_value=completed_process("semgrep", stdout=json.dumps(payload), returncode=returncode)) + monkeypatch.setattr(subprocess, "run", process) + + findings = (run_semgrep_bugs if bug_pass else run_semgrep)([target], bundle_root=bundle) + + assert len(findings) == 1 + assert findings[0].category == "tool_error" + assert findings[0].severity == "error" + assert "count=1" in findings[0].message +> assert '"type":"ParseError"' in findings[0].message +E assert '"type":"ParseError"' in 'Unable to parse Semgrep output: semgrep returned structured errors (count=1)' +E + where 'Unable to parse Semgrep output: semgrep returned structured errors (count=1)' = ReviewFinding(category='tool_error', severity='error', tool='semgrep', rule='tool_error', file='/private/var/folders/l..., before_ref=None, after_ref=None, improvement=None, signal_trace=None, preserve_reasons=None, remediation_packet=None).message + +tests/unit/specfact_code_review/tools/test_semgrep_runner.py:287: AssertionError +_______ test_structured_semgrep_errors_retain_diagnostic_context[1-bugs] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-652/test_structured_semgrep_errors3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109d657b0> +bug_pass = True, returncode = 1 + + @pytest.mark.parametrize("bug_pass", [False, True], ids=["clean", "bugs"]) + @pytest.mark.parametrize("returncode", [0, 1]) + def test_structured_semgrep_errors_retain_diagnostic_context( + tmp_path: Path, monkeypatch: MonkeyPatch, bug_pass: bool, returncode: int + ) -> None: + """Both passes retain the native error without accepting partial execution.""" + bundle = tmp_path / "bundle" + (bundle / ".semgrep").mkdir(parents=True) + for name in ("clean_code", "bugs"): + (bundle / f".semgrep/{name}.yaml").write_text("rules: []\n", encoding="utf-8") + target = tmp_path / "target.py" + target.write_text("VALUE = 1\n", encoding="utf-8") + payload = { + "results": [], + "errors": [{"type": "ParseError", "code": 3, "message": "target parse failed", "source": "PRIVATE_SOURCE"}], + } + process = Mock(return_value=completed_process("semgrep", stdout=json.dumps(payload), returncode=returncode)) + monkeypatch.setattr(subprocess, "run", process) + + findings = (run_semgrep_bugs if bug_pass else run_semgrep)([target], bundle_root=bundle) + + assert len(findings) == 1 + assert findings[0].category == "tool_error" + assert findings[0].severity == "error" + assert "count=1" in findings[0].message +> assert '"type":"ParseError"' in findings[0].message +E assert '"type":"ParseError"' in 'Unable to parse Semgrep bugs pass output: semgrep returned structured errors (count=1)' +E + where 'Unable to parse Semgrep bugs pass output: semgrep returned structured errors (count=1)' = ReviewFinding(category='tool_error', severity='error', tool='semgrep', rule='tool_error', file='/private/var/folders/l..., before_ref=None, after_ref=None, improvement=None, signal_trace=None, preserve_reasons=None, remediation_packet=None).message + +tests/unit/specfact_code_review/tools/test_semgrep_runner.py:287: AssertionError +______ test_structured_semgrep_error_diagnostics_are_bounded_and_escaped _______ + + def test_structured_semgrep_error_diagnostics_are_bounded_and_escaped() -> None: + """Untrusted nested metadata and long control-filled messages stay bounded.""" + from specfact_code_review.tools.semgrep_runner import _validate_semgrep_completion + + payload = { + "errors": [ + {"type": "ParseError", "code": 3, "message": "parse\n\t\x1b" + "x" * 1000}, + {"type": {"source": "PRIVATE_SOURCE"}, "code": ["PRIVATE_SOURCE"], "message": {"source": "PRIVATE_SOURCE"}}, + {"type": "z" * 1000, "code": "z" * 1000, "message": "\x00" * 1000}, + {"type": "FOURTH_ERROR", "message": "OMITTED_MESSAGE"}, + ] + } + + with pytest.raises(ValueError) as error: + _validate_semgrep_completion(completed_process("semgrep", stdout="", returncode=0), payload) + + diagnostic = str(error.value) + assert "count=4" in diagnostic +> assert "omitted=1" in diagnostic +E AssertionError: assert 'omitted=1' in 'semgrep returned structured errors (count=4)' + +tests/unit/specfact_code_review/tools/test_semgrep_runner.py:312: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/tools/test_semgrep_runner.py::test_structured_semgrep_errors_retain_diagnostic_context[0-clean] +FAILED tests/unit/specfact_code_review/tools/test_semgrep_runner.py::test_structured_semgrep_errors_retain_diagnostic_context[0-bugs] +FAILED tests/unit/specfact_code_review/tools/test_semgrep_runner.py::test_structured_semgrep_errors_retain_diagnostic_context[1-clean] +FAILED tests/unit/specfact_code_review/tools/test_semgrep_runner.py::test_structured_semgrep_errors_retain_diagnostic_context[1-bugs] +FAILED tests/unit/specfact_code_review/tools/test_semgrep_runner.py::test_structured_semgrep_error_diagnostics_are_bounded_and_escaped +======================= 5 failed, 58 deselected in 0.27s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_SEMGREP_PARSER_EVIDENCE.json b/openspec/changes/code-review-16-portable-project-runtime/PR478_SEMGREP_PARSER_EVIDENCE.json new file mode 100644 index 00000000..50680ce1 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_SEMGREP_PARSER_EVIDENCE.json @@ -0,0 +1,119 @@ +{ + "before": [ + { + "file": "actual.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 593,\n \"error_line_count\": 593,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/actual.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 593,\n \"error_line_count\": 593,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/actual.py\n/probe/actual.py: exn = Parsing_error.Syntax_error (/probe/actual.py:148:10 \"scenario\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/actual.py\n000.3s: [Python] done parsing /probe/actual.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + }, + { + "file": "actual.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 593,\n \"error_line_count\": 593,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/actual.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 593,\n \"error_line_count\": 593,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/actual.py\n/probe/actual.py: exn = Common.Impossible\nRaised at Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1119, characters 15-38\nCalled from Parse_python_tree_sitter.map_anon_choice_exp_aad5b2d in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 215, characters 14-35\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_argument_list in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", lines 231-236, characters 10-14\nCalled from Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1160, characters 25-48\nCalled from Parse_python_tree_sitter.map_with_item in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1426, characters 15-36\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_with_clause in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1589, characters 8-72\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1955, characters 12-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_python_tree_sitter.map_block in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1834, characters 11-29\nCalled from Parse_python_tree_sitter.map_function_definition in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2122, characters 13-29\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1962, characters 16-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_tree_sitter_helpers.wrap_parser in file \"OSS/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml\", line 131, characters 13-40\nCalled from Pfff_or_tree_sitter.run_parser in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 161, characters 18-24\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/actual.py\n000.3s: [Python] done parsing /probe/actual.py\n\nTODO statistics:\n\nParse_python_tree_sitter.map_anon_choice_exp_aad5b2d:215, -> 1\n" + }, + { + "file": "actual.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 593,\n \"error_line_count\": 593,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/actual.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 593,\n \"error_line_count\": 593,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/actual.py\n/probe/actual.py: exn = Parsing_error.Syntax_error (/probe/actual.py:148:10 \"scenario\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/actual.py\n000.3s: [Python] done parsing /probe/actual.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + } + ], + "after": [ + { + "file": "actual.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 592,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 12702,\n \"untranslated_node_count\": 12\n },\n \"projects\": [\n {\n \"name\": \"/probe/actual.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 592,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 12702,\n \"untranslated_node_count\": 12\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/actual.py\n000.3s: [Python] done parsing /probe/actual.py\n\nTODO statistics:\n\n" + }, + { + "file": "actual.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 592,\n \"error_line_count\": 592,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/actual.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 592,\n \"error_line_count\": 592,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/actual.py\n/probe/actual.py: exn = Common.Impossible\nRaised at Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1119, characters 15-38\nCalled from Parse_python_tree_sitter.map_anon_choice_exp_aad5b2d in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 215, characters 14-35\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_argument_list in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", lines 231-236, characters 10-14\nCalled from Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1160, characters 25-48\nCalled from Parse_python_tree_sitter.map_with_item in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1426, characters 15-36\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_with_clause in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1589, characters 8-72\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1955, characters 12-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_python_tree_sitter.map_block in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1834, characters 11-29\nCalled from Parse_python_tree_sitter.map_function_definition in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2122, characters 13-29\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1962, characters 16-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Stdlib__List.prepend_concat_map in file \"list.ml\", line 290, characters 20-46\nCalled from Parse_tree_sitter_helpers.wrap_parser in file \"OSS/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml\", line 131, characters 13-40\nCalled from Pfff_or_tree_sitter.run_parser in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 161, characters 18-24\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/actual.py\n000.3s: [Python] done parsing /probe/actual.py\n\nTODO statistics:\n\nParse_python_tree_sitter.map_anon_choice_exp_aad5b2d:215, -> 1\n" + }, + { + "file": "actual.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 592,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 12702,\n \"untranslated_node_count\": 12\n },\n \"projects\": [\n {\n \"name\": \"/probe/actual.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 592,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 12702,\n \"untranslated_node_count\": 12\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/actual.py\n000.3s: [Python] done parsing /probe/actual.py\n\nTODO statistics:\n\n" + } + ], + "minimal_inputs": { + "minimal.py": "def check(scenario):\n match scenario:\n case 'changed':\n return 1\n case _:\n return 2\n", + "empty.py": "with open(\"file\", newline=\"\") as stream:\n pass\n", + "combo.py": "with open(\"file\", newline=\"\") as stream:\n pass\ndef check(scenario):\n match scenario:\n case \"changed\":\n return 1\n" + }, + "minimal_results": [ + { + "file": "empty.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 31,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/empty.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 31,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/empty.py\n000.2s: [Python] done parsing /probe/empty.py\n\nTODO statistics:\n\n" + }, + { + "file": "empty.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 2,\n \"error_line_count\": 2,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/empty.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 2,\n \"error_line_count\": 2,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/empty.py\n/probe/empty.py: exn = Common.Impossible\nRaised at Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1119, characters 15-38\nCalled from Parse_python_tree_sitter.map_anon_choice_exp_aad5b2d in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 215, characters 14-35\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_argument_list in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", lines 231-236, characters 10-14\nCalled from Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1160, characters 25-48\nCalled from Parse_python_tree_sitter.map_with_item in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1426, characters 15-36\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_with_clause in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1589, characters 8-72\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1955, characters 12-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Parse_tree_sitter_helpers.wrap_parser in file \"OSS/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml\", line 131, characters 13-40\nCalled from Pfff_or_tree_sitter.run_parser in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 161, characters 18-24\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/empty.py\n000.2s: [Python] done parsing /probe/empty.py\n\nTODO statistics:\n\nParse_python_tree_sitter.map_anon_choice_exp_aad5b2d:215, -> 1\n" + }, + { + "file": "empty.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 31,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/empty.py\",\n \"parsing_rate\": 1.0,\n \"line_count\": 2,\n \"error_line_count\": 0,\n \"file_count\": 1,\n \"error_file_count\": 0,\n \"total_node_count\": 31,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/empty.py\n000.2s: [Python] done parsing /probe/empty.py\n\nTODO statistics:\n\n" + }, + { + "file": "combo.py", + "flags": [], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 6,\n \"error_line_count\": 6,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/combo.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 6,\n \"error_line_count\": 6,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/combo.py\n/probe/combo.py: exn = Parsing_error.Syntax_error (/probe/combo.py:4:10 \"scenario\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/combo.py\n000.2s: [Python] done parsing /probe/combo.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + }, + { + "file": "combo.py", + "flags": [ + "-tree_sitter_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 6,\n \"error_line_count\": 6,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/combo.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 6,\n \"error_line_count\": 6,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/combo.py\n/probe/combo.py: exn = Common.Impossible\nRaised at Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1119, characters 15-38\nCalled from Parse_python_tree_sitter.map_anon_choice_exp_aad5b2d in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 215, characters 14-35\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_argument_list in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", lines 231-236, characters 10-14\nCalled from Parse_python_tree_sitter.map_primary_expression in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1160, characters 25-48\nCalled from Parse_python_tree_sitter.map_with_item in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1426, characters 15-36\nCalled from List_.fast_map in file \"OSS/libs/collections/List_.ml\", line 79, characters 17-20\nCalled from Parse_python_tree_sitter.map_with_clause in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1589, characters 8-72\nCalled from Parse_python_tree_sitter.map_compound_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 1955, characters 12-45\nCalled from Parse_python_tree_sitter.map_statement in file \"OSS/languages/python/tree-sitter/Parse_python_tree_sitter.ml\", line 2142, characters 27-55\nCalled from Stdlib__List.concat_map in file \"list.ml\", line 286, characters 32-37\nCalled from Parse_tree_sitter_helpers.wrap_parser in file \"OSS/libs/lib_parsing_tree_sitter/Parse_tree_sitter_helpers.ml\", line 131, characters 13-40\nCalled from Pfff_or_tree_sitter.run_parser in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 161, characters 18-24\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/combo.py\n000.2s: [Python] done parsing /probe/combo.py\n\nTODO statistics:\n\nParse_python_tree_sitter.map_anon_choice_exp_aad5b2d:215, -> 1\n" + }, + { + "file": "combo.py", + "flags": [ + "-pfff_only" + ], + "exit": 0, + "stdout": "{\n \"language\": \"Python\",\n \"global\": {\n \"name\": \"*\",\n \"parsing_rate\": 0.0,\n \"line_count\": 6,\n \"error_line_count\": 6,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n },\n \"projects\": [\n {\n \"name\": \"/probe/combo.py\",\n \"parsing_rate\": 0.0,\n \"line_count\": 6,\n \"error_line_count\": 6,\n \"file_count\": 1,\n \"error_file_count\": 1,\n \"total_node_count\": 0,\n \"untranslated_node_count\": 0\n }\n ]\n}\n", + "stderr": "000.2s: [Python] processing /probe/combo.py\n/probe/combo.py: exn = Parsing_error.Syntax_error (/probe/combo.py:4:10 \"scenario\")\nRaised at Parse_python.parse in file \"OSS/languages/python/menhir/Parse_python.ml\", line 152, characters 10-65\nCalled from Pfff_or_tree_sitter.throw_tokens in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 325, characters 12-18\nCalled from Pfff_or_tree_sitter.run_parser.(fun) in file \"OSS/src/parsing/Pfff_or_tree_sitter.ml\", line 148, characters 28-34\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Parse_target.parse_and_resolve_name in file \"OSS/src/parsing/Parse_target.ml\", line 250, characters 12-42\nCalled from Time_limit.timed_computation_and_clear_timer.timed_computation in file \"OSS/libs/process_limits/Time_limit.ml\", line 109, characters 12-16\nCalled from Time_limit.set_timeout in file \"OSS/libs/process_limits/Time_limit.ml\", line 198, characters 27-37\nRe-raised at Exception.reraise in file \"OSS/libs/commons/Exception.ml\", line 31, characters 33-72\nCalled from Stdlib__Fun.protect in file \"fun.ml\", line 34, characters 8-15\nRe-raised at Stdlib__Fun.protect in file \"fun.ml\", line 39, characters 6-52\nCalled from Common.protect in file \"OSS/libs/commons/Common.ml\", line 53, characters 6-31\nCalled from Memory_limit.run_with_memory_limit in file \"OSS/libs/process_limits/Memory_limit.ml\", line 132, characters 8-67\nCalled from Test_parsing.parsing_common.(fun) in file \"OSS/src/parsing/tests/Test_parsing.ml\", lines 401-409, characters 17-81\n\nFAILED TO FULLY PARSE: /probe/combo.py\n000.2s: [Python] done parsing /probe/combo.py\n\nTODO statistics:\n\nPfff_or_tree_sitter.throw_tokens:325, -> 1\n" + } + ], + "source_sha256": { + "before": "3898564aca7ad580bd034d76b563d60cc54a82bdae72ab54ec13959cc73d0fa0", + "after": "50b38b4b9f11b07487a4d74767d35f0831d5acce19405bfd38993db4bbf6a15d" + }, + "method": "Official Semgrep1.144.0 core in offline disposable Linux; parsing diagnostics only, not capsule acceptance. Parser statistics, not process exit alone, determine parse completion.", + "limit": "Equivalent fixture if/elif avoids this engine limitation; customer match plus empty-string parser failures remain explicit incomplete analysis." +} diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_COLLISION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_COLLISION_RED.txt new file mode 100644 index 00000000..39e18475 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_COLLISION_RED.txt @@ -0,0 +1,57 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 32 items / 26 deselected / 6 selected + +tests/unit/specfact_code_review/run/test_target_coverage.py ...F.. [100%] + +=================================== FAILURES =================================== +_ test_top_level_installed_module_respects_reviewer_and_native_scope[directory-collision-default] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-886/test_top_level_installed_modul3') +scope = 'default', collision = True + + @pytest.mark.parametrize("scope", ["default", "configured", "native"]) + @pytest.mark.parametrize("collision", [False, True], ids=["unambiguous", "directory-collision"]) + def test_top_level_installed_module_respects_reviewer_and_native_scope(tmp_path, scope, collision): + source = _project(tmp_path) + if collision: + (source / "standalone").mkdir() + site = tmp_path / "site-packages" + site.mkdir() + (site / "standalone.py").write_text("import foreign\ndef answer():\n return foreign.VALUE\n") + (site / "foreign.py").write_text("VALUE = 42\n") + (source / "tests/test_installed.py").write_text( + "from standalone import answer\ndef test_answer():\n assert answer() == 42\n" + ) + configuration = source / "pyproject.toml" + configuration.write_text(configuration.read_text().replace('source=["app"]\n', "")) + if scope == "configured": + configuration.write_text( + configuration.read_text().replace("[tool.coverage.run]", '[tool.coverage.run]\nsource=["app"]') + ) + extra = ("--cov=app", "--cov-fail-under=0") if scope == "native" else () + completed, receipt = _run(source, tmp_path, extra, startup=site, modules=["standalone"]) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage_policy"]["mode"] == ("native" if scope == "native" else "reviewer") + assert "2 passed" in completed.stdout + if scope == "default" and collision: +> assert receipt["coverage"] == {} +E AssertionError: assert {'meta': {'fo...'81.82', ...}} == {} +E +E Left contains 3 more items: +E {'files': {'app.py': {'classes': {'': {'excluded_lines': [], +E 'executed_lines': [1, 2, 3], +E 'missing_lines': [4], +E 'start_line': 1, +E 'summary': {'covered_lines': 3,... +E +E ...Full output truncated (200 lines hidden), use '-vv' to show + +tests/unit/specfact_code_review/run/test_target_coverage.py:334: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_top_level_installed_module_respects_reviewer_and_native_scope[directory-collision-default] +================== 1 failed, 5 passed, 26 deselected in 1.98s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_RED.txt new file mode 100644 index 00000000..6fff0e18 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_RED.txt @@ -0,0 +1,63 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 53 items / 47 deselected / 6 selected + +tests/unit/specfact_code_review/run/test_installed_coverage.py FF.... [100%] + +=================================== FAILURES =================================== +_ test_native_single_module_keeps_narrow_measurement_and_verified_attribution[root] _ + +installed_single_module = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-864/test_n...00gn/T/pytest-of-dom/pytest-864/test_native_single_module_keep0/runtime/site-packages/standalone-owned-1.0.dist-info')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-864/test_native_single_module_keep0') +layout = 'root' + + @pytest.mark.parametrize("layout", ["root", "src"]) + def test_native_single_module_keeps_narrow_measurement_and_verified_attribution( + installed_single_module, tmp_path, layout + ): + project = installed_single_module + if layout == "src": + target = project.snapshot / "src/standalone.py" + target.parent.mkdir() + project.source.rename(target) + project.source = target + bridge = _bridge(project) +> assert len(bridge.mappings) == 1, bridge.diagnostics +E AssertionError: {'/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-864/test_native_single_module_keep0/snapshot/standalone.py': 'coverage_directory_ownership_unavailable'} +E assert 0 == 1 +E + where 0 = len(()) +E + where () = CoverageBridge(directories=(), mappings=(), diagnostics={'/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pyt.../pytest-of-dom/pytest-864/test_native_single_module_keep0/runtime/site-packages/standalone.py',)}, measured_origins=()).mappings + +tests/unit/specfact_code_review/run/test_installed_coverage.py:637: AssertionError +_ test_native_single_module_keeps_narrow_measurement_and_verified_attribution[src] _ + +installed_single_module = namespace(snapshot=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-864/test_n...00gn/T/pytest-of-dom/pytest-864/test_native_single_module_keep1/runtime/site-packages/standalone-owned-1.0.dist-info')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-864/test_native_single_module_keep1') +layout = 'src' + + @pytest.mark.parametrize("layout", ["root", "src"]) + def test_native_single_module_keeps_narrow_measurement_and_verified_attribution( + installed_single_module, tmp_path, layout + ): + project = installed_single_module + if layout == "src": + target = project.snapshot / "src/standalone.py" + target.parent.mkdir() + project.source.rename(target) + project.source = target + bridge = _bridge(project) +> assert len(bridge.mappings) == 1, bridge.diagnostics +E AssertionError: {'/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-864/test_native_single_module_keep1/snapshot/src/standalone.py': 'ambiguous_snapshot_source'} +E assert 0 == 1 +E + where 0 = len(()) +E + where () = CoverageBridge(directories=(), mappings=(), diagnostics={'/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pyt.../pytest-of-dom/pytest-864/test_native_single_module_keep1/runtime/site-packages/standalone.py',)}, measured_origins=()).mappings + +tests/unit/specfact_code_review/run/test_installed_coverage.py:637: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_native_single_module_keeps_narrow_measurement_and_verified_attribution[root] +FAILED tests/unit/specfact_code_review/run/test_installed_coverage.py::test_native_single_module_keeps_narrow_measurement_and_verified_attribution[src] +================== 2 failed, 4 passed, 47 deselected in 0.68s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_WIRING_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_WIRING_RED.txt new file mode 100644 index 00000000..ca778d10 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_SINGLE_MODULE_WIRING_RED.txt @@ -0,0 +1,249 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, xdist-3.8.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 122 items / 109 deselected / 13 selected + +tests/unit/specfact_code_review/run/test_target_pytest.py FFFFFFFFF [ 69%] +tests/unit/specfact_code_review/run/test_target_coverage.py F.. [ 92%] +tests/unit/specfact_code_review/run/test_portable_worker.py F [100%] + +=================================== FAILURES =================================== +___ test_installed_module_selector_rejects_unattached_or_invalid_names[None] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1087c4180> +modules = None + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[standalone] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1087c42b0> +modules = 'standalone' + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[modules2] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1087e6450> +modules = [1] + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[modules3] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10871dae0> +modules = ['../escape'] + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[modules4] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10871e250> +modules = ['pkg.module'] + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[modules5] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector5') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10895d350> +modules = ['class'] + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[modules6] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector6') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10895d950> +modules = ['missing'] + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_ test_installed_module_selector_rejects_unattached_or_invalid_names[modules7] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector7') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108767d40> +modules = ['linked'] + + @pytest.mark.parametrize("modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]]) + def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): +> target_pytest._installed_coverage_modules({"coverage_modules": modules}) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:87: AttributeError +_____ test_installed_module_selector_accepts_only_regular_attached_module ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_installed_module_selector8') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108766e40> + + def test_installed_module_selector_accepts_only_regular_attached_module(tmp_path, monkeypatch): + site = tmp_path / "site-packages" + site.mkdir() + (site / "standalone.py").write_text("VALUE = 1\n") + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) +> assert target_pytest._installed_coverage_modules({"coverage_modules": ["standalone"]}) == ["standalone"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.target_pytest' has no attribute '_installed_coverage_modules'. Did you mean: '_installed_coverage_candidates'? + +tests/unit/specfact_code_review/run/test_target_pytest.py:95: AttributeError +_ test_top_level_installed_module_respects_reviewer_and_native_scope[default] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-868/test_top_level_installed_modul0') +scope = 'default' + + @pytest.mark.parametrize("scope", ["default", "configured", "native"]) + def test_top_level_installed_module_respects_reviewer_and_native_scope(tmp_path, scope): + source = _project(tmp_path) + site = tmp_path / "site-packages" + site.mkdir() + (site / "standalone.py").write_text("import foreign\ndef answer():\n return foreign.VALUE\n") + (site / "foreign.py").write_text("VALUE = 42\n") + (source / "tests/test_installed.py").write_text( + "from standalone import answer\ndef test_answer():\n assert answer() == 42\n" + ) + configuration = source / "pyproject.toml" + configuration.write_text(configuration.read_text().replace('source=["app"]\n', "")) + if scope == "configured": + configuration.write_text(configuration.read_text().replace("[tool.coverage.run]", '[tool.coverage.run]\nsource=["app"]')) + extra = ("--cov=app", "--cov-fail-under=0") if scope == "native" else () + completed, receipt = _run(source, tmp_path, extra, startup=site, modules=["standalone"]) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage_policy"]["mode"] == ("native" if scope == "native" else "reviewer") + measured = receipt["coverage"]["files"] +> assert any(path.endswith("standalone.py") for path in measured) is (scope == "default") +E AssertionError: assert False is ('default' == 'default' +E + where False = any(. at 0x1088fbe60>) +E +E default) + +tests/unit/specfact_code_review/run/test_target_coverage.py:321: AssertionError +_________ test_portable_pytest_command_transmits_verified_module_names _________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1086dc2f0> + + def test_portable_pytest_command_transmits_verified_module_names(monkeypatch): + bridge = CoverageBridge((), (), {}, {}, modules=("standalone",)) + monkeypatch.setattr(portable_worker, "plan_installed_coverage", lambda *_args, **_kwargs: bridge) + monkeypatch.setattr(portable_worker, "target_command", lambda domain, arguments: [domain, *arguments]) + actual, command = portable_worker._portable_pytest_command([], json.dumps({"selectors": ["tests"]})) + assert actual is bridge + assert command[0] == "pytest-observe" + request = json.loads(command[1]) +> assert request["coverage_modules"] == ["standalone"] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'coverage_modules' + +tests/unit/specfact_code_review/run/test_portable_worker.py:448: KeyError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[None] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[standalone] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[modules2] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[modules3] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[modules4] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[modules5] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[modules6] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_rejects_unattached_or_invalid_names[modules7] +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_installed_module_selector_accepts_only_regular_attached_module +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_top_level_installed_module_respects_reviewer_and_native_scope[default] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_portable_pytest_command_transmits_verified_module_names +================= 11 failed, 2 passed, 109 deselected in 0.62s ================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_TARGET_COVERAGE_WIRING_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_TARGET_COVERAGE_WIRING_RED.txt new file mode 100644 index 00000000..658983bd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_TARGET_COVERAGE_WIRING_RED.txt @@ -0,0 +1,51 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items / 6 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_target_coverage.py F [100%] + +=================================== FAILURES =================================== +_______ test_target_observer_does_not_override_native_coverage_arguments _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-777/test_target_observer_does_not_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1077c82b0> + + def test_target_observer_does_not_override_native_coverage_arguments(tmp_path, monkeypatch): + from types import SimpleNamespace + + runtime = tmp_path / "runtime" + runtime.mkdir() + (runtime / "project-runtime.json").write_text(json.dumps({ + "project": {"pytest_config": {"addopts": "--cov=app --cov-report=json:customer.json"}}, + "inventory": {"pytest_arguments": []}, + })) + measurement = {"coverage": {}, "coverage_policy": {"mode": "reviewer"}, "coverage_threshold": 95} + collector = SimpleNamespace(configure=lambda **_kwargs: None, evidence=lambda: measurement) + monkeypatch.setitem(sys.modules, "_specfact_target_coverage", collector) + monkeypatch.setattr(target_pytest, "ROOT", runtime) + monkeypatch.setattr(target_pytest, "SNAPSHOT_ROOT", tmp_path) + monkeypatch.setattr(target_pytest, "Path", lambda value: Path(str(value).replace("/opt/specfact/tmp", str(tmp_path)))) + monkeypatch.setattr(sys, "argv", ["observer", json.dumps({"selectors": ["tests"]})]) + captured = [] + + def run(arguments, *, plugins): + assert len(plugins) == 1 + captured.extend(arguments) + return 0 + + monkeypatch.setattr(pytest, "main", run) + with pytest.raises(SystemExit) as result: + target_pytest.main() + assert result.value.code == 0 +> assert not any(token.startswith(("--cov=", "--cov-report=")) for token in captured) +E assert not True +E + where True = any(. at 0x108361e00>) + +tests/unit/specfact_code_review/run/test_target_coverage.py:150: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_coverage.py::test_target_observer_does_not_override_native_coverage_arguments +======================= 1 failed, 6 deselected in 0.10s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR478_TEMPLATE_FREE_GRAFTS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR478_TEMPLATE_FREE_GRAFTS_RED.txt new file mode 100644 index 00000000..0ef9bc71 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR478_TEMPLATE_FREE_GRAFTS_RED.txt @@ -0,0 +1,51 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 56 items / 54 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py .F [100%] + +=================================== FAILURES =================================== +__________ test_unbound_grafts_cannot_remove_selected_ancestry[True] ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-762/test_unbound_grafts_cannot_rem1') +empty_template = True + + @pytest.mark.parametrize("empty_template", [False, True]) + def test_unbound_grafts_cannot_remove_selected_ancestry(tmp_path: Path, empty_template: bool) -> None: + root = tmp_path / "source" + _repository(root, empty_template=empty_template) + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + before = vcs_context(root) +> (root / ".git/info/grafts").write_text(selected + "\n") + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:476: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:809: in write_text + with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-762/test_unbound_grafts_cannot_rem1/source/.git/info/grafts') +mode = 'w', buffering = -1, encoding = 'locale', errors = None, newline = None + + def open(self, mode='r', buffering=-1, encoding=None, + errors=None, newline=None): + """ + Open the file pointed to by this path and return a file object, as + the built-in open() function does. + """ + if "b" not in mode: + encoding = io.text_encoding(encoding) +> return io.open(self, mode, buffering, encoding, errors, newline) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E FileNotFoundError: [Errno 2] No such file or directory: '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-762/test_unbound_grafts_cannot_rem1/source/.git/info/grafts' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:771: FileNotFoundError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unbound_grafts_cannot_remove_selected_ancestry[True] +================== 1 failed, 1 passed, 54 deselected in 0.51s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PREPARATION_DIAGNOSTIC_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PREPARATION_DIAGNOSTIC_RED.txt new file mode 100644 index 00000000..7e189e36 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PREPARATION_DIAGNOSTIC_RED.txt @@ -0,0 +1,384 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 43 items + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py .... [ 9%] +.............s....FFF... [ 65%] +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py FF [ 69%] +FFF... [ 83%] +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py . [ 86%] +...FF. [100%] + +=================================== FAILURES =================================== +_________ test_preparation_reports_numeric_builder_exit[sys.exit(7)-7] _________ + +tmp_path = PosixPath('/test_preparation_reports_numer0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c99db70> +termination = 'sys.exit(7)', exit_status = 7 + + @pytest.mark.parametrize(("termination", "exit_status"), [("sys.exit(7)", 7), ("os.kill(os.getpid(), 15)", -15)]) + def test_preparation_reports_numeric_builder_exit(tmp_path: Path, monkeypatch, termination, exit_status) -> None: + plan, runtime, cache, _ = _builder( + tmp_path, monkeypatch, "print('PRIVATE_FIXTURE_OUTPUT', flush=True); " + termination + ) + error = _failed_build(plan, runtime, cache) +> assert f"project_runtime_prepare_failed:exit={exit_status}" in str(error) +E AssertionError: assert 'project_runtime_prepare_failed:exit=7' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...numer0/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-20ur2ppj.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...numer0/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-20ur2ppj.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...umer0/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-20ur2ppj.log; ProjectRuntimeError')) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:349: AssertionError +_ test_preparation_reports_numeric_builder_exit[os.kill(os.getpid(), 15)--15] __ + +tmp_path = PosixPath('/test_preparation_reports_numer1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c99faf0> +termination = 'os.kill(os.getpid(), 15)', exit_status = -15 + + @pytest.mark.parametrize(("termination", "exit_status"), [("sys.exit(7)", 7), ("os.kill(os.getpid(), 15)", -15)]) + def test_preparation_reports_numeric_builder_exit(tmp_path: Path, monkeypatch, termination, exit_status) -> None: + plan, runtime, cache, _ = _builder( + tmp_path, monkeypatch, "print('PRIVATE_FIXTURE_OUTPUT', flush=True); " + termination + ) + error = _failed_build(plan, runtime, cache) +> assert f"project_runtime_prepare_failed:exit={exit_status}" in str(error) +E AssertionError: assert 'project_runtime_prepare_failed:exit=-15' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...numer1/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-s30jnw93.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...numer1/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-s30jnw93.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...umer1/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-s30jnw93.log; ProjectRuntimeError')) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:349: AssertionError +_ test_preparation_separates_public_codes_from_private_details[failure0-project_runtime_inputs_changed_during_build] _ + +tmp_path = PosixPath('/test_preparation_separates_pub0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c99cde0> +failure = ProjectRuntimeError('project_runtime_inputs_changed_during_build:PRIVATE_FIXTURE_PATH') +expected = 'project_runtime_inputs_changed_during_build' + + @pytest.mark.parametrize( + ("failure", "expected"), + [ + (ProjectRuntimeError("project_runtime_inputs_changed_during_build:PRIVATE_FIXTURE_PATH"), + "project_runtime_inputs_changed_during_build"), + (ProjectRuntimeError("PRIVATE_FIXTURE_MESSAGE"), "ProjectRuntimeError"), + (OSError("PRIVATE_FIXTURE_IO"), "OSError"), + (ValueError("PRIVATE_FIXTURE_VALUE"), "ValueError"), + ], + ) + def test_preparation_separates_public_codes_from_private_details(tmp_path: Path, monkeypatch, failure, expected) -> None: + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, "pass") + + def fail_build(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_builder, "_build", fail_build) + error = _failed_build(plan, runtime, cache) +> assert expected in str(error) +E AssertionError: assert 'project_runtime_inputs_changed_during_build' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...s_pub0/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-b9idbnc1.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...s_pub0/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-b9idbnc1.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000..._pub0/cache/failed-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-b9idbnc1.log; ProjectRuntimeError')) + +tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:373: AssertionError +_____ test_prepare_rejects_artifact_topology_before_inventory_access[root] _____ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c99cd70> +mutation = 'root' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + original_read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return original_read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") +> assert "project_runtime_build_artifact_invalid" in str(error.value) +E AssertionError: assert 'project_runtime_build_artifact_invalid' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_0/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-3rex58dz.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_0/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-3rex58dz.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_0/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-3rex58dz.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_0/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-3rex58dz.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:84: AssertionError +_ test_prepare_rejects_artifact_topology_before_inventory_access[native-directory] _ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ca09320> +mutation = 'native-directory' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + original_read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return original_read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") +> assert "project_runtime_build_artifact_invalid" in str(error.value) +E AssertionError: assert 'project_runtime_build_artifact_invalid' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_1/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-ksk0e70g.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_1/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-ksk0e70g.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_1/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-ksk0e70g.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_1/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-ksk0e70g.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:84: AssertionError +__ test_prepare_rejects_artifact_topology_before_inventory_access[inventory] ___ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c99fb60> +mutation = 'inventory' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + original_read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return original_read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") +> assert "project_runtime_build_artifact_invalid" in str(error.value) +E AssertionError: assert 'project_runtime_build_artifact_invalid' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_2/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-mofg87ks.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_2/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-mofg87ks.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_2/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-mofg87ks.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_2/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-mofg87ks.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:84: AssertionError +_ test_prepare_rejects_artifact_topology_before_inventory_access[nested-file] __ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ca0a040> +mutation = 'nested-file' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + original_read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return original_read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") +> assert "project_runtime_build_artifact_invalid" in str(error.value) +E AssertionError: assert 'project_runtime_build_artifact_invalid' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_3/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-wx8ikhxm.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_3/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-wx8ikhxm.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_3/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-wx8ikhxm.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_3/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-wx8ikhxm.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:84: AssertionError +_____ test_prepare_rejects_artifact_topology_before_inventory_access[fifo] _____ + +tmp_path = PosixPath('/test_prepare_rejects_artifact_4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ca0aeb0> +mutation = 'fifo' + + @pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) + def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str + ) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + original_read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return original_read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") +> assert "project_runtime_build_artifact_invalid" in str(error.value) +E AssertionError: assert 'project_runtime_build_artifact_invalid' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_4/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-2gc5zgxc.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...fact_4/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-2gc5zgxc.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_4/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-2gc5zgxc.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...act_4/cache/failed-d6b15d9f73279cfc616ffb49956280420e171de48a1434d9abeea04fcfe7417e-2gc5zgxc.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py:84: AssertionError +_______ test_prepare_rejects_log_alias_before_inventory[inventory.json] ________ + +tmp_path = PosixPath('/test_prepare_rejects_log_alias0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ca0bd20> +linked_name = 'inventory.json' + + @pytest.mark.parametrize("linked_name", ["inventory.json", "site-packages/retained.log"]) + def test_prepare_rejects_log_alias_before_inventory(tmp_path: Path, monkeypatch, linked_name: str) -> None: + source = tmp_path / "source" + source.mkdir() + plan = discover_project(source) + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="worker", root=tmp_path / "capsule") + cache = tmp_path / "cache" + + def build(_plan, _runtime, staging, *, build_log): + artifact = staging / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "inventory.json").write_text("{}", encoding="utf-8") + build_log.write(b"DISPOSABLE_PRIVATE_DIAGNOSTIC\n") + build_log.flush() + target = artifact / linked_name + target.unlink(missing_ok=True) + # Reconstruct the topology; this does not simulate Linux bind-mount traversal. + os.link(build_log.name, target) + return artifact + + original = Path.read_text + + def read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("inventory read before linked artifact rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) +> assert "hardlink" in str(error.value) +E AssertionError: assert 'hardlink' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...alias0/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-uzttb3ej.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...alias0/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-uzttb3ej.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...lias0/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-uzttb3ej.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...lias0/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-uzttb3ej.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:107: AssertionError +_ test_prepare_rejects_log_alias_before_inventory[site-packages/retained.log] __ + +tmp_path = PosixPath('/test_prepare_rejects_log_alias1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ca28210> +linked_name = 'site-packages/retained.log' + + @pytest.mark.parametrize("linked_name", ["inventory.json", "site-packages/retained.log"]) + def test_prepare_rejects_log_alias_before_inventory(tmp_path: Path, monkeypatch, linked_name: str) -> None: + source = tmp_path / "source" + source.mkdir() + plan = discover_project(source) + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="worker", root=tmp_path / "capsule") + cache = tmp_path / "cache" + + def build(_plan, _runtime, staging, *, build_log): + artifact = staging / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "inventory.json").write_text("{}", encoding="utf-8") + build_log.write(b"DISPOSABLE_PRIVATE_DIAGNOSTIC\n") + build_log.flush() + target = artifact / linked_name + target.unlink(missing_ok=True) + # Reconstruct the topology; this does not simulate Linux bind-mount traversal. + os.link(build_log.name, target) + return artifact + + original = Path.read_text + + def read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("inventory read before linked artifact rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) +> assert "hardlink" in str(error.value) +E AssertionError: assert 'hardlink' in 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...alias1/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-ods2gavo.log; ProjectRuntimeError' +E + where 'project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/p...alias1/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-ods2gavo.log; ProjectRuntimeError' = str(ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...lias1/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-ods2gavo.log; ProjectRuntimeError')) +E + where ProjectRuntimeError('project_runtime_prepare_failed; private log: /private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000...lias1/cache/failed-5c2048021f5989baa2c437c0739d476921673c53ce39b729b42a4e4a568516f1-ods2gavo.log; ProjectRuntimeError') = .value + +tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py:107: AssertionError +=========================== short test summary info ============================ +SKIPPED [1] tests/unit/specfact_code_review/run/test_runtime_builder_logging.py:247: Linux proc descriptor contract +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_preparation_reports_numeric_builder_exit[sys.exit(7)-7] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_preparation_reports_numeric_builder_exit[os.kill(os.getpid(), 15)--15] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder_logging.py::test_preparation_separates_public_codes_from_private_details[failure0-project_runtime_inputs_changed_during_build] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[root] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[native-directory] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[inventory] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[nested-file] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py::test_prepare_rejects_artifact_topology_before_inventory_access[fifo] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_prepare_rejects_log_alias_before_inventory[inventory.json] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py::test_prepare_rejects_log_alias_before_inventory[site-packages/retained.log] +=================== 10 failed, 32 passed, 1 skipped in 1.69s =================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND1_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND1_RED.txt new file mode 100644 index 00000000..bc1d8fde --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND1_RED.txt @@ -0,0 +1,65 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 13 items + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py ....... [ 53%] +FF [ 69%] +tests/unit/specfact_code_review/run/test_runtime_native.py ...F [100%] + +=================================== FAILURES =================================== +________ test_exact_hatch_environment_cannot_select_an_incompatible_abi ________ + + def test_exact_hatch_environment_cannot_select_an_incompatible_abi() -> None: + import pytest + from specfact_code_review.run.runtime_build_driver import select_hatch_environment +> with pytest.raises(RuntimeError, match='project_python_incompatible'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE RuntimeError + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:109: Failed +____________ test_python_prefixed_distribution_command_is_preserved ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-121/test_python_prefixed_distribut0') + + def test_python_prefixed_distribution_command_is_preserved(tmp_path) -> None: + from specfact_code_review.run.runtime_build_driver import copy_executables + environment, artifact = tmp_path / 'environment', tmp_path / 'artifact' + (environment / 'bin').mkdir(parents=True) + artifact.mkdir() + command = environment / 'bin/python-lsp-server' + command.write_text('#!/build/python\nprint("language server")\n') + copied = copy_executables(environment, artifact, [{'path': str(command), 'distribution': 'python-lsp-server'}]) +> assert copied == [{'name': 'python-lsp-server', 'distribution': 'python-lsp-server'}] +E AssertionError: assert [] == [{'name': 'py...-lsp-server'}] +E +E Right contains one more item: {'name': 'python-lsp-server', 'distribution': 'python-lsp-server'} +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_build_driver.py:122: AssertionError +______ test_owned_native_executables_include_their_shared_library_closure ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-121/test_owned_native_executables_0') + + def test_owned_native_executables_include_their_shared_library_closure(tmp_path: Path) -> None: + from specfact_code_review.run.runtime_native import inventory_native + artifact, system = tmp_path / 'artifact', tmp_path / 'system' + (artifact / 'executables').mkdir(parents=True) + system.mkdir() + _elf(artifact / 'executables/customer-tool', ('libcustomer.so.1',)) + _elf(system / 'libcustomer.so.1', ()) + records = inventory_native(artifact, capsule_root=tmp_path / 'capsule', declared=(), system_roots=(system,)) +> assert (artifact / 'native/libcustomer.so.1').is_file() +E AssertionError: assert False +E + where False = is_file() +E + where is_file = (PosixPath('/pytest-of-dom/pytest-121/test_owned_native_executables_0/artifact') / 'native/libcustomer.so.1').is_file + +tests/unit/specfact_code_review/run/test_runtime_native.py:77: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_exact_hatch_environment_cannot_select_an_incompatible_abi +FAILED tests/unit/specfact_code_review/run/test_runtime_build_driver.py::test_python_prefixed_distribution_command_is_preserved +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_owned_native_executables_include_their_shared_library_closure +========================= 3 failed, 10 passed in 0.11s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND2_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND2_RED.txt new file mode 100644 index 00000000..a9b935ba --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND2_RED.txt @@ -0,0 +1,334 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 110 items + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py ....FFF [ 6%] +tests/unit/specfact_code_review/run/test_runtime_builder.py ......FF [ 13%] +tests/unit/specfact_code_review/run/test_target_bootstrap.py .....FFF [ 20%] +tests/unit/specfact_code_review/run/test_commands.py ................... [ 38%] +...........................................................F [ 92%] +tests/unit/test_external_capsule_corpus.py .......F [100%] + +=================================== FAILURES =================================== +___________ test_descriptor_rejects_malformed_inventory[inventory0] ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-129/test_descriptor_rejects_malfor0') +inventory = [] + + @pytest.mark.parametrize("inventory", [[], {"analyzer_conflicts": []}, {"member_graphs": []}]) + def test_descriptor_rejects_malformed_inventory(tmp_path: Path, inventory) -> None: + from specfact_code_review.run.runtime_models import document_digest + + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") + data = prepared.descriptor + data["inventory"] = inventory + data["identity"] = document_digest({key: value for key, value in data.items() if key != "identity"}) + prepared.descriptor_path.write_text(json.dumps(data)) +> with pytest.raises(ProjectRuntimeError, match="inventory_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:129: Failed +___________ test_descriptor_rejects_malformed_inventory[inventory1] ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-129/test_descriptor_rejects_malfor1') +inventory = {'analyzer_conflicts': []} + + @pytest.mark.parametrize("inventory", [[], {"analyzer_conflicts": []}, {"member_graphs": []}]) + def test_descriptor_rejects_malformed_inventory(tmp_path: Path, inventory) -> None: + from specfact_code_review.run.runtime_models import document_digest + + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") + data = prepared.descriptor + data["inventory"] = inventory + data["identity"] = document_digest({key: value for key, value in data.items() if key != "identity"}) + prepared.descriptor_path.write_text(json.dumps(data)) +> with pytest.raises(ProjectRuntimeError, match="inventory_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:129: Failed +___________ test_descriptor_rejects_malformed_inventory[inventory2] ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-129/test_descriptor_rejects_malfor2') +inventory = {'member_graphs': []} + + @pytest.mark.parametrize("inventory", [[], {"analyzer_conflicts": []}, {"member_graphs": []}]) + def test_descriptor_rejects_malformed_inventory(tmp_path: Path, inventory) -> None: + from specfact_code_review.run.runtime_models import document_digest + + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") + data = prepared.descriptor + data["inventory"] = inventory + data["identity"] = document_digest({key: value for key, value in data.items() if key != "identity"}) + prepared.descriptor_path.write_text(json.dumps(data)) +> with pytest.raises(ProjectRuntimeError, match="inventory_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:129: Failed +________ test_concurrent_cache_publication_accepts_existing_winner[17] _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-129/test_concurrent_cache_publicat0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10df23e10> +number = 17 + + @pytest.mark.parametrize("number", [17, 39]) + def test_concurrent_cache_publication_accepts_existing_winner(tmp_path, monkeypatch, number) -> None: + from specfact_code_review.run import runtime_builder + + def raced(source, destination): + raise OSError(number, "concurrent publication") + + monkeypatch.setattr(runtime_builder.os, "rename", raced) +> runtime_builder.publish_artifact(tmp_path / "candidate", tmp_path / "winner") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.runtime_builder' has no attribute 'publish_artifact' + +tests/unit/specfact_code_review/run/test_runtime_builder.py:73: AttributeError +________ test_concurrent_cache_publication_accepts_existing_winner[39] _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-129/test_concurrent_cache_publicat1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10e234640> +number = 39 + + @pytest.mark.parametrize("number", [17, 39]) + def test_concurrent_cache_publication_accepts_existing_winner(tmp_path, monkeypatch, number) -> None: + from specfact_code_review.run import runtime_builder + + def raced(source, destination): + raise OSError(number, "concurrent publication") + + monkeypatch.setattr(runtime_builder.os, "rename", raced) +> runtime_builder.publish_artifact(tmp_path / "candidate", tmp_path / "winner") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'specfact_code_review.run.runtime_builder' has no attribute 'publish_artifact' + +tests/unit/specfact_code_review/run/test_runtime_builder.py:73: AttributeError +___________ test_python_startup_cannot_bypass_runtime[arguments0--I] ___________ + +arguments = ['-I', '-c', 'pass'], option = '-I' + + @pytest.mark.parametrize("arguments, option", [(["-I", "-c", "pass"], "-I"), (["-uS", "script"], "-S"), (["-E", "-m", "pytest"], "-E")]) + def test_python_startup_cannot_bypass_runtime(arguments, option) -> None: +> with pytest.raises(RuntimeError, match=f"project_python_option_unsupported:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE RuntimeError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:76: Failed +___________ test_python_startup_cannot_bypass_runtime[arguments1--S] ___________ + +arguments = ['-uS', 'script'], option = '-S' + + @pytest.mark.parametrize("arguments, option", [(["-I", "-c", "pass"], "-I"), (["-uS", "script"], "-S"), (["-E", "-m", "pytest"], "-E")]) + def test_python_startup_cannot_bypass_runtime(arguments, option) -> None: +> with pytest.raises(RuntimeError, match=f"project_python_option_unsupported:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE RuntimeError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:76: Failed +___________ test_python_startup_cannot_bypass_runtime[arguments2--E] ___________ + +arguments = ['-E', '-m', 'pytest'], option = '-E' + + @pytest.mark.parametrize("arguments, option", [(["-I", "-c", "pass"], "-I"), (["-uS", "script"], "-S"), (["-E", "-m", "pytest"], "-E")]) + def test_python_startup_cannot_bypass_runtime(arguments, option) -> None: +> with pytest.raises(RuntimeError, match=f"project_python_option_unsupported:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE RuntimeError + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:76: Failed +_______ test_normalized_simplify_request_preserves_project_runtime_paths _______ + + def test_normalized_simplify_request_preserves_project_runtime_paths() -> None: + request = run_commands.ReviewRunRequest(files=[], focus_facets=("simplify",), project_config=Path("project.toml"), project_runtime=Path("project-runtime.json")) + normalized = run_commands._normalize_review_request(request) +> assert normalized.project_config == request.project_config +E AssertionError: assert None == PosixPath('project.toml') +E + where None = ReviewRunRequest(files=[], include_tests=False, scope=None, path_filters=None, base_ref=None, head_ref=None, pr_contex...s_facets=('simplify',), review_focus='simplify', requirements_evidence=None, project_config=None, project_runtime=None).project_config +E + and PosixPath('project.toml') = ReviewRunRequest(files=[], include_tests=False, scope=None, path_filters=None, base_ref=None, head_ref=None, pr_contex...equirements_evidence=None, project_config=PosixPath('project.toml'), project_runtime=PosixPath('project-runtime.json')).project_config + +tests/unit/specfact_code_review/run/test_commands.py:1801: AssertionError +_________ test_source_identity_detects_new_untracked_and_ignored_files _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-129/test_source_identity_detects_n0') + + def test_source_identity_detects_new_untracked_and_ignored_files(tmp_path: Path) -> None: + module = _load() + (tmp_path / ".git").mkdir() + (tmp_path / "source.py").write_text("VALUE = 1\n") +> before = module.tracked_identity(tmp_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/test_external_capsule_corpus.py:104: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +scripts/external_capsule_corpus.py:114: in tracked_identity + subprocess.check_output(["git", "ls-files", "-z"], cwd=root).decode().split("\0") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:473: in check_output + return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +input = None, capture_output = False, timeout = None, check = True +popenargs = (['git', 'ls-files', '-z'],) +kwargs = {'stdout': -1, 'cwd': PosixPath('/pytest-of-dom/pytest-129/test_source_identity_detects_n0')} + + def run(*popenargs, + input=None, capture_output=False, timeout=None, check=False, **kwargs): + """Run command with arguments and return a CompletedProcess instance. + + The returned instance will have attributes args, returncode, stdout and + stderr. By default, stdout and stderr are not captured, and those attributes + will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them, + or pass capture_output=True to capture both. + + If check is True and the exit code was non-zero, it raises a + CalledProcessError. The CalledProcessError object will have the return code + in the returncode attribute, and output & stderr attributes if those streams + were captured. + + If timeout (seconds) is given and the process takes too long, + a TimeoutExpired exception will be raised. + + There is an optional argument "input", allowing you to + pass bytes or a string to the subprocess's stdin. If you use this argument + you may not also use the Popen constructor's "stdin" argument, as + it will be used internally. + + By default, all communication is in bytes, and therefore any "input" should + be bytes, and the stdout and stderr will be bytes. If in text mode, any + "input" should be a string, and stdout and stderr will be strings decoded + according to locale encoding, or by "encoding" if set. Text mode is + triggered by setting any of text, encoding, errors or universal_newlines. + + The other arguments are the same as for the Popen constructor. + """ + if input is not None: + if kwargs.get('stdin') is not None: + raise ValueError('stdin and input arguments may not both be used.') + kwargs['stdin'] = PIPE + + if capture_output: + if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None: + raise ValueError('stdout and stderr arguments may not be used ' + 'with capture_output.') + kwargs['stdout'] = PIPE + kwargs['stderr'] = PIPE + + with Popen(*popenargs, **kwargs) as process: + try: + stdout, stderr = process.communicate(input, timeout=timeout) + except TimeoutExpired as exc: + process.kill() + if _mswindows: + # Windows accumulates the output in a single blocking + # read() call run on child threads, with the timeout + # being done in a join() on those threads. communicate() + # _after_ kill() is required to collect that and add it + # to the exception. + exc.stdout, exc.stderr = process.communicate() + else: + # POSIX _communicate already populated the output so + # far into the TimeoutExpired exception. + process.wait() + raise + except: # Including KeyboardInterrupt, communicate handled that. + process.kill() + # We don't call process.wait() as .__exit__ does that for us. + raise + retcode = process.poll() + if check and retcode: +> raise CalledProcessError(retcode, process.args, + output=stdout, stderr=stderr) +E subprocess.CalledProcessError: Command '['git', 'ls-files', '-z']' returned non-zero exit status 128. + +/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/subprocess.py:578: CalledProcessError +----------------------------- Captured stderr call ----------------------------- +fatal: not a git repository (or any of the parent directories): .git +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_descriptor_rejects_malformed_inventory[inventory0] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_descriptor_rejects_malformed_inventory[inventory1] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_descriptor_rejects_malformed_inventory[inventory2] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_concurrent_cache_publication_accepts_existing_winner[17] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_concurrent_cache_publication_accepts_existing_winner[39] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_python_startup_cannot_bypass_runtime[arguments0--I] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_python_startup_cannot_bypass_runtime[arguments1--S] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_python_startup_cannot_bypass_runtime[arguments2--E] +FAILED tests/unit/specfact_code_review/run/test_commands.py::test_normalized_simplify_request_preserves_project_runtime_paths +FAILED tests/unit/test_external_capsule_corpus.py::test_source_identity_detects_new_untracked_and_ignored_files +======================== 10 failed, 100 passed in 0.74s ======================== +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 139 items + +tests/unit/specfact_code_review/run/test_portable_worker.py .......F [ 5%] +tests/unit/specfact_code_review/run/test_runtime_compatibility.py ... [ 7%] +tests/unit/specfact_code_review/run/test_scope.py ...................... [ 23%] +........................................................................ [ 75%] +.................................F [100%] + +=================================== FAILURES =================================== +_________ test_failed_pytest_startup_cannot_reuse_previous_observation _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-130/test_failed_pytest_startup_can0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1083b0510> + + def test_failed_pytest_startup_cannot_reuse_previous_observation(tmp_path, monkeypatch) -> None: + import json + from types import SimpleNamespace + from specfact_code_review.run import portable_worker, target_launch + + observation = tmp_path / "pytest-observation.json" + observation.write_text(json.dumps({"exit_code": 0, "collected": ["a"], "records": [{"nodeid": "a", "phase": "call", "outcome": "passed"}], "coverage": {"files": {"app.py": {}}}})) + monkeypatch.setattr(portable_worker, "Path", lambda value: observation if value == "/opt/specfact/tmp/pytest-observation.json" else Path(value)) + monkeypatch.setattr(target_launch, "target_command", lambda *args: ["child"]) + monkeypatch.setattr(portable_worker.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0)) +> assert portable_worker.run_portable_pytest([tmp_path / "app.py"], ("portable-pytest-v2", "{}")) +E AssertionError: assert [] +E + where [] = ([PosixPath('/pytest-of-dom/pytest-130/test_failed_pytest_startup_can0/app.py')], ('portable-pytest-v2', '{}')) +E + where = /packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py'>.run_portable_pytest + +tests/unit/specfact_code_review/run/test_portable_worker.py:89: AssertionError +___________ test_portable_range_records_candidate_referenced_policy ____________ + +scope_api = /packages/specfact-code-review/src/specfact_code_review/run/scope.py'> +git_repo = PosixPath('/pytest-of-dom/pytest-130/test_portable_range_records_ca0/repo') + + def test_portable_range_records_candidate_referenced_policy(scope_api: Any, git_repo: Path) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo / "config").mkdir() + (git_repo / "ruff.toml").write_text('extend="config/candidate.toml"\n') + (git_repo / "config/candidate.toml").write_text('line-length=99\n') + head = _commit(git_repo, "candidate policy closure") + result = scope_api.resolve_scope(_range_request(scope_api, git_repo, base, head, portable_project_runtime=True)) + try: + assert result.status == "PASS", result.diagnostics +> assert "config/candidate.toml" in result.selected_paths +E AssertionError: assert 'config/candidate.toml' in ('ruff.toml',) +E + where ('ruff.toml',) = ScopeResolution(status='PASS', reason='resolved', selected_paths=('ruff.toml',), assurance_kind='range_preview', effec...01471d9c4d4', candidate_policy_change_digest='sha256:1feb555f4d55fceafc9d7679835ecf326b96cfafe6bc64a2db0b58e68195c644').selected_paths + +tests/unit/specfact_code_review/run/test_scope.py:1375: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_failed_pytest_startup_cannot_reuse_previous_observation +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_portable_range_records_candidate_referenced_policy +======================== 2 failed, 137 passed in 9.18s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND3_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND3_RED.txt new file mode 100644 index 00000000..1b64892a --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND3_RED.txt @@ -0,0 +1,192 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 53 items + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py .......FF [ 16%] +tests/unit/specfact_code_review/run/test_runtime_native.py ....F [ 26%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 45%] +.................F [ 79%] +tests/unit/specfact_code_review/run/test_portable_worker.py ........FF [ 98%] +tests/unit/specfact_code_review/run/test_runner.py E [100%] + +==================================== ERRORS ==================================== +_ ERROR at setup of test_cached_portable_request_carries_repository_and_index_context _ +file /tests/unit/specfact_code_review/run/test_runner.py, line 7735 + def test_cached_portable_request_carries_repository_and_index_context(runner_api, tmp_path, monkeypatch) -> None: +E fixture 'runner_api' not found +> available fixtures: _asyncio_loop_factory, _class_scoped_runner, _function_scoped_runner, _module_scoped_runner, _package_scoped_runner, _session_scoped_runner, anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, class_mocker, cov, doctest_namespace, event_loop_policy, free_tcp_port, free_tcp_port_factory, free_udp_port, free_udp_port_factory, host_analyzer_cli_contract, isolate_legacy_runtime_boundary, mocker, module_mocker, monkeypatch, no_cover, package_mocker, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, session_mocker, subtests, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, unused_tcp_port, unused_tcp_port_factory, unused_udp_port, unused_udp_port_factory +> use 'pytest --fixtures [testpath]' for help on them. + +/tests/unit/specfact_code_review/run/test_runner.py:7735 +=================================== FAILURES =================================== +___________ test_descriptor_rejects_malformed_inventory[inventory3] ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-154/test_descriptor_rejects_malfor3') +inventory = {'pytest_arguments': 7} + + @pytest.mark.parametrize("inventory", [[], {"analyzer_conflicts": []}, {"member_graphs": []}, {"pytest_arguments": 7}, {"pytest_arguments": [False]}]) + def test_descriptor_rejects_malformed_inventory(tmp_path: Path, inventory) -> None: + + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") + data = prepared.descriptor + data["inventory"] = inventory + data["identity"] = document_digest({key: value for key, value in data.items() if key != "identity"}) + prepared.descriptor_path.write_text(json.dumps(data), encoding="utf-8") +> with pytest.raises(ProjectRuntimeError, match="inventory_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:128: Failed +___________ test_descriptor_rejects_malformed_inventory[inventory4] ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-154/test_descriptor_rejects_malfor4') +inventory = {'pytest_arguments': [False]} + + @pytest.mark.parametrize("inventory", [[], {"analyzer_conflicts": []}, {"member_graphs": []}, {"pytest_arguments": 7}, {"pytest_arguments": [False]}]) + def test_descriptor_rejects_malformed_inventory(tmp_path: Path, inventory) -> None: + + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") + data = prepared.descriptor + data["inventory"] = inventory + data["identity"] = document_digest({key: value for key, value in data.items() if key != "identity"}) + prepared.descriptor_path.write_text(json.dumps(data), encoding="utf-8") +> with pytest.raises(ProjectRuntimeError, match="inventory_invalid"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:128: Failed +_____________ test_wrong_machine_elf_is_rejected_before_inventory ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-154/test_wrong_machine_elf_is_reje0') + + def test_wrong_machine_elf_is_rejected_before_inventory(tmp_path: Path) -> None: + import struct + + payload = bytearray(64) + payload[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" with pytest.raises(ProjectRuntimeError, match="architecture_unsupported"): + ^^^^^^^^^^^^^^^^^^^ +E NameError: name 'ProjectRuntimeError' is not defined + +tests/unit/specfact_code_review/run/test_runtime_native.py:89: NameError +_______ test_source_identity_distinguishes_executable_permission_classes _______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-154/test_source_identity_distingui0') + + def test_source_identity_distinguishes_executable_permission_classes(tmp_path: Path) -> None: + script = tmp_path / "manage" + script.write_text("#!/usr/bin/env python\n") + script.chmod(0o754) + before = discover_project(tmp_path).identity + script.chmod(0o745) +> assert discover_project(tmp_path).identity != before +E AssertionError: assert 'sha256:831e2affb5e4c731e04bf14e1b619362b52bf2e58a4c5a944f1e2229fa339fba' != 'sha256:831e2affb5e4c731e04bf14e1b619362b52bf2e58a4c5a944f1e2229fa339fba' +E + where 'sha256:831e2affb5e4c731e04bf14e1b619362b52bf2e58a4c5a944f1e2229fa339fba' = ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-154/test_sou...=(), extras=(), requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={}).identity +E + where ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-154/test_sou...=(), extras=(), requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={}) = discover_project(PosixPath('/pytest-of-dom/pytest-154/test_source_identity_distingui0')) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:232: AssertionError +_ test_real_failure_does_not_hide_partial_pytest_execution[collection_errors] __ + +field = 'collection_errors' + + @pytest.mark.parametrize("field", ["collection_errors", "internal_errors"]) + def test_real_failure_does_not_hide_partial_pytest_execution(field) -> None: + observation = {"exit_code": 1, "collected": ["a"], "records": [{"nodeid": "a", "phase": "call", "outcome": "failed"}], "coverage": {"files": {"app.py": {}}}, field: ["controlled startup or collection error"]} +> with pytest.raises(ProjectRuntimeError, match="project_pytest_.*error"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_portable_worker.py:110: Failed +__ test_real_failure_does_not_hide_partial_pytest_execution[internal_errors] ___ + +field = 'internal_errors' + + @pytest.mark.parametrize("field", ["collection_errors", "internal_errors"]) + def test_real_failure_does_not_hide_partial_pytest_execution(field) -> None: + observation = {"exit_code": 1, "collected": ["a"], "records": [{"nodeid": "a", "phase": "call", "outcome": "failed"}], "coverage": {"files": {"app.py": {}}}, field: ["controlled startup or collection error"]} +> with pytest.raises(ProjectRuntimeError, match="project_pytest_.*error"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_portable_worker.py:110: Failed +=========================== short test summary info ============================ +ERROR tests/unit/specfact_code_review/run/test_runner.py::test_cached_portable_request_carries_repository_and_index_context +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_descriptor_rejects_malformed_inventory[inventory3] +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_descriptor_rejects_malformed_inventory[inventory4] +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_wrong_machine_elf_is_rejected_before_inventory +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_source_identity_distinguishes_executable_permission_classes +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_real_failure_does_not_hide_partial_pytest_execution[collection_errors] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_real_failure_does_not_hide_partial_pytest_execution[internal_errors] +==================== 6 failed, 46 passed, 1 error in 0.48s ===================== +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 2 items + +tests/unit/specfact_code_review/run/test_runtime_native.py F [ 50%] +tests/unit/specfact_code_review/run/test_runner.py F [100%] + +=================================== FAILURES =================================== +_____________ test_wrong_machine_elf_is_rejected_before_inventory ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-155/test_wrong_machine_elf_is_reje0') + + def test_wrong_machine_elf_is_rejected_before_inventory(tmp_path: Path) -> None: + payload = bytearray(64) + payload[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" with pytest.raises(ProjectRuntimeError, match="architecture_unsupported"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_native.py:88: Failed +______ test_cached_portable_request_carries_repository_and_index_context _______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-155/test_cached_portable_request_c0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a021e00> + + def test_cached_portable_request_carries_repository_and_index_context(tmp_path, monkeypatch) -> None: + runner_api = _c14_runner() + from specfact_code_review.run import portable_snapshot + + source = tmp_path / "index" + source.mkdir() + cached = runner_api.CachedAnalysisSnapshot(tmp_path, source, [], {}, (), runner_api.CachedDiffIdentity("a" * 40, "b" * 40, "")) + requests = [] + monkeypatch.setattr(runner_api, "_cached_review_requested", lambda *_: True) + monkeypatch.setattr(runner_api, "_cached_analysis_snapshot", lambda *_: cached) + monkeypatch.setattr(portable_snapshot, "project_runtime_requested", lambda *_: True) + monkeypatch.setattr(portable_snapshot, "run_project_snapshot", lambda _runtime, request: (requests.append(request), {})) + monkeypatch.setattr(runner_api, "_finalize_local_capsule_snapshot", lambda *_: object()) + runner_api._run_local_capsule_context(object(), [], runner_api.ReviewOptions(), {}, "worktree") +> assert requests[0].source_snapshot is cached +E AssertionError: assert None is CachedAnalysisSnapshot(repository=PosixPath('/pytest-of-dom/py...e='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', index_tree='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', caller_prefix='')) +E + where None = ProjectSnapshotRequest(snapshot_root=PosixPath('/pytest-of-dom...w_mode='full', focus=None, project_config=None, project_runtime=None), assurance_kind='worktree', source_snapshot=None).source_snapshot + +tests/unit/specfact_code_review/run/test_runner.py:7749: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_wrong_machine_elf_is_rejected_before_inventory +FAILED tests/unit/specfact_code_review/run/test_runner.py::test_cached_portable_request_carries_repository_and_index_context +============================== 2 failed in 0.40s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND4_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND4_RED.txt new file mode 100644 index 00000000..68b1637c --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND4_RED.txt @@ -0,0 +1,256 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 53 items + +tests/unit/specfact_code_review/run/test_target_pytest.py .....F [ 11%] +tests/unit/specfact_code_review/run/test_runtime_sources.py .F [ 15%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 33%] +..................FFFF [ 75%] +tests/unit/test_external_capsule_corpus.py ...........FF [100%] + +=================================== FAILURES =================================== +_____ test_disabled_autoload_preserves_only_explicit_plugins_and_coverage ______ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1080335c0> + + def test_disabled_autoload_preserves_only_explicit_plugins_and_coverage(monkeypatch) -> None: + packages = [ + SimpleNamespace(metadata={"Name": name}, entry_points=[SimpleNamespace(group="pytest11", name=entry, value=value)]) + for name, entry, value in [ + ("pytest-asyncio", "asyncio", "pytest_asyncio.plugin"), + ("pytest-cov", "pytest_cov", "pytest_cov.plugin"), + ] + ] + monkeypatch.setattr(target_pytest.importlib.metadata, "distributions", lambda **_kwargs: packages) + for options in ["--disable-plugin-autoload", ["--disable-plugin-autoload"]]: +> assert target_pytest._plugins({"addopts": options}) == ["-p", "pytest_cov"] +E AssertionError: assert ['-p', 'async... 'pytest_cov'] == ['-p', 'pytest_cov'] +E +E At index 1 diff: 'asyncio' != 'pytest_cov' +E Left contains 2 more items, first extra item: '-p' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pytest.py:67: AssertionError +____________ test_directory_presence_and_modes_bind_source_identity ____________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_directory_presence_and_mo0') + + def test_directory_presence_and_modes_bind_source_identity(tmp_path: Path) -> None: + before = source_identity(tmp_path) + empty = tmp_path / "optional-source" + empty.mkdir() + added = source_identity(tmp_path) +> assert added != before +E AssertionError: assert 'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a' != 'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a' + +tests/unit/specfact_code_review/run/test_runtime_sources.py:23: AssertionError +_______ test_pylock_and_uncompiled_requirements_need_explicit_selection ________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_pylock_and_uncompiled_req0') + + def test_pylock_and_uncompiled_requirements_need_explicit_selection(tmp_path: Path) -> None: + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\n') + (tmp_path / "requirements.in").write_text('requests\n') +> with pytest.raises(ProjectRuntimeError, match=r"ambiguous.*pylock.toml.*requirements.in"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:239: Failed +____________ test_malformed_ini_is_a_project_diagnostic[setup.cfg] _____________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec0') +filename = 'setup.cfg' + + @pytest.mark.parametrize("filename", ["setup.cfg", "pytest.ini", "tox.ini"]) + def test_malformed_ini_is_a_project_diagnostic(tmp_path: Path, filename: str) -> None: + (tmp_path / filename).write_text('not an INI section\n') + with pytest.raises(ProjectRuntimeError, match=r"project_config_invalid:" + filename): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:250: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:306: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:206: in _pytest_config + parser.read(path, encoding="utf-8") +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:761: in read + self._read(fp, filename) +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1079: in _read + ParsingError._raise_all(self._read_inner(fp, fpname)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1107: in _read_inner + self._handle_rest(st, line, fpname) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = +st = +line = 'not an INI section\n' +fpname = PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec0/setup.cfg') + + def _handle_rest(self, st, line, fpname): + # a section header or option header? + if self._allow_unnamed_section and st.cursect is None: + self._handle_header(st, UNNAMED_SECTION, fpname) + + st.indent_level = st.cur_indent_level + # is it a section header? + mo = self.SECTCRE.match(line.clean) + + if not mo and st.cursect is None: +> raise MissingSectionHeaderError(fpname, st.lineno, line) +E configparser.MissingSectionHeaderError: File contains no section headers. +E file: PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec0/setup.cfg'), line: 1 +E 'not an INI section\n' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1131: MissingSectionHeaderError +____________ test_malformed_ini_is_a_project_diagnostic[pytest.ini] ____________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec1') +filename = 'pytest.ini' + + @pytest.mark.parametrize("filename", ["setup.cfg", "pytest.ini", "tox.ini"]) + def test_malformed_ini_is_a_project_diagnostic(tmp_path: Path, filename: str) -> None: + (tmp_path / filename).write_text('not an INI section\n') + with pytest.raises(ProjectRuntimeError, match=r"project_config_invalid:" + filename): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:250: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:306: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:206: in _pytest_config + parser.read(path, encoding="utf-8") +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:761: in read + self._read(fp, filename) +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1079: in _read + ParsingError._raise_all(self._read_inner(fp, fpname)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1107: in _read_inner + self._handle_rest(st, line, fpname) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = +st = +line = 'not an INI section\n' +fpname = PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec1/pytest.ini') + + def _handle_rest(self, st, line, fpname): + # a section header or option header? + if self._allow_unnamed_section and st.cursect is None: + self._handle_header(st, UNNAMED_SECTION, fpname) + + st.indent_level = st.cur_indent_level + # is it a section header? + mo = self.SECTCRE.match(line.clean) + + if not mo and st.cursect is None: +> raise MissingSectionHeaderError(fpname, st.lineno, line) +E configparser.MissingSectionHeaderError: File contains no section headers. +E file: PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec1/pytest.ini'), line: 1 +E 'not an INI section\n' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1131: MissingSectionHeaderError +_____________ test_malformed_ini_is_a_project_diagnostic[tox.ini] ______________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec2') +filename = 'tox.ini' + + @pytest.mark.parametrize("filename", ["setup.cfg", "pytest.ini", "tox.ini"]) + def test_malformed_ini_is_a_project_diagnostic(tmp_path: Path, filename: str) -> None: + (tmp_path / filename).write_text('not an INI section\n') + with pytest.raises(ProjectRuntimeError, match=r"project_config_invalid:" + filename): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:250: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:306: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:206: in _pytest_config + parser.read(path, encoding="utf-8") +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:761: in read + self._read(fp, filename) +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1079: in _read + ParsingError._raise_all(self._read_inner(fp, fpname)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1107: in _read_inner + self._handle_rest(st, line, fpname) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = +st = +line = 'not an INI section\n' +fpname = PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec2/tox.ini') + + def _handle_rest(self, st, line, fpname): + # a section header or option header? + if self._allow_unnamed_section and st.cursect is None: + self._handle_header(st, UNNAMED_SECTION, fpname) + + st.indent_level = st.cur_indent_level + # is it a section header? + mo = self.SECTCRE.match(line.clean) + + if not mo and st.cursect is None: +> raise MissingSectionHeaderError(fpname, st.lineno, line) +E configparser.MissingSectionHeaderError: File contains no section headers. +E file: PosixPath('/pytest-of-user/pytest-171/test_malformed_ini_is_a_projec2/tox.ini'), line: 1 +E 'not an INI section\n' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/configparser.py:1131: MissingSectionHeaderError +____________ test_offline_corpus_rejects_customer_writable_launcher ____________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_offline_corpus_rejects_cu0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108837d40> + + def test_offline_corpus_rejects_customer_writable_launcher(tmp_path: Path, monkeypatch) -> None: + module = _load() + launcher = tmp_path / "bwrap" + launcher.write_text('#!/bin/sh\necho SPECFACT_OFFLINE_NETWORK_VERIFIED >&2\nexec "$@"\n') + launcher.chmod(0o755) + monkeypatch.setenv("SPECFACT_CORPUS_OFFLINE_LAUNCHER", str(launcher)) + monkeypatch.setattr(module.os, "readlink", lambda _path: "net:[fixture]") +> with pytest.raises(ValueError, match="offline_launcher"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:177: Failed +________________ test_corpus_identity_detects_empty_directories ________________ + +tmp_path = PosixPath('/pytest-of-user/pytest-171/test_corpus_identity_detects_e0') + + def test_corpus_identity_detects_empty_directories(tmp_path: Path) -> None: + module = _load() + before = module.tracked_identity(tmp_path) + (tmp_path / "new-empty-directory").mkdir() +> assert module.tracked_identity(tmp_path) != before +E AssertionError: assert {} != {} +E + where {} = (PosixPath('/pytest-of-user/pytest-171/test_corpus_identity_detects_e0')) +E + where = /scripts/external_capsule_corpus.py'>.tracked_identity + +tests/unit/test_external_capsule_corpus.py:185: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_disabled_autoload_preserves_only_explicit_plugins_and_coverage +FAILED tests/unit/specfact_code_review/run/test_runtime_sources.py::test_directory_presence_and_modes_bind_source_identity +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_pylock_and_uncompiled_requirements_need_explicit_selection +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_ini_is_a_project_diagnostic[setup.cfg] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_ini_is_a_project_diagnostic[pytest.ini] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_ini_is_a_project_diagnostic[tox.ini] +FAILED tests/unit/test_external_capsule_corpus.py::test_offline_corpus_rejects_customer_writable_launcher +FAILED tests/unit/test_external_capsule_corpus.py::test_corpus_identity_detects_empty_directories +========================= 8 failed, 45 passed in 0.76s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND5_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND5_RED.txt new file mode 100644 index 00000000..5b4858a8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND5_RED.txt @@ -0,0 +1,46 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 22 items + +tests/unit/specfact_code_review/run/test_runtime_domains.py ...F. [ 22%] +tests/unit/specfact_code_review/run/test_runtime_builder.py ............ [ 77%] +....F [100%] + +=================================== FAILURES =================================== +_____ test_pytest_worker_validates_selected_dependency_versions[7.0-True] ______ + +tmp_path = PosixPath('/pytest-of-user/pytest-179/test_pytest_worker_validates_s0') +version = '7.0', conflict = True + + @pytest.mark.parametrize("version,conflict", [("7.0", True), ("8.4", False)]) + def test_pytest_worker_validates_selected_dependency_versions(tmp_path: Path, version: str, conflict: bool) -> None: + _distribution(tmp_path, "pylint") + metadata = tmp_path / "pytest-cov-1.0.dist-info/METADATA" + metadata.write_text(metadata.read_text() + "Requires-Dist: pytest>=8.2\n") + inventory = {"installed": [{"metadata": {"name": "pytest", "version": version}}]} + if conflict: +> with pytest.raises(ProjectRuntimeError, match="project_analyzer_dependency_incompatible:pytest-observe:pytest"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_domains.py:60: Failed +_________________ test_builder_sets_private_git_transport_path _________________ + +tmp_path = PosixPath('/pytest-of-user/pytest-179/test_builder_sets_private_git_0') + + def test_builder_sets_private_git_transport_path(tmp_path: Path) -> None: + runtime = SimpleNamespace(root=tmp_path / "capsule", interpreter="/opt/specfact/python/bin/python") + command = builder_command(runtime, staging=tmp_path / "staging", executable="/proc/self/fd/12") +> index = command.index("GIT_EXEC_PATH") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: list.index(x): x not in list + +tests/unit/specfact_code_review/run/test_runtime_builder.py:135: ValueError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_domains.py::test_pytest_worker_validates_selected_dependency_versions[7.0-True] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_builder_sets_private_git_transport_path +========================= 2 failed, 20 passed in 0.84s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND6_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND6_RED.txt new file mode 100644 index 00000000..7dbef896 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PR_REVIEW_474_ROUND6_RED.txt @@ -0,0 +1,296 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 23 items + +tests/unit/specfact_code_review/run/test_runtime_vcs.py ....F [ 21%] +tests/unit/specfact_code_review/run/test_portable_worker.py ..........FF [ 73%] +F [ 78%] +tests/unit/specfact_code_review/run/test_runtime_git.py ..FFF [100%] + +=================================== FAILURES =================================== +______________ test_analyzer_copy_excludes_reachable_git_history _______________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_analyzer_copy_excludes_re0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ce6b230> + + def test_analyzer_copy_excludes_reachable_git_history(tmp_path: Path, monkeypatch) -> None: + root = tmp_path / "project" + _repository(root) + observed = [] + + def analyze(_runtime, *, snapshot_root, **_kwargs): + assert not (snapshot_root / ".git").exists() + assert (snapshot_root / "app.py").read_text() == "VALUE = 1\n" + observed.append(snapshot_root) + + monkeypatch.setattr(runner, "_run_capsule_snapshot", analyze) + request = portable_snapshot.ProjectSnapshotRequest(root, [root / "app.py"], runner.ReviewOptions(), "explicit_files") +> portable_snapshot._run_in_private_source(object(), request, runner.CapsuleSnapshotSettings()) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:99: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:119: in _run_in_private_source + return _run_capsule_snapshot( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_runtime = +snapshot_root = PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-project-source-1ghax8nk/source') +_kwargs = {'files': [PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-project-source-1ghax8nk/source/app.py'...s=(), member_argv=None, project_runtime_root=None, scope_paths=None, portable_runtime=False, unavailable_members=None)} + + def analyze(_runtime, *, snapshot_root, **_kwargs): +> assert not (snapshot_root / ".git").exists() +E AssertionError: assert not True +E + where True = exists() +E + where exists = (PosixPath('/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/specfact-project-source-1ghax8nk/source') / '.git').exists + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:93: AssertionError +_________ test_pytest_native_default_root_is_preserved[configuration0] _________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_pytest_native_default_roo0') +configuration = {} + + @pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) + def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) +> assert select_test_paths(plan, [source], full=True) == (".",) +E AssertionError: assert ('tests',) == ('.',) +E +E At index 0 diff: 'tests' != '.' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_worker.py:126: AssertionError +_________ test_pytest_native_default_root_is_preserved[configuration1] _________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_pytest_native_default_roo1') +configuration = {'testpaths': []} + + @pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) + def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) + assert select_test_paths(plan, [source], full=True) == (".",) +> assert select_test_paths(plan, [source], full=False) == ("test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:127: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/pytest-of-user/pytest-192/test_pyt... requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={'testpaths': []}) +files = [PosixPath('/pytest-of-user/pytest-192/test_pytest_native_default_roo1/app.py')] +full = False + + @ensure(lambda result: bool(result) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + roots = _strings(plan.pytest_config.get("testpaths", ["tests"])) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + if full: + return roots or (".",) + candidates = { + path.relative_to(plan.root).as_posix() + for root in roots + for path in (plan.root / root).rglob("*.py") + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns) + } + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:59: ProjectRuntimeError +_________ test_pytest_native_default_root_is_preserved[configuration2] _________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_pytest_native_default_roo2') +configuration = {'testpaths': ''} + + @pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) + def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) + assert select_test_paths(plan, [source], full=True) == (".",) +> assert select_test_paths(plan, [source], full=False) == ("test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:127: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/pytest-of-user/pytest-192/test_pyt... requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={'testpaths': ''}) +files = [PosixPath('/pytest-of-user/pytest-192/test_pytest_native_default_roo2/app.py')] +full = False + + @ensure(lambda result: bool(result) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + roots = _strings(plan.pytest_config.get("testpaths", ["tests"])) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + if full: + return roots or (".",) + candidates = { + path.relative_to(plan.root).as_posix() + for root in roots + for path in (plan.root / root).rglob("*.py") + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns) + } + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:59: ProjectRuntimeError +_________ test_git_identity_failures_use_runtime_diagnostic[failure0] __________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_git_identity_failures_use0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10cfc5b50> +failure = CalledProcessError(1, 'git') + + @pytest.mark.parametrize("failure", [subprocess.CalledProcessError(1, "git"), subprocess.TimeoutExpired("git", 10), OSError("unavailable")]) + def test_git_identity_failures_use_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + git = tmp_path / "git" + git.write_bytes(b"\x7fELFgit") + monkeypatch.setattr(runtime_git, "GIT", git) + + def fail(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_git.subprocess, "run", fail) + with pytest.raises(ProjectRuntimeError, match="project_builder_git_identity_failed") as caught: +> runtime_git.git_identity() + +tests/unit/specfact_code_review/run/test_runtime_git.py:55: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:43: in git_identity + return {name: content_digest(path.read_bytes()) for name, path in _programs().items()} + ^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:22: in _programs + result = subprocess.run( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (['/pytest-of-user/pytest-192/test_git_identity_failures_use0/git', '--exec-path'],) +_kwargs = {'env': {'PATH': '/usr/bin:/bin'}, 'capture_output': True, 'text': True, 'check': True, ...} + + def fail(*_args, **_kwargs): +> raise failure +E subprocess.CalledProcessError: Command 'git' returned non-zero exit status 1. + +tests/unit/specfact_code_review/run/test_runtime_git.py:51: CalledProcessError +_________ test_git_identity_failures_use_runtime_diagnostic[failure1] __________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_git_identity_failures_use1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10cfc6950> +failure = TimeoutExpired('git', 10) + + @pytest.mark.parametrize("failure", [subprocess.CalledProcessError(1, "git"), subprocess.TimeoutExpired("git", 10), OSError("unavailable")]) + def test_git_identity_failures_use_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + git = tmp_path / "git" + git.write_bytes(b"\x7fELFgit") + monkeypatch.setattr(runtime_git, "GIT", git) + + def fail(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_git.subprocess, "run", fail) + with pytest.raises(ProjectRuntimeError, match="project_builder_git_identity_failed") as caught: +> runtime_git.git_identity() + +tests/unit/specfact_code_review/run/test_runtime_git.py:55: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:43: in git_identity + return {name: content_digest(path.read_bytes()) for name, path in _programs().items()} + ^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:22: in _programs + result = subprocess.run( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (['/pytest-of-user/pytest-192/test_git_identity_failures_use1/git', '--exec-path'],) +_kwargs = {'env': {'PATH': '/usr/bin:/bin'}, 'capture_output': True, 'text': True, 'check': True, ...} + + def fail(*_args, **_kwargs): +> raise failure +E subprocess.TimeoutExpired: Command 'git' timed out after 10 seconds + +tests/unit/specfact_code_review/run/test_runtime_git.py:51: TimeoutExpired +_________ test_git_identity_failures_use_runtime_diagnostic[failure2] __________ + +tmp_path = PosixPath('/pytest-of-user/pytest-192/test_git_identity_failures_use2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d0d45f0> +failure = OSError('unavailable') + + @pytest.mark.parametrize("failure", [subprocess.CalledProcessError(1, "git"), subprocess.TimeoutExpired("git", 10), OSError("unavailable")]) + def test_git_identity_failures_use_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + git = tmp_path / "git" + git.write_bytes(b"\x7fELFgit") + monkeypatch.setattr(runtime_git, "GIT", git) + + def fail(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_git.subprocess, "run", fail) + with pytest.raises(ProjectRuntimeError, match="project_builder_git_identity_failed") as caught: +> runtime_git.git_identity() + +tests/unit/specfact_code_review/run/test_runtime_git.py:55: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:43: in git_identity + return {name: content_digest(path.read_bytes()) for name, path in _programs().items()} + ^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py:22: in _programs + result = subprocess.run( +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +_args = (['/pytest-of-user/pytest-192/test_git_identity_failures_use2/git', '--exec-path'],) +_kwargs = {'env': {'PATH': '/usr/bin:/bin'}, 'capture_output': True, 'text': True, 'check': True, ...} + + def fail(*_args, **_kwargs): +> raise failure +E OSError: unavailable + +tests/unit/specfact_code_review/run/test_runtime_git.py:51: OSError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_analyzer_copy_excludes_reachable_git_history +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_native_default_root_is_preserved[configuration0] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_native_default_root_is_preserved[configuration1] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_native_default_root_is_preserved[configuration2] +FAILED tests/unit/specfact_code_review/run/test_runtime_git.py::test_git_identity_failures_use_runtime_diagnostic[failure0] +FAILED tests/unit/specfact_code_review/run/test_runtime_git.py::test_git_identity_failures_use_runtime_diagnostic[failure1] +FAILED tests/unit/specfact_code_review/run/test_runtime_git.py::test_git_identity_failures_use_runtime_diagnostic[failure2] +========================= 7 failed, 16 passed in 0.65s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PTH_IMPORT_STATE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PTH_IMPORT_STATE_RED.txt new file mode 100644 index 00000000..fe0c4a4e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PTH_IMPORT_STATE_RED.txt @@ -0,0 +1,207 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 27 items + +tests/unit/specfact_code_review/run/test_target_bootstrap.py .........FF [ 40%] +FFF.F.F......... [100%] + +=================================== FAILURES =================================== +_ test_executable_pth_cannot_change_protected_import_state[sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]] _ + +collision_runtime = {'PROJECT': PosixPath('/test_executab.../test_executable_pth_cannot_cha0/analyzers')} +operation = "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]" + + @pytest.mark.parametrize( + "operation", + [ + "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None", + "importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None", + "builtins.__import__ = lambda *args, **kwargs: None", + ], + ) + def test_executable_pth_cannot_change_protected_import_state( + collision_runtime: dict[str, Path], operation: str + ) -> None: + pth = collision_runtime["PROJECT"] / "site-packages/changed-imports.pth" + pth.write_text(f"import sys, importlib.machinery, builtins; {operation}\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") +> assert completed.returncode != 0 +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,.../test_executable_pth_cannot_cha0/snapshot/packaging.py", "project", false]\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:149: AssertionError +_ test_executable_pth_cannot_change_protected_import_state[next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()] _ + +collision_runtime = {'PROJECT': PosixPath('/test_executab.../test_executable_pth_cannot_cha1/analyzers')} +operation = "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()" + + @pytest.mark.parametrize( + "operation", + [ + "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None", + "importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None", + "builtins.__import__ = lambda *args, **kwargs: None", + ], + ) + def test_executable_pth_cannot_change_protected_import_state( + collision_runtime: dict[str, Path], operation: str + ) -> None: + pth = collision_runtime["PROJECT"] / "site-packages/changed-imports.pth" + pth.write_text(f"import sys, importlib.machinery, builtins; {operation}\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") +> assert completed.returncode != 0 +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,.../test_executable_pth_cannot_cha1/snapshot/packaging.py", "project", false]\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:149: AssertionError +_ test_executable_pth_cannot_change_protected_import_state[next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None] _ + +collision_runtime = {'PROJECT': PosixPath('/test_executab.../test_executable_pth_cannot_cha2/analyzers')} +operation = "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None" + + @pytest.mark.parametrize( + "operation", + [ + "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None", + "importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None", + "builtins.__import__ = lambda *args, **kwargs: None", + ], + ) + def test_executable_pth_cannot_change_protected_import_state( + collision_runtime: dict[str, Path], operation: str + ) -> None: + pth = collision_runtime["PROJECT"] / "site-packages/changed-imports.pth" + pth.write_text(f"import sys, importlib.machinery, builtins; {operation}\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") +> assert completed.returncode != 0 +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,.../test_executable_pth_cannot_cha2/snapshot/packaging.py", "project", false]\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:149: AssertionError +_ test_executable_pth_cannot_change_protected_import_state[importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None] _ + +collision_runtime = {'PROJECT': PosixPath('/test_executab.../test_executable_pth_cannot_cha3/analyzers')} +operation = 'importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None' + + @pytest.mark.parametrize( + "operation", + [ + "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None", + "importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None", + "builtins.__import__ = lambda *args, **kwargs: None", + ], + ) + def test_executable_pth_cannot_change_protected_import_state( + collision_runtime: dict[str, Path], operation: str + ) -> None: + pth = collision_runtime["PROJECT"] / "site-packages/changed-imports.pth" + pth.write_text(f"import sys, importlib.machinery, builtins; {operation}\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode != 0 +> assert "project_worker_startup_import_state_changed" in completed.stderr +E assert 'project_worker_startup_import_state_changed' in 'Traceback (most recent call last):\n File "", line 17, in \n File "/repository...project_worker_analyzer_origin_mismatch:{fullname}")\nImportError: project_worker_analyzer_origin_mismatch:packaging\n' +E + where 'Traceback (most recent call last):\n File "", line 17, in \n File "/repository...project_worker_analyzer_origin_mismatch:{fullname}")\nImportError: project_worker_analyzer_origin_mismatch:packaging\n' = CompletedProcess(args=['/worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,...roject_worker_analyzer_origin_mismatch:{fullname}")\nImportError: project_worker_analyzer_origin_mismatch:packaging\n').stderr + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:150: AssertionError +_ test_executable_pth_cannot_change_protected_import_state[builtins.__import__ = lambda *args, **kwargs: None] _ + +collision_runtime = {'PROJECT': PosixPath('/test_executab.../test_executable_pth_cannot_cha4/analyzers')} +operation = 'builtins.__import__ = lambda *args, **kwargs: None' + + @pytest.mark.parametrize( + "operation", + [ + "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None", + "importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None", + "builtins.__import__ = lambda *args, **kwargs: None", + ], + ) + def test_executable_pth_cannot_change_protected_import_state( + collision_runtime: dict[str, Path], operation: str + ) -> None: + pth = collision_runtime["PROJECT"] / "site-packages/changed-imports.pth" + pth.write_text(f"import sys, importlib.machinery, builtins; {operation}\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode != 0 +> assert "project_worker_startup_import_state_changed" in completed.stderr +E assert 'project_worker_startup_import_state_changed' in 'Traceback (most recent call last):\n File "", line 18, in \nAttributeError: \'NoneType\' object has no attribute \'OWNER\'\n' +E + where 'Traceback (most recent call last):\n File "", line 18, in \nAttributeError: \'NoneType\' object has no attribute \'OWNER\'\n' = CompletedProcess(args=['/worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,...all last):\n File "", line 18, in \nAttributeError: \'NoneType\' object has no attribute \'OWNER\'\n').stderr + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:150: AssertionError +_ test_executable_editable_finder_remains_available_behind_sealed_imports[prepend] _ + +collision_runtime = {'PROJECT': PosixPath('/test_executab.../test_executable_editable_finde1/analyzers')} +placement = 'prepend' + + @pytest.mark.parametrize("placement", ["append", "prepend"]) + def test_executable_editable_finder_remains_available_behind_sealed_imports( + collision_runtime: dict[str, Path], placement: str + ) -> None: + workspace = _workspace_dependency(collision_runtime) + installed = collision_runtime["PROJECT"] / "site-packages" + hook = f""" + import importlib.machinery, sys + class EditableFinder: + def find_spec(self, fullname, path=None, target=None): + roots = {{'project_owned': {str(workspace)!r}, 'packaging': {str(collision_runtime['SNAPSHOT'])!r}}} + if fullname in roots: + return importlib.machinery.PathFinder.find_spec(fullname, [roots[fullname]]) + def install(): + if {placement!r} == 'prepend': + sys.meta_path.insert(0, EditableFinder()) + else: + sys.meta_path.append(EditableFinder()) + """ + (installed / "editable_fixture.py").write_text(hook) + (installed / "editable.pth").write_text("import editable_fixture; editable_fixture.install()\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode == 0, completed.stderr + owner, origin, project_owner, _ = json.loads(completed.stdout) +> assert owner == "analyzer" +E AssertionError: assert 'snapshot' == 'analyzer' +E +E - analyzer +E + snapshot + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:186: AssertionError +_______ test_pth_paths_outside_runtime_roots_have_actionable_diagnostic ________ + +collision_runtime = {'PROJECT': PosixPath('/test_pth_path.../test_pth_paths_outside_runtime0/analyzers')} +tmp_path = PosixPath('/test_pth_paths_outside_runtime0') + + def test_pth_paths_outside_runtime_roots_have_actionable_diagnostic( + collision_runtime: dict[str, Path], tmp_path: Path + ) -> None: + outside = tmp_path / "outside-runtime" + outside.mkdir() + pth = collision_runtime["PROJECT"] / "site-packages/external.pth" + pth.write_text(str(outside) + "\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") +> assert completed.returncode != 0 +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport json, runpy,.../test_pth_paths_outside_runtime0/analyzers/packaging.py", "project", false]\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:208: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_change_protected_import_state[sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_change_protected_import_state[next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_change_protected_import_state[next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_change_protected_import_state[importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_change_protected_import_state[builtins.__import__ = lambda *args, **kwargs: None] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_editable_finder_remains_available_behind_sealed_imports[prepend] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_pth_paths_outside_runtime_roots_have_actionable_diagnostic +========================= 7 failed, 20 passed in 0.66s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYLINT_CORPUS_IMPORT_GUARD_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_CORPUS_IMPORT_GUARD_RED.txt new file mode 100644 index 00000000..a908eb3d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_CORPUS_IMPORT_GUARD_RED.txt @@ -0,0 +1,217 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-implementation +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 34 items / 19 deselected / 15 selected + +tests/unit/test_external_capsule_corpus.py FFFFFFFFFFFF... [100%] + +=================================== FAILURES =================================== +_____ test_poetry_corpus_rejects_known_import_misresolution[E0401-7-cold] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109048c30> +phase = 'cold', line = 7, rule = 'E0401' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0401-7-warm] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1090482b0> +phase = 'warm', line = 7, rule = 'E0401' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0401-9-cold] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109076450> +phase = 'cold', line = 9, rule = 'E0401' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0401-9-warm] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108fa3ac0> +phase = 'warm', line = 9, rule = 'E0401' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0401-10-cold] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108fa3680> +phase = 'cold', line = 10, rule = 'E0401' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0401-10-warm] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno5') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10905e350> +phase = 'warm', line = 10, rule = 'E0401' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0611-7-cold] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno6') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10905d250> +phase = 'cold', line = 7, rule = 'E0611' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0611-7-warm] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno7') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1090bd6d0> +phase = 'warm', line = 7, rule = 'E0611' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0611-9-cold] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno8') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1090bef30> +phase = 'cold', line = 9, rule = 'E0611' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0611-9-warm] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno9') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109064590> +phase = 'warm', line = 9, rule = 'E0611' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0611-10-cold] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno10') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109065470> +phase = 'cold', line = 10, rule = 'E0611' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +_____ test_poetry_corpus_rejects_known_import_misresolution[E0611-10-warm] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-448/test_poetry_corpus_rejects_kno11') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1091065b0> +phase = 'warm', line = 10, rule = 'E0611' + + @pytest.mark.parametrize("phase", ["cold", "warm"]) + @pytest.mark.parametrize("line", [7, 9, 10]) + @pytest.mark.parametrize("rule", ["E0401", "E0611"]) + def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} +> with pytest.raises(ValueError, match="verified import"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ValueError + +tests/unit/test_external_capsule_corpus.py:259: Failed +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0401-7-cold] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0401-7-warm] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0401-9-cold] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0401-9-warm] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0401-10-cold] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0401-10-warm] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0611-7-cold] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0611-7-warm] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0611-9-cold] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0611-9-warm] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0611-10-cold] +FAILED tests/unit/test_external_capsule_corpus.py::test_poetry_corpus_rejects_known_import_misresolution[E0611-10-warm] +================= 12 failed, 3 passed, 19 deselected in 0.11s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYLINT_DISPATCH_CACHE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_DISPATCH_CACHE_RED.txt new file mode 100644 index 00000000..66afdd14 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_DISPATCH_CACHE_RED.txt @@ -0,0 +1,59 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-implementation +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 21 items / 20 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_builder.py F [100%] + +=================================== FAILURES =================================== +_______ test_pylint_dispatcher_change_invalidates_offline_runtime_reuse ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-454/test_pylint_dispatcher_change_0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c3916e0> + + def test_pylint_dispatcher_change_invalidates_offline_runtime_reuse(tmp_path: Path, monkeypatch) -> None: + helpers = tmp_path / "helpers" + helpers.mkdir() + for helper in Path(runtime_builder.__file__).parent.glob("*.py"): + (helpers / helper.name).write_bytes(helper.read_bytes()) + lock = Path("resources/contracts/pr-range-v1-toolchain-lock.json") + (tmp_path / lock).parent.mkdir(parents=True) + (tmp_path / lock).write_bytes((Path(runtime_builder.__file__).parents[1] / lock).read_bytes()) + monkeypatch.setattr(runtime_builder, "__file__", str(helpers / "runtime_builder.py")) + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "fixed-git-identity") + plan = ProjectPlan(tmp_path, manager="pip") + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + cache = tmp_path / "cache" + identities = [] + digest = runtime_builder.document_digest + + def record_identity(values): + identities.append(values) + return digest(values) + + monkeypatch.setattr(runtime_builder, "document_digest", record_identity) + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + cached_path = cache / digest(identities[-1])[7:] + cached_path.mkdir() + loaded = [] + + def load_cached(path, **_kwargs): + loaded.append(path) + return SimpleNamespace(descriptor={"project_identity": plan.identity}) + + monkeypatch.setattr(runtime_builder, "load_runtime", load_cached) + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + helper = helpers / "target_pylint.py" + helper.write_bytes(helper.read_bytes() + b"\n# changed dispatcher fixture\n") +> with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:236: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_pylint_dispatcher_change_invalidates_offline_runtime_reuse +======================= 1 failed, 20 deselected in 0.11s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYLINT_NAMESPACE_ROOTS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_NAMESPACE_ROOTS_RED.txt new file mode 100644 index 00000000..b1c87ce6 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_NAMESPACE_ROOTS_RED.txt @@ -0,0 +1,76 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-implementation +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 9 items + +tests/unit/specfact_code_review/run/test_target_pylint.py FFF....F. [100%] + +=================================== FAILURES =================================== +______ test_attached_namespace_root_prevents_leaf_shadowing[roots0-True] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-441/test_attached_namespace_root_p0') +roots = [], editable = True + + @pytest.mark.parametrize("roots,editable", [([], True), (["src"], False), (["."], True)]) + def test_attached_namespace_root_prevents_leaf_shadowing(tmp_path: Path, roots: list[str], editable: bool) -> None: + paths = _runtime(tmp_path, roots, editable) +> assert _import_findings(paths, []) == [] +E AssertionError: assert [{'type': 'er...ine': 1, ...}] == [] +E +E Left contains 2 more items, first extra item: {'type': 'error', 'module': 'tests.test_values', 'obj': '', 'line': 1, ...} +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pylint.py:82: AssertionError +______ test_attached_namespace_root_prevents_leaf_shadowing[roots1-False] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-441/test_attached_namespace_root_p1') +roots = ['src'], editable = False + + @pytest.mark.parametrize("roots,editable", [([], True), (["src"], False), (["."], True)]) + def test_attached_namespace_root_prevents_leaf_shadowing(tmp_path: Path, roots: list[str], editable: bool) -> None: + paths = _runtime(tmp_path, roots, editable) +> assert _import_findings(paths, []) == [] +E AssertionError: assert [{'type': 'er...ine': 1, ...}] == [] +E +E Left contains 2 more items, first extra item: {'type': 'error', 'module': 'tests.test_values', 'obj': '', 'line': 1, ...} +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pylint.py:82: AssertionError +______ test_attached_namespace_root_prevents_leaf_shadowing[roots2-True] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-441/test_attached_namespace_root_p2') +roots = ['.'], editable = True + + @pytest.mark.parametrize("roots,editable", [([], True), (["src"], False), (["."], True)]) + def test_attached_namespace_root_prevents_leaf_shadowing(tmp_path: Path, roots: list[str], editable: bool) -> None: + paths = _runtime(tmp_path, roots, editable) +> assert _import_findings(paths, []) == [] +E AssertionError: assert [{'type': 'er...ine': 1, ...}] == [] +E +E Left contains 2 more items, first extra item: {'type': 'error', 'module': 'tests.test_values', 'obj': '', 'line': 1, ...} +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pylint.py:82: AssertionError +_____________ test_missing_import_is_preserved_with_attached_roots _____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-441/test_missing_import_is_preserv0') + + def test_missing_import_is_preserved_with_attached_roots(tmp_path: Path) -> None: + paths = _runtime(tmp_path, [], True) + test = paths["SNAPSHOT"] / "tests/test_values.py" + test.write_text(test.read_text() + "import genuinely_missing_customer_dependency\n") + findings = _import_findings(paths, []) +> assert len(findings) == 1 +E AssertionError: assert 3 == 1 +E + where 3 = len([{'type': 'error', 'module': 'tests.test_values', 'obj': '', 'line': 1, ...}, {'type': 'error', 'module': 'tests.test_values', 'obj': '', 'line': 1, ...}, {'type': 'error', 'module': 'tests.test_values', 'obj': '', 'line': 2, ...}]) + +tests/unit/specfact_code_review/run/test_target_pylint.py:104: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_pylint.py::test_attached_namespace_root_prevents_leaf_shadowing[roots0-True] +FAILED tests/unit/specfact_code_review/run/test_target_pylint.py::test_attached_namespace_root_prevents_leaf_shadowing[roots1-False] +FAILED tests/unit/specfact_code_review/run/test_target_pylint.py::test_attached_namespace_root_prevents_leaf_shadowing[roots2-True] +FAILED tests/unit/specfact_code_review/run/test_target_pylint.py::test_missing_import_is_preserved_with_attached_roots +========================= 4 failed, 5 passed in 1.78s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYLINT_STARTUP_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_STARTUP_RED.txt new file mode 100644 index 00000000..0def2879 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYLINT_STARTUP_RED.txt @@ -0,0 +1,34 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 15 items / 14 deselected / 1 selected + +tests/unit/specfact_code_review/tools/test_pylint_runner.py F [100%] + +=================================== FAILURES =================================== +___________ test_run_pylint_rejects_startup_failure_with_clean_json ____________ + +tmp_path = PosixPath('') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c051cd0> + + def test_run_pylint_rejects_startup_failure_with_clean_json(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + diagnostic = "project_worker_startup_import_state_changed" + monkeypatch.setattr( + subprocess, + "run", + Mock(return_value=completed_process("pylint", stdout="[]", stderr=diagnostic, returncode=78)), + ) + + findings = run_pylint([tmp_path / "target.py"]) + +> assert len(findings) == 1 +E assert 0 == 1 +E + where 0 = len([]) + +tests/unit/specfact_code_review/tools/test_pylint_runner.py:286: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/tools/test_pylint_runner.py::test_run_pylint_rejects_startup_failure_with_clean_json +======================= 1 failed, 14 deselected in 0.12s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_DESELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_DESELECTION_RED.txt new file mode 100644 index 00000000..e462c0d8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_DESELECTION_RED.txt @@ -0,0 +1,105 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 12 items + +tests/unit/specfact_code_review/run/test_target_pytest.py ...... [ 50%] +tests/unit/specfact_code_review/run/test_target_pytest_selection.py FFF. [ 83%] +FF [100%] + +=================================== FAILURES =================================== +___ test_native_addopts_deselection_does_not_require_removed_tests[-k keep] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-222/test_native_addopts_deselectio0') +addopts = '-k keep' + + @pytest.mark.parametrize("addopts", ["-k keep", "--deselect=test_case.py::test_drop"]) + def test_native_addopts_deselection_does_not_require_removed_tests(tmp_path: Path, addopts: str) -> None: + observation = _observe(tmp_path, addopts, "def test_keep(): pass\ndef test_drop(): pass\n") + assert observation["exit_code"] == 0 +> assert observation["collected"] == ["test_case.py::test_keep"] +E AssertionError: assert ['test_case.p...y::test_keep'] == ['test_case.py::test_keep'] +E +E At index 0 diff: 'test_case.py::test_drop' != 'test_case.py::test_keep' +E Left contains one more item: 'test_case.py::test_keep' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py:55: AssertionError +_ test_native_addopts_deselection_does_not_require_removed_tests[--deselect=test_case.py::test_drop] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-222/test_native_addopts_deselectio1') +addopts = '--deselect=test_case.py::test_drop' + + @pytest.mark.parametrize("addopts", ["-k keep", "--deselect=test_case.py::test_drop"]) + def test_native_addopts_deselection_does_not_require_removed_tests(tmp_path: Path, addopts: str) -> None: + observation = _observe(tmp_path, addopts, "def test_keep(): pass\ndef test_drop(): pass\n") + assert observation["exit_code"] == 0 +> assert observation["collected"] == ["test_case.py::test_keep"] +E AssertionError: assert ['test_case.p...y::test_keep'] == ['test_case.py::test_keep'] +E +E At index 0 diff: 'test_case.py::test_drop' != 'test_case.py::test_keep' +E Left contains one more item: 'test_case.py::test_keep' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py:55: AssertionError +_________ test_final_native_inventory_honors_plugin_collection_changes _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-222/test_final_native_inventory_ho0') + + def test_final_native_inventory_honors_plugin_collection_changes(tmp_path: Path) -> None: + (tmp_path / "conftest.py").write_text( + "def pytest_collection_modifyitems(items):\n" + " items[:] = [item for item in items if item.name == 'test_keep']\n" + ) + observation = _observe(tmp_path, "", "def test_keep(): pass\ndef test_drop(): pass\n") + assert observation["exit_code"] == 0 +> assert observation["collected"] == ["test_case.py::test_keep"] +E AssertionError: assert ['test_case.p...y::test_keep'] == ['test_case.py::test_keep'] +E +E At index 0 diff: 'test_case.py::test_drop' != 'test_case.py::test_keep' +E Left contains one more item: 'test_case.py::test_keep' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py:69: AssertionError +_______ test_final_inventory_preserves_distributed_selected_items[True] ________ + +local_finish_first = True + + @pytest.mark.parametrize("local_finish_first", [True, False]) + def test_final_inventory_preserves_distributed_selected_items(local_finish_first: bool) -> None: + observer = target_pytest.Observer() + session = SimpleNamespace(items=[]) + if local_finish_first: +> observer.pytest_collection_finish(session) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'Observer' object has no attribute 'pytest_collection_finish' + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py:88: AttributeError +_______ test_final_inventory_preserves_distributed_selected_items[False] _______ + +local_finish_first = False + + @pytest.mark.parametrize("local_finish_first", [True, False]) + def test_final_inventory_preserves_distributed_selected_items(local_finish_first: bool) -> None: + observer = target_pytest.Observer() + session = SimpleNamespace(items=[]) + if local_finish_first: + observer.pytest_collection_finish(session) + observer.pytest_xdist_node_collection_finished(None, ["test_case.py::test_keep"]) + observer.pytest_xdist_node_collection_finished(None, ["test_case.py::test_keep"]) + if not local_finish_first: +> observer.pytest_collection_finish(session) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'Observer' object has no attribute 'pytest_collection_finish' + +tests/unit/specfact_code_review/run/test_target_pytest_selection.py:92: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_pytest_selection.py::test_native_addopts_deselection_does_not_require_removed_tests[-k keep] +FAILED tests/unit/specfact_code_review/run/test_target_pytest_selection.py::test_native_addopts_deselection_does_not_require_removed_tests[--deselect=test_case.py::test_drop] +FAILED tests/unit/specfact_code_review/run/test_target_pytest_selection.py::test_final_native_inventory_honors_plugin_collection_changes +FAILED tests/unit/specfact_code_review/run/test_target_pytest_selection.py::test_final_inventory_preserves_distributed_selected_items[True] +FAILED tests/unit/specfact_code_review/run/test_target_pytest_selection.py::test_final_inventory_preserves_distributed_selected_items[False] +========================= 5 failed, 7 passed in 0.49s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_EVIDENCE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_EVIDENCE_RED.txt new file mode 100644 index 00000000..748014e9 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_EVIDENCE_RED.txt @@ -0,0 +1,37 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 7 items + +tests/unit/specfact_code_review/run/test_portable_worker.py ....FFF [100%] + +=================================== FAILURES =================================== +___________________ test_pytest_collection_only_cannot_pass ____________________ + + def test_pytest_collection_only_cannot_pass() -> None: +> from specfact_code_review.run.portable_worker import validate_observation +E ImportError: cannot import name 'validate_observation' from 'specfact_code_review.run.portable_worker' (/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py) + +tests/unit/specfact_code_review/run/test_portable_worker.py:49: ImportError +_________ test_pytest_early_stop_is_incomplete_even_with_real_failure __________ + + def test_pytest_early_stop_is_incomplete_even_with_real_failure() -> None: +> from specfact_code_review.run.portable_worker import validate_observation +E ImportError: cannot import name 'validate_observation' from 'specfact_code_review.run.portable_worker' (/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py) + +tests/unit/specfact_code_review/run/test_portable_worker.py:55: ImportError +________________ test_pytest_success_requires_coverage_evidence ________________ + + def test_pytest_success_requires_coverage_evidence() -> None: +> from specfact_code_review.run.portable_worker import validate_observation +E ImportError: cannot import name 'validate_observation' from 'specfact_code_review.run.portable_worker' (/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py) + +tests/unit/specfact_code_review/run/test_portable_worker.py:62: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_collection_only_cannot_pass +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_early_stop_is_incomplete_even_with_real_failure +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_success_requires_coverage_evidence +========================= 3 failed, 4 passed in 0.35s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_NATIVE_DISCOVERY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_NATIVE_DISCOVERY_RED.txt new file mode 100644 index 00000000..8ffe8a56 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_NATIVE_DISCOVERY_RED.txt @@ -0,0 +1,370 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 51 items + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py FF [ 3%] +FFF....FF [ 21%] +tests/unit/specfact_code_review/run/test_portable_worker.py ..........FF [ 45%] +F.....................FF.... [100%] + +=================================== FAILURES =================================== +_ test_full_review_matches_native_discovery[testpaths = absent-expected0-True] _ + +tmp_path = PosixPath('/test_full_review_matches_nativ0') +configuration = 'testpaths = absent' +expected = ['tests/test_app.py', 'other/test_else.py'], warns = True + + @pytest.mark.parametrize( + ("configuration", "expected", "warns"), + [ + ("testpaths = absent", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = absent tests", ["tests/test_app.py"], False), + ("testpaths = tests/*.py", ["tests/test_app.py"], False), + ("testpaths = absent*", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = tests\naddopts = other/test_else.py", ["other/test_else.py"], False), + ("testpaths =", ["tests/test_app.py", "other/test_else.py"], False), + ], + ) + def test_full_review_matches_native_discovery( + tmp_path: Path, configuration: str, expected: list[str], warns: bool + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\n{configuration}\n") + for name in ("tests/test_app.py", "other/test_else.py"): + test = root / name + test.parent.mkdir(exist_ok=True) + test.write_text("def test_valid(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native.json", ()) + selectors = select_test_paths(discover_project(root), [], full=True) + portable, portable_output = _observe_discovery(root, tmp_path / "portable.json", selectors) + assert native["exit_code"] == 0, native_output +> assert portable["exit_code"] == 0, portable_output +E AssertionError: +E no tests ran in 0.00s +E ERROR: file or directory not found: absent +E +E +E assert 4 == 0 + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:99: AssertionError +_ test_full_review_matches_native_discovery[testpaths = absent tests-expected1-False] _ + +tmp_path = PosixPath('/test_full_review_matches_nativ1') +configuration = 'testpaths = absent tests', expected = ['tests/test_app.py'] +warns = False + + @pytest.mark.parametrize( + ("configuration", "expected", "warns"), + [ + ("testpaths = absent", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = absent tests", ["tests/test_app.py"], False), + ("testpaths = tests/*.py", ["tests/test_app.py"], False), + ("testpaths = absent*", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = tests\naddopts = other/test_else.py", ["other/test_else.py"], False), + ("testpaths =", ["tests/test_app.py", "other/test_else.py"], False), + ], + ) + def test_full_review_matches_native_discovery( + tmp_path: Path, configuration: str, expected: list[str], warns: bool + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\n{configuration}\n") + for name in ("tests/test_app.py", "other/test_else.py"): + test = root / name + test.parent.mkdir(exist_ok=True) + test.write_text("def test_valid(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native.json", ()) + selectors = select_test_paths(discover_project(root), [], full=True) + portable, portable_output = _observe_discovery(root, tmp_path / "portable.json", selectors) + assert native["exit_code"] == 0, native_output +> assert portable["exit_code"] == 0, portable_output +E AssertionError: +E no tests ran in 0.00s +E ERROR: file or directory not found: absent +E +E +E assert 4 == 0 + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:99: AssertionError +_ test_full_review_matches_native_discovery[testpaths = tests/*.py-expected2-False] _ + +tmp_path = PosixPath('/test_full_review_matches_nativ2') +configuration = 'testpaths = tests/*.py', expected = ['tests/test_app.py'] +warns = False + + @pytest.mark.parametrize( + ("configuration", "expected", "warns"), + [ + ("testpaths = absent", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = absent tests", ["tests/test_app.py"], False), + ("testpaths = tests/*.py", ["tests/test_app.py"], False), + ("testpaths = absent*", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = tests\naddopts = other/test_else.py", ["other/test_else.py"], False), + ("testpaths =", ["tests/test_app.py", "other/test_else.py"], False), + ], + ) + def test_full_review_matches_native_discovery( + tmp_path: Path, configuration: str, expected: list[str], warns: bool + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\n{configuration}\n") + for name in ("tests/test_app.py", "other/test_else.py"): + test = root / name + test.parent.mkdir(exist_ok=True) + test.write_text("def test_valid(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native.json", ()) + selectors = select_test_paths(discover_project(root), [], full=True) + portable, portable_output = _observe_discovery(root, tmp_path / "portable.json", selectors) + assert native["exit_code"] == 0, native_output +> assert portable["exit_code"] == 0, portable_output +E AssertionError: +E no tests ran in 0.00s +E ERROR: file or directory not found: tests/*.py +E +E +E assert 4 == 0 + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:99: AssertionError +_ test_full_review_matches_native_discovery[testpaths = absent*-expected3-True] _ + +tmp_path = PosixPath('/test_full_review_matches_nativ3') +configuration = 'testpaths = absent*' +expected = ['tests/test_app.py', 'other/test_else.py'], warns = True + + @pytest.mark.parametrize( + ("configuration", "expected", "warns"), + [ + ("testpaths = absent", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = absent tests", ["tests/test_app.py"], False), + ("testpaths = tests/*.py", ["tests/test_app.py"], False), + ("testpaths = absent*", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = tests\naddopts = other/test_else.py", ["other/test_else.py"], False), + ("testpaths =", ["tests/test_app.py", "other/test_else.py"], False), + ], + ) + def test_full_review_matches_native_discovery( + tmp_path: Path, configuration: str, expected: list[str], warns: bool + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\n{configuration}\n") + for name in ("tests/test_app.py", "other/test_else.py"): + test = root / name + test.parent.mkdir(exist_ok=True) + test.write_text("def test_valid(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native.json", ()) + selectors = select_test_paths(discover_project(root), [], full=True) + portable, portable_output = _observe_discovery(root, tmp_path / "portable.json", selectors) + assert native["exit_code"] == 0, native_output +> assert portable["exit_code"] == 0, portable_output +E AssertionError: +E no tests ran in 0.00s +E ERROR: file or directory not found: absent* +E +E +E assert 4 == 0 + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:99: AssertionError +_ test_full_review_matches_native_discovery[testpaths = tests\naddopts = other/test_else.py-expected4-False] _ + +tmp_path = PosixPath('/test_full_review_matches_nativ4') +configuration = 'testpaths = tests\naddopts = other/test_else.py' +expected = ['other/test_else.py'], warns = False + + @pytest.mark.parametrize( + ("configuration", "expected", "warns"), + [ + ("testpaths = absent", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = absent tests", ["tests/test_app.py"], False), + ("testpaths = tests/*.py", ["tests/test_app.py"], False), + ("testpaths = absent*", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = tests\naddopts = other/test_else.py", ["other/test_else.py"], False), + ("testpaths =", ["tests/test_app.py", "other/test_else.py"], False), + ], + ) + def test_full_review_matches_native_discovery( + tmp_path: Path, configuration: str, expected: list[str], warns: bool + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\n{configuration}\n") + for name in ("tests/test_app.py", "other/test_else.py"): + test = root / name + test.parent.mkdir(exist_ok=True) + test.write_text("def test_valid(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native.json", ()) + selectors = select_test_paths(discover_project(root), [], full=True) + portable, portable_output = _observe_discovery(root, tmp_path / "portable.json", selectors) + assert native["exit_code"] == 0, native_output + assert portable["exit_code"] == 0, portable_output + expected_nodes = sorted(f"{name}::test_valid" for name in expected) +> assert native["collected"] == portable["collected"] == expected_nodes +E AssertionError: assert ['other/test_...::test_valid'] == ['other/test_...::test_valid'] +E +E Right contains one more item: 'tests/test_app.py::test_valid' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:101: AssertionError +_______ test_native_fallback_preserves_failure_and_empty_execution[True] _______ + +tmp_path = PosixPath('/test_native_fallback_preserves0') +has_failure = True + + @pytest.mark.parametrize("has_failure", [True, False]) + def test_native_fallback_preserves_failure_and_empty_execution(tmp_path: Path, has_failure: bool) -> None: + (tmp_path / "pytest.ini").write_text("[pytest]\ntestpaths = absent\n") + if has_failure: + (tmp_path / "test_case.py").write_text("def test_defect(): assert 2 + 2 == 5\n") + selectors = select_test_paths(discover_project(tmp_path), [], full=True) + observation, output = _observe_discovery(tmp_path, tmp_path / "portable.json", selectors) +> assert "No files were found in testpaths" in output +E AssertionError: assert 'No files were found in testpaths' in '\nno tests ran in 0.00s\nERROR: file or directory not found: absent\n\n' + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:130: AssertionError +______ test_native_fallback_preserves_failure_and_empty_execution[False] _______ + +tmp_path = PosixPath('/test_native_fallback_preserves1') +has_failure = False + + @pytest.mark.parametrize("has_failure", [True, False]) + def test_native_fallback_preserves_failure_and_empty_execution(tmp_path: Path, has_failure: bool) -> None: + (tmp_path / "pytest.ini").write_text("[pytest]\ntestpaths = absent\n") + if has_failure: + (tmp_path / "test_case.py").write_text("def test_defect(): assert 2 + 2 == 5\n") + selectors = select_test_paths(discover_project(tmp_path), [], full=True) + observation, output = _observe_discovery(tmp_path, tmp_path / "portable.json", selectors) +> assert "No files were found in testpaths" in output +E AssertionError: assert 'No files were found in testpaths' in '\nno tests ran in 0.00s\nERROR: file or directory not found: absent\n\n' + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:130: AssertionError +_________ test_pytest_native_default_root_is_preserved[configuration0] _________ + +tmp_path = PosixPath('/test_pytest_native_default_roo0') +configuration = {} + + @pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) + def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) +> assert not select_test_paths(plan, [source], full=True) +E AssertionError: assert not ('.',) +E + where ('.',) = select_test_paths(ProjectPlan(root=PosixPath('/test_pyt...oups=(), extras=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={}), [PosixPath('/test_pytest_native_default_roo0/app.py')], full=True) + +tests/unit/specfact_code_review/run/test_portable_worker.py:126: AssertionError +_________ test_pytest_native_default_root_is_preserved[configuration1] _________ + +tmp_path = PosixPath('/test_pytest_native_default_roo1') +configuration = {'testpaths': []} + + @pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) + def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) +> assert not select_test_paths(plan, [source], full=True) +E AssertionError: assert not ('.',) +E + where ('.',) = select_test_paths(ProjectPlan(root=PosixPath('/test_pyt...=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={'testpaths': []}), [PosixPath('/test_pytest_native_default_roo1/app.py')], full=True) + +tests/unit/specfact_code_review/run/test_portable_worker.py:126: AssertionError +_________ test_pytest_native_default_root_is_preserved[configuration2] _________ + +tmp_path = PosixPath('/test_pytest_native_default_roo2') +configuration = {'testpaths': ''} + + @pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) + def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) +> assert not select_test_paths(plan, [source], full=True) +E AssertionError: assert not ('.',) +E + where ('.',) = select_test_paths(ProjectPlan(root=PosixPath('/test_pyt...=(), requirements=(), constraints=(), source_roots=(), native_libraries=(), inputs={}, pytest_config={'testpaths': ''}), [PosixPath('/test_pytest_native_default_roo2/app.py')], full=True) + +tests/unit/specfact_code_review/run/test_portable_worker.py:126: AssertionError +____________ test_quoted_pytest_paths_match_native_configuration["] ____________ + +tmp_path = PosixPath('/test_quoted_pytest_paths_match0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f4ed70> +quote = '"' + + @pytest.mark.parametrize("quote", ['"', "'"]) + def test_quoted_pytest_paths_match_native_configuration(tmp_path: Path, monkeypatch, quote: str) -> None: + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + (tmp_path / "source tree").mkdir() + (tmp_path / "integration tests").mkdir() + source = tmp_path / "source tree/app name.py" + source.touch() + selected = tmp_path / "integration tests/test_app name.py" + selected.write_text("def test_example(): assert True\n") + configuration = tmp_path / "pytest.ini" + configuration.write_text( + "[pytest]\n" + f"testpaths = {quote}integration tests{quote}\n" + f"pythonpath = {quote}source tree{quote}\n" + f"python_files = {quote}test_* name.py{quote}\n" + ) + native = pytest.Config.fromdictargs({}, ["-c", str(configuration)]) + try: + plan = discover_project(tmp_path) + assert plan.source_roots == tuple(path.relative_to(tmp_path).as_posix() for path in native.getini("pythonpath")) +> assert not select_test_paths(plan, [source], full=True) +E assert not ('integration tests',) +E + where ('integration tests',) = select_test_paths(ProjectPlan(root=PosixPath('/test_quo... pytest_config={'testpaths': '"integration tests"', 'pythonpath': '"source tree"', 'python_files': '"test_* name.py"'}), [PosixPath('/test_quoted_pytest_paths_match0/source tree/app name.py')], full=True) + +tests/unit/specfact_code_review/run/test_portable_worker.py:216: AssertionError +____________ test_quoted_pytest_paths_match_native_configuration['] ____________ + +tmp_path = PosixPath('/test_quoted_pytest_paths_match1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f72580> +quote = "'" + + @pytest.mark.parametrize("quote", ['"', "'"]) + def test_quoted_pytest_paths_match_native_configuration(tmp_path: Path, monkeypatch, quote: str) -> None: + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + (tmp_path / "source tree").mkdir() + (tmp_path / "integration tests").mkdir() + source = tmp_path / "source tree/app name.py" + source.touch() + selected = tmp_path / "integration tests/test_app name.py" + selected.write_text("def test_example(): assert True\n") + configuration = tmp_path / "pytest.ini" + configuration.write_text( + "[pytest]\n" + f"testpaths = {quote}integration tests{quote}\n" + f"pythonpath = {quote}source tree{quote}\n" + f"python_files = {quote}test_* name.py{quote}\n" + ) + native = pytest.Config.fromdictargs({}, ["-c", str(configuration)]) + try: + plan = discover_project(tmp_path) + assert plan.source_roots == tuple(path.relative_to(tmp_path).as_posix() for path in native.getini("pythonpath")) +> assert not select_test_paths(plan, [source], full=True) +E assert not ('integration tests',) +E + where ('integration tests',) = select_test_paths(ProjectPlan(root=PosixPath('/test_quo... pytest_config={'testpaths': "'integration tests'", 'pythonpath': "'source tree'", 'python_files': "'test_* name.py'"}), [PosixPath('/test_quoted_pytest_paths_match1/source tree/app name.py')], full=True) + +tests/unit/specfact_code_review/run/test_portable_worker.py:216: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_full_review_matches_native_discovery[testpaths = absent-expected0-True] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_full_review_matches_native_discovery[testpaths = absent tests-expected1-False] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_full_review_matches_native_discovery[testpaths = tests/*.py-expected2-False] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_full_review_matches_native_discovery[testpaths = absent*-expected3-True] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_full_review_matches_native_discovery[testpaths = tests\naddopts = other/test_else.py-expected4-False] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_native_fallback_preserves_failure_and_empty_execution[True] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_native_fallback_preserves_failure_and_empty_execution[False] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_native_default_root_is_preserved[configuration0] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_native_default_root_is_preserved[configuration1] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_pytest_native_default_root_is_preserved[configuration2] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_quoted_pytest_paths_match_native_configuration["] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_quoted_pytest_paths_match_native_configuration['] +======================== 12 failed, 39 passed in 5.15s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_PATH_CONTAINMENT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_PATH_CONTAINMENT_RED.txt new file mode 100644 index 00000000..0612d6c5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_PATH_CONTAINMENT_RED.txt @@ -0,0 +1,337 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- /worktree/.venv/bin/python +cachedir: .pytest_cache +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collecting ... collected 237 items / 221 deselected / 16 selected + +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-"-testpaths] FAILED [ 6%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-"-pythonpath] FAILED [ 12%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-'-testpaths] FAILED [ 18%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-'-pythonpath] FAILED [ 25%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-"-testpaths] FAILED [ 31%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-"-pythonpath] FAILED [ 37%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-'-testpaths] FAILED [ 43%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-'-pythonpath] FAILED [ 50%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-"-testpaths] FAILED [ 56%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-"-pythonpath] FAILED [ 62%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-'-testpaths] FAILED [ 68%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-'-pythonpath] FAILED [ 75%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_contained_pytest_paths_with_spaces_retain_native_values[ini-testpaths] PASSED [ 81%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_contained_pytest_paths_with_spaces_retain_native_values[ini-pythonpath] PASSED [ 87%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_contained_pytest_paths_with_spaces_retain_native_values[toml-list-testpaths] PASSED [ 93%] +tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_contained_pytest_paths_with_spaces_retain_native_values[toml-list-pythonpath] PASSED [100%] + +=================================== FAILURES =================================== +____ test_quoted_pytest_paths_are_checked_after_parsing[parent-"-testpaths] ____ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c0') +option = 'testpaths', quote = '"', path_kind = 'parent' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +___ test_quoted_pytest_paths_are_checked_after_parsing[parent-"-pythonpath] ____ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c1') +option = 'pythonpath', quote = '"', path_kind = 'parent' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +____ test_quoted_pytest_paths_are_checked_after_parsing[parent-'-testpaths] ____ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c2') +option = 'testpaths', quote = "'", path_kind = 'parent' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +___ test_quoted_pytest_paths_are_checked_after_parsing[parent-'-pythonpath] ____ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c3') +option = 'pythonpath', quote = "'", path_kind = 'parent' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +___ test_quoted_pytest_paths_are_checked_after_parsing[absolute-"-testpaths] ___ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c4') +option = 'testpaths', quote = '"', path_kind = 'absolute' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +__ test_quoted_pytest_paths_are_checked_after_parsing[absolute-"-pythonpath] ___ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c5') +option = 'pythonpath', quote = '"', path_kind = 'absolute' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +___ test_quoted_pytest_paths_are_checked_after_parsing[absolute-'-testpaths] ___ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c6') +option = 'testpaths', quote = "'", path_kind = 'absolute' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +__ test_quoted_pytest_paths_are_checked_after_parsing[absolute-'-pythonpath] ___ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c7') +option = 'pythonpath', quote = "'", path_kind = 'absolute' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: Failed +___ test_quoted_pytest_paths_are_checked_after_parsing[symlink-"-testpaths] ____ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c8') +option = 'testpaths', quote = '"', path_kind = 'symlink' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_input_(escape|symlink)' +E Actual message: 'project_source_symlink_invalid:external tree' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: AssertionError +___ test_quoted_pytest_paths_are_checked_after_parsing[symlink-"-pythonpath] ___ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c9') +option = 'pythonpath', quote = '"', path_kind = 'symlink' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_input_(escape|symlink)' +E Actual message: 'project_source_symlink_invalid:external tree' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: AssertionError +___ test_quoted_pytest_paths_are_checked_after_parsing[symlink-'-testpaths] ____ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c10') +option = 'testpaths', quote = "'", path_kind = 'symlink' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_input_(escape|symlink)' +E Actual message: 'project_source_symlink_invalid:external tree' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: AssertionError +___ test_quoted_pytest_paths_are_checked_after_parsing[symlink-'-pythonpath] ___ + +tmp_path = PosixPath('/test_quoted_pytest_paths_are_c11') +option = 'pythonpath', quote = "'", path_kind = 'symlink' + + @pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) + @pytest.mark.parametrize("quote", ['"', "'"]) + @pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) + def test_quoted_pytest_paths_are_checked_after_parsing( + tmp_path: Path, option: str, quote: str, path_kind: str + ) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") +> with pytest.raises(ProjectRuntimeError, match="project_input_(escape|symlink)"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_input_(escape|symlink)' +E Actual message: 'project_source_symlink_invalid:external tree' + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:144: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-"-testpaths] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-"-pythonpath] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-'-testpaths] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[parent-'-pythonpath] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-"-testpaths] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-"-pythonpath] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-'-testpaths] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[absolute-'-pythonpath] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-"-testpaths] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-"-pythonpath] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-'-testpaths] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_quoted_pytest_paths_are_checked_after_parsing[symlink-'-pythonpath] +================= 12 failed, 4 passed, 221 deselected in 0.25s ================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_PATH_TYPES_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_PATH_TYPES_RED.txt new file mode 100644 index 00000000..5e33e5cf --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_PATH_TYPES_RED.txt @@ -0,0 +1,1898 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 112 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py .......... [ 8%] +...........................................FFFFFFFFFFFFFFFFFFFFFFFFFFFFF [ 73%] +FFFFFFFFFFF.... [ 86%] +tests/unit/specfact_code_review/run/test_portable_snapshot.py ........FF [ 95%] +..... [100%] + +=================================== FAILURES =================================== +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape0') +filename = 'pytest.toml', section = 'pytest', option = 'pythonpath', value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape0') +config = {}, pytest = {'pythonpath': 1} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape1') +filename = '.pytest.toml', section = 'pytest', option = 'pythonpath' +value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape1') +config = {}, pytest = {'pythonpath': 1} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape2') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'pythonpath', value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape2') +config = {}, pytest = {'pythonpath': 1} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape3') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'pythonpath' +value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape3') +config = {}, pytest = {'pythonpath': 1} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape4') +filename = 'pytest.toml', section = 'pytest', option = 'testpaths', value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape4') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape5') +filename = '.pytest.toml', section = 'pytest', option = 'testpaths', value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape5') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape6') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'testpaths', value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape6') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape7') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'testpaths' +value = '1' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape7') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape8') +filename = 'pytest.toml', section = 'pytest', option = 'pythonpath' +value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape8') +config = {}, pytest = {'pythonpath': True} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape9') +filename = '.pytest.toml', section = 'pytest', option = 'pythonpath' +value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape9') +config = {}, pytest = {'pythonpath': True} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape10') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'pythonpath', value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape10') +config = {}, pytest = {'pythonpath': True} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape11') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'pythonpath' +value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape11') +config = {}, pytest = {'pythonpath': True} + + def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + roots = config.get("source_roots", pytest.get("pythonpath", ["src", "."] if (root / "src").is_dir() else ["."])) + if isinstance(roots, str): + roots = roots.split() +> result = tuple(dict.fromkeys(roots)) + ^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:229: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape12') +filename = 'pytest.toml', section = 'pytest', option = 'testpaths' +value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape12') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape13') +filename = '.pytest.toml', section = 'pytest', option = 'testpaths' +value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape13') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape14') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'testpaths', value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape14') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape15') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'testpaths' +value = 'true' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape15') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape16') +filename = 'pytest.toml', section = 'pytest', option = 'pythonpath' +value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10af83ca0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape17') +filename = '.pytest.toml', section = 'pytest', option = 'pythonpath' +value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x1093328f0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape18') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'pythonpath', value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x109332ff0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape19') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'pythonpath' +value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x1093319a0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape20') +filename = 'pytest.toml', section = 'pytest', option = 'testpaths' +value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x109331bd0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape21') +filename = '.pytest.toml', section = 'pytest', option = 'testpaths' +value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x109331c40> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape22') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'testpaths', value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed9850> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape23') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'testpaths' +value = '[1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed9b60> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape24') +filename = 'pytest.toml', section = 'pytest', option = 'pythonpath' +value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed9d90> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape25') +filename = '.pytest.toml', section = 'pytest', option = 'pythonpath' +value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed8dd0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape26') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'pythonpath', value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aeda420> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape27') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'pythonpath' +value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aeda1f0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape28') +filename = 'pytest.toml', section = 'pytest', option = 'testpaths' +value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed9620> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape29') +filename = '.pytest.toml', section = 'pytest', option = 'testpaths' +value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aedbc30> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape30') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'testpaths', value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed90e0> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape31') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'testpaths' +value = '["tests", 1]' + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:320: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:336: in discover_project + _safe_input(root, testpath) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aed9b60> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape32') +filename = 'pytest.toml', section = 'pytest', option = 'pythonpath' +value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape33') +filename = '.pytest.toml', section = 'pytest', option = 'pythonpath' +value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape34') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'pythonpath', value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape35') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'pythonpath' +value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape36') +filename = 'pytest.toml', section = 'pytest', option = 'testpaths' +value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-.pytest.toml-pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape37') +filename = '.pytest.toml', section = 'pytest', option = 'testpaths' +value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-pyproject.toml-tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape38') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +option = 'testpaths', value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-pyproject.toml-tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_invalid_pytest_path_shape39') +filename = 'pyproject.toml', section = 'tool.pytest', option = 'testpaths' +value = "{path = 'tests'}" + + @pytest.mark.parametrize("filename,section", [("pytest.toml", "pytest"), (".pytest.toml", "pytest"), ("pyproject.toml", "tool.pytest.ini_options"), ("pyproject.toml", "tool.pytest")]) + @pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) + @pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) + def test_invalid_pytest_path_shape_has_project_diagnostic(tmp_path: Path, filename: str, section: str, option: str, value: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:319: Failed +_ test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool.pytest.ini_options]\npythonpath=[1]\n-project_pytest_config_invalid:pyproject.toml:pythonpath] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_malformed_configuration_p1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10aedfa70> +filename = 'pyproject.toml' +contents = '[tool.pytest.ini_options]\npythonpath=[1]\n' +diagnostic = 'project_pytest_config_invalid:pyproject.toml:pythonpath' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pyproject.toml", "[tool.pytest.ini_options]\npythonpath=[1]\n", "project_pytest_config_invalid:pyproject.toml:pythonpath"), + ("pyproject.toml", "[tool.pytest.ini_options]\ntestpaths=1\n", "project_pytest_config_invalid:pyproject.toml:testpaths"), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:179: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:77: in run_project_snapshot + plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:50: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:352: in discover_project + source_roots=_source_roots(root, values, pytest), + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:231: in _source_roots + _safe_input(root, relative) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:61: in _safe_input + path = Path(relative) + ^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = <[AttributeError("'pathlib.PosixPath' object has no attribute '_raw_paths'") raised in repr()] PosixPath object at 0x10aedb220> +args = (1,) + + def __init__(self, *args): + paths = [] + for arg in args: + if isinstance(arg, PurePath): + if arg.parser is not self.parser: + # GH-103631: Convert separators for backwards compatibility. + paths.append(arg.as_posix()) + else: + paths.extend(arg._raw_paths) + else: + try: + path = os.fspath(arg) + except TypeError: + path = arg + if not isinstance(path, str): +> raise TypeError( + "argument should be a str or an os.PathLike " + "object where __fspath__ returns a str, " + f"not {type(path).__name__!r}") +E TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:150: TypeError +_ test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool.pytest.ini_options]\ntestpaths=1\n-project_pytest_config_invalid:pyproject.toml:testpaths] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_malformed_configuration_p2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a32e510> +filename = 'pyproject.toml' +contents = '[tool.pytest.ini_options]\ntestpaths=1\n' +diagnostic = 'project_pytest_config_invalid:pyproject.toml:testpaths' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pyproject.toml", "[tool.pytest.ini_options]\npythonpath=[1]\n", "project_pytest_config_invalid:pyproject.toml:pythonpath"), + ("pyproject.toml", "[tool.pytest.ini_options]\ntestpaths=1\n", "project_pytest_config_invalid:pyproject.toml:testpaths"), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:179: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:77: in run_project_snapshot + plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:50: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-227/test_malformed_configuration_p2') +config_path = None + + @require(lambda root: root.is_dir()) + def discover_project(root: Path, *, config_path: Path | None = None) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _toml(root / "pyproject.toml") + hatch = _toml(root / "hatch.toml") + selection = {**_active_selection(root, project, hatch), **values} if "manager" not in values else values + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + testpaths = pytest.get("testpaths", []) +> for testpath in testpaths.split() if isinstance(testpaths, str) else testpaths: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:335: TypeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-pythonpath-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[1-testpaths-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-pythonpath-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[true-testpaths-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-pythonpath-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[[1]-testpaths-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-pythonpath-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[["tests", 1]-testpaths-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-pythonpath-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-.pytest.toml-pytest] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-pyproject.toml-tool.pytest.ini_options] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_invalid_pytest_path_shape_has_project_diagnostic[{path = 'tests'}-testpaths-pyproject.toml-tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool.pytest.ini_options]\npythonpath=[1]\n-project_pytest_config_invalid:pyproject.toml:pythonpath] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool.pytest.ini_options]\ntestpaths=1\n-project_pytest_config_invalid:pyproject.toml:testpaths] +======================== 42 failed, 70 passed in 0.87s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_QUOTED_PATHS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_QUOTED_PATHS_RED.txt new file mode 100644 index 00000000..c1dfcb61 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_QUOTED_PATHS_RED.txt @@ -0,0 +1,137 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 40 items / 34 deselected / 6 selected + +tests/unit/specfact_code_review/run/test_portable_worker.py FFFFFF [100%] + +=================================== FAILURES =================================== +____________ test_quoted_pytest_paths_match_native_configuration["] ____________ + +tmp_path = PosixPath('/test_quoted_pytest_paths_match0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10894cc30> +quote = '"' + + @pytest.mark.parametrize("quote", ['"', "'"]) + def test_quoted_pytest_paths_match_native_configuration(tmp_path: Path, monkeypatch, quote: str) -> None: + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + (tmp_path / "source tree").mkdir() + (tmp_path / "integration tests").mkdir() + source = tmp_path / "source tree/app name.py" + source.touch() + selected = tmp_path / "integration tests/test_app name.py" + selected.write_text("def test_example(): assert True\n") + configuration = tmp_path / "pytest.ini" + configuration.write_text( + "[pytest]\n" + f"testpaths = {quote}integration tests{quote}\n" + f"pythonpath = {quote}source tree{quote}\n" + f"python_files = {quote}test_* name.py{quote}\n" + ) + native = pytest.Config.fromdictargs({}, ["-c", str(configuration)]) + try: + plan = discover_project(tmp_path) +> assert plan.source_roots == tuple(path.relative_to(tmp_path).as_posix() for path in native.getini("pythonpath")) +E assert ('"source', 'tree"') == ('source tree',) +E +E At index 0 diff: '"source' != 'source tree' +E Left contains one more item: 'tree"' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_worker.py:215: AssertionError +____________ test_quoted_pytest_paths_match_native_configuration['] ____________ + +tmp_path = PosixPath('/test_quoted_pytest_paths_match1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1088f70b0> +quote = "'" + + @pytest.mark.parametrize("quote", ['"', "'"]) + def test_quoted_pytest_paths_match_native_configuration(tmp_path: Path, monkeypatch, quote: str) -> None: + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + (tmp_path / "source tree").mkdir() + (tmp_path / "integration tests").mkdir() + source = tmp_path / "source tree/app name.py" + source.touch() + selected = tmp_path / "integration tests/test_app name.py" + selected.write_text("def test_example(): assert True\n") + configuration = tmp_path / "pytest.ini" + configuration.write_text( + "[pytest]\n" + f"testpaths = {quote}integration tests{quote}\n" + f"pythonpath = {quote}source tree{quote}\n" + f"python_files = {quote}test_* name.py{quote}\n" + ) + native = pytest.Config.fromdictargs({}, ["-c", str(configuration)]) + try: + plan = discover_project(tmp_path) +> assert plan.source_roots == tuple(path.relative_to(tmp_path).as_posix() for path in native.getini("pythonpath")) +E assert ("'source", "tree'") == ('source tree',) +E +E At index 0 diff: "'source" != 'source tree' +E Left contains one more item: "tree'" +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_portable_worker.py:215: AssertionError +______ test_invalid_pytest_quoting_names_configuration_option[pythonpath] ______ + +tmp_path = PosixPath('/test_invalid_pytest_quoting_na0') +option = 'pythonpath' + + @pytest.mark.parametrize("option", ["pythonpath", "testpaths", "python_files", "norecursedirs"]) + def test_invalid_pytest_quoting_names_configuration_option(tmp_path: Path, option: str) -> None: + (tmp_path / "pytest.ini").write_text(f'[pytest]\n{option} = "unterminated\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:pytest.ini:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_portable_worker.py:226: Failed +______ test_invalid_pytest_quoting_names_configuration_option[testpaths] _______ + +tmp_path = PosixPath('/test_invalid_pytest_quoting_na1') +option = 'testpaths' + + @pytest.mark.parametrize("option", ["pythonpath", "testpaths", "python_files", "norecursedirs"]) + def test_invalid_pytest_quoting_names_configuration_option(tmp_path: Path, option: str) -> None: + (tmp_path / "pytest.ini").write_text(f'[pytest]\n{option} = "unterminated\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:pytest.ini:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_portable_worker.py:226: Failed +_____ test_invalid_pytest_quoting_names_configuration_option[python_files] _____ + +tmp_path = PosixPath('/test_invalid_pytest_quoting_na2') +option = 'python_files' + + @pytest.mark.parametrize("option", ["pythonpath", "testpaths", "python_files", "norecursedirs"]) + def test_invalid_pytest_quoting_names_configuration_option(tmp_path: Path, option: str) -> None: + (tmp_path / "pytest.ini").write_text(f'[pytest]\n{option} = "unterminated\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:pytest.ini:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_portable_worker.py:226: Failed +____ test_invalid_pytest_quoting_names_configuration_option[norecursedirs] _____ + +tmp_path = PosixPath('/test_invalid_pytest_quoting_na3') +option = 'norecursedirs' + + @pytest.mark.parametrize("option", ["pythonpath", "testpaths", "python_files", "norecursedirs"]) + def test_invalid_pytest_quoting_names_configuration_option(tmp_path: Path, option: str) -> None: + (tmp_path / "pytest.ini").write_text(f'[pytest]\n{option} = "unterminated\n') +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:pytest.ini:{option}"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_portable_worker.py:226: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_quoted_pytest_paths_match_native_configuration["] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_quoted_pytest_paths_match_native_configuration['] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_invalid_pytest_quoting_names_configuration_option[pythonpath] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_invalid_pytest_quoting_names_configuration_option[testpaths] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_invalid_pytest_quoting_names_configuration_option[python_files] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_invalid_pytest_quoting_names_configuration_option[norecursedirs] +======================= 6 failed, 34 deselected in 0.27s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_RECURSION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_RECURSION_RED.txt new file mode 100644 index 00000000..688e7802 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_RECURSION_RED.txt @@ -0,0 +1,455 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 31 items + +tests/unit/specfact_code_review/run/test_portable_worker.py ............ [ 38%] +....FFFFFFFFFFFF... [100%] + +=================================== FAILURES =================================== +____ test_default_recursion_exclusions_prevent_false_test_ambiguity[build] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi0') +directory = 'build' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'build/test_app.py', 'tests/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_____ test_default_recursion_exclusions_prevent_false_test_ambiguity[dist] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi1') +directory = 'dist' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'dist/test_app.py', 'tests/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +____ test_default_recursion_exclusions_prevent_false_test_ambiguity[.cache] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi2') +directory = '.cache' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'.cache/test_app.py', 'tests/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_ test_default_recursion_exclusions_prevent_false_test_ambiguity[package.egg] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi3') +directory = 'package.egg' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py' +candidates = {'package.egg/test_app.py', 'tests/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +____ test_default_recursion_exclusions_prevent_false_test_ambiguity[_darcs] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi4') +directory = '_darcs' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'_darcs/test_app.py', 'tests/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_____ test_default_recursion_exclusions_prevent_false_test_ambiguity[CVS] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi5') +directory = 'CVS' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'CVS/test_app.py', 'tests/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_____ test_default_recursion_exclusions_prevent_false_test_ambiguity[venv] _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi6') +directory = 'venv' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'tests/test_app.py', 'venv/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +____ test_default_recursion_exclusions_prevent_false_test_ambiguity[{arch}] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_default_recursion_exclusi7') +directory = '{arch}' + + @pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) + def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() +> assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:149: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py', candidates = {'tests/test_app.py', '{arch}/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_____ test_custom_recursion_exclusions_replace_pytest_defaults[patterns0] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_custom_recursion_exclusio0') +patterns = ['generated'] + + @pytest.mark.parametrize("patterns", [["generated"], "generated", ["fixtures/generated"], "fixtures/generated"]) + def test_custom_recursion_exclusions_replace_pytest_defaults(tmp_path: Path, patterns: object) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("build", "fixtures/generated"): + (tmp_path / name).mkdir(parents=True) + (tmp_path / name / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) +> assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:160: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py' +candidates = {'build/test_app.py', 'fixtures/generated/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_____ test_custom_recursion_exclusions_replace_pytest_defaults[generated] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_custom_recursion_exclusio1') +patterns = 'generated' + + @pytest.mark.parametrize("patterns", [["generated"], "generated", ["fixtures/generated"], "fixtures/generated"]) + def test_custom_recursion_exclusions_replace_pytest_defaults(tmp_path: Path, patterns: object) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("build", "fixtures/generated"): + (tmp_path / name).mkdir(parents=True) + (tmp_path / name / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) +> assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:160: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py' +candidates = {'build/test_app.py', 'fixtures/generated/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_____ test_custom_recursion_exclusions_replace_pytest_defaults[patterns2] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_custom_recursion_exclusio2') +patterns = ['fixtures/generated'] + + @pytest.mark.parametrize("patterns", [["generated"], "generated", ["fixtures/generated"], "fixtures/generated"]) + def test_custom_recursion_exclusions_replace_pytest_defaults(tmp_path: Path, patterns: object) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("build", "fixtures/generated"): + (tmp_path / name).mkdir(parents=True) + (tmp_path / name / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) +> assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:160: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py' +candidates = {'build/test_app.py', 'fixtures/generated/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +_ test_custom_recursion_exclusions_replace_pytest_defaults[fixtures/generated] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-208/test_custom_recursion_exclusio3') +patterns = 'fixtures/generated' + + @pytest.mark.parametrize("patterns", [["generated"], "generated", ["fixtures/generated"], "fixtures/generated"]) + def test_custom_recursion_exclusions_replace_pytest_defaults(tmp_path: Path, patterns: object) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("build", "fixtures/generated"): + (tmp_path / name).mkdir(parents=True) + (tmp_path / name / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) +> assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_worker.py:160: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:70: in select_test_paths + selected.update(_matching_source_tests(relative, candidates)) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +relative = 'app.py' +candidates = {'build/test_app.py', 'fixtures/generated/test_app.py'} + + def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: +> raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_ambiguous:app.py; include explicit test paths + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:37: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[build] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[dist] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[.cache] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[package.egg] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[_darcs] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[CVS] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[venv] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_default_recursion_exclusions_prevent_false_test_ambiguity[{arch}] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_custom_recursion_exclusions_replace_pytest_defaults[patterns0] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_custom_recursion_exclusions_replace_pytest_defaults[generated] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_custom_recursion_exclusions_replace_pytest_defaults[patterns2] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_custom_recursion_exclusions_replace_pytest_defaults[fixtures/generated] +======================== 12 failed, 19 passed in 0.96s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_SETUP_EXECUTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_SETUP_EXECUTION_RED.txt new file mode 100644 index 00000000..596646e6 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_SETUP_EXECUTION_RED.txt @@ -0,0 +1,56 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 16 items / 11 deselected / 5 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py F. [ 40%] +... [100%] + +=================================== FAILURES =================================== +_______ test_mixed_phase_failures_preserve_actual_body_execution[setup] ________ + +tmp_path = PosixPath('/test_mixed_phase_failures_pres0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b050050> +phase = 'setup' + + @pytest.mark.parametrize("phase", ["setup", "call", "teardown"]) + def test_mixed_phase_failures_preserve_actual_body_execution(tmp_path: Path, monkeypatch, phase: str) -> None: + effects = { + "setup": " raise RuntimeError('controlled setup failure')\n yield\n", + "call": " yield\n", + "teardown": " yield\n raise RuntimeError('controlled teardown failure')\n", + } + body = "assert False" if phase == "call" else "assert True" + (tmp_path / "pytest.ini").write_text("[pytest]\n") + (tmp_path / "test_case.py").write_text( + "import pytest\n" + "@pytest.fixture\n" + "def fixture_effect():\n" + f"{effects[phase]}" + f"def test_affected(fixture_effect): {body}\n" + "def test_healthy(): assert True\n" + ) + observation, output = _observe_discovery(tmp_path, tmp_path / "phases.json", ("test_case.py",)) + assert observation["exit_code"] == 1, output + executed = {row["nodeid"] for row in observation["records"] if row["phase"] == "call"} + assert "test_case.py::test_healthy" in executed + assert ("test_case.py::test_affected" in executed) == (phase != "setup") + findings = _parse_recorded_observation(tmp_path, observation, monkeypatch) + failures = [finding for finding in findings if finding.rule == "TEST_OUTCOME_NOT_PASS"] + assert len(failures) == 1 + assert f"failed during {phase}" in failures[0].message + response = runner._capsule_member_response("targeted-pytest-coverage", findings) + if phase == "setup": +> assert response["execution_state"] == "error" +E AssertionError: assert 'ran' == 'error' +E +E - error +E + ran + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:187: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_mixed_phase_failures_preserve_actual_body_execution[setup] +================== 1 failed, 4 passed, 11 deselected in 1.19s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTEST_TABLE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_TABLE_RED.txt new file mode 100644 index 00000000..108fe5c5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTEST_TABLE_RED.txt @@ -0,0 +1,1441 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 143 items / 109 deselected / 34 selected + +tests/unit/specfact_code_review/run/test_runtime_discovery.py FFFFFFFFFF [ 29%] +FFFFFFFFFFFFFFFFFF [ 82%] +tests/unit/specfact_code_review/run/test_portable_snapshot.py .FFF.. [100%] + +=================================== FAILURES =================================== +__ test_malformed_pytest_table_has_project_diagnostic[1-pytest.toml-pytest-] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha0') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = '1' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha0') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +__ test_malformed_pytest_table_has_project_diagnostic[1-.pytest.toml-pytest-] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha1') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = '1' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha1') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[1-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha2') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = '1' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha2') +project = {'tool': {'pytest': 1}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic[1-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha3') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = '1' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha3') +project = {'tool': {'pytest': {'ini_options': 1}}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: TypeError +__ test_malformed_pytest_table_has_project_diagnostic[0-pytest.toml-pytest-] ___ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha4') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = '0' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha4') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +__ test_malformed_pytest_table_has_project_diagnostic[0-.pytest.toml-pytest-] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha5') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = '0' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha5') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[0-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha6') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = '0' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha6') +project = {'tool': {'pytest': 0}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: + return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + continue + path = root / name + if not path.exists(): + continue + parser = _ini(path) + section = "tool:pytest" if name == "setup.cfg" else "pytest" + if parser.has_section(section): + return _validated_pytest_paths(dict(parser[section]), name) + tools = project.get("tool", {}) + pytest = tools.get("pytest", {}) +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), "pyproject.toml") + ^^^^^^^^^^ +E AttributeError: 'int' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:233: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic[0-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha7') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = '0' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha7') +project = {'tool': {'pytest': {'ini_options': 0}}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[true-pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha8') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = 'true' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha8') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[true-.pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha9') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = 'true' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha9') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[true-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha10') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = 'true' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha10') +project = {'tool': {'pytest': True}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic[true-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha11') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = 'true' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha11') +project = {'tool': {'pytest': {'ini_options': True}}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[false-pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha12') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = 'false' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha12') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[false-.pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha13') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = 'false' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha13') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_pytest_table_has_project_diagnostic[false-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha14') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = 'false' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha14') +project = {'tool': {'pytest': False}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: + return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + continue + path = root / name + if not path.exists(): + continue + parser = _ini(path) + section = "tool:pytest" if name == "setup.cfg" else "pytest" + if parser.has_section(section): + return _validated_pytest_paths(dict(parser[section]), name) + tools = project.get("tool", {}) + pytest = tools.get("pytest", {}) +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), "pyproject.toml") + ^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:233: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic[false-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha15') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = 'false' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha15') +project = {'tool': {'pytest': {'ini_options': False}}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'bool' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: TypeError +__ test_malformed_pytest_table_has_project_diagnostic[[]-pytest.toml-pytest-] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha16') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = '[]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:356: Failed +_ test_malformed_pytest_table_has_project_diagnostic[[]-.pytest.toml-pytest-] __ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha17') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = '[]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:356: Failed +_ test_malformed_pytest_table_has_project_diagnostic[[]-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha18') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = '[]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha18') +project = {'tool': {'pytest': []}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: + return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + continue + path = root / name + if not path.exists(): + continue + parser = _ini(path) + section = "tool:pytest" if name == "setup.cfg" else "pytest" + if parser.has_section(section): + return _validated_pytest_paths(dict(parser[section]), name) + tools = project.get("tool", {}) + pytest = tools.get("pytest", {}) +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), "pyproject.toml") + ^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:233: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic[[]-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha19') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = '[]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") +> with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:356: Failed +_ test_malformed_pytest_table_has_project_diagnostic[["tests"]-pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha20') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = '["tests"]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha20') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: dictionary update sequence element #0 has length 5; 2 is required + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: ValueError +_ test_malformed_pytest_table_has_project_diagnostic[["tests"]-.pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha21') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = '["tests"]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha21') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: dictionary update sequence element #0 has length 5; 2 is required + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: ValueError +_ test_malformed_pytest_table_has_project_diagnostic[["tests"]-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha22') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = '["tests"]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha22') +project = {'tool': {'pytest': ['tests']}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^ +E AttributeError: 'list' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic[["tests"]-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha23') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = '["tests"]' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha23') +project = {'tool': {'pytest': {'ini_options': ['tests']}}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: dictionary update sequence element #0 has length 5; 2 is required + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: ValueError +_ test_malformed_pytest_table_has_project_diagnostic["invalid"-pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha24') +filename = 'pytest.toml', section = 'pytest', prefix = '', value = '"invalid"' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha24') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: dictionary update sequence element #0 has length 1; 2 is required + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: ValueError +_ test_malformed_pytest_table_has_project_diagnostic["invalid"-.pytest.toml-pytest-] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha25') +filename = '.pytest.toml', section = 'pytest', prefix = '', value = '"invalid"' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha25') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: dictionary update sequence element #0 has length 1; 2 is required + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: ValueError +_ test_malformed_pytest_table_has_project_diagnostic["invalid"-pyproject.toml-tool.pytest-[tool]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha26') +filename = 'pyproject.toml', section = 'tool.pytest', prefix = '[tool]\n' +value = '"invalid"' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha26') +project = {'tool': {'pytest': 'invalid'}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^ +E AttributeError: 'str' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: AttributeError +_ test_malformed_pytest_table_has_project_diagnostic["invalid"-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha27') +filename = 'pyproject.toml', section = 'tool.pytest.ini_options' +prefix = '[tool.pytest]\n', value = '"invalid"' + + @pytest.mark.parametrize("filename,section,prefix", [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ]) + @pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) + def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str + ) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{section}; expected a table"): +> discover_project(tmp_path) + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:357: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_pytest_table_ha27') +project = {'tool': {'pytest': {'ini_options': 'invalid'}}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E ValueError: dictionary update sequence element #0 has length 1; 2 is required + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:222: ValueError +_ test_malformed_configuration_preserves_independent_analyzer_evidence[pytest.toml-pytest=1\n-project_pytest_config_invalid:pytest.toml:pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_configuration_p1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108d20fc0> +filename = 'pytest.toml', contents = 'pytest=1\n' +diagnostic = 'project_pytest_config_invalid:pytest.toml:pytest' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pytest.toml", "pytest=1\n", "project_pytest_config_invalid:pytest.toml:pytest"), + ("pyproject.toml", "[tool]\npytest=false\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest"), + ("pyproject.toml", "[tool.pytest]\nini_options=[]\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options"), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\npythonpath=[1]\n", + "project_pytest_config_invalid:pyproject.toml:pythonpath", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\ntestpaths=1\n", + "project_pytest_config_invalid:pyproject.toml:testpaths", + ), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:190: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:80: in run_project_snapshot + plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:50: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_configuration_p1') +project = {} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): +> return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E TypeError: 'int' object is not iterable + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:217: TypeError +_ test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool]\npytest=false\n-project_pytest_config_invalid:pyproject.toml:tool.pytest] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_configuration_p2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1080a0cb0> +filename = 'pyproject.toml', contents = '[tool]\npytest=false\n' +diagnostic = 'project_pytest_config_invalid:pyproject.toml:tool.pytest' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pytest.toml", "pytest=1\n", "project_pytest_config_invalid:pytest.toml:pytest"), + ("pyproject.toml", "[tool]\npytest=false\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest"), + ("pyproject.toml", "[tool.pytest]\nini_options=[]\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options"), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\npythonpath=[1]\n", + "project_pytest_config_invalid:pyproject.toml:pythonpath", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\ntestpaths=1\n", + "project_pytest_config_invalid:pyproject.toml:testpaths", + ), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") +> snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:190: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:80: in run_project_snapshot + plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:50: in discover_snapshot + plan = discover_project(root, config_path=config_path) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:344: in discover_project + pytest = _pytest_config(root, project) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_configuration_p2') +project = {'tool': {'pytest': False}} + + def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(dict(_toml(root / name).get("pytest", {})), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = project.get("tool", {}).get("pytest", {}) + if pytest: + return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), name) + continue + path = root / name + if not path.exists(): + continue + parser = _ini(path) + section = "tool:pytest" if name == "setup.cfg" else "pytest" + if parser.has_section(section): + return _validated_pytest_paths(dict(parser[section]), name) + tools = project.get("tool", {}) + pytest = tools.get("pytest", {}) +> return _validated_pytest_paths(dict(pytest.get("ini_options", pytest)), "pyproject.toml") + ^^^^^^^^^^ +E AttributeError: 'bool' object has no attribute 'get' + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:233: AttributeError +_ test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool.pytest]\nini_options=[]\n-project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-249/test_malformed_configuration_p3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108a7a580> +filename = 'pyproject.toml', contents = '[tool.pytest]\nini_options=[]\n' +diagnostic = 'project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options' + + @pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pytest.toml", "pytest=1\n", "project_pytest_config_invalid:pytest.toml:pytest"), + ("pyproject.toml", "[tool]\npytest=false\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest"), + ("pyproject.toml", "[tool.pytest]\nini_options=[]\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options"), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\npythonpath=[1]\n", + "project_pytest_config_invalid:pyproject.toml:pythonpath", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\ntestpaths=1\n", + "project_pytest_config_invalid:pyproject.toml:testpaths", + ), + ], + ) + def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str + ) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + assert "ruff" in calls and not set(calls) & DEPENDENT_MEMBERS +> assert evidence["diagnostic"].startswith(diagnostic) +E assert False +E + where False = ('project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options') +E + where = "[Errno 1] Operation not permitted: '/.cache/specfact'".startswith + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:194: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[1-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[1-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[1-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[1-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[0-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[0-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[0-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[0-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[true-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[true-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[true-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[true-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[false-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[false-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[false-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[false-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[[]-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[[]-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[[]-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[[]-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[["tests"]-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[["tests"]-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[["tests"]-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic[["tests"]-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic["invalid"-pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic["invalid"-.pytest.toml-pytest-] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic["invalid"-pyproject.toml-tool.pytest-[tool]\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_malformed_pytest_table_has_project_diagnostic["invalid"-pyproject.toml-tool.pytest.ini_options-[tool.pytest]\n] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[pytest.toml-pytest=1\n-project_pytest_config_invalid:pytest.toml:pytest] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool]\npytest=false\n-project_pytest_config_invalid:pyproject.toml:tool.pytest] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_malformed_configuration_preserves_independent_analyzer_evidence[pyproject.toml-[tool.pytest]\nini_options=[]\n-project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options] +================= 31 failed, 3 passed, 109 deselected in 0.56s ================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTHON_PATCH_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_PATCH_RED.txt new file mode 100644 index 00000000..9d3eb73f --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_PATCH_RED.txt @@ -0,0 +1,28 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 6 items / 5 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_runtime_builder.py F [100%] + +=================================== FAILURES =================================== +_________ test_python_patch_constraint_uses_signed_interpreter_version _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-57/test_python_patch_constraint_u0') + + def test_python_patch_constraint_uses_signed_interpreter_version(tmp_path: Path) -> None: + plan = ProjectPlan(tmp_path, manager='pip', requires_python='>=3.12.1,<3.13') + runtime = SimpleNamespace(environment_id='linux-x86_64-cp312', identity='sha256:' + 'a' * 64) +> with pytest.raises(ProjectRuntimeError, match='offline_cache_miss'): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'offline_cache_miss' +E Actual message: 'project_python_incompatible:linux-x86_64-cp312:>=3.12.1,<3.13' + +tests/unit/specfact_code_review/run/test_runtime_builder.py:61: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_python_patch_constraint_uses_signed_interpreter_version +======================= 1 failed, 5 deselected in 0.09s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTHON_SELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_SELECTION_RED.txt new file mode 100644 index 00000000..84417c6e --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_SELECTION_RED.txt @@ -0,0 +1,56 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py FFFF [100%] + +=================================== FAILURES =================================== +__ test_python_version_file_is_selected_and_explicit_config_takes_precedence ___ + +tmp_path = PosixPath('/pytest-of-dom/pytest-94/test_python_version_file_is_se0') + + def test_python_version_file_is_selected_and_explicit_config_takes_precedence(tmp_path: Path) -> None: + (tmp_path / '.python-version').write_text('3.11\n') +> assert discover_project(tmp_path).python == '3.11' +E AssertionError: assert '' == '3.11' +E +E - 3.11 + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py:12: AssertionError +______________ test_python_selection_uses_matching_signed_worker _______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-94/test_python_selection_uses_mat0') + + def test_python_selection_uses_matching_signed_worker(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_interpreter import select_environment +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_interpreter' + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py:19: ModuleNotFoundError +________________ test_python_selection_rejects_conflicting_pin _________________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-94/test_python_selection_rejects_0') + + def test_python_selection_rejects_conflicting_pin(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_interpreter import select_environment +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_interpreter' + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py:25: ModuleNotFoundError +______ test_python_constraints_can_select_compatible_worker_without_a_pin ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-94/test_python_constraints_can_se0') + + def test_python_constraints_can_select_compatible_worker_without_a_pin(tmp_path: Path) -> None: +> from specfact_code_review.run.runtime_interpreter import select_environment +E ModuleNotFoundError: No module named 'specfact_code_review.run.runtime_interpreter' + +tests/unit/specfact_code_review/run/test_runtime_interpreter.py:33: ModuleNotFoundError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_interpreter.py::test_python_version_file_is_selected_and_explicit_config_takes_precedence +FAILED tests/unit/specfact_code_review/run/test_runtime_interpreter.py::test_python_selection_uses_matching_signed_worker +FAILED tests/unit/specfact_code_review/run/test_runtime_interpreter.py::test_python_selection_rejects_conflicting_pin +FAILED tests/unit/specfact_code_review/run/test_runtime_interpreter.py::test_python_constraints_can_select_compatible_worker_without_a_pin +============================== 4 failed in 0.12s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTHON_VERSION_TRIGGER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_VERSION_TRIGGER_RED.txt new file mode 100644 index 00000000..ea176960 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_VERSION_TRIGGER_RED.txt @@ -0,0 +1,113 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 -- /worktree/.venv/bin/python +cachedir: .pytest_cache +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collecting ... collected 23 items / 20 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_python_version_only_project_routes_preparation_and_preserves_static_fallback[None] PASSED [ 33%] +tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_python_version_only_project_routes_preparation_and_preserves_static_fallback[3.12] FAILED [ 66%] +tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_python_version_only_project_routes_preparation_and_preserves_static_fallback[3.99] FAILED [100%] + +=================================== FAILURES =================================== +_ test_python_version_only_project_routes_preparation_and_preserves_static_fallback[3.12] _ + +tmp_path = PosixPath('/test_python_version_only_proje1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108bfba80> +python_pin = '3.12' + + @pytest.mark.parametrize("python_pin", [None, "3.12", "3.99"]) + def test_python_version_only_project_routes_preparation_and_preserves_static_fallback( + tmp_path: Path, monkeypatch, python_pin: str | None + ) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + if python_pin is not None: + (tmp_path / ".python-version").write_text(python_pin + "\n") + monkeypatch.chdir(tmp_path) + prepared_pins = [] + members = [] + local_snapshot = runner.CapsuleSnapshotResult({}, {}) + + def prepare_project(plan, **_kwargs): + prepared_pins.append(plan.python) + raise ProjectRuntimeError("project_native_library_missing:libfixture.so") + + def dispatch(request, **_kwargs): + members.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare_project) + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + monkeypatch.setattr(runner, "_run_local_capsule_snapshot", lambda *_args, **_kwargs: local_snapshot) + monkeypatch.setattr(runner, "_finalize_local_capsule_snapshot", lambda snapshot, _context: snapshot) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + evidence = {"assurance_kind": "explicit_files"} + snapshot = runner._run_local_capsule_context(runtime, [source], runner.ReviewOptions(), evidence, "explicit_files") + if python_pin is None: + assert snapshot is local_snapshot + assert not prepared_pins and not members + assert "project_runtime" not in evidence + else: +> assert prepared_pins == (["3.12"] if python_pin == "3.12" else []) +E AssertionError: assert [] == ['3.12'] +E +E Right contains one more item: '3.12' +E +E Full diff: +E + [] +E - [ +E - '3.12', +E - ] + +/tests/unit/specfact_code_review/run/test_portable_snapshot.py:163: AssertionError +_ test_python_version_only_project_routes_preparation_and_preserves_static_fallback[3.99] _ + +tmp_path = PosixPath('/test_python_version_only_proje2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10ade8950> +python_pin = '3.99' + + @pytest.mark.parametrize("python_pin", [None, "3.12", "3.99"]) + def test_python_version_only_project_routes_preparation_and_preserves_static_fallback( + tmp_path: Path, monkeypatch, python_pin: str | None + ) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + if python_pin is not None: + (tmp_path / ".python-version").write_text(python_pin + "\n") + monkeypatch.chdir(tmp_path) + prepared_pins = [] + members = [] + local_snapshot = runner.CapsuleSnapshotResult({}, {}) + + def prepare_project(plan, **_kwargs): + prepared_pins.append(plan.python) + raise ProjectRuntimeError("project_native_library_missing:libfixture.so") + + def dispatch(request, **_kwargs): + members.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare_project) + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + monkeypatch.setattr(runner, "_run_local_capsule_snapshot", lambda *_args, **_kwargs: local_snapshot) + monkeypatch.setattr(runner, "_finalize_local_capsule_snapshot", lambda snapshot, _context: snapshot) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + evidence = {"assurance_kind": "explicit_files"} + snapshot = runner._run_local_capsule_context(runtime, [source], runner.ReviewOptions(), evidence, "explicit_files") + if python_pin is None: + assert snapshot is local_snapshot + assert not prepared_pins and not members + assert "project_runtime" not in evidence + else: + assert prepared_pins == (["3.12"] if python_pin == "3.12" else []) +> assert "ruff" in members and not set(members) & DEPENDENT_MEMBERS +E AssertionError: assert ('ruff' in []) + +/tests/unit/specfact_code_review/run/test_portable_snapshot.py:164: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_python_version_only_project_routes_preparation_and_preserves_static_fallback[3.12] +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_python_version_only_project_routes_preparation_and_preserves_static_fallback[3.99] +================== 2 failed, 1 passed, 20 deselected in 0.79s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/PYTHON_WORKER_CLI_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_WORKER_CLI_RED.txt new file mode 100644 index 00000000..9ebcd7f0 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/PYTHON_WORKER_CLI_RED.txt @@ -0,0 +1,35 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_target_bootstrap.py .F [ 66%] +tests/unit/specfact_code_review/run/test_sitecustomize.py F [100%] + +=================================== FAILURES =================================== +______________ test_python_cli_keeps_code_separate_from_bootstrap ______________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108d57490> + + def test_python_cli_keeps_code_separate_from_bootstrap(monkeypatch) -> None: +> from specfact_code_review.run.target_bootstrap import _project_python_command +E ImportError: cannot import name '_project_python_command' from 'specfact_code_review.run.target_bootstrap' (/packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py) + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:23: ImportError +__ test_project_python_fails_closed_before_executing_code_without_descriptor ___ + + def test_project_python_fails_closed_before_executing_code_without_descriptor() -> None: + env = {**os.environ, 'PYTHONPATH': str(Path(target_bootstrap.__file__).parent), 'SPECFACT_PROJECT_PYTHON': '1'} + result = subprocess.run([sys.executable, '-s', '-c', 'print("MUST_NOT_EXECUTE")'], env=env, capture_output=True, text=True, check=False) +> assert result.returncode == 78 +E assert 0 == 78 +E + where 0 = CompletedProcess(args=['/.venv/bin/python', '-s', '-c', 'print("MUST_NOT_EXECUTE")'], returncode=0, stdout='MUST_NOT_EXECUTE\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_sitecustomize.py:12: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_python_cli_keeps_code_separate_from_bootstrap +FAILED tests/unit/specfact_code_review/run/test_sitecustomize.py::test_project_python_fails_closed_before_executing_code_without_descriptor +========================= 2 failed, 1 passed in 0.84s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/RECONSTRUCTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RECONSTRUCTION_RED.txt new file mode 100644 index 00000000..92975aa1 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RECONSTRUCTION_RED.txt @@ -0,0 +1,30 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 4 items / 3 deselected / 1 selected + +tests/unit/test_external_capsule_corpus.py F [100%] + +=================================== FAILURES =================================== +____ test_reconstruction_materializes_real_source_without_mutating_template ____ + +tmp_path = PosixPath('/pytest-of-dom/pytest-75/test_reconstruction_materializ0') + + def test_reconstruction_materializes_real_source_without_mutating_template(tmp_path: Path) -> None: + module = _load() + fixture = tmp_path / 'template' + fixture.mkdir() + template = fixture / 'test_native.py.in' + template.write_text('def test_native():\n assert True\n') + destination = tmp_path / 'checkout' +> module.materialize_reconstruction(fixture, destination) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: module 'external_capsule_corpus' has no attribute 'materialize_reconstruction' + +tests/unit/test_external_capsule_corpus.py:56: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/test_external_capsule_corpus.py::test_reconstruction_materializes_real_source_without_mutating_template +======================= 1 failed, 3 deselected in 0.02s ======================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELATIVE_INCLUDE_CLI_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELATIVE_INCLUDE_CLI_RED.txt new file mode 100644 index 00000000..bdcdcdc7 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELATIVE_INCLUDE_CLI_RED.txt @@ -0,0 +1,92 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 2 items + +tests/unit/specfact_code_review/run/test_runtime_discovery.py F [ 50%] +tests/unit/specfact_code_review/run/test_target_launch.py F [100%] + +=================================== FAILURES =================================== +______________ test_requirements_parent_include_inside_repository ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-46/test_requirements_parent_inclu0') + + def test_requirements_parent_include_inside_repository(tmp_path: Path) -> None: + (tmp_path / 'requirements').mkdir() + (tmp_path / 'requirements.txt').write_text('-r requirements/dev.txt\n') + (tmp_path / 'requirements/dev.txt').write_text('-c ../constraints.txt\nrequests\n') + (tmp_path / 'constraints.txt').write_text('requests<3\n') +> assert 'constraints.txt' in discover_project(tmp_path).inputs + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:143: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:228: in discover_project + for name in sorted(_requirements_inputs(root, requirements + constraints)): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:150: in _requirements_inputs + visit(name, frozenset()) +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:147: in visit + visit((path.parent.relative_to(root) / child).as_posix(), active | {canonical}) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +name = 'requirements/dev.txt', active = frozenset({'requirements.txt'}) + + def visit(name: str, active: frozenset[str]) -> None: + path = _safe_input(root, name) + canonical = path.relative_to(root).as_posix() + if canonical in active: + raise ProjectRuntimeError(f"project_requirements_cycle:{canonical}") + if canonical in visited: + return + if not path.is_file(): + raise ProjectRuntimeError(f"project_input_missing:{canonical}") + visited.add(canonical) + logical_lines = path.read_text(encoding="utf-8").replace("\\\n", "").splitlines() + for line in logical_lines: + tokens = shlex.split(line, comments=True) + if not tokens: + continue + flag = tokens[0] + included = "" + if flag in {"-r", "--requirement", "-c", "--constraint"} and len(tokens) >= 2: + included = tokens[1] + elif flag.startswith(("--requirement=", "--constraint=")): + included = flag.split("=", 1)[1] + elif flag.startswith(("-r", "-c")) and len(flag) > 2: + included = flag[2:] + if included: + child = Path(included) + if child.is_absolute() or ".." in child.parts: +> raise ProjectRuntimeError(f"project_input_escape:{included}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_input_escape:../constraints.txt + +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:146: ProjectRuntimeError +_________ test_project_python_preserves_extensionless_script_arguments _________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a44b360> + + def test_project_python_preserves_extensionless_script_arguments(monkeypatch) -> None: + from specfact_code_review.run import target_launch + + captured = [] + monkeypatch.setattr(target_launch.sys, 'argv', ['python', 'manage', '--version']) + monkeypatch.setattr(target_launch.os, 'execv', lambda executable, argv: captured.append(argv)) + target_launch.main() +> assert captured[0][-3:] == ['python-argv', 'manage', '--version'] +E AssertionError: assert ['/opt/specfa..., '--version'] == ['python-argv..., '--version'] +E +E At index 0 diff: '/opt/specfact/builtin/specfact_code_review/run/target_bootstrap.py' != 'python-argv' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_target_launch.py:25: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_requirements_parent_include_inside_repository +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_project_python_preserves_extensionless_script_arguments +============================== 2 failed in 0.10s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELATIVE_SELECTION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELATIVE_SELECTION_RED.txt new file mode 100644 index 00000000..9a96fc19 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELATIVE_SELECTION_RED.txt @@ -0,0 +1,73 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 10 items + +tests/unit/specfact_code_review/run/test_portable_snapshot.py ......F... [100%] + +=================================== FAILURES =================================== +_______ test_analysis_source_copy_excludes_local_environment_files[True] _______ + +tmp_path = PosixPath('/pytest-of-user/pytest-176/test_analysis_source_copy_excl1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10c8b1590> +relative = True + + @pytest.mark.parametrize("relative", [False, True]) + def test_analysis_source_copy_excludes_local_environment_files(tmp_path: Path, monkeypatch, relative: bool) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / ".env").write_text("SYNTHETIC_SECRET=fixture\n") + (source / ".venv").mkdir() + (source / ".venv/private.txt").write_text("synthetic excluded fixture") + observed = [] + + def analyze(_runtime, *, snapshot_root, files, **_context): + assert snapshot_root != source + assert not (snapshot_root / ".env").exists() + assert not (snapshot_root / ".venv").exists() + assert files == [snapshot_root / "app.py"] + assert files[0].read_text() == "VALUE = 1\n" + observed.append(snapshot_root) + return runner.CapsuleSnapshotResult({}, {}) + + monkeypatch.setattr(runner, "_run_capsule_snapshot", analyze) + selected = Path("app.py") if relative else source / "app.py" + request = ProjectSnapshotRequest(source, [selected], runner.ReviewOptions(), "explicit_files") +> portable_snapshot._run_in_private_source(object(), request, runner.CapsuleSnapshotSettings()) + +tests/unit/specfact_code_review/run/test_portable_snapshot.py:152: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py:114: in _run_in_private_source + files = [source / path.relative_to(request.snapshot_root) for path in request.files] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +self = PosixPath('app.py') +other = PosixPath('/pytest-of-user/pytest-176/test_analysis_source_copy_excl1/source') +walk_up = False + + def relative_to(self, other, *, walk_up=False): + """Return the relative path to another path identified by the passed + arguments. If the operation is not possible (because this is not + related to the other path), raise ValueError. + + The *walk_up* parameter controls whether `..` may be used to resolve + the path. + """ + if not hasattr(other, 'with_segments'): + other = self.with_segments(other) + for step, path in enumerate(chain([other], other.parents)): + if path == self or path in self.parents: + break + elif not walk_up: +> raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") +E ValueError: 'app.py' is not in the subpath of '/pytest-of-user/pytest-176/test_analysis_source_copy_excl1/source' + +/opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/pathlib/__init__.py:490: ValueError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_snapshot.py::test_analysis_source_copy_excludes_local_environment_files[True] +========================= 1 failed, 9 passed in 0.22s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELEASE_COVERAGE_ROOT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_COVERAGE_ROOT_RED.txt new file mode 100644 index 00000000..5af93d1f --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_COVERAGE_ROOT_RED.txt @@ -0,0 +1,61 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 25 items / 22 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py FFF [100%] + +=================================== FAILURES =================================== +_____________ test_nested_pytest_root_resolves_custom_test_modules _____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-542/test_nested_pytest_root_resolv0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f87490> + + def test_nested_pytest_root_resolves_custom_test_modules(tmp_path, monkeypatch): + observation = _observation(_record("check_app.py::test_pass")) + observation["pytest_root"] = "checks" + observation["test_roots"] = [] +> assert not _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_app.py")) +E AssertionError: assert not [ReviewFinding(category='tool_error', severity='error', tool='pytest', rule='tool_error', file='/private/var/folders/l... before_ref=None, after_ref=None, improvement=None, signal_trace=None, preserve_reasons=None, remediation_packet=None)] +E + where [ReviewFinding(category='tool_error', severity='error', tool='pytest', rule='tool_error', file='/private/var/folders/l... before_ref=None, after_ref=None, improvement=None, signal_trace=None, preserve_reasons=None, remediation_packet=None)] = _run(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-542/test_nested_pytest_root_resolv0'), <_pytest.monkeypatch.MonkeyPatch object at 0x109f87490>, {'exit_code': 0, 'collected': ['check_app.py::test_pass'], 'records': [{'nodeid': 'check_app.py::test_pass', 'phase': ...come': 'passed', 'wasxfail': ''}], 'coverage': {'files': {'src/app.py': {'summary': {'percent_covered': 100.0}}}}, ...}, ('src/app.py', 'checks/check_app.py')) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:170: AssertionError +________ test_package_discovery_root_does_not_exempt_production_source _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-542/test_package_discovery_root_do0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f876f0> + + def test_package_discovery_root_does_not_exempt_production_source(tmp_path, monkeypatch): + observation = _observation( + _record("package/tests/test_app.py::test_pass"), + coverage={"package/app.py": {"summary": {"percent_covered": 0}}}, + ) + observation["test_roots"] = ["package"] + findings = _run(tmp_path, monkeypatch, observation, ("package/app.py", "package/tests/test_app.py")) +> assert any(item.rule == "TEST_COVERAGE_LOW" for item in findings) +E assert False +E + where False = any(. at 0x10b241490>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:180: AssertionError +___________________ test_external_pytest_root_is_incomplete ____________________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-542/test_external_pytest_root_is_i0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10b123530> + + def test_external_pytest_root_is_incomplete(tmp_path, monkeypatch): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = str(tmp_path.parent) + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == "tool_error" and "pytest_root" in item.message for item in findings) +E assert False +E + where False = any(. at 0x10b241cb0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:187: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_nested_pytest_root_resolves_custom_test_modules +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_package_discovery_root_does_not_exempt_production_source +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_external_pytest_root_is_incomplete +======================= 3 failed, 22 deselected in 0.19s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELEASE_EMPTY_XPASS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_EMPTY_XPASS_RED.txt new file mode 100644 index 00000000..ff87ffec --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_EMPTY_XPASS_RED.txt @@ -0,0 +1,29 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 22 items / 21 deselected / 1 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py F [100%] + +=================================== FAILURES =================================== +_____________________ test_empty_reason_xpass_cannot_pass ______________________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-538/test_empty_reason_xpass_cannot0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1081e2d70> + + def test_empty_reason_xpass_cannot_pass(tmp_path, monkeypatch): + record = _record("tests/test_app.py::test_xpass") + record["has_xfail"] = True + observation = _observation(record) + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == "TEST_OUTCOME_NOT_PASS" for item in findings) +E assert False +E + where False = any(. at 0x1087fdf20>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:136: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_empty_reason_xpass_cannot_pass +======================= 1 failed, 21 deselected in 0.19s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELEASE_NATIVE_POLICY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_NATIVE_POLICY_RED.txt new file mode 100644 index 00000000..5da52164 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_NATIVE_POLICY_RED.txt @@ -0,0 +1,277 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 13 items + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py FFFFF [ 38%] +FFFFF... [100%] + +=================================== FAILURES =================================== +_ test_native_coverage_threshold_preserves_configuration_and_cli_precedence[arguments0-70.0] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_coverage_threshold0') +arguments = [], expected = 70.0 + + @pytest.mark.parametrize( + "arguments,expected", [([], 70.0), (["--cov-fail-under=60"], 60.0), (["--cov-config=alternate.ini"], 90.0)] + ) + def test_native_coverage_threshold_preserves_configuration_and_cli_precedence( + tmp_path: Path, arguments: list[str], expected: float + ) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "pytest.ini").write_text("[pytest]\n") + (project / ".coveragerc").write_text("[report]\nfail_under = 70\n") + (project / "alternate.ini").write_text("[report]\nfail_under = 90\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "policy.json", arguments) +> assert result["coverage_threshold"] == expected +E assert None == 70.0 + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:83: AssertionError +_ test_native_coverage_threshold_preserves_configuration_and_cli_precedence[arguments1-60.0] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_coverage_threshold1') +arguments = ['--cov-fail-under=60'], expected = 60.0 + + @pytest.mark.parametrize( + "arguments,expected", [([], 70.0), (["--cov-fail-under=60"], 60.0), (["--cov-config=alternate.ini"], 90.0)] + ) + def test_native_coverage_threshold_preserves_configuration_and_cli_precedence( + tmp_path: Path, arguments: list[str], expected: float + ) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "pytest.ini").write_text("[pytest]\n") + (project / ".coveragerc").write_text("[report]\nfail_under = 70\n") + (project / "alternate.ini").write_text("[report]\nfail_under = 90\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "policy.json", arguments) +> assert result["coverage_threshold"] == expected +E assert None == 60.0 + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:83: AssertionError +_ test_native_coverage_threshold_preserves_configuration_and_cli_precedence[arguments2-90.0] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_coverage_threshold2') +arguments = ['--cov-config=alternate.ini'], expected = 90.0 + + @pytest.mark.parametrize( + "arguments,expected", [([], 70.0), (["--cov-fail-under=60"], 60.0), (["--cov-config=alternate.ini"], 90.0)] + ) + def test_native_coverage_threshold_preserves_configuration_and_cli_precedence( + tmp_path: Path, arguments: list[str], expected: float + ) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "pytest.ini").write_text("[pytest]\n") + (project / ".coveragerc").write_text("[report]\nfail_under = 70\n") + (project / "alternate.ini").write_text("[report]\nfail_under = 90\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "policy.json", arguments) +> assert result["coverage_threshold"] == expected +E assert None == 90.0 + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:83: AssertionError +_ test_native_test_support_roots_expand_from_configuration_root[pytest.ini-tests-expected0] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots0') +configuration = 'pytest.ini', testpaths = 'tests', expected = ['tests'] + + @pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], + ) + def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] + ) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) +> assert result["test_roots"] == expected +E AssertionError: assert None == ['tests'] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:105: AssertionError +_ test_native_test_support_roots_expand_from_configuration_root[pytest.ini-tests/*-expected1] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots1') +configuration = 'pytest.ini', testpaths = 'tests/*', expected = ['tests/unit'] + + @pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], + ) + def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] + ) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) +> assert result["test_roots"] == expected +E AssertionError: assert None == ['tests/unit'] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:105: AssertionError +_ test_native_test_support_roots_expand_from_configuration_root[pytest.ini-.-expected2] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots2') +configuration = 'pytest.ini', testpaths = '.', expected = [] + + @pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], + ) + def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] + ) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) +> assert result["test_roots"] == expected +E assert None == [] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:105: AssertionError +_ test_native_test_support_roots_expand_from_configuration_root[pytest.ini-tests/unit/test_policy.py-expected3] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots3') +configuration = 'pytest.ini', testpaths = 'tests/unit/test_policy.py' +expected = [] + + @pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], + ) + def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] + ) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) +> assert result["test_roots"] == expected +E assert None == [] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:105: AssertionError +_ test_native_test_support_roots_expand_from_configuration_root[tests/pytest.ini-.-expected4] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots4') +configuration = 'tests/pytest.ini', testpaths = '.', expected = ['tests'] + + @pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], + ) + def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] + ) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) +> assert result["test_roots"] == expected +E AssertionError: assert None == ['tests'] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:105: AssertionError +_ test_native_test_support_roots_expand_from_configuration_root[tests/pytest.ini-unit-expected5] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots5') +configuration = 'tests/pytest.ini', testpaths = 'unit' +expected = ['tests/unit'] + + @pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], + ) + def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] + ) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) +> assert result["test_roots"] == expected +E AssertionError: assert None == ['tests/unit'] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:105: AssertionError +________ test_native_test_support_roots_exclude_paths_outside_snapshot _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-533/test_native_test_support_roots6') + + def test_native_test_support_roots_exclude_paths_outside_snapshot(tmp_path: Path) -> None: + project = tmp_path / "project" + project.mkdir() + outside = tmp_path / "outside" + outside.mkdir() + (outside / "test_external.py").write_text("def test_external(): assert True\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + (project / "pytest.ini").write_text("[pytest]\ntestpaths = ../outside\n") + result = _observe_policy(project, tmp_path / "outside.json", ["test_policy.py"]) +> assert result["test_roots"] == [] +E assert None == [] + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:117: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_coverage_threshold_preserves_configuration_and_cli_precedence[arguments0-70.0] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_coverage_threshold_preserves_configuration_and_cli_precedence[arguments1-60.0] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_coverage_threshold_preserves_configuration_and_cli_precedence[arguments2-90.0] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_expand_from_configuration_root[pytest.ini-tests-expected0] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_expand_from_configuration_root[pytest.ini-tests/*-expected1] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_expand_from_configuration_root[pytest.ini-.-expected2] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_expand_from_configuration_root[pytest.ini-tests/unit/test_policy.py-expected3] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_expand_from_configuration_root[tests/pytest.ini-.-expected4] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_expand_from_configuration_root[tests/pytest.ini-unit-expected5] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_test_support_roots_exclude_paths_outside_snapshot +========================= 10 failed, 3 passed in 3.35s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELEASE_NATIVE_ROOT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_NATIVE_ROOT_RED.txt new file mode 100644 index 00000000..221d60cd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_NATIVE_ROOT_RED.txt @@ -0,0 +1,74 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 17 items / 8 deselected / 9 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py ..... [ 55%] +.FFF [100%] + +=================================== FAILURES =================================== +______ test_native_custom_test_nodeids_retain_their_configuration_root[.] ______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_custom_test_nodeid0') +directory = '.' + + @pytest.mark.parametrize("directory", [".", "checks"]) + def test_native_custom_test_nodeids_retain_their_configuration_root(tmp_path: Path, directory: str) -> None: + project = tmp_path / "project" + config_root = project / directory + config_root.mkdir(parents=True) + configuration = config_root / "pytest.ini" + configuration.write_text("[pytest]\ntestpaths = check_app.py\npython_files = check_*.py\n") + (config_root / "check_app.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "root-context.json", ["-c", str(configuration)]) + assert result["collected"] == ["check_app.py::test_valid"] +> assert result["pytest_root"] == directory +E AssertionError: assert None == '.' + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:131: AssertionError +___ test_native_custom_test_nodeids_retain_their_configuration_root[checks] ____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_custom_test_nodeid1') +directory = 'checks' + + @pytest.mark.parametrize("directory", [".", "checks"]) + def test_native_custom_test_nodeids_retain_their_configuration_root(tmp_path: Path, directory: str) -> None: + project = tmp_path / "project" + config_root = project / directory + config_root.mkdir(parents=True) + configuration = config_root / "pytest.ini" + configuration.write_text("[pytest]\ntestpaths = check_app.py\npython_files = check_*.py\n") + (config_root / "check_app.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "root-context.json", ["-c", str(configuration)]) + assert result["collected"] == ["check_app.py::test_valid"] +> assert result["pytest_root"] == directory +E AssertionError: assert None == 'checks' + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:131: AssertionError +______________ test_native_outside_configuration_root_is_explicit ______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_outside_configurat0') + + def test_native_outside_configuration_root_is_explicit(tmp_path: Path) -> None: + project = tmp_path / "project" + project.mkdir() + configuration = tmp_path / "pytest.ini" + configuration.write_text("[pytest]\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "outside-root.json", ["-c", str(configuration), "test_policy.py"]) + assert result["collected"] == ["project/test_policy.py::test_valid"] +> assert result["pytest_root"] == str(tmp_path.resolve()) +E AssertionError: assert None == '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_outside_configurat0' +E + where '/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_outside_configurat0' = str(PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_outside_configurat0')) +E + where PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_outside_configurat0') = resolve() +E + where resolve = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-543/test_native_outside_configurat0').resolve + +tests/unit/specfact_code_review/run/test_portable_pytest_policy.py:142: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_custom_test_nodeids_retain_their_configuration_root[.] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_custom_test_nodeids_retain_their_configuration_root[checks] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_policy.py::test_native_outside_configuration_root_is_explicit +================== 3 failed, 6 passed, 8 deselected in 1.46s =================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/RELEASE_PYTEST_EVIDENCE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_PYTEST_EVIDENCE_RED.txt new file mode 100644 index 00000000..4f8d5704 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/RELEASE_PYTEST_EVIDENCE_RED.txt @@ -0,0 +1,348 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-477-pytest-release +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 21 items + +tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py FFF [ 14%] +F.FFFF...FFFFFFFFF [100%] + +=================================== FAILURES =================================== +___________ test_mixed_non_pass_outcomes_cannot_pass[skipped-setup-] ___________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_mixed_non_pass_outcomes_c0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a115f30> +outcome = 'skipped', phase = 'setup', xfail = '' + + @pytest.mark.parametrize("outcome,phase,xfail", [ + ("skipped", "setup", ""), ("skipped", "call", ""), + ("skipped", "call", "known bug"), ("passed", "call", "known bug"), + ("failed", "teardown", ""), + ]) + def test_mixed_non_pass_outcomes_cannot_pass(tmp_path, monkeypatch, outcome, phase, xfail): + observation = _observation( + _record("tests/test_app.py::test_pass"), + _record("tests/test_app.py::test_other", outcome, phase, xfail), + ) + if outcome == "failed": + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == "TEST_OUTCOME_NOT_PASS" and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a1dedc0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:61: AssertionError +___________ test_mixed_non_pass_outcomes_cannot_pass[skipped-call-] ____________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_mixed_non_pass_outcomes_c1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a115a70> +outcome = 'skipped', phase = 'call', xfail = '' + + @pytest.mark.parametrize("outcome,phase,xfail", [ + ("skipped", "setup", ""), ("skipped", "call", ""), + ("skipped", "call", "known bug"), ("passed", "call", "known bug"), + ("failed", "teardown", ""), + ]) + def test_mixed_non_pass_outcomes_cannot_pass(tmp_path, monkeypatch, outcome, phase, xfail): + observation = _observation( + _record("tests/test_app.py::test_pass"), + _record("tests/test_app.py::test_other", outcome, phase, xfail), + ) + if outcome == "failed": + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == "TEST_OUTCOME_NOT_PASS" and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a1df440>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:61: AssertionError +_______ test_mixed_non_pass_outcomes_cannot_pass[skipped-call-known bug] _______ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_mixed_non_pass_outcomes_c2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109bc7e30> +outcome = 'skipped', phase = 'call', xfail = 'known bug' + + @pytest.mark.parametrize("outcome,phase,xfail", [ + ("skipped", "setup", ""), ("skipped", "call", ""), + ("skipped", "call", "known bug"), ("passed", "call", "known bug"), + ("failed", "teardown", ""), + ]) + def test_mixed_non_pass_outcomes_cannot_pass(tmp_path, monkeypatch, outcome, phase, xfail): + observation = _observation( + _record("tests/test_app.py::test_pass"), + _record("tests/test_app.py::test_other", outcome, phase, xfail), + ) + if outcome == "failed": + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == "TEST_OUTCOME_NOT_PASS" and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a1dfed0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:61: AssertionError +_______ test_mixed_non_pass_outcomes_cannot_pass[passed-call-known bug] ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_mixed_non_pass_outcomes_c3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a2245a0> +outcome = 'passed', phase = 'call', xfail = 'known bug' + + @pytest.mark.parametrize("outcome,phase,xfail", [ + ("skipped", "setup", ""), ("skipped", "call", ""), + ("skipped", "call", "known bug"), ("passed", "call", "known bug"), + ("failed", "teardown", ""), + ]) + def test_mixed_non_pass_outcomes_cannot_pass(tmp_path, monkeypatch, outcome, phase, xfail): + observation = _observation( + _record("tests/test_app.py::test_pass"), + _record("tests/test_app.py::test_other", outcome, phase, xfail), + ) + if outcome == "failed": + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == "TEST_OUTCOME_NOT_PASS" and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a29b370>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:61: AssertionError +_ test_each_reviewed_source_requires_sufficient_coverage[coverage0-None-tool_error] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_each_reviewed_source_requ0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a203050> +coverage = {'src/other.py': {'summary': {'percent_covered': 100}}} +threshold = None, expected = 'tool_error' + + @pytest.mark.parametrize("coverage,threshold,expected", [ + ({"src/other.py": {"summary": {"percent_covered": 100}}}, None, "tool_error"), + ({"src/app.py": {"summary": {"percent_covered": 20}}}, None, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 85}}, + "src/other.py": {"summary": {"percent_covered": 100}}}, 90, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 79}}}, 20, "TEST_COVERAGE_LOW"), + ]) + def test_each_reviewed_source_requires_sufficient_coverage(tmp_path, monkeypatch, coverage, threshold, expected): + observation = _observation(_record("tests/test_app.py::test_pass"), coverage=coverage, threshold=threshold) + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == expected and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a20b220>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:76: AssertionError +_ test_each_reviewed_source_requires_sufficient_coverage[coverage1-None-TEST_COVERAGE_LOW] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_each_reviewed_source_requ1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a203250> +coverage = {'src/app.py': {'summary': {'percent_covered': 20}}} +threshold = None, expected = 'TEST_COVERAGE_LOW' + + @pytest.mark.parametrize("coverage,threshold,expected", [ + ({"src/other.py": {"summary": {"percent_covered": 100}}}, None, "tool_error"), + ({"src/app.py": {"summary": {"percent_covered": 20}}}, None, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 85}}, + "src/other.py": {"summary": {"percent_covered": 100}}}, 90, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 79}}}, 20, "TEST_COVERAGE_LOW"), + ]) + def test_each_reviewed_source_requires_sufficient_coverage(tmp_path, monkeypatch, coverage, threshold, expected): + observation = _observation(_record("tests/test_app.py::test_pass"), coverage=coverage, threshold=threshold) + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == expected and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a20a960>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:76: AssertionError +_ test_each_reviewed_source_requires_sufficient_coverage[coverage2-90-TEST_COVERAGE_LOW] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_each_reviewed_source_requ2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a222990> +coverage = {'src/app.py': {'summary': {'percent_covered': 85}}, 'src/other.py': {'summary': {'percent_covered': 100}}} +threshold = 90, expected = 'TEST_COVERAGE_LOW' + + @pytest.mark.parametrize("coverage,threshold,expected", [ + ({"src/other.py": {"summary": {"percent_covered": 100}}}, None, "tool_error"), + ({"src/app.py": {"summary": {"percent_covered": 20}}}, None, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 85}}, + "src/other.py": {"summary": {"percent_covered": 100}}}, 90, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 79}}}, 20, "TEST_COVERAGE_LOW"), + ]) + def test_each_reviewed_source_requires_sufficient_coverage(tmp_path, monkeypatch, coverage, threshold, expected): + observation = _observation(_record("tests/test_app.py::test_pass"), coverage=coverage, threshold=threshold) + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == expected and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a20a0a0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:76: AssertionError +_ test_each_reviewed_source_requires_sufficient_coverage[coverage3-20-TEST_COVERAGE_LOW] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_each_reviewed_source_requ3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a220aa0> +coverage = {'src/app.py': {'summary': {'percent_covered': 79}}}, threshold = 20 +expected = 'TEST_COVERAGE_LOW' + + @pytest.mark.parametrize("coverage,threshold,expected", [ + ({"src/other.py": {"summary": {"percent_covered": 100}}}, None, "tool_error"), + ({"src/app.py": {"summary": {"percent_covered": 20}}}, None, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 85}}, + "src/other.py": {"summary": {"percent_covered": 100}}}, 90, "TEST_COVERAGE_LOW"), + ({"src/app.py": {"summary": {"percent_covered": 79}}}, 20, "TEST_COVERAGE_LOW"), + ]) + def test_each_reviewed_source_requires_sufficient_coverage(tmp_path, monkeypatch, coverage, threshold, expected): + observation = _observation(_record("tests/test_app.py::test_pass"), coverage=coverage, threshold=threshold) + findings = _run(tmp_path, monkeypatch, observation) +> assert any(item.rule == expected and item.severity == "error" for item in findings) +E assert False +E + where False = any(. at 0x10a20a880>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:76: AssertionError +_______ test_project_root_as_test_root_cannot_exempt_production_sources ________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_project_root_as_test_root0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a249d90> + + def test_project_root_as_test_root_cannot_exempt_production_sources(tmp_path, monkeypatch): + observation = _observation( + _record("tests/test_app.py::test_pass"), + coverage={"src/other.py": {"summary": {"percent_covered": 100}}}, + ) + observation["test_roots"] = ["."] +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a2a8fb0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:105: AssertionError +____ test_customer_nonempty_initializer_cannot_use_specfact_omit_exemption _____ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_customer_nonempty_initial0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a249fd0> + + def test_customer_nonempty_initializer_cannot_use_specfact_omit_exemption(tmp_path, monkeypatch): + observation = _observation(_record("tests/test_app.py::test_pass")) +> assert any(item.rule == "tool_error" for item in _run( + tmp_path, monkeypatch, observation, ("src/customer/__init__.py", "tests/test_app.py"), + )) +E assert False +E + where False = any(. at 0x10a2a9a40>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:110: AssertionError +_________ test_real_failure_remains_visible_alongside_missing_coverage _________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_real_failure_remains_visi0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a20f8b0> + + def test_real_failure_remains_visible_alongside_missing_coverage(tmp_path, monkeypatch): + observation = _observation( + _record("tests/test_app.py::test_pass", "failed"), + coverage={"src/other.py": {"summary": {"percent_covered": 100}}}, + ) + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) +> assert {"TEST_OUTCOME_NOT_PASS", "tool_error"} <= {item.rule for item in findings} +E AssertionError: assert {'TEST_OUTCOM... 'tool_error'} <= {'TEST_OUTCOME_NOT_PASS'} +E +E Extra items in the left set: +E 'tool_error' + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:122: AssertionError +______________ test_invalid_effective_threshold_cannot_pass[bad] _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_invalid_effective_thresho0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a20f960> +threshold = 'bad' + + @pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) + def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a2b3440>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:128: AssertionError +_______________ test_invalid_effective_threshold_cannot_pass[-1] _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_invalid_effective_thresho1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a25b7f0> +threshold = -1 + + @pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) + def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a2b2b50>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:128: AssertionError +______________ test_invalid_effective_threshold_cannot_pass[101] _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_invalid_effective_thresho2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a25bb10> +threshold = 101 + + @pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) + def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a1df100>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:128: AssertionError +______________ test_invalid_effective_threshold_cannot_pass[nan] _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_invalid_effective_thresho3') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x108e83da0> +threshold = nan + + @pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) + def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a1dedc0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:128: AssertionError +______________ test_invalid_effective_threshold_cannot_pass[inf] _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_invalid_effective_thresho4') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a285350> +threshold = inf + + @pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) + def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a1df370>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:128: AssertionError +______________ test_invalid_effective_threshold_cannot_pass[True] ______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-532/test_invalid_effective_thresho5') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a23e5d0> +threshold = True + + @pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) + def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) +> assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) +E assert False +E + where False = any(. at 0x10a2b5ff0>) + +/private/tmp/specfact-477-pytest-release/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py:128: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_mixed_non_pass_outcomes_cannot_pass[skipped-setup-] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_mixed_non_pass_outcomes_cannot_pass[skipped-call-] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_mixed_non_pass_outcomes_cannot_pass[skipped-call-known bug] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_mixed_non_pass_outcomes_cannot_pass[passed-call-known bug] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_each_reviewed_source_requires_sufficient_coverage[coverage0-None-tool_error] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_each_reviewed_source_requires_sufficient_coverage[coverage1-None-TEST_COVERAGE_LOW] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_each_reviewed_source_requires_sufficient_coverage[coverage2-90-TEST_COVERAGE_LOW] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_each_reviewed_source_requires_sufficient_coverage[coverage3-20-TEST_COVERAGE_LOW] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_project_root_as_test_root_cannot_exempt_production_sources +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_customer_nonempty_initializer_cannot_use_specfact_omit_exemption +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_real_failure_remains_visible_alongside_missing_coverage +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_invalid_effective_threshold_cannot_pass[bad] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_invalid_effective_threshold_cannot_pass[-1] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_invalid_effective_threshold_cannot_pass[101] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_invalid_effective_threshold_cannot_pass[nan] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_invalid_effective_threshold_cannot_pass[inf] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py::test_invalid_effective_threshold_cannot_pass[True] +========================= 17 failed, 4 passed in 1.38s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/REVIEW_EXCEPTIONS.md b/openspec/changes/code-review-16-portable-project-runtime/REVIEW_EXCEPTIONS.md new file mode 100644 index 00000000..2accd248 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/REVIEW_EXCEPTIONS.md @@ -0,0 +1,87 @@ +# Review disposition for portable runtime boundaries + +These exceptions apply only to the named findings and execution boundaries in #473. They do not waive runtime failures, missing evidence, security findings, or new complexity regressions. The final fresh report and test outcome are recorded in TDD_EVIDENCE.md. + +| Finding and location | Disposition and supporting boundary | +| --- | --- | +| `MISSING_ICONTRACT` in `runtime_build_driver.py`, `target_bootstrap.py`, `target_launch.py`, `target_pytest.py`, and `scripts/external_capsule_corpus.py` | These entry points run before project dependencies are installed, inside the isolated interpreter startup path, or in the standalone customer harness. This includes `AnalyzerFinder.validate_loaded`, which checks analyzer origins before target site initialization, and `target_launch.execution_domain`, which validates the inherited read-only member identity before project startup. Importing icontract would add a controller dependency to the target startup path. Keep explicit input checks, fail-closed exits, and subprocess/corpus tests. Ordinary supervisor runtime APIs retain contracts. | +| `C0415` and `R0401` for runtime/runner integration in `portable_worker.py`, `portable_snapshot.py`, `runtime_interpreter.py`, `runtime_commands.py`, `_review_utils.py`, `scope.py`, `target_pytest.py`, and `runner.py` | Deferred imports avoid partially initialized runner symbols and keep worker-only code out of controller startup. Python resolves these imports at invocation after module initialization. Pytest is imported only after its selected worker environment is established. Scope discovery is deferred to avoid the VCS/scope initialization cycle. CLI, snapshot, alternate-ABI, and full-suite tests exercise these boundaries. This is not permission for unrelated deferred imports; unnecessary imports in new tests were moved to module scope. | +| `W0212` in `runtime_builder._build` | Reuse `sandbox._verified_bubblewrap_descriptor` to inherit the existing descriptor-bound launcher verification. Duplicating that security-sensitive primitive solely to avoid the private-name warning would create a second validator. The dependency remains within the review bundle. | +| `W0718` in `sitecustomize.py` | Python can ignore startup-hook failures and continue. Catching `BaseException` here deliberately terminates with exit 78, preventing unconfigured project execution. The fail-closed startup regression covers this behavior. | +| `kiss.nesting.warning` in `runner._run_local_capsule_context` | The extra lexical level is an `ExitStack` owning index/source resources. Retain deterministic cleanup across static, portable, and cached paths. No new unbounded iteration or environment-selection branch is exempted. | +| `kiss.parameter-count.warning` in `external_capsule_corpus.review` | Four execution inputs plus two named options preserve explicit descriptor attachment and distinct cold/warm/controlled evidence names. A regression proves the controlled report cannot overwrite the untouched report. The optional artifact name is intentional and is not inferred from arbitrary source filenames. | +| `ai-bloat.loc-vs-complexity` in `target_pytest.main` | This startup function assembles the explicit observation schema and native pytest invocation. The additional deselection fields retain selected-versus-deselected evidence and label controller-only visibility under xdist. They are required evidence, not redundant control flow; preserve the explicit schema and actual native selection regressions. | +| `banned-generic-public-names` for test names containing `metadata` | The generic-name pattern matches the substring `data` inside the precise term `metadata`. Preserve descriptive regression names; these are pytest tests, not customer APIs named `data` or `process`. | +| `dry.duplicate-function-shape` in distinct source/configuration and environment-isolation regressions | The shape detector ignores the differing configuration literals, environment variables, and expected outcomes. Separate cases preserve failure attribution for environment leaks, path escapes, manager ambiguity, extras, and permission classes. Shared setup is already factored where it conveys the same contract. | + +## Nested launcher informational dispositions + +- `ai-bloat.loc-vs-complexity` in `target_launch.target_command`: retain the explicit bubblewrap argument vector. Mount order, descriptor paths and environment-clearing order define the isolation boundary; a compact generated mapping would make that boundary harder to audit. The function adds a single read-only domain bind and does not introduce duplicate resolution logic. First-entry isolation and all five nested-domain subprocess cases pass. +- `ai-bloat.loc-vs-complexity` in `test_target_launch._native_nested_wrapper`: retain the ordered creation of private descriptor, installed member/project modules, relocated copies of actual trusted helpers, domain identity and executable wrapper. The ten real child tests rely on the complete startup chain. Splitting this one fixture into single-use factories would obscure which production bytes are executed without reducing behavior or duplication. + +## Existing review surface + +The branch-wide report also analyzes pre-existing large modules and tests in `runner.py`, `scope.py`, and `commands.py`. Existing module-length, selector-complexity, fixture-parameter, collection-internals, and duplicate-test-shape findings are outside this runtime change when their implementation is unchanged from dev `976460c0541b685f5fae256faef4cec32fe9ba7e`. This exception requires comparing the actual diff; it does not apply merely because a file existed before the change. New portable implementation is split into dedicated discovery, builder, inventory, source, native, domain, and worker modules. + +Parent-process coverage does not measure isolated Linux builder/startup subprocesses. If a targeted local review reports reduced coverage for these boundaries, the relevant proof is the focused failure-path tests plus the non-root Linux corpus. Do not describe parent coverage as complete subprocess coverage or substitute a successful bootstrap for completed analysis. + +## Local coverage boundary in the final branch review + +The local branch review runs on macOS and does not merge coverage from the independent Linux customer CLI or isolated child interpreters. Its measured percentages are retained, not relabelled as meeting the 80% threshold: `portable_worker.py` 71.4%, `runtime_artifacts.py` 78.7%, `runtime_build_driver.py` 51.4%, `runtime_builder.py` 52.9%, `runtime_commands.py` 50.0%, `runtime_interpreter.py` 69.2%, `sitecustomize.py` 0.0%, `target_bootstrap.py` 67.8%, and `target_pytest.py` 75.0%. The older `_review_utils.py` and `sandbox.py` also remain below the local threshold. + +For this Linux-only capsule change, accept these local coverage warnings conditional on the full Linux candidate and public-release corpus gates: those execute real CLI preparation, native-manager installation, descriptor loading/attachment, worker bootstrap and test observation that local mocks cannot establish. Focused unit tests retain independent checks for malformed/corrupt descriptors, source escapes, stale observations, incompatible interpreters and missing native libraries. This does not establish exhaustive failure-branch coverage; any actual incomplete analyzer result still blocks acceptance. The final release must not rely on this exception if the Linux corpus is incomplete. + +## Controller pipe capture follow-up + +- `TEST_OUTCOME_NOT_PASS` in the local explicit-file review is caused by the single `test_builder_proc_reopen_cannot_access_regular_log_inode` Linux `/proc` test being skipped on macOS. Keep that actual incomplete platform evidence visible. This is a local validation limitation only: the Linux CI full suite and new candidate corpus must pass before resolution or integration. Do not waive a failed, skipped or missing Linux run or call the local review error-free. +- `ai-bloat.loc-vs-complexity` in `runtime_builder.builder_command`: keep the explicit ordered namespace, mount and descriptor argv, as for `target_launch.target_command` above. No change to its command semantics was made in the pipe-capture follow-up. +- `ai-bloat.loc-vs-complexity` in `test_runtime_builder_logging._builder`: this fixture explicitly assembles source, the capsule handle, a verified-launcher substitution, a real child command and capture tracking. Preserve their visible sequence for the real subprocess security regressions; extracting single-use setup functions would obscure the exact trusted/untrusted boundary without removing behavior. + + +## Portable pytest release correction + +The deferred coverage-policy import in `portable_worker.run_portable_pytest` reuses the supervisor's established coverage evaluator only when executing analysis. Test selection and runtime discovery can import `portable_worker` without initializing the large runner and its analyzer integrations. Keep this scoped deferred import rather than duplicate coverage policy or make discovery import the analyzer stack. `Observer.pytest_sessionfinish` is covered by the existing target-worker contract exception: importing icontract into the customer interpreter would add supervisor dependencies; native subprocess tests prove its evidence fields and precedence. + +## Native pytest usage-error proof + +`ai-bloat.loc-vs-complexity` in `test_native_usage_error_retains_capsule_member_and_target_execution`: retain the explicit native subprocess, captured observation and capsule-envelope replay in one integration regression. Most length is the isolated observer script and boundary-specific assertions; these establish that a real usage error preserves member identity and the exact target evidence. Extracting single-use setup would hide the sequence without removing behavior. Six failing-before cases and the combined 120-case passing run cover this correction. + + +## Installed coverage attribution integration + +- `W0212` at `target_pytest._native_coverage_relocations`: the worker calls the installed Coverage object's `_make_aliases` to use exactly the same path-mapping semantics as pytest-cov's native combine step. This is a deliberately bounded native API dependency, not a reimplementation or coverage-policy override. `test_native_coverage_alias_cannot_credit_transformed_source` exercises the actual native rewrite; identical and unrelated alias cases pass. Missing/incompatible method interfaces produce a named origin diagnostic while preserving test records and pytest root. This exception does not permit inferred aliases or treating unavailable origin evidence as proof. +- The existing `C0415`/`R0401` runner-cycle disposition applies to the same deferred `portable_worker.run_portable_pytest -> runner` edge. Pylint may attach `R0401` to the new `test_installed_coverage.py` batch-analysis anchor; that test imports the established modules at module scope and introduces no new production import edge. The bridge evaluator is passed into its helper, avoiding an additional deferred import. +- The existing macOS-only `TEST_OUTCOME_NOT_PASS` disposition remains tied exclusively to `test_builder_proc_reopen_cannot_access_regular_log_inode`. An explicit-file run may attach the aggregate outcome to the first selected file, now `installed_coverage.py`. Keep that genuine Linux-only skip visible; this does not waive any installed-coverage regression, native Linux test, or external capsule acceptance. + +## Bounded Semgrep diagnostic follow-up baseline + +The fresh follow-up review also scans unchanged Semgrep adapter/test bodies. AST comparison against signed local baseline `efe7303efb32ac31379906a38e3f960ec4e7a744` confirms the following flagged functions are unchanged; only the bounded formatter helpers, its completion-error branch and three new diagnostic test functions changed. Retain these narrowly scoped existing findings rather than broaden this runtime-failure fix into unrelated adapter API/test refactoring: + +- `semgrep_runner.py`: existing `R0916` in `reconcile_passes`; existing `MISSING_ICONTRACT` on `reconcile_scanned_paths`, `validate_rule_pack`, `reconcile_passes`, and `build_snapshot_invocation`. No new public entry point is introduced by this formatter change. +- `test_semgrep_runner.py`: existing fixture `_stub_semgrep_on_path` is requested by pytest's `usefixtures`, so the AST private-call detector does not see its use; existing duplicate empty-input test shapes and `C1803` empty-list assertions remain distinct pre-existing cases; existing local scope/runner imports and repeated local `shutil` imports produce `C0415`, `W0621`, and `W0404`. These bodies are unchanged from that baseline. + +New diagnostic line-length/import warnings and new activation-discovery complexity were corrected. Existing deferred imports in `portable_snapshot.py` retain the earlier runner-cycle disposition. This exception does not waive nonempty Semgrep errors, malformed observations, missing runtime attachment or incomplete native acceptance. + +## Combined PR478 native-tools and Node review (2026-09-16) + +The fresh combined developer review `/private/tmp/specfact-478-combined-final-review.json` reports PASS_WITH_ADVISORY, 215 warnings and55 informational findings, with zero errors. Introduced test complexity, fixture shadowing/unused-private-name, empty-map assertions, generic naming and duplicate wrapper-shape findings were corrected; no native-tools production helper findings remain. Independent review compared production findings with signed HEAD efe7303efb32ac31379906a38e3f960ec4e7a744:96 of104 refer to AST-identical enclosing functions. The remaining production findings are the already-scoped deferred import, runner module length, Coverage private native API and target startup contracts above, plus the measured coverage warnings below. This is not a blanket exemption for a changed file. + +The targeted parent-process review measured runtime_builder.py67.1% and target_pytest.py71.8%, below80%; its deleted temporary data alone could not establish new-branch execution. The subsequently retained full-suite JSON (`/private/tmp/specfact-478-combined-full-coverage.json`) measures runtime_builder.py92.4444% with every introduced executable line covered, and target_pytest.py79.8851%, still below the individual80% threshold. Thirteen additional boundary cases cover the installed-coverage candidate validator, including malformed inputs and a real symlink escape. Parent coverage still does not observe native alias mapping line158 or startup candidate assignment line198; native child controls remain separate evidence, with no invented parent percentage credit. The unchanged aggregate80% gate passes at86.9674% for these two measured modules. This is not whole-repository coverage or completed native acceptance. Native unchanged hooks and the signed external corpus remain mandatory. + +Retain the informational length suggestion for test_runtime_builder._assert_policy_change_invalidates_reuse: the shared fixture visibly arranges the real helper bytes and cache identity, verifies initial miss then warm reuse, modifies exactly one policy file and verifies a renewed miss. Two parametrized policies reuse this sequence without duplicating assertions. The unchanged banned-generic-public-names finding on test_builder_provides_vcs_metadata_tool_without_host_configuration at test_runtime_builder.py86 is outside the new tests; it is not waived for new names. + +## Native follow-up fixture review (2026-09-16) + +The new informational `ai-bloat.loc-vs-complexity` finding on `test_runtime_vcs.attached_vcs_git_fixture` is retained narrowly: its real executable captures sanitized environment values before forwarding to actual Git, and its descriptor, builtin inode/context and empty system search path establish independently asserted trust boundaries. Keeping that explicit setup makes the provenance control auditable. Existing `_builder` and `_assert_policy_change_invalidates_reuse` readability dispositions above still apply after their explicit synthetic public-trust mocks. The new nested scenario setup warnings were corrected with early-return fixture helpers without dropping scenarios or assertions. + +The first new native-follow-up parent review measured runtime_builder.py at69.3%; that selected-test measurement remains a warning, not a claim about full-suite coverage or unchanged lines. The final selected-test review measures target_pytest.py at75.9% as well. Earlier full measurements precede the public-trust/VCS follow-up; updated full coverage and actual native acceptance remain required. + +The completed follow-up full suite replaces that uncertainty with retained line-level data: scoped aggregate87.0646766%, runtime_builder92.5438596%, target_pytest79.8850575%. All introduced builder execution lines are covered. Target pytest's new lines158/198 execute in native subprocess controls but are absent from parent coverage; its individual result remains below80% and is not rounded up. Full and smart each pass2867 tests, with the genuine Linux/proc skip still visible on macOS. Neither this qualified coverage disposition nor local test success substitutes for Linux capsule acceptance. + +### Target coverage lifecycle boundary (2026-09-16) + +`run/target_coverage.py` defers Coverage and pytest-cov imports until after the verified pytest-observe target bootstrap. Its fixed module is also loaded during child startup and must remain importable with only the standard library; moving these imports to module scope would violate the tested startup boundary. This is the same narrow C0415/target-entry contract rationale already applied to target_pytest, not an allowance to import customer plugins in the supervisor. Public hook/configuration functions cannot depend on supervisor icontract in the isolated target dependency graph. Real subprocess tests exercise their input/evidence contracts instead. + +The helper's `_should_report` override and one native `_should_report` observation use the explicitly version-checked pytest-cov7.0.0/7.1.0 lifecycle contract. The override applies only to a separate reviewer collector; native plugin methods/options are never replaced. The single protected-access warning is retained under this bounded compatibility rationale, with real native/reviewer xdist, subprocess, report-preservation and incomplete-worker tests. Unknown versions receive an actionable incomplete diagnostic. No general warning, threshold or test-outcome exemption follows. + +The newly included `target_bootstrap.py` parent coverage is measured, not labelled unchanged or sufficient: full four-module tracing initially records39.2157%; direct real-file loader tests then measure53.3333% for the whole legacy file and27/28 statements (96.4286%) in the introduced loader. Real first-entry/sitecustomize/nested-exec controls execute copied bootstrap code under isolated interpreter paths, which parent-file coverage does not count. The call site is exercised by those real startup controls. This narrowly qualified boundary evidence supports the developer warning disposition, not a waiver of native customer coverage findings or final Linux acceptance. The new target_coverage helper itself measures97.0414% with real parent-process Coverage boundary tests; observer84.0426% and builder92.5439% meet80 in the retained full scoped measurement. diff --git a/openspec/changes/code-review-16-portable-project-runtime/SECTIONLESS_ELF_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/SECTIONLESS_ELF_RED.txt new file mode 100644 index 00000000..996b9155 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SECTIONLESS_ELF_RED.txt @@ -0,0 +1,128 @@ +Synthetic ELF64 dependency inventory regression before implementation. Host: macOS; byte parsing only, no ELF execution. +Command: PYTHONPATH=packages/specfact-code-review/src:src .venv/bin/python -m pytest tests/unit/specfact_code_review/run/test_runtime_native.py -q + +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 19 items + +tests/unit/specfact_code_review/run/test_runtime_native.py .....FF...... [ 68%] +.....F [100%] + +=================================== FAILURES =================================== +_______ test_dependency_inventory_survives_removed_elf_section_table[0] ________ + +tmp_path = PosixPath('') +entry_size = 0 + + @pytest.mark.parametrize("entry_size", [0, 64]) + def test_dependency_inventory_survives_removed_elf_section_table(tmp_path: Path, entry_size: int) -> None: + artifact, system = tmp_path / "artifact", tmp_path / "system" + artifact.mkdir() + system.mkdir() + binary = artifact / "extension.so" + payload = _segmented_elf() + binary.write_bytes(payload) + assert elf_dependencies(binary) == ("libcustomer.so.1",) + binary.write_bytes(_remove_section_table(payload, entry_size)) +> assert elf_dependencies(binary) == ("libcustomer.so.1",) + ^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_native.py:127: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py:60: in elf_dependencies + sections = _elf_sections(payload, path.name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +payload = b'\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00>\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00...00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +name = 'extension.so' + + def _elf_sections(payload: bytes, name: str) -> list[tuple[int, ...]]: + if len(payload) < 64 or payload[4:6] != b"\x02\x01": + raise ProjectRuntimeError(f"project_native_elf_unsupported:{name}") + machine = struct.unpack_from(" len(payload): +> raise ProjectRuntimeError(f"project_native_elf_invalid:{name}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_native_elf_invalid:extension.so + +packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py:30: ProjectRuntimeError +_______ test_dependency_inventory_survives_removed_elf_section_table[64] _______ + +tmp_path = PosixPath('') +entry_size = 64 + + @pytest.mark.parametrize("entry_size", [0, 64]) + def test_dependency_inventory_survives_removed_elf_section_table(tmp_path: Path, entry_size: int) -> None: + artifact, system = tmp_path / "artifact", tmp_path / "system" + artifact.mkdir() + system.mkdir() + binary = artifact / "extension.so" + payload = _segmented_elf() + binary.write_bytes(payload) + assert elf_dependencies(binary) == ("libcustomer.so.1",) + binary.write_bytes(_remove_section_table(payload, entry_size)) +> assert elf_dependencies(binary) == ("libcustomer.so.1",) +E AssertionError: assert () == ('libcustomer.so.1',) +E +E Right contains one more item: 'libcustomer.so.1' +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_native.py:127: AssertionError +_______________ test_sectionless_static_elf_has_no_dependencies ________________ + +tmp_path = PosixPath('') + + def test_sectionless_static_elf_has_no_dependencies(tmp_path: Path) -> None: + payload = _remove_section_table(_segmented_elf()) + struct.pack_into(" assert elf_dependencies(binary) == () + ^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_native.py:166: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py:60: in elf_dependencies + sections = _elf_sections(payload, path.name) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +payload = b'\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00>\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00...00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +name = 'static' + + def _elf_sections(payload: bytes, name: str) -> list[tuple[int, ...]]: + if len(payload) < 64 or payload[4:6] != b"\x02\x01": + raise ProjectRuntimeError(f"project_native_elf_unsupported:{name}") + machine = struct.unpack_from(" len(payload): +> raise ProjectRuntimeError(f"project_native_elf_invalid:{name}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_native_elf_invalid:static + +packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py:30: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_dependency_inventory_survives_removed_elf_section_table[0] +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_dependency_inventory_survives_removed_elf_section_table[64] +FAILED tests/unit/specfact_code_review/run/test_runtime_native.py::test_sectionless_static_elf_has_no_dependencies +========================= 3 failed, 16 passed in 1.01s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/SECURITY_FIX.md b/openspec/changes/code-review-16-portable-project-runtime/SECURITY_FIX.md new file mode 100644 index 00000000..dc4c1f87 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SECURITY_FIX.md @@ -0,0 +1,13 @@ +# Source-link exclusion fix + +Finding: PR #474 comment 4005446099, verified against the candidate on 2026-09-14. A committed alias to excluded repository-local `.env`, virtual-environment contents, or `.git/config` was dereferenced into the disposable builder. Network-enabled dependency hooks could read those copied bytes. Synthetic reproductions confirmed availability, without using real secrets or attempting exfiltration. + +The shared source-link validator now rejects excluded resolved path components. Source copying preserves links and normalizes valid targets into the copied tree. It verifies the complete copied identity before build hooks execute. The original source identity is checked before and after copying/building. This closes the alias route at discovery and acquisition, preserving ordinary included files and directory links. Tests cover excluded files, directories, descendants, Git configuration, absolute links, and parent-traversing relative links. + +A fresh read-only investigator independently reproduced four alias classes. A separate fresh patch reviewer found one compatibility regression: valid `../source/app.py` links needed rebasing too. Its failing regression was recorded and fixed. The reviewer also exercised deterministic link replacements after the copy callback; destination validation rejected them before execution. No surviving symlink disclosure was found. + +Focused validation: `hatch run pytest tests/unit/specfact_code_review/run/test_runtime_builder.py tests/unit/specfact_code_review/run/test_runtime_discovery.py -q` passes. Whole-repository lint/type and final publication gates are recorded separately in TDD_EVIDENCE.md; this document does not grant release acceptance. + +Boundary limitation: hardlinks are ordinary source files and are not preserved by Git. A local process able to create hardlinks to local secrets is outside the committed-symlink trigger addressed here. Private staging must remain inaccessible to untrusted processes; same-user host compromise is outside the capsule's source-isolation claim. + +Portable analysis now also mounts the verified private source copy. The new regression demonstrates that `.env` and local virtual environments are absent from analyzer source mounts while included source remains readable; see `ANALYSIS_EXCLUSIONS_RED.txt`. This extends the exclusion boundary from acquisition to analysis. Final Linux candidate verification must cover this additional change. diff --git a/openspec/changes/code-review-16-portable-project-runtime/SETUP_CFG_INSTALL_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/SETUP_CFG_INSTALL_RED.txt new file mode 100644 index 00000000..6bd3a827 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SETUP_CFG_INSTALL_RED.txt @@ -0,0 +1,106 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 18 items / 10 deselected / 8 selected + +tests/unit/specfact_code_review/run/test_runtime_adapters.py FFF...FF [100%] + +=================================== FAILURES =================================== +____ test_tool_only_setup_cfg_prepares_requirements_without_installing_root ____ + +tmp_path = PosixPath('/test_tool_only_setup_cfg_prepa0') + + def test_tool_only_setup_cfg_prepares_requirements_without_installing_root(tmp_path: Path) -> None: + (tmp_path / "setup.cfg").write_text("[tool:pytest]\naddopts = --strict-markers\npythonpath = .\n") + (tmp_path / "requirements.txt").write_text("packaging\n") + (tmp_path / "constraints.txt").write_text("packaging>=1\n") + config = tmp_path / "review.toml" + config.write_text('manager="pip"\nconstraints=["constraints.txt"]\n') + before = {path.name: path.read_bytes() for path in tmp_path.iterdir()} + plan = discover_project(tmp_path, config_path=config) + assert plan.pytest_config["addopts"] == "--strict-markers" + assert plan.source_roots == (".",) + commands = install_commands(plan, python=sys.executable) + # pip's no-index dry run validates the actual adapter command without installs. + completed = subprocess.run( + [*commands[0], "--no-index", "--no-deps", "--dry-run"], + cwd=tmp_path, capture_output=True, text=True, check=False, + ) +> assert completed.returncode == 0, completed.stdout + completed.stderr +E AssertionError: WARNING: The directory '/Users/dom/Library/Caches/pip' or its parent directory is not owned or is not writable by the current user. The cache has been disabled. Check the permissions and owner of that directory. If executing pip with sudo, you should use sudo's -H flag. +E ERROR: Directory '.' is not installable. Neither 'setup.py' nor 'pyproject.toml' found. +E +E assert 1 == 0 +E + where 1 = CompletedProcess(args=['/bin/python', '-m', 'pip', 'install', '--disable-pip-v...should use sudo's -H flag.\nERROR: Directory '.' is not installable. Neither 'setup.py' nor 'pyproject.toml' found.\n").returncode + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:80: AssertionError +_ test_setup_cfg_without_package_entrypoint_requires_no_install[[tool:pytest]\naddopts = --strict-markers\n] _ + +tmp_path = PosixPath('/test_setup_cfg_without_package0') +contents = '[tool:pytest]\naddopts = --strict-markers\n' + + @pytest.mark.parametrize("contents", ["[tool:pytest]\naddopts = --strict-markers\n", "[metadata]\nname = example\n"]) + def test_setup_cfg_without_package_entrypoint_requires_no_install(tmp_path: Path, contents: str) -> None: + (tmp_path / "setup.cfg").write_text(contents) +> assert install_commands(discover_project(tmp_path), python="python") == () +E AssertionError: assert (('python', '...check', '.'),) == () +E +E Left contains one more item: ('python', '-m', 'pip', 'install', '--disable-pip-version-check', '.') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:88: AssertionError +_ test_setup_cfg_without_package_entrypoint_requires_no_install[[metadata]\nname = example\n] _ + +tmp_path = PosixPath('/test_setup_cfg_without_package1') +contents = '[metadata]\nname = example\n' + + @pytest.mark.parametrize("contents", ["[tool:pytest]\naddopts = --strict-markers\n", "[metadata]\nname = example\n"]) + def test_setup_cfg_without_package_entrypoint_requires_no_install(tmp_path: Path, contents: str) -> None: + (tmp_path / "setup.cfg").write_text(contents) +> assert install_commands(discover_project(tmp_path), python="python") == () +E AssertionError: assert (('python', '...check', '.'),) == () +E +E Left contains one more item: ('python', '-m', 'pip', 'install', '--disable-pip-version-check', '.') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:88: AssertionError +______ test_pip_package_entrypoint_must_be_a_regular_file[pyproject.toml] ______ + +tmp_path = PosixPath('/test_pip_package_entrypoint_mu0') +name = 'pyproject.toml' + + @pytest.mark.parametrize("name", ["pyproject.toml", "setup.py"]) + def test_pip_package_entrypoint_must_be_a_regular_file(tmp_path: Path, name: str) -> None: + (tmp_path / name).mkdir() +> assert install_commands(ProjectPlan(root=tmp_path, manager="pip"), python="python") == () +E AssertionError: assert (('python', '...check', '.'),) == () +E +E Left contains one more item: ('python', '-m', 'pip', 'install', '--disable-pip-version-check', '.') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:108: AssertionError +_________ test_pip_package_entrypoint_must_be_a_regular_file[setup.py] _________ + +tmp_path = PosixPath('/test_pip_package_entrypoint_mu1') +name = 'setup.py' + + @pytest.mark.parametrize("name", ["pyproject.toml", "setup.py"]) + def test_pip_package_entrypoint_must_be_a_regular_file(tmp_path: Path, name: str) -> None: + (tmp_path / name).mkdir() +> assert install_commands(ProjectPlan(root=tmp_path, manager="pip"), python="python") == () +E AssertionError: assert (('python', '...check', '.'),) == () +E +E Left contains one more item: ('python', '-m', 'pip', 'install', '--disable-pip-version-check', '.') +E Use -v to get more diff + +tests/unit/specfact_code_review/run/test_runtime_adapters.py:108: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_tool_only_setup_cfg_prepares_requirements_without_installing_root +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_setup_cfg_without_package_entrypoint_requires_no_install[[tool:pytest]\naddopts = --strict-markers\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_setup_cfg_without_package_entrypoint_requires_no_install[[metadata]\nname = example\n] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_pip_package_entrypoint_must_be_a_regular_file[pyproject.toml] +FAILED tests/unit/specfact_code_review/run/test_runtime_adapters.py::test_pip_package_entrypoint_must_be_a_regular_file[setup.py] +================== 5 failed, 3 passed, 10 deselected in 0.89s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/SNAPSHOT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/SNAPSHOT_RED.txt new file mode 100644 index 00000000..c12017b7 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SNAPSHOT_RED.txt @@ -0,0 +1,20 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 0 items / 1 error + +==================================== ERRORS ==================================== +_ ERROR collecting tests/unit/specfact_code_review/run/test_portable_snapshot.py _ +ImportError while importing test module '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/tests/unit/specfact_code_review/run/test_portable_snapshot.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +tests/unit/specfact_code_review/run/test_portable_snapshot.py:7: in + from specfact_code_review.run.portable_snapshot import run_project_snapshot +E ModuleNotFoundError: No module named 'specfact_code_review.run.portable_snapshot' +=========================== short test summary info ============================ +ERROR tests/unit/specfact_code_review/run/test_portable_snapshot.py +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +=============================== 1 error in 0.18s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/SOURCE_LINK_EXCLUSIONS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/SOURCE_LINK_EXCLUSIONS_RED.txt new file mode 100644 index 00000000..cfbeb78b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SOURCE_LINK_EXCLUSIONS_RED.txt @@ -0,0 +1,207 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 15 items + +tests/unit/specfact_code_review/run/test_runtime_builder.py .......FFFFF [ 80%] +FFF [100%] + +=================================== FAILURES =================================== +________ test_concurrent_cache_publication_accepts_existing_winner[39] _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_concurrent_cache_publicat1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10a340fc0> +number = 39 + + @pytest.mark.parametrize("number", [17, 39]) + def test_concurrent_cache_publication_accepts_existing_winner(tmp_path, monkeypatch, number) -> None: + from specfact_code_review.run import runtime_builder + + def raced(source, destination): + raise OSError(number, "concurrent publication") + + monkeypatch.setattr(runtime_builder.os, "rename", raced) +> runtime_builder.publish_artifact(tmp_path / "candidate", tmp_path / "winner") + +tests/unit/specfact_code_review/run/test_runtime_builder.py:73: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:178: in publish_artifact + os.rename(artifact, destination) +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +source = PosixPath('/pytest-of-dom/pytest-132/test_concurrent_cache_publicat1/candidate') +destination = PosixPath('/pytest-of-dom/pytest-132/test_concurrent_cache_publicat1/winner') + + def raced(source, destination): +> raise OSError(number, "concurrent publication") +E OSError: [Errno 39] concurrent publication + +tests/unit/specfact_code_review/run/test_runtime_builder.py:70: OSError +____________ test_project_links_cannot_alias_excluded_inputs[.env] _____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_project_links_cannot_alia0') +target = '.env' + + @pytest.mark.parametrize("target", [".env", ".venv/private.txt", ".venv", ".git/config", "nested/.env"]) + def test_project_links_cannot_alias_excluded_inputs(tmp_path: Path, target: str) -> None: + from specfact_code_review.run.runtime_sources import source_identity + + source = tmp_path / "source" + source.mkdir() + secret = source / target + if target == ".venv": + secret.mkdir() + (secret / "private.txt").write_text("synthetic excluded fixture") + else: + secret.parent.mkdir(parents=True, exist_ok=True) + secret.write_text("synthetic excluded fixture") + (source / "alias").symlink_to(target) +> with pytest.raises(ProjectRuntimeError, match="symlink.*excluded"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:90: Failed +______ test_project_links_cannot_alias_excluded_inputs[.venv/private.txt] ______ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_project_links_cannot_alia1') +target = '.venv/private.txt' + + @pytest.mark.parametrize("target", [".env", ".venv/private.txt", ".venv", ".git/config", "nested/.env"]) + def test_project_links_cannot_alias_excluded_inputs(tmp_path: Path, target: str) -> None: + from specfact_code_review.run.runtime_sources import source_identity + + source = tmp_path / "source" + source.mkdir() + secret = source / target + if target == ".venv": + secret.mkdir() + (secret / "private.txt").write_text("synthetic excluded fixture") + else: + secret.parent.mkdir(parents=True, exist_ok=True) + secret.write_text("synthetic excluded fixture") + (source / "alias").symlink_to(target) +> with pytest.raises(ProjectRuntimeError, match="symlink.*excluded"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:90: Failed +____________ test_project_links_cannot_alias_excluded_inputs[.venv] ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_project_links_cannot_alia2') +target = '.venv' + + @pytest.mark.parametrize("target", [".env", ".venv/private.txt", ".venv", ".git/config", "nested/.env"]) + def test_project_links_cannot_alias_excluded_inputs(tmp_path: Path, target: str) -> None: + from specfact_code_review.run.runtime_sources import source_identity + + source = tmp_path / "source" + source.mkdir() + secret = source / target + if target == ".venv": + secret.mkdir() + (secret / "private.txt").write_text("synthetic excluded fixture") + else: + secret.parent.mkdir(parents=True, exist_ok=True) + secret.write_text("synthetic excluded fixture") + (source / "alias").symlink_to(target) +> with pytest.raises(ProjectRuntimeError, match="symlink.*excluded"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:90: Failed +_________ test_project_links_cannot_alias_excluded_inputs[.git/config] _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_project_links_cannot_alia3') +target = '.git/config' + + @pytest.mark.parametrize("target", [".env", ".venv/private.txt", ".venv", ".git/config", "nested/.env"]) + def test_project_links_cannot_alias_excluded_inputs(tmp_path: Path, target: str) -> None: + from specfact_code_review.run.runtime_sources import source_identity + + source = tmp_path / "source" + source.mkdir() + secret = source / target + if target == ".venv": + secret.mkdir() + (secret / "private.txt").write_text("synthetic excluded fixture") + else: + secret.parent.mkdir(parents=True, exist_ok=True) + secret.write_text("synthetic excluded fixture") + (source / "alias").symlink_to(target) +> with pytest.raises(ProjectRuntimeError, match="symlink.*excluded"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:90: Failed +_________ test_project_links_cannot_alias_excluded_inputs[nested/.env] _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_project_links_cannot_alia4') +target = 'nested/.env' + + @pytest.mark.parametrize("target", [".env", ".venv/private.txt", ".venv", ".git/config", "nested/.env"]) + def test_project_links_cannot_alias_excluded_inputs(tmp_path: Path, target: str) -> None: + from specfact_code_review.run.runtime_sources import source_identity + + source = tmp_path / "source" + source.mkdir() + secret = source / target + if target == ".venv": + secret.mkdir() + (secret / "private.txt").write_text("synthetic excluded fixture") + else: + secret.parent.mkdir(parents=True, exist_ok=True) + secret.write_text("synthetic excluded fixture") + (source / "alias").symlink_to(target) +> with pytest.raises(ProjectRuntimeError, match="symlink.*excluded"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:90: Failed +_ test_source_copy_preserves_directory_alias_without_copying_excluded_children _ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_source_copy_preserves_dir0') + + def test_source_copy_preserves_directory_alias_without_copying_excluded_children(tmp_path: Path) -> None: + source = tmp_path / "source" + (source / "package").mkdir(parents=True) + (source / "package/app.py").write_text("VALUE = 1\n") + (source / "package/.env").write_text("synthetic excluded fixture") + (source / "alias").symlink_to("package") + target = tmp_path / "copy" + copy_project(source, target) +> assert (target / "alias").is_symlink() +E AssertionError: assert False +E + where False = is_symlink() +E + where is_symlink = (PosixPath('/pytest-of-dom/pytest-132/test_source_copy_preserves_dir0/copy') / 'alias').is_symlink + +tests/unit/specfact_code_review/run/test_runtime_builder.py:104: AssertionError +_______________ test_source_copy_rebases_absolute_internal_alias _______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-132/test_source_copy_rebases_absol0') + + def test_source_copy_rebases_absolute_internal_alias(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / "alias.py").symlink_to(source / "app.py") + target = tmp_path / "copy" + copy_project(source, target) +> assert (target / "alias.py").is_symlink() +E AssertionError: assert False +E + where False = is_symlink() +E + where is_symlink = (PosixPath('/pytest-of-dom/pytest-132/test_source_copy_rebases_absol0/copy') / 'alias.py').is_symlink + +tests/unit/specfact_code_review/run/test_runtime_builder.py:116: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_concurrent_cache_publication_accepts_existing_winner[39] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_project_links_cannot_alias_excluded_inputs[.env] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_project_links_cannot_alias_excluded_inputs[.venv/private.txt] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_project_links_cannot_alias_excluded_inputs[.venv] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_project_links_cannot_alias_excluded_inputs[.git/config] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_project_links_cannot_alias_excluded_inputs[nested/.env] +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_source_copy_preserves_directory_alias_without_copying_excluded_children +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_source_copy_rebases_absolute_internal_alias +========================= 8 failed, 7 passed in 0.14s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/SOURCE_LINK_REBASING_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/SOURCE_LINK_REBASING_RED.txt new file mode 100644 index 00000000..738bcd4d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SOURCE_LINK_REBASING_RED.txt @@ -0,0 +1,60 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 1 item + +tests/unit/specfact_code_review/run/test_runtime_builder.py F [100%] + +=================================== FAILURES =================================== +________ test_source_copy_rebases_internal_link_spelled_through_parent _________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-136/test_source_copy_rebases_inter0') + + def test_source_copy_rebases_internal_link_spelled_through_parent(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / "alias.py").symlink_to("../source/app.py") + target = tmp_path / "copy" +> copy_project(source, target) + +tests/unit/specfact_code_review/run/test_runtime_builder.py:127: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py:57: in copy_project + if source_identity(destination) != expected: + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py:66: in source_identity + identity = _source_entry(path, root) + ^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py:49: in _source_entry + return "link:" + source_link_target(path, root).relative_to(root).as_posix() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +path = PosixPath('/pytest-of-dom/pytest-136/test_source_copy_rebases_inter0/copy/alias.py') +root = PosixPath('/pytest-of-dom/pytest-136/test_source_copy_rebases_inter0/copy') + + def source_link_target(path: Path, root: Path) -> Path: + """Apply the same exclusion boundary to aliases and ordinary source paths.""" + relative = path.relative_to(root).as_posix() + try: + target = path.resolve(strict=True) + except (OSError, RuntimeError) as exc: + raise ProjectRuntimeError(f"project_source_symlink_invalid:{relative}") from exc + if not target.is_relative_to(root) or target == root or path.is_relative_to(target): +> raise ProjectRuntimeError(f"project_source_symlink_escape:{relative}") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_source_symlink_escape:alias.py + +packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py:40: ProjectRuntimeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_source_copy_rebases_internal_link_spelled_through_parent +============================== 1 failed in 0.12s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/SOURCE_VENV_PACKAGE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/SOURCE_VENV_PACKAGE_RED.txt new file mode 100644 index 00000000..273b6d87 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/SOURCE_VENV_PACKAGE_RED.txt @@ -0,0 +1,48 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 19 items + +tests/unit/specfact_code_review/run/test_runtime_builder.py ............ [ 63%] +.....FF [100%] + +=================================== FAILURES =================================== +_ test_source_package_named_venv_is_retained_but_real_environments_are_excluded _ + +tmp_path = PosixPath('/pytest-of-user/pytest-197/test_source_package_named_venv0') + + def test_source_package_named_venv_is_retained_but_real_environments_are_excluded(tmp_path: Path) -> None: + source = tmp_path / "project" + package = source / "src/customer/venv" + package.mkdir(parents=True) + (package / "core.py").write_text("VALUE = 1\n") + before = source_identity(source) + (package / "core.py").write_text("VALUE = 2\n") +> assert source_identity(source) != before +E AssertionError: assert 'sha256:9c24107acb765ef69a15395d1db9b0eaa009aa470c2f64214636a3b24a8ce076' != 'sha256:9c24107acb765ef69a15395d1db9b0eaa009aa470c2f64214636a3b24a8ce076' +E + where 'sha256:9c24107acb765ef69a15395d1db9b0eaa009aa470c2f64214636a3b24a8ce076' = source_identity(PosixPath('/pytest-of-user/pytest-197/test_source_package_named_venv0/project')) + +tests/unit/specfact_code_review/run/test_runtime_builder.py:146: AssertionError +___________ test_alias_cannot_import_actual_custom_named_environment ___________ + +tmp_path = PosixPath('/pytest-of-user/pytest-197/test_alias_cannot_import_actua0') + + def test_alias_cannot_import_actual_custom_named_environment(tmp_path: Path) -> None: + source = tmp_path / "project" + environment = source / "custom-python" + environment.mkdir(parents=True) + (environment / "pyvenv.cfg").write_text("home = /usr/bin\n") + (environment / "private.txt").write_text("excluded fixture") + (source / "alias.txt").symlink_to("custom-python/private.txt") +> with pytest.raises(ProjectRuntimeError, match="symlink_excluded"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_builder.py:165: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_source_package_named_venv_is_retained_but_real_environments_are_excluded +FAILED tests/unit/specfact_code_review/run/test_runtime_builder.py::test_alias_cannot_import_actual_custom_named_environment +========================= 2 failed, 17 passed in 0.75s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/STAGED_VCS_TREE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/STAGED_VCS_TREE_RED.txt new file mode 100644 index 00000000..4675c30b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/STAGED_VCS_TREE_RED.txt @@ -0,0 +1,154 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 9 items + +tests/unit/specfact_code_review/run/test_runtime_vcs.py .....FFFF [100%] + +=================================== FAILURES =================================== +_____ test_staged_snapshot_binds_captured_tree_independently_of_live_index _____ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_staged_snapshot_bin...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) + + def test_staged_snapshot_binds_captured_tree_independently_of_live_index(staged_project) -> None: + root, resolution = staged_project + snapshot = resolution.head_snapshot + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) +> assert plan.vcs["tree"] == snapshot.tree + ^^^^^^^^^^^^^^^^ +E KeyError: 'tree' + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:126: KeyError +___________ test_private_build_index_retains_staged_rename_and_head ____________ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_private_build_index...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_private_build_index_retai0') + + def test_private_build_index_retains_staged_rename_and_head(staged_project, tmp_path: Path) -> None: + root, resolution = staged_project + snapshot = resolution.head_snapshot + copied = tmp_path / "build-copy" + copy_project(snapshot.root, copied, include_vcs=False) +> copy_vcs_context(root, copied, resolution.base_snapshot.commit, tree=snapshot.tree) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:138: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +args = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_private_build_index...gn/T/pytest-of-dom/pytest-245/test_private_build_index_retai0/build-copy'), '4a944466ac95a51758a9e4cf5ab1617349a5569f') +kwargs = {'tree': '14dd0b830997655370b92b481566b173120d1c06'} + + def wrapper(*args, **kwargs): # type: ignore + """Wrap func by checking the preconditions and postconditions.""" + kwargs_error = _assert_no_invalid_kwargs(kwargs) + if kwargs_error: + raise kwargs_error + + # We need to create a new in-progress set if it is None as the ``ContextVar`` does not accept + # a factory function for the default argument. If we didn't do this, and simply set an empty + # set as the default, ``ContextVar`` would always point to the same set by copying the default + # by reference. + in_progress = _IN_PROGRESS.get() + if in_progress is None: + in_progress = set() + _IN_PROGRESS.set(in_progress) + + # Use try-finally instead of ExitStack for performance. + try: + # If the wrapper is already checking the contracts for the wrapped function, avoid a recursive loop + # by skipping any subsequent contract checks for the same function. + if id_func in in_progress: + return func(*args, **kwargs) + + in_progress.add(id_func) + + (preconditions, snapshots, postconditions) = _unpack_pre_snap_posts( + wrapper + ) + + resolved_kwargs = kwargs_from_call( + param_names=param_names, + kwdefaults=kwdefaults, + args=args, + kwargs=kwargs, + ) + + type_error = _assert_resolved_kwargs_valid( + postconditions=postconditions, resolved_kwargs=resolved_kwargs + ) + if type_error: + raise type_error + + violation_error = _assert_preconditions( + preconditions=preconditions, + resolved_kwargs=resolved_kwargs, + func=func, + ) + if violation_error: + raise violation_error + + # Capture the snapshots + if postconditions and snapshots: + resolved_kwargs["OLD"] = _capture_old( + snapshots=snapshots, resolved_kwargs=resolved_kwargs, func=func + ) + + # Ideally, we would catch any exception here and strip the checkers from the traceback. + # Unfortunately, this can not be done in Python 3, see + # https://stackoverflow.com/questions/44813333/how-can-i-elide-a-function-wrapper-from-the-traceback-in-python-3 +> result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +E TypeError: copy_vcs_context() got an unexpected keyword argument 'tree' + +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: TypeError +_____ test_index_snapshot_keeps_captured_commit_when_repository_head_moves _____ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_index_snapshot_keep...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) + + def test_index_snapshot_keeps_captured_commit_when_repository_head_moves(staged_project) -> None: + root, resolution = staged_project + _git(root, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "-c", "commit.gpgsign=false", "commit", "-qm", "advance HEAD") + assert _git(root, "rev-parse", "HEAD") != resolution.base_snapshot.commit + snapshot = resolution.head_snapshot + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) +> assert plan.vcs["commit"] == resolution.base_snapshot.commit +E AssertionError: assert '5ae693dc3959...c68b5e92622d0' == '4a944466ac95...1617349a5569f' +E +E - 4a944466ac95a51758a9e4cf5ab1617349a5569f +E + 5ae693dc39598279b3090c20ee9c68b5e92622d0 + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:152: AssertionError +_______ test_builder_attaches_captured_tree_before_package_manager_runs ________ + +staged_project = (PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_builder_attaches_ca...manifest_digest='', policy_bundle=None, policy_paths=(), policy_manifest_digest='', candidate_policy_change_digest='')) +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-245/test_builder_attaches_captured0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x10d55b890> + + def test_builder_attaches_captured_tree_before_package_manager_runs(staged_project, tmp_path: Path, monkeypatch) -> None: + _root, resolution = staged_project + snapshot = resolution.head_snapshot + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + staging = tmp_path / "staging" + staging.mkdir() + + def stop_after_vcs(*_args, **_kwargs): + raise RuntimeError("stop before dependency acquisition") + + monkeypatch.setattr(runtime_builder, "stage_git", stop_after_vcs) + with pytest.raises(RuntimeError, match="stop before dependency acquisition"): + runtime_builder._build(plan, object(), staging) +> assert _git(staging / "project", "write-tree") == snapshot.tree +E AssertionError: assert 'ae7df65c33f2...90c0053ddf0b7' == '14dd0b830997...6b173120d1c06' +E +E - 14dd0b830997655370b92b481566b173120d1c06 +E + ae7df65c33f278677e0d7eb633690c0053ddf0b7 + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:169: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_staged_snapshot_binds_captured_tree_independently_of_live_index +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_private_build_index_retains_staged_rename_and_head +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_index_snapshot_keeps_captured_commit_when_repository_head_moves +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_builder_attaches_captured_tree_before_package_manager_runs +========================= 4 failed, 5 passed in 1.11s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/STARTUP_DISPATCH_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/STARTUP_DISPATCH_RED.txt new file mode 100644 index 00000000..885c2c71 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/STARTUP_DISPATCH_RED.txt @@ -0,0 +1,432 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 59 items / 45 deselected / 14 selected + +tests/unit/specfact_code_review/run/test_target_bootstrap.py FFFFFFFFFFF [ 78%] +F.. [100%] + +=================================== FAILURES =================================== +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pytest-observe' +operation = "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...et_bootstrap.py', 'pytest-observe']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-runpy._get_code_from_file = lambda *a: compile('print("COUNTERFEIT_DISPATCH")', '', 'exec')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pytest-observe' +operation = 'runpy._get_code_from_file = lambda *a: compile(\'print("COUNTERFEIT_DISPATCH")\', \'\', \'exec\')' + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...et_bootstrap.py', 'pytest-observe']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-pkgutil.read_code = lambda *a: compile('print("COUNTERFEIT_DISPATCH")', '', 'exec')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pytest-observe' +operation = 'pkgutil.read_code = lambda *a: compile(\'print("COUNTERFEIT_DISPATCH")\', \'\', \'exec\')' + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...et_bootstrap.py', 'pytest-observe']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-io.open_code = lambda *a: io.BytesIO(b'print("COUNTERFEIT_DISPATCH")')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pytest-observe' +operation = 'io.open_code = lambda *a: io.BytesIO(b\'print("COUNTERFEIT_DISPATCH")\')' + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...et_bootstrap.py', 'pytest-observe']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) + assert completed.returncode != 0, completed.stdout +> assert "project_worker_startup_import_state_changed" in completed.stderr +E assert 'project_worker_startup_import_state_changed' in 'Traceback (most recent call last):\n File "", line 8, in \n bootstrap[\'main\']()\n ~~~~~~~~~~...string>", line 1, in \nNameError: name \'importlib\' is not defined. Did you forget to import \'importlib\'?\n' +E + where 'Traceback (most recent call last):\n File "", line 8, in \n bootstrap[\'main\']()\n ~~~~~~~~~~...string>", line 1, in \nNameError: name \'importlib\' is not defined. Did you forget to import \'importlib\'?\n' = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...tring>", line 1, in \nNameError: name \'importlib\' is not defined. Did you forget to import \'importlib\'?\n').stderr + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:507: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print("COUNTERFEIT_DISPATCH")', '', 'exec')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = 'importlib.machinery.SourceFileLoader.get_code = lambda *a: compile(\'print("COUNTERFEIT_DISPATCH")\', \'\', \'exec\')' + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' +operation = "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) +> assert completed.returncode != 0, completed.stdout +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 != 0 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:506: AssertionError +_ test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pytest-observe' +operation = "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')" + + @pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pytest-observe", "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ("pylint", "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')"), + ("pylint", "importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')"), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')"), + ], + ) + def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str + ) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) + assert completed.returncode != 0, completed.stdout +> assert "project_worker_startup_import_state_changed" in completed.stderr +E assert 'project_worker_startup_import_state_changed' in 'Traceback (most recent call last):\n File "", line 8, in \n bootstrap[\'main\']()\n ~~~~~~~~~~...t-of-dom/pytest-357/test_executable_pth_cannot_sub11/snapshot/unverified/specfact_code_review/run/target_pytest.py\'\n' +E + where 'Traceback (most recent call last):\n File "", line 8, in \n bootstrap[\'main\']()\n ~~~~~~~~~~...t-of-dom/pytest-357/test_executable_pth_cannot_sub11/snapshot/unverified/specfact_code_review/run/target_pytest.py\'\n' = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...-of-dom/pytest-357/test_executable_pth_cannot_sub11/snapshot/unverified/specfact_code_review/run/target_pytest.py\'\n').stderr + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:507: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-runpy._get_code_from_file = lambda *a: compile('print("COUNTERFEIT_DISPATCH")', '', 'exec')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-pkgutil.read_code = lambda *a: compile('print("COUNTERFEIT_DISPATCH")', '', 'exec')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-io.open_code = lambda *a: io.BytesIO(b'print("COUNTERFEIT_DISPATCH")')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-importlib.machinery.SourceFileLoader.get_code = lambda *a: compile('print("COUNTERFEIT_DISPATCH")', '', 'exec')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pylint-builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_executable_pth_cannot_substitute_analyzer_dispatch[pytest-observe-next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')] +================= 12 failed, 2 passed, 45 deselected in 0.85s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/STARTUP_EXIT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/STARTUP_EXIT_RED.txt new file mode 100644 index 00000000..8b51be07 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/STARTUP_EXIT_RED.txt @@ -0,0 +1,49 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 63 items / 61 deselected / 2 selected + +tests/unit/specfact_code_review/run/test_target_bootstrap.py FF [100%] + +=================================== FAILURES =================================== +_ test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout[pylint] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pylint' + + @pytest.mark.parametrize("module", ["pylint", "pytest-observe"]) + def test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout( + collision_runtime: dict[str, Path], module: str + ) -> None: + completed = _probe_member_dispatch( + collision_runtime, module, "print('[]'); runpy._run_code = lambda *a, **k: None" + ) +> assert completed.returncode == 78 +E assert 1 == 78 +E + where 1 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...ntimeError: project_worker_startup_import_state_changed; target .pth hooks must preserve protected import machinery\n').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:546: AssertionError +_ test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout[pytest-observe] _ + +collision_runtime = {'PROJECT': PosixPath('')} +module = 'pytest-observe' + + @pytest.mark.parametrize("module", ["pylint", "pytest-observe"]) + def test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout( + collision_runtime: dict[str, Path], module: str + ) -> None: + completed = _probe_member_dispatch( + collision_runtime, module, "print('[]'); runpy._run_code = lambda *a, **k: None" + ) +> assert completed.returncode == 78 +E assert 1 == 78 +E + where 1 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...ntimeError: project_worker_startup_import_state_changed; target .pth hooks must preserve protected import machinery\n').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:546: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout[pylint] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout[pytest-observe] +======================= 2 failed, 61 deselected in 0.21s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/STARTUP_LOADER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/STARTUP_LOADER_RED.txt new file mode 100644 index 00000000..0894d066 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/STARTUP_LOADER_RED.txt @@ -0,0 +1,68 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 66 items / 63 deselected / 3 selected + +tests/unit/specfact_code_review/run/test_target_bootstrap.py FFF [100%] + +=================================== FAILURES =================================== +___________ test_preloaded_analyzer_origin_failure_uses_startup_exit ___________ + +collision_runtime = {'PROJECT': PosixPath('')} + + def test_preloaded_analyzer_origin_failure_uses_startup_exit(collision_runtime: dict[str, Path]) -> None: + operation = ( + "spec = importlib.util.spec_from_file_location('packaging', 'packaging.py'); " + "module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module); " + "sys.modules['packaging'] = module; print('[]')" + ) + completed = _probe_member_dispatch(collision_runtime, "pylint", operation) +> assert completed.returncode == 78 +E assert 1 == 78 +E + where 1 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...roject_worker_analyzer_origin_mismatch:{fullname}")\nImportError: project_worker_analyzer_origin_mismatch:packaging\n').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:556: AssertionError +_ test_loader_filename_substitution_cannot_redirect_verified_dispatch[SourceFileLoader] _ + +collision_runtime = {'PROJECT': PosixPath('')} +owner = 'SourceFileLoader' + + @pytest.mark.parametrize("owner", ["SourceFileLoader", "SourceFileLoader.__mro__[1]"]) + def test_loader_filename_substitution_cannot_redirect_verified_dispatch( + collision_runtime: dict[str, Path], owner: str + ) -> None: + operation = f"importlib.machinery.{owner}.get_filename = lambda self, fullname=None: 'counterfeit.py'" + completed = _probe_member_dispatch(collision_runtime, "pylint", operation) +> assert completed.returncode == 78, completed.stdout + completed.stderr +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 == 78 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:567: AssertionError +_ test_loader_filename_substitution_cannot_redirect_verified_dispatch[SourceFileLoader.__mro__[1]] _ + +collision_runtime = {'PROJECT': PosixPath('')} +owner = 'SourceFileLoader.__mro__[1]' + + @pytest.mark.parametrize("owner", ["SourceFileLoader", "SourceFileLoader.__mro__[1]"]) + def test_loader_filename_substitution_cannot_redirect_verified_dispatch( + collision_runtime: dict[str, Path], owner: str + ) -> None: + operation = f"importlib.machinery.{owner}.get_filename = lambda self, fullname=None: 'counterfeit.py'" + completed = _probe_member_dispatch(collision_runtime, "pylint", operation) +> assert completed.returncode == 78, completed.stdout + completed.stderr +E AssertionError: COUNTERFEIT_DISPATCH +E +E assert 0 == 78 +E + where 0 = CompletedProcess(args=['/private/tmp/specfact-473-worktree/.venv/bin/python', '-I', '-S', '-c', "\nimport runpy, sys\n...= ['target_bootstrap.py', 'pylint']\nbootstrap['main']()\n"], returncode=0, stdout='COUNTERFEIT_DISPATCH\n', stderr='').returncode + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:567: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_preloaded_analyzer_origin_failure_uses_startup_exit +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_loader_filename_substitution_cannot_redirect_verified_dispatch[SourceFileLoader] +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_loader_filename_substitution_cannot_redirect_verified_dispatch[SourceFileLoader.__mro__[1]] +======================= 3 failed, 63 deselected in 1.31s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/TARGETED_TESTPATHS_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/TARGETED_TESTPATHS_RED.txt new file mode 100644 index 00000000..142924e5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/TARGETED_TESTPATHS_RED.txt @@ -0,0 +1,610 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /private/tmp/specfact-473-delivery +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 68 items / 56 deselected / 12 selected + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py FF [ 16%] +FFFFFF.. [ 83%] +tests/unit/specfact_code_review/run/test_portable_worker.py F. [100%] + +=================================== FAILURES =================================== +_ test_source_review_expands_native_testpaths[tests/test_app.py-tests/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat0') +testpaths = 'tests/test_app.py', relative = 'tests/test_app.py' +patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...add7ec246cc73abeb134bafd4106e08b1e8fd'}, pytest_config={'testpaths': 'tests/test_app.py', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat0/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths[tests/*.py-tests/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat1') +testpaths = 'tests/*.py', relative = 'tests/test_app.py', patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...f84c21b986de0c5b21865e77ad75c8c21e0a88c98109'}, pytest_config={'testpaths': 'tests/*.py', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat1/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths[tests/*-tests/unit/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat2') +testpaths = 'tests/*', relative = 'tests/unit/test_app.py' +patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...262e55ad02850eb4ae0ab0601fa88403eb88e8f10d6cb35'}, pytest_config={'testpaths': 'tests/*', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat2/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths[tests/**/test_app.py-tests/unit/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat3') +testpaths = 'tests/**/test_app.py', relative = 'tests/unit/test_app.py' +patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...0a4abb3ec4615c56b4916f6945c3b2de24'}, pytest_config={'testpaths': 'tests/**/test_app.py', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat3/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths[absent tests/*.py-tests/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat4') +testpaths = 'absent tests/*.py', relative = 'tests/test_app.py' +patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...32b040791164cc43490143cd757e3557fdb67'}, pytest_config={'testpaths': 'absent tests/*.py', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat4/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths[absent*-tests/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat5') +testpaths = 'absent*', relative = 'tests/test_app.py', patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...dcd2af121b9159c90c8cfed85b629938b3fe6d02ea41dd7'}, pytest_config={'testpaths': 'absent*', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat5/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths["integration tests/*.py"-integration tests/test_app.py-test_*.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat6') +testpaths = '"integration tests/*.py"' +relative = 'integration tests/test_app.py', patterns = 'test_*.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...260d8e84c49d18e783fa4f6f893290'}, pytest_config={'testpaths': '"integration tests/*.py"', 'python_files': 'test_*.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat6/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_ test_source_review_expands_native_testpaths[tests/*.py-tests/app_test.py-*_test.py] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat7') +testpaths = 'tests/*.py', relative = 'tests/app_test.py', patterns = '*_test.py' + + @pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], + ) + def test_source_review_expands_native_testpaths( + tmp_path: Path, testpaths: str, relative: str, patterns: str + ) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected +> selectors = select_test_paths(discover_project(root), [source], full=False) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py:255: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +plan = ProjectPlan(root=PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_sou...0c5a554530b223184f97912de744ac1fb1968689f845'}, pytest_config={'testpaths': 'tests/*.py', 'python_files': '*_test.py'}) +files = [PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_source_review_expands_nat7/project/app.py')] +full = False + + @ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) + def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: +> raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_test_selection_empty: include corresponding test files + +packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py:99: ProjectRuntimeError +_______________ test_expanded_testpaths_reject_escaping_symlinks _______________ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-466/test_expanded_testpaths_reject0') + + def test_expanded_testpaths_reject_escaping_symlinks(tmp_path: Path) -> None: + root = tmp_path / "project" + (root / "tests").mkdir(parents=True) + source = root / "app.py" + source.touch() + outside = tmp_path / "external" + outside.mkdir() + (outside / "test_app.py").touch() + (root / "tests/external").symlink_to(outside, target_is_directory=True) + plan = ProjectPlan(root, manager="pip", pytest_config={"testpaths": ["tests/*"]}) +> with pytest.raises(ProjectRuntimeError, match="project_test_path_escape"): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AssertionError: Regex pattern did not match. +E Expected regex: 'project_test_path_escape' +E Actual message: 'project_test_selection_empty: include corresponding test files' + +tests/unit/specfact_code_review/run/test_portable_worker.py:240: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[tests/test_app.py-tests/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[tests/*.py-tests/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[tests/*-tests/unit/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[tests/**/test_app.py-tests/unit/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[absent tests/*.py-tests/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[absent*-tests/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths["integration tests/*.py"-integration tests/test_app.py-test_*.py] +FAILED tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py::test_source_review_expands_native_testpaths[tests/*.py-tests/app_test.py-*_test.py] +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_expanded_testpaths_reject_escaping_symlinks +================== 9 failed, 3 passed, 56 deselected in 1.69s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/TDD_EVIDENCE.md b/openspec/changes/code-review-16-portable-project-runtime/TDD_EVIDENCE.md new file mode 100644 index 00000000..2830aad8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/TDD_EVIDENCE.md @@ -0,0 +1,590 @@ +# TDD evidence + +Implementation begins from dev 976460c0541b685f5fae256faef4cec32fe9ba7e on 2026-09-14 (Europe/Berlin). The original customer reproduction is inaccessible. Historical runs and subsequent external execution evidence are recorded below. Record each mapped failing-before run before its production changes and passing-after evidence below. + +## Discovery — failing before production changes + +`hatch run pytest -q tests/unit/specfact_code_review/run/test_runtime_discovery.py` failed collection with `ModuleNotFoundError: specfact_code_review.run.runtime_discovery` (12 cases planned). No discovery production implementation existed. Output retained in DISCOVERY_RED.txt. + +## Discovery passing / artifacts failing + +Discovery: 12 passed in 0.10s. Runtime artifact/adapter tests failed collection before production changes: ModuleNotFoundError runtime_artifacts. See ARTIFACTS_RED.txt. + +## Artifact passing / builder failing + +18 discovery/artifact/adapter tests passed in 0.11s. Builder/source-copy tests fail before implementation with ModuleNotFoundError runtime_builder; see BUILDER_RED.txt. Builder versions verified against PyPI JSON on 2026-09-14: pip 26.2.1, uv 0.12.13, Hatch 1.18.0, Poetry 2.4.3. + +## Worker and CLI failing before integration + +Worker tests fail collection for missing portable_worker. All three CLI tests fail: missing runtime command, missing handoff options, and rejected project_config/project_runtime kwargs. See WORKER_RED.txt and CLI_RED.txt. + +## Snapshot integration failing + +Seven CLI/worker tests pass; automatic snapshot attachment fails before production integration with missing portable_snapshot. See SNAPSHOT_RED.txt. + +## Immutable pair failing + +Independent pair test fails for missing run_project_scope_pair; two other snapshot tests pass. Existing command/runner/sandbox/tool regressions: 511 passed, 14 failed. Failures identify accidental ScopeRequest option forwarding and legacy mocked runtimes entering the newly added preparation boundary. Preserve original legacy assertions and isolate their new dependency boundary. + +## Runtime hardening and real external preparation + +Additional failing-before artifacts cover pytest configuration precedence and execution/coverage evidence; changed local workspace source; safe internal symlinks; explicit dependency groups; customer Actions source layouts; worker PID/mount isolation; worker identity attachment; native library inventory; credential-free dependency inventory; native Git builder support; analyzer/project dependency conflicts; and dependency-only immutable ranges. Each corresponding `*_RED.txt` was captured before retaining its implementation. + +On 2026-09-14, 49 portable-runtime/corpus unit tests passed in 0.31 seconds; the subsequently added worker identity and compatibility scenarios passed separately. Scope plus portable snapshot regressions passed 130 tests in 9.90 seconds. Formatting, typing, lint, manifest/YAML validation, bundle import checks, development checksum/version checks, and strict OpenSpec validation passed before the first full-suite run. + +The first complete suite reported 1,972 passed and 22 failed in 75.63 seconds. Five failures were caused by the existing paired core checkout being 0.54.0, below this bundle's already-declared >=0.55.1 requirement. Fifteen Semgrep cases could not load macOS trust anchors. Two failures identified missing command documentation/generated command artifacts. Verification now uses a separate checkout of public core 0.55.4 at d5799705, an explicit certificate bundle, and regenerated documentation; rerun evidence follows. + +Real non-root Ubuntu 24.04 x86-64/Python 3.12 preparation succeeded for all four exact pinned upstream repositories: Requests/pip, Hatch/hatch-test, Flask/uv, and Poetry/test. The controller used public core 0.55.4 and the signed 0.49.85 base, with this worktree's development preparation implementation. This is acquisition/bootstrap evidence, not updated signed analyzer or release acceptance. Hatch first exposed missing Git and then its newer glibc requirement; Git now runs with its own private loader/library closure, leaving the supervisor's runtime unchanged. + +The dedicated worktree was moved to `/private/tmp/specfact-473-worktree` after automatic approval-service timeouts on edits outside writable roots. Branch identity and all uncommitted changes were preserved; the main dev checkout remains untouched. + +## Review-driven corrections and current delivery boundary + +The corrected core/certificate/documentation baseline passed 1,994 tests in 95.74 seconds; smart tests passed the same 1,994 tests and contracts passed 28 tests (1,966 deselected). Subsequent review-driven changes require fresh evidence and do not inherit those passing claims. + +The active worktree is now `/private/tmp/specfact-473-implementation`, with writable Git metadata in `/private/tmp/specfact-473-git.git`, the same feature branch and base, and canonical pre-commit hooks installed. Its task-owned development virtualenv symlink is locally excluded from Git. The user checkout remains untouched. + +Additional failing-before artifacts record native Python CLI handoff and fail-closed startup, in-repository parent requirements includes, verified active environment selection, exact signed-interpreter patch constraints, and complete external analyzer-roster validation. Grouped adapter/CLI/worker tests were split into corresponding module test files; historical failing transcripts retain their original filenames. + +A fresh bug-hunt review exposed an invalid development-only BasedPyright command combining `--venvpath` and `--pythonpath`. That report is not passing analyzer evidence. A real BasedPyright subprocess regression now verifies the corrected `--pythonpath` invocation and dependency imports (9 focused command tests passed). The later full run found 16 failures: foreign Hatch activation, four test observers still reading parameters moved into snapshot settings, and five worktree-identity tests rejecting the task's untracked virtualenv symlink. Discovery now verifies the active environment belongs to the reviewed root; observers inspect the equivalent settings fields; local Git exclusion fixes the task environment without relaxing source identity rules. Runner/discovery verification passed 424 tests in 12.63 seconds after these corrections. + +Open acceptance work remains: full updated capsule execution across all twelve Linux corpus combinations, the labelled reconstructed Hatch/native fixture, member-specific dependency closure isolation, interpreter selection across differing base/head constraints, complete preparation/cache failure cases, and published signed installation acceptance. Successful preparation of four cp312 environments does not satisfy these criteria. This change must remain open and any initial PR must remain draft until acceptance and review are complete; #472 additionally requires access to and validation of the original customer reproduction. + +Latest local full run on 2026-09-14: `hatch run test` with public core 0.55.4 and the explicit CA bundle passed **2,008 tests in 81.90 seconds**, with two pre-existing lark deprecation warnings. Contract tests passed **28 tests**, with 1,980 deselected. Formatting and typing passed. Formal development verification passed with `--payload-from-filesystem --enforce-version-bump --allow-missing-public-key --version-check-base refs/remotes/origin/dev`, matching the canonical branch-aware hook; this is not a release-signature claim. + +Smart-test verification subsequently passed **2,008 tests in 82.95 seconds**. The development virtualenv lacked CrossHair, causing the review tool to discover a globally installed executable using a different interpreter. Installed the capsule-pinned `crosshair-tool==0.0.109` into the task-owned virtualenv; fresh review must be rerun with that environment. Earlier advisory reports containing the CrossHair import error are not complete passing analyzer evidence. + +The staged-snapshot pytest failure was isolated to `test_github_candidate_context_failure_never_uses_stale_official_payload`: it depended on the caller checkout containing `.git`. The test now constructs its own publisher-checkout fixture and preserves the original fail-closed assertions. Recorded failing transcripts are normalized only for trailing whitespace; their outcomes and diagnostics are unchanged. + + +### Follow-up from candidate Linux evidence (2026-09-14) + +- Signed branch head `6499136806637c6c2075ea44c857e0b90a8fb301`, PR orchestrator run `34839312884`: existing capsule namespace/cold/warm/controlled-defect fixtures completed as expected on Python 3.11/3.12/3.13. Python 3.12 Requests external acceptance passed; Hatch analysis remained incomplete. Original customer reproduction remains inaccessible and unvalidated. +- The Hatch adapter incorrectly supplied global `-e` to native `env create/find`, which take a positional name. The host command reported `Unknown environment: hatch-test` and the former harness accepted exit 1 without test inventory. These were implementation defects, not evidence of customer compatibility. +- `HATCH_EXECUTION_RED.txt`, `HATCH_NATIVE_EXPORT_RED.txt`, `EXECUTABLE_INVENTORY_RED.txt`, and `OFFLINE_PLUGIN_RED.txt` record failures before fixes. Native exports now select the concrete ABI environment; artifacts retain owned scripts/native executables; host acceptance requires actual JUnit test execution; setup/collection/internal errors remain observable. Local Linux preparation selected `hatch-test.py3.12`; execution exposed missing localhost resolution in pytest-rerunfailures, addressed with private hosts configuration inside the offline namespace. +- `MEMBER_CLOSURE_RED.txt`, `MEMBER_IMPORT_DOMAIN_RED.txt`, `NATIVE_LOADER_RED.txt`, and `NATIVE_WORKER_LAUNCH_RED.txt` precede member-specific sealed import graphs and target native loader/libc isolation. Unit checks do not substitute for Linux acceptance. +- `RECONSTRUCTION_RED.txt` and `CORPUS_CONTINUATION_RED.txt` precede the labelled detached Hatch/src-layout/asyncio/requests/pyodbc fixture and continuation across failed corpus entries. Any failed entry still fails the gate. +- `VCS_VERSION_RED.txt`, `VCS_SNAPSHOT_RED.txt`, and `PYTHON_SELECTION_RED.txt` precede Git version/cache binding and signed target Python selection independent from the controller. +- Follow-up full run: 2011 passes and 15 Semgrep failures when the task's required SSL_CERT_FILE was omitted. Rerun with the established certificate bundle is required; that failed run is not passing evidence. + + +### Passing follow-up evidence (2026-09-14) + +- Local full and smart suites: 2032 passed, two existing lark deprecation warnings, 100.91s and 99.93s respectively. Contract suite: 28 passed. Formatting, typing, lint (10.00/10), YAML, import boundaries, strict OpenSpec, planned Requirements mapping, and development checksum/version verification passed before review refinements. +- Actual local Linux x86-64/non-root/Python 3.12 component runs: pinned Hatch `hatch-test.py3.12` collected/executed all five selected tests, exit 0; labelled detached reconstruction executed its asyncio/requests/pyodbc test, exit 0. Source and artifacts remained in private task storage. These use signed baseline Python with development worker components and are **not signed release acceptance**. +- `HATCH_INSTALLER_DOMAIN_RED.txt` records the pip-backed detached failure before manager-specific environment controls were separated. Upstream Hatch's uv installer was unaffected; the reconstruction was essential to expose pip redirection. +- Fresh SpecFact bug-hunt review identified a preparation complexity regression and long review function; refactoring passed 411 focused tests. Targeted pytest instrumentation subsequently recorded 1058 passes; a fresh review is required after the fixes. + + +- Instrumenting the actual review subprocess (not a separately invoked test suite) revealed 30 worktree-test failures caused by inherited `SPECFACT_CODE_REVIEW_CHANGED_DIFF=cached`. `NESTED_SCOPE_RED.txt` precedes removal of that controller-only setting from child pytest environments. The controller setting remains intact, and tests may explicitly set their own scope. + + +- Fresh SpecFact `--bug-hunt` after nested scope isolation: exit 0, no error findings, 43.54s. Lint/type/format recheck passed (10.00/10). Warning-level review work remains before final acceptance, including explicit standard-library bootstrap contract exceptions and snapshot argument grouping. The PR remains a draft and no release acceptance is claimed. + + +- The first follow-up commit attempt was stopped by canonical hooks. A new VCS fixture inherited the hook's GIT_INDEX_FILE and staged its temporary app.py into the calling index. No commit was created. The fixture-only staged artifact was removed; `HOOK_INDEX_ISOLATION_RED.txt` records regression failures before sanitizing both fixture Git processes and the child pytest environment. Source changes were retained. + +## PR #474 feedback and second candidate matrix + +The signed candidate `b8f762551430566b9fc2f9a03eb5ca83e2f97ad9` failed all three ABI jobs in run 34845391063. All four upstream cold reviews reached analysis. Pylint could not map its bundled checker files to modules because its sealed dependency closure was exposed only by an import finder. Poetry additionally failed xdist startup because nested Python lost its private coverage destination. The reconstructed Hatch host baseline duplicated the explicitly loaded asyncio plugin under its entry-point alias. These are failures, not completed acceptance. + +The follow-up introduces a real read-only member package directory, preserves pytest child-domain/coverage settings and native Hatch extra arguments, and records the host baseline's explicit `-p no:asyncio` alias suppression while retaining `-p pytest_asyncio.plugin`. The host command and justification are checked into the corpus manifest. No upstream source or pytest selection is changed. + +Local Ubuntu 24.04 x86-64 non-root cp312 component observations (not signed candidate or release acceptance): Hatch 5 tests, Flask 19 tests, Poetry 7 tests with unchanged `-n logical`, and reconstructed Hatch 1 test including pyodbc all executed successfully. Pylint separately completed with exit 0 on the Hatch slice after the member-directory fix. The reconstructed native-manager host baseline now executes successfully. The preceding full local suite passed 2046 tests in 105.67 seconds (two existing lark warnings); later review fixes require a fresh suite. + +PR findings have failing-before regressions in `PR_REVIEW_474_ROUND1_RED.txt`, `PR_REVIEW_474_ROUND2_RED.txt`, `SOURCE_LINK_EXCLUSIONS_RED.txt`, and the specific option/evidence transcripts. Transcript machine paths have been replaced with stable placeholders; test assertions and observed outcomes are preserved. Every `*RED.txt` is historical failing-before evidence, not a current acceptance gate. In particular, `NATIVE_RED.txt` does not claim the module remains absent. Native Linux candidate and public-release acceptance remain mandatory and incomplete. + +PR follow-up verification: full local suite passed **2066 tests in 101.72 seconds**, with two existing lark warnings. The subsequent focused command/scope/descriptor/source/startup regression set passed **238 tests in 11.36 seconds**; the source-link compatibility subset passed **36 tests**. The second fresh `--bug-hunt` review completed in 43.94 seconds after refactoring the four initially reported blockers. Warning cleanup and external acceptance remain open; development checksum verification does not substitute for release signing. + +## Complete signed candidate and subsequent review refinements + +Signed candidate `790c39eb47b89046f6525cce9f7345f746896706`, Actions run [34850206113](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34850206113), passed the customer capsule jobs on Python 3.11, 3.12 and 3.13. All four pinned upstream repositories and the reconstructed Hatch/native fixture completed applicable analysis with unchanged source. Initial collected/executed test counts were Requests 24, Hatch 5, Flask 19, Poetry 7, and reconstruction 1 per ABI. Real findings remain visible; acceptance does not require zero findings. This is signed candidate evidence, not public release acceptance. + +Subsequent local refinements cover pip-tools/pylock inputs, setup.cfg Python constraints, group/extra ambiguity, complete permission-mode source identity, required member dependency closure, private analyzer source copies, index VCS context, pytest collection/internal errors, inventory argument shape, and native ELF architecture. Failing-before transcripts are `PIP_METADATA_SELECTION_RED.txt`, `MEMBER_CLOSURE_RED.txt`, `ANALYSIS_EXCLUSIONS_RED.txt`, and `PR_REVIEW_474_ROUND3_RED.txt`. Local full verification passed **2,085 tests in 102.51 seconds** with two existing lark warnings; targeted PR regressions passed 53 tests. Type checking and lint passed after the corpus additions. These refinements need a new signed candidate matrix. + +The initial warm corpus requested offline reuse without independently denying host networking. `OFFLINE_CORPUS_RED.txt` records the stronger harness requirements and a controlled-report naming regression. The corrected harness passes 11 unit tests and uses administrator-provisioned Bubblewrap solely to deny external networking around warm customer commands. A child probe verifies non-root execution, a distinct network namespace and no active non-loopback interfaces. This test harness does not grant capsule or protected PR authority. `CORPUS_TRANSFER_RED.txt` covers host-interface byte counters, explicitly labelled as host-wide measurements. + +Windows/macOS automatic worker handoff and platform-native test support remain outside the accepted Linux x86-64 execution scope. Metadata discovery must not be presented as cross-platform execution acceptance. The original customer reproduction remains required before closing #472. + +The stronger network wrapper was exercised on the non-root Linux component host: the child ran as UID 1001 in a distinct network namespace with no active non-loopback interface. The strict member closure also prepared the reconstructed Hatch environment and executed its native-extension test successfully (pytest 9.1.1, pytest-cov 7.1.0). This remains component evidence; the updated customer-interface candidate matrix is still required. Requirements evidence passed at the repository-required `planned` maturity; it does not claim implementation-verification maturity. Narrow review exceptions are enumerated in `REVIEW_EXCEPTIONS.md`. + +Final local gate pass before the next candidate push: full tests **2,087 passed in 100.79 seconds**; smart tests **2,087 passed in 101.22 seconds**; contracts **28 passed, 2,059 deselected**. Format, type-check, lint, YAML, bundle imports, development signature/version checks, strict OpenSpec, planned Requirements evidence, and canonical publish pre-check passed. Core compatibility remains intentionally `>=0.55.1,<1.0.0`; the signed candidate's minimum-core checks passed on all three ABIs. + +Fresh branch-wide SpecFact review with `--enforcement changed --bug-hunt` completed at 2026-09-14 16:33:01 Europe/Berlin in **49.14 seconds**: **zero errors**, 285 warnings and 69 informational findings. Actionable new mechanical/complexity findings were fixed. Boundary-specific exceptions and unchanged baseline findings are documented in `REVIEW_EXCEPTIONS.md`; coverage limits remain explicit and conditional on Linux acceptance. The preceding review's failing stale-observation mock was fixed at its actual imported launch boundary, and the full suite then passed. + +The independent offline wrapper additionally needed a private `/dev`: the Linux user namespace could not open the bind-mounted host `/dev/null`, so GitPython failed before runtime discovery. `OFFLINE_DEVICES_RED.txt` records this failure. Adding `--dev /dev` restored `git --version` inside the non-root offline namespace, and all 11 corpus harness tests passed. Complete warm preparation/attachment remains an updated signed-CI acceptance gate; the local command-level correction alone is not that acceptance. + +## PR #474 review rounds 4 and 5 (2026-09-14) + +`PR_REVIEW_474_ROUND4_RED.txt` records eight failures before honoring pytest's autoload-disable control, binding directory presence/modes, detecting pylock/requirements.in ambiguity, converting malformed INI to a runtime diagnostic, and protecting the offline launcher's ownership and identity. Focused validation passed 74 tests; the then-current full suite passed 2,101 tests in 103.07 seconds. The Linux UID-1001 offline wrapper executed Git successfully; overwriting the root-owned launcher raised PermissionError. + +Candidate `6eda1a0c950a09417dd563371799e08b14cef856`, run [34857677581](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34857677581), failed all corpus ABI jobs. The downloaded cp312 reports identify repository-relative paths being passed to `relative_to(absolute_root)` during private source mapping. `RELATIVE_SELECTION_RED.txt` records one failing and nine passing tests before the mapping fix; afterward all ten passed, including rejection of escaping selections. Quality jobs failed their required capsule prerequisite, not a separate lint/type gate. This failure remains historical evidence; a new signed candidate run is required. + +`PR_REVIEW_474_ROUND5_RED.txt` records failures before adding the builder's Git exec path and enforcing selected worker dependency versions. Git's HTTP/HTTPS transport helpers now carry their native library closure, and their bytes enter cache identity. This follows Git's [exec-path contract](https://git-scm.com/docs/git) and [remote-helper contract](https://git-scm.com/docs/gitremote-helpers), accessed 2026-09-14. An actual non-root Ubuntu 24.04 x86-64 builder executed `git ls-remote https://github.com/psf/requests.git HEAD`, exit 0, returning the pinned Requests revision `dae7ef63b4df6eded86637f251fc4e3a06c3b479`. This is transport component evidence, not signed release acceptance. The focused builder, transport and dependency-domain suite passed 24 tests. + +An intermediate full suite overlapped the addition of new failing-first tests and a source mutation: 3 failed, 2,104 passed. An earlier accidentally unscoped test invocation omitted the established SSL certificate bundle and also failed. Neither is final passing evidence. The subsequent stable full/smart suites and fresh review are recorded below. + +Stable round-5 validation: full suite **2,109 passed in 90.20 seconds**, smart suite **2,109 passed in 101.43 seconds**, and contracts **28 passed**. Both large suites retain two existing lark deprecation warnings. Type/lint (10.00/10), format, YAML, import boundaries, strict OpenSpec, planned Requirements evidence, the generated command overview, 118 module command-contract paths, and three CLI contract scenarios passed. A final transport-identity postcondition was then checked by its focused tests. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` review completed on **2026-09-14 at 17:21:15 Europe/Berlin**, in **41.62 seconds**. The initial review caught the corpus root identity's optional value type; the type refinement and two test assertion warnings were corrected. The final focused harness/pytest/transport set passed **26 tests in 0.54 seconds**. The final review has **zero errors**, with existing/boundary warnings covered by `REVIEW_EXCEPTIONS.md`. Development checksum/version verification and the canonical publish pre-check passed; core compatibility was intentionally retained. Updated signed candidate and public-release acceptance remain required. + +## PR #474 review round 6 (2026-09-14) + +`PR_REVIEW_474_ROUND6_RED.txt` records seven failures before excluding Git history from analyzer source copies, preserving pytest's repository-root default when `testpaths` is absent/empty, and converting Git identity probe failures into actionable runtime diagnostics. The Git source-copy regression uses a real committed repository and asserts that the analyzer mount has no `.git`, while existing build-copy tests still verify sanitized VCS state and uncommitted source modifications. Parameterized tests cover missing/empty pytest roots and failed/timed-out/unavailable Git probes. The focused set subsequently passed **23 tests**. This follow-up still requires a new signed candidate run; earlier component/candidate receipts are not evidence for the updated artifact. + +Signed candidate `dcc30b46fd7477200b55b5f07292963d62012be7`, run [34861791235](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34861791235), failed the updated corpus. Downloaded cp311/cp312 receipts identify two causes: the hosted runner's writable `/opt` ancestor was rejected by the offline launcher, and Hatch's pytest exited 4 while importing its legitimate `hatch.venv` package. The latter was reproduced in the real non-root Linux private-source worker: `ModuleNotFoundError: No module named 'hatch.venv'`. The common-name `venv` exclusion had removed `src/hatch/venv` from both build and analyzer copies. + +`HOSTED_RUNNER_ANCESTOR_RED.txt` precedes relocating the administrator-provisioned launcher directly under `/specfact-corpus-offline`, with all ownership, ancestor and digest checks retained. Actual Linux UID-1001 execution passed the relocated wrapper's network probe and Git command; replacement remained denied. `SOURCE_VENV_PACKAGE_RED.txt` precedes recognizing actual virtual environments by `pyvenv.cfg`, retaining ordinary `venv` packages and rejecting aliases into custom-named environments. The source/copy/corpus regression set passed **55 tests**. Source and test selection were not patched in the upstream Hatch checkout. + +The corrected private Linux Hatch worker executed all five selected tests successfully with unchanged `--dist worksteal`, pytest 9.1.1, coverage 7.16.1 and pytest-cov 7.1.0. Runtime descriptor identity at that component observation was `603ad4ff2fb113e1f56b9ca355dc6c0bbbf3ad83ad308fa82ab5ca6754dd46fa`. This is component evidence, not signed candidate acceptance. Both local full/smart suites then passed **2,119 tests** (87.95s/88.02s). + +`ENVIRONMENT_TEST_SELECTION_RED.txt` additionally records three failures before pruning actual environments from repository-root test candidate traversal. Installed dependency tests no longer create false ambiguity for a customer's root-level test file. The focused worker/source set passed **18 tests**. Fresh preliminary SpecFact review found no errors and one new line-length advisory, which was corrected before final verification. + +Final round-6 validation: full suite **2,122 passed in 87.74 seconds**, smart suite **2,122 passed in 87.53 seconds**, contracts **28 passed**. Format, type/lint (10.00/10), YAML, bundle imports, development checksum/version verification, strict OpenSpec and planned Requirements evidence passed. Fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14 18:00:01 Europe/Berlin**, in **41.66 seconds**, with **zero errors**. The narrow advisory exceptions remain documented; no current signed-candidate or public-release acceptance is claimed. + +## PR #474 review round 7 (2026-09-14) + +`FALLBACK_SANITIZATION_RED.txt` records three failures before routing runtime-preparation fallback through the same sanitized private source copy as successful preparation. If copying fails, all members record incomplete evidence against an empty private root; no member receives the original unsafe source. The focused snapshot suite subsequently passed **13 tests**. + +`POETRY_MANAGER_INPUTS_RED.txt` records 25 failures before importing/intersecting Poetry Python constraints and rejecting explicit requirements/constraints that non-pip adapters cannot consume. Native managers no longer implicitly select exported pip requirements. Poetry PEP 440 ranges are supported; caret, tilde, union and table declarations receive an exact unsupported-constraint diagnostic and remedy instead of being silently ignored. This is an explicit remaining compatibility limit. The focused discovery, adapter, interpreter, builder and build-driver suite passed **97 tests**. Poetry permits its locking constraint to be narrower than project metadata ([Poetry 2.4 pyproject contract](https://python-poetry.org/docs/pyproject/#requires-python), accessed 2026-09-14). + +`PYTEST_RECURSION_RED.txt` records 12 failures before applying native pytest default/configured `norecursedirs` during corresponding-test discovery. Explicit settings replace defaults, including an empty setting; explicit initial test roots retain native behavior. Native pytest **9.1.1** collection comparisons verified basename, path and override semantics. The focused worker suite passed **34 tests**. These results do not claim plugin-driven collection discovery or cross-platform execution acceptance. + +Round-7 repository validation passed: smart suite **2,172 tests in 87.41 seconds**, full suite **2,172 tests in 88.32 seconds**, and **28 contract tests**. Both large suites retained two existing lark deprecation warnings. Format, type/lint (10.00/10), YAML, bundle imports, strict OpenSpec, planned Requirements evidence, development checksum/version validation and canonical publish pre-check passed. + +The first fresh bug-hunt found a fallback evidence-map type mismatch and new complexity/naming/lambda advisories. After the type refinement, candidate-selection extraction and parameterized interpreter regression, **116 focused tests** passed and lint/type/format plus checksum/version checks passed again. The final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14 18:32:47 Europe/Berlin**, with **zero errors**, 287 warnings and 69 informational findings. The additional deferred import belongs to the already documented runner initialization boundary; other retained advisories remain covered by `REVIEW_EXCEPTIONS.md`. Independent subagent cross-reviews reported no further actionable defects. Updated signed-candidate and public-release acceptance remain required. + +## PR #474 review round 8 (2026-09-14) + +`PYTEST_PATH_TYPES_RED.txt` records **42 failures and 70 passes** before validating consumed pytest `pythonpath` and `testpaths` shapes from supported TOML/INI sources. Invalid values now name the configuration and option in a `ProjectRuntimeError`; ordinary review retains the sanitized static fallback. The discovery/snapshot regression suite then passed **112 tests**. + +`PYTEST_DESELECTION_RED.txt` records **5 failures and 7 passes** before reconciling the observed inventory with pytest's final selected items. Native `-k`, `--deselect`, and plugin selection no longer create false unexecuted findings; interrupted selected tests remain required. Focused observer/worker tests passed **46 tests**. An independent private environment with pytest **9.1.1**, pytest-xdist **3.8.0**, and pytest-cov **7.1.0** ran the actual `-n 2 -k keep` selection: two selected tests passed, the excluded failing test did not execute, coverage was recorded, and production observation validation passed. Distributed deselection evidence is explicitly controller-observed rather than a claim of complete worker deselection inventories. + +`IMMUTABLE_PROJECT_LINKS_RED.txt` records **8 failures and 2 passes** before portable range/index materialization preserved Git symlink blobs. Regular files are written first, then links are reconstructed with original targets and validated before policy/configuration readers run. Escaping or excluded-source aliases fail closed, temporary roots are cleaned on failure, and existing governed-Python symlink restrictions remain enforced. The scope/VCS/builder suite passed **162 tests**. + +An additional local CLI fallback probe in the task's emulated Linux container reached the correct malformed-configuration diagnostic, but the supervisor rejected the host Rosetta wrapper before static analyzer execution (`pre_namespace_host_object`). This probe is **not passing analysis or acceptance evidence**. The real Linux x86-64 hosted matrix remains authoritative; no emulation guard was weakened. + +`EMPTY_XDIST_INVENTORY_RED.txt` records one failure before retaining the controller-observed label even when distributed collection selects zero items. The final observer/worker suite passed **47 tests**. + +Signed candidate `a413f700855e081aa0edec8dea35e3a90be41d7d`, run [34869521909](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34869521909), completed **all three customer corpus ABI jobs**. `CANDIDATE_A413_EVIDENCE.json` retains all **15 passing repository/ABI acceptance records**, exact commands, runtime/worker identities, selected and executed tests, outcome records, versions, timings, host-interface transfer counters and report hashes. All applicable analyzers ran; every offline preparation and explicit warm attachment independently verified denied networking and reused its cold runtime identity. Every controlled copy recorded an actual failing test. The recorded merge checkout `34db09df344d7d76cd764df7c2b05ebdee40fc1c` has parents dev `976460c0` and candidate `a413f700`; the GitHub compare API verified an identical source tree to the candidate. The signed baseline installation is 0.49.85 and source execution is candidate mode, not public 0.50.0 acceptance. These receipts precede the round-8 follow-up code; a new signed matrix remains required. + +Round-8 local gates: smart suite **2,235 passed in 89.83 seconds**, full suite **2,235 passed in 90.63 seconds**, and **28 contract tests passed**. Formatting, type/lint (10.00/10), YAML, bundle imports, development checksum/version validation, strict OpenSpec and planned Requirements evidence passed. Final snapshot simplification removed added complexity and length findings; **169 focused tests passed in 11.28 seconds** after that refinement. The final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14 19:10:30 Europe/Berlin**, with **zero errors**, 287 warnings and 69 informational findings. The required observation schema and bootstrap-hook exceptions are explicit in `REVIEW_EXCEPTIONS.md`; unchanged baseline advisories remain distinguished from regressions. Independent cross-reviews found no additional actionable defects. + +## Round-9 PR findings and cached invocation cross-review + +`STAGED_VCS_TREE_RED.txt` records four failures before binding the staged tree separately from the captured HEAD commit. `CACHED_COMMAND_VCS_RED.txt` records three additional failures through the actual cached changed-enforcement capture path, including a HEAD move whose tree remains unchanged. Both snapshot types now carry the captured commit and tree into cache identity and the private build index. The focused cached/VCS suite passed **67 tests** (352 unrelated tests deselected), including existing unborn-branch cached-analysis behavior. + +`OPTIONAL_GIT_RED.txt` records three failures before allowing package-index builds without Git or unused HTTP helpers. Available capabilities remain fingerprinted; an actual missing HTTP-helper invocation reports its installation remedy and exits 127. The focused Git/builder suite passed **26 tests**. `PYTEST_TABLE_RED.txt` records **31 failures** before validating pytest table shapes; the discovery/fallback suite passed **143 tests**, including false and empty-array values and retained independent static analysis. + +`INDEX_CONTEXT_CLEANUP_RED.txt` records three failures in the **pre-existing** downstream index-context cleanup gap. Snapshot ownership now covers changed-path, policy, and manifest discovery exceptions. The combined scope/VCS/portable/builder suite passed **188 tests** before the additional cached-command cases. This is narrow hardening requested during review, not a newly introduced regression. Independent review found no defect in the optional-Git and pytest-table changes and caught the separate cached snapshot path before publication. Final combined gates and a fresh signed candidate matrix remain required for this round. + +Final round-9 local gates: smart suite **2,278 passed in 91.89 seconds**, full suite **2,278 passed in 93.51 seconds**, and **28 contract tests passed**. Final cached-capture simplification and property contract passed **70 affected tests in 6.34 seconds**; capture complexity remains at its prior value. Fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14 19:40:23 Europe/Berlin** with **zero errors, 287 warnings and 69 informational findings**, after fixing both new warnings. Remaining baseline/boundary dispositions are unchanged in `REVIEW_EXCEPTIONS.md`; local runtime-builder coverage improved to 73.3% but is not represented as meeting 80%. Formatting, typing/lint (10.00/10), YAML, imports, strict OpenSpec, planned Requirements evidence, development integrity/version checks and publish pre-check passed. This evidence does not replace the required updated Linux candidate and public signed-release acceptance. + +## Round-10 isolation and metadata review + +`BOUND_GIT_CLOSURE_RED.txt` records five failures before exporting only the captured commit/tree/tag object closure. `GIT_GRAFTS_RED.txt` records an unbound graft failure; `GIT_EXPORT_ERRORS_RED.txt` records two unhandled process failures; `FROZEN_GIT_CONTEXT_RED.txt` records three failures before using frozen plan tags/shallow boundaries in both builder paths. Private Git metadata is initialized fresh, native Git plumbing writes only the explicit closure, and its exact object inventory is checked. Replacement refs, grafts, source-local configuration, unbound refs/objects and implicit host transports are excluded. Selected ancestry, staged objects, annotated/lightweight tags and shallow boundaries remain available. The stable VCS/builder/portable subset passed **63 tests in 4.34 seconds**. A separate intermediate combined run encountered the concurrent policy tests while they were still RED; it is not passing evidence. + +`ANALYZER_ORIGIN_RED.txt` records **seven failures** before pinning analyzer-owned imports ahead of ordinary lookup and target site initialization. Real isolated Python subprocesses cover source collisions, `.pth` imports, preexisting wrong-origin modules, and project-owned import behavior. Module, namespace, symlink and missing origins are checked against the resolved member mount. Bootstrap/launcher tests passed **24 tests**. This governs import origin; it does not claim to sandbox arbitrary malicious Python startup code inside one interpreter. + +`CONSUMED_METADATA_TABLES_RED.txt` records **90 failures** before validating only the pyproject/Hatch tables consumed by discovery, including explicit manager selection. The discovery/fallback suite passed **233 tests**. `UNCHANGED_POLICY_MANIFEST_RED.txt` records **10 failures** for the pre-existing policy-manifest omission. Both manifests now retain the base/head policy closure without widening selected paths or changing clean-index NOT_APPLICABLE semantics; **151 scope tests passed**. Independent review found no actionable metadata or bootstrap defects. Final combined gates, signed candidate acceptance and public release acceptance remain required. + +Independent VCS review additionally verified SHA-1 and SHA-256 closed exports and reproduced a missing-origin race after input verification. `BOUND_GIT_ORIGIN_RED.txt` records two failures before source copying and frozen Git export were separated: a nonempty bound origin now must remain available before acquisition. The final VCS/builder/portable subset passed **67 tests in 4.57 seconds**, and an independent eight-test regression pass found no additional issue. The first combined smart/full invocation was interrupted for this confirmed fix and is not final passing evidence. + +Final round-10 functional gates: smart suite **2,403 passed in 97.75 seconds**, full suite **2,403 passed in 99.47 seconds**, and **28 contract tests passed**. The final scope/bootstrap refactors passed **175 affected tests in 12.14 seconds**; the private Git fixture refactor passed **27 tests in 6.01 seconds**. Formatting, typing/lint (10.00/10), YAML, imports, strict OpenSpec, planned Requirements evidence, development checksum/version verification and publish pre-check passed. The scope refactor reduces `_materialized_index_context_from_capture` to 65 lines; its low-complexity informational advisory reflects that reduction, not a new control-flow regression. The startup `validate_loaded` contract exception is explicit in `REVIEW_EXCEPTIONS.md`. Local parent coverage for bootstrap remains 61.7%, not evidence of complete child-process coverage. + +The preceding signed round-9 candidate `29c167bce4cdd1f35eac9ede0aceddaa620b63bd` completed every required check in [run 34876434736](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34876434736). Independent receipt validation passed all 15 repository/ABI combinations, including 168 host tests, 168 cold and 168 warm capsule test calls, and 15 controlled failing tests. Tested merge `6155d90e2ceac5d2b3fb7add0e2a4ad3795a948a` has the expected dev/candidate parents and the same tree as that candidate. Recorded offline probes, source-unchanged checks and runtime reuse passed. These historical receipts execute candidate source against signed baseline 0.49.85; they do not establish round-10 or public 0.50.0 acceptance. + +Fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T18:26:40.508764Z** (UTC), with **zero errors, 287 warnings and 68 informational findings**. New fixture warnings were removed; retained baseline/boundary dispositions remain explicit. Updated signed-candidate and public signed-installation acceptance remain required. + +## Round-11 review and native pytest identity hardening + +The narrow round-10 hook emitted one targeted-pytest reconciliation error despite **1,417 passing tests and process exit 0**. Retained observer/JUnit evidence identified one pre-existing parser defect: `::` inside a parameter ID was treated as a structural class/name separator. `JUNIT_PARAMETER_IDENTITIES_RED.txt` records a real six-case function/class fixture whose tests pass while the old adapter returns UNKNOWN. The corrected adapter partitions the parameter suffix before structural parsing; **226 focused pytest/JUnit/TDD/coverage tests passed in 8.84 seconds**, and the original 1,417-case evidence reconciles PASS unchanged. The hook's initial passing exit alone is not represented as complete evidence. + +`PYTHON_VERSION_TRIGGER_RED.txt` records two failures before `.python-version` alone triggered ordinary portable preparation. Actual runner-path tests now cover supported-pin preparation, unsupported-pin diagnostics retaining static members, and unchanged metadata-free source behavior; **23 tests passed**. + +`PYTEST_QUOTED_PATHS_RED.txt` records six failures before importing quoted pytest paths/patterns with native argument boundaries. The fix follows the installed pytest **9.1.1** `Config._getini_ini` paths/args contract: `shlex.split` for string values, unchanged list boundaries. Real pytest configuration parsing confirms full/source-only selection and source-root behavior; malformed quotes name the file and option. The discovery/snapshot/worker suite passed **276 tests in 0.69 seconds**. Independent review found no introduced defect. Final combined gates and updated signed acceptance remain required. + +`PTH_IMPORT_STATE_RED.txt` records **seven failures** before checking protected import state around executable `.pth` initialization. Member lookup is restored to first precedence after legitimate additive hooks; removed/mutated protected machinery and paths outside approved roots fail explicitly. Real subprocess regressions retain executable editable finders, namespace path hooks and rebased path-only workspace imports; **34 bootstrap/launch tests passed**. Project-Python preserves native startup. These checks cover startup integrity and are not a sandbox for arbitrary malicious Python inside one interpreter. + +Round-11 combined gates passed **2,423 smart tests in 121.07 seconds** and **2,423 full tests in 123.54 seconds**, with the two existing lark deprecation warnings. Formatting, typing/lint (10.00/10), YAML, bundle imports, strict OpenSpec and planned Requirements evidence passed. The fresh combined review at 2026-09-14T18:56:23Z reported **zero errors, 287 warnings and 68 informational findings**, with no new advisory regressions. + +The completed CodeRabbit round then identified an additional consumed manager signal. `UV_TABLE_RED.txt` records **eight failures** before validating `tool.uv` as a table, including explicit manager selection. Adding that table to the existing validator passed **244 discovery/snapshot tests in 1.55 seconds**. These focused tests follow the full-suite run above. Diagnostic regex assertions now escape complete expected messages literally. Host-specific temporary prefixes in the three requested historical transcripts and four round-11 transcripts were replaced with stable placeholders; failure/result lines and evidence content remain intact. Final signed-candidate acceptance remains required. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` at **2026-09-14T18:58:37Z** completed with **zero errors, 287 warnings and 68 informational findings**. The existing baseline/startup dispositions remain unchanged. Final format, typing/lint, strict OpenSpec, development checksum/version validation and canonical publish pre-check passed after the uv-table addition. Public review completion on signed `6b733865` covered the previous round; this follow-up requires its own signed CI and completed review round. + +## Round-12 native ownership correction + +Signed candidate `6b733865cbefdcfb7633b7390779c3b0eb8f969e`, [run 34881298236](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34881298236), failed the Poetry corpus entry on all three ABIs. Runtime preparation and all seven Poetry tests passed, but CrossHair contracts analysis reported `project_worker_analyzer_origin_mismatch:build` and remained UNKNOWN. `NATIVE_6B733_FAILURE.json` preserves the tested merge, exact commands, worker identities and report hashes. Quality jobs failed the capsule prerequisite; this is a genuine native acceptance failure, not cancellation. + +The cause is our import-ownership mapping: installed CrossHair **0.0.109** metadata declares `build` and `wheelhouse` despite not recording corresponding installed payload. Its separately declared `doc` namespace does own the present `doc/source/conf.py` and remains admitted. Trusting these declarations seals Poetry's separate `build` import incorrectly. The pending correction derives analyzer-owned names from the distribution's recorded, present import payload and retains strict origin checks. + +A separate synthetic review confirmed that mixed analyzer/project portions of one namespace are not currently composed. No such graph was identified in the signed baseline member closures. This remains an explicit support limitation, with origin mismatch and incomplete evidence; it is not misclassified as a version conflict and is not resolved by the stale-metadata fix. + +`ANALYZER_IMPORT_OWNERSHIP_RED.txt` records **24 failures and five passes** before deriving names from present distribution-recorded Python and native-extension files. Tests cover missing/stale declarations, ordinary modules/packages/namespaces, native suffixes independent of the controller ABI, absent payload and another distribution's `build` directory. Missing file inventory fails explicitly. The corrected suite passed **29 tests in 0.84 seconds**; the combined domain/bootstrap/builder suite passed **77 tests in 0.77 seconds**. Direct inspection of installed CrossHair 0.0.109 confirms `build` and `wheelhouse` are excluded, while actual `_crosshair_tracers`, `crosshair` and `doc` payload remains admitted. Strict analyzer-origin checks are unchanged. This is local component evidence, not a successful native Poetry/capsule run. + +Independent reuse checks confirmed that automatic cache keys include `runtime_domains.py`, and normal explicit attachment binds the composite worker identity containing the verified module payload and final capsule root. Both stale automatic preparation and old explicit descriptors therefore invalidate across the packaged fix; no new descriptor field was required. The earlier round-11 local commit `10783c75` passed canonical hooks with zero review errors and **28 contract tests**; it was held from push when the native failure arrived. + +Round-12 combined gates passed **2,455 smart tests in 112.47 seconds** and **2,455 full tests in 119.60 seconds**, with the two existing lark warnings. Formatting, typing/lint (10.00/10), YAML, bundle imports, strict OpenSpec and planned Requirements evidence passed. The corrected import-ownership implementation still requires a fresh signed Linux corpus; the previous Poetry failure is not relabelled as acceptance. + +Final ownership type-boundary correction passed **77 affected tests in 1.86 seconds**. Fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T19:21:49Z** with **zero errors, 287 warnings and 68 informational findings**. The stricter metadata path type finding and redundant test-name advisory were removed; retained baseline/startup dispositions are unchanged. This local review does not supersede the outstanding native Poetry acceptance requirement. + +## Round-13 range symmetry and parsed path containment + +`PYTEST_PATH_CONTAINMENT_RED.txt` records **12 failures and four passes** before containment checked the same shell-parsed pytest `testpaths` and `pythonpath` values consumed during execution. Single/double quoted parent traversal, absolute paths and escaping links now fail before preparation. Explicit review source roots no longer bypass validation of pytest’s own Python paths. Valid spaced INI values and native lists retain their recorded representation. The discovery suite passed **237 tests in 1.30 seconds**; combined discovery/worker tests passed **277 tests in 0.66 seconds**. + +`PORTABLE_RANGE_SELECTION_RED.txt` records **six failures and two passes** before range-wide selection determined whether both snapshots expand for a metadata-only change. The regression uses actual snapshot copying, dispatch/applicability, range classification and report construction; only runtime artifact preparation/lookup and analyzer execution are substituted. Deleted and added Python files now preserve the absent side, empty selections stay empty, and metadata-only selections retain symmetric analysis. Both prepared execution and missing-metadata preparation failure are covered. The snapshot/worker suite passed **71 tests in 0.35 seconds**. Independent review passed another 16 containment and nine range regressions and found no additional actionable defect. + +Round-13 combined gates passed **2,479 smart tests in 98.28 seconds** and **2,479 full tests in 99.90 seconds**, retaining two existing lark deprecation warnings. Formatting, typing/lint (10.00/10), YAML, bundle imports, strict OpenSpec, planned Requirements evidence, development checksum/version checks and canonical publish pre-check passed. The range fixture’s empty-tuple typing was corrected before these final runs. Both new RED transcripts use stable workspace/temporary placeholders without changing failure evidence. + +CodeRabbit review 5202051605 on signed `6c3738ef` raised an outside-diff exception-handling claim. Independent review confirmed it is already covered: `ProjectRuntimeError` derives from `ValueError`, caught by both preparation boundaries; existing preparation-failure regressions retain static findings and UNKNOWN dependent evidence. No redundant exception clause was added. Updated signed-candidate and public-release acceptance remain required for this follow-up. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T19:48:13Z**, with **zero errors, 287 warnings and 68 informational findings**. No new advisory category or source defect was introduced; the documented baseline/startup and local parent-coverage dispositions remain in force, conditional on native acceptance. This report is local evidence and does not replace the required signed Linux matrix. + +## Round-14 native import precedence and unborn repositories + +`NATIVE_IMPORT_PRECEDENCE_RED.txt` records **four failures and 14 passes** before discovery stopped inventing runtime import roots. A real wheel installed through offline pip contains generated package output distinct from the raw `src` tree: native pytest selected the installed output, while the prior capsule bootstrap selected raw source under both prepend and importlib modes. Missing path declarations now produce empty import overrides; explicit review roots, pytest paths and editable hooks retain their behavior. Discovery/bootstrap/worker tests passed **324 cases in 5.20 seconds**, including six native Python current-directory/script cases and flat source-only pytest. No analyzer file-selection or bootstrap production logic changed. + +`UNBORN_VCS_CONTEXT_RED.txt` records **three failures and 11 passes** before distinguishing an unborn selected branch from invalid Git state. Fresh, staged and orphan repositories now support discovery, CLI inspection and private copying without VCS context; a first commit invalidates the old binding. Default HEAD absence must be independently confirmed through a symbolic branch and exact missing-ref result. Explicit missing revisions/trees, malformed or detached HEAD, corrupt refs/configuration and execution failures retain errors. The hardened Git subprocess environment and bounded export logic remain unchanged. VCS/builder/source-binding tests passed **63 cases in 6.25 seconds**. An intermediate combined run encountered the other agent’s then-failing import-path regressions and is not final passing evidence. + +Independent review of both patches passed **16 targeted regressions in 1.85 seconds** and found no additional actionable defect. Combined repository gates and updated signed native acceptance remain required. Registry publication and manifest signing remain owned by the existing automatic GitHub CI workflows, as the user reconfirmed. + +Round-14 combined gates passed **2,512 smart tests in 118.94 seconds** and **2,512 full tests in 122.07 seconds**, retaining two existing lark deprecation warnings. Formatting, typing/lint (10.00/10), YAML, bundle imports, strict OpenSpec and planned Requirements evidence passed. The native-import transcript retains failure outcomes with stable workspace, temporary-directory and test-environment placeholders. Updated signed native acceptance remains required. + +The first fresh round-14 bug-hunt reported five new test-code advisories: fixture branch nesting, a generic-name substring, two empty-sequence comparisons and a long wheel-metadata literal. These were corrected without changing production behavior or assertions. The final affected discovery/VCS/bootstrap/worker suite passed **365 tests in 12.71 seconds**; formatting and typing/lint passed again. Native acceptance remains tied to a new signed candidate, not the still-running prior source revision. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T20:18:09Z**, with **zero errors, 287 warnings and 68 informational findings**. All five new advisories were removed; retained baseline/startup and local coverage dispositions are unchanged. Development checksum/version verification and canonical publish pre-check passed. Automatic CI signing and updated native acceptance follow the push. + +## Round-15 dispatch integrity and sectionless native inventory + +`STARTUP_DISPATCH_RED.txt` records **12 failures and two passes** before checking the runpy namespace, function code and code-loading primitives used by analyzer dispatch. `STARTUP_EXIT_RED.txt` records **two failures** before startup rejection used reserved exit 78. Captured entry functions and built-in observer paths are preserved across target hooks; ordinary additive editable hooks remain supported. The bootstrap/domain/selection subset passed **99 tests in 6.24 seconds**. This is startup integrity, not a general sandbox against arbitrary code in the same interpreter, consistent with the [Python 3.13 runpy contract](https://docs.python.org/3.13/library/runpy.html), accessed 2026-09-14. + +The runner audit reproduced a related false-clean result: a rejected hook wrote valid `[]` JSON, and pylint parsed it despite startup failure. `PYLINT_STARTUP_RED.txt` records **one failing regression** before rejecting reserved exit 78 ahead of JSON interpretation. The resulting tool error preserves the startup diagnostic instead of claiming completed analysis. + +`SECTIONLESS_ELF_RED.txt` records **three failures and 16 passes** before program-header dependency discovery for valid sectionless ELF64 x86-64 objects. The bounded fallback reads PT_DYNAMIC/DT_NEEDED through a file-backed PT_LOAD string table, requires termination and rejects malformed ranges. The existing section route is retained. Native/builder/source/model tests passed **45 cases**; a synthetic fixture's terminator mutation offset was corrected from 192 to 193 during validation. The [ELF gABI 4.2 dynamic section](https://gabi.xinuos.com/v42/elf/08-dynamic.html) and [program headers](https://gabi.xinuos.com/v42/elf/07-pheader.html) were checked on 2026-09-14. Synthetic byte parsing on macOS is not Linux execution evidence; the final signed native matrix remains required. + +The first combined smart run passed 2,533 tests but failed 15 Semgrep fixtures because the local command omitted the configured CA bundle (`ca-certs: empty trust anchors`). Repeating with the existing certifi `SSL_CERT_FILE` passed **2,548 smart tests in 126.94 seconds**. This intermediate pass precedes the final startup-origin/loader follow-ups. No production certificate behavior was changed. + +CodeRabbit review 5202637906 correctly identified stale paired-core command documentation at core dev `45776bf0ee64e0a9cef07ee5d3c324114d8ac44f`. The existing accountability checker checks package ownership and command roots; its success is not full subcommand/option parity evidence. Task 4.7 retains the exact-revision documentation and registration follow-up. The separate Requirements fixture's approved commit/tree must remain intact; public module signing and publication remain the canonical GitHub CI workflow's responsibility. + +The full intermediate suite passed **2,548 tests in 122.29 seconds**, with the same two existing lark deprecations. Independent ELF review found no actionable defect; 666 additional synthetic truncation/extreme-field probes produced no unexpected exceptions. Startup review then confirmed two residual paths: preloaded protected imports raised ImportError with an ordinary findings exit, and loader filename methods could redirect dispatch. `STARTUP_LOADER_RED.txt` records **three failures** before mapping origin errors to startup exit 78 and fingerprinting source/sourceless loader class namespaces through their inheritance chain. The final bootstrap/domain/selection subset passed **102 tests in 6.98 seconds**. These bounded checks do not claim to authenticate arbitrary hook output or sandbox project code sharing the interpreter. Final combined lint/review and the signed native matrix remain required. + +The first fresh round-15 bug-hunt at **2026-09-14T20:49:25Z** reported zero errors and seven additional test-code advisories, including previously untouched pylint-test duplication newly included in this review surface. The tests now parameterize the same four field-normalization cases, use precise names and empty-sequence assertions, and split long hook literals. All assertions and actual hook behavior are preserved. The combined bootstrap/native/pylint subset passed **103 tests** after this cleanup. Parent-process bootstrap coverage is 50.8%; real child-process regressions are reported separately, and this local metric does not establish Linux acceptance. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T20:54:22.923830Z**, with **zero errors, 287 warnings and 68 informational findings**. All new advisories were removed; retained baseline/startup and local parent-coverage dispositions remain explicit. The normalized pylint tests passed all **15 cases** after reducing helper arguments. Formatting, typing/lint (10.00/10), YAML, imports, strict OpenSpec, planned Requirements evidence, development checksum/version verification and canonical publish pre-check passed. Automatic CI signing, updated native acceptance and core #728 remain pending after the push. + +## Round-16 native full-review discovery + +Codex finding 4009588049 identified that full review converted configured pytest roots into explicit selectors. `PYTEST_NATIVE_DISCOVERY_RED.txt` records **12 failures and 39 passes** before delegating full-review discovery to pytest. Real pytest **9.1.1**, the actual target Observer and pytest-cov reproduce missing/partially missing paths, glob expansion and positional `addopts`; supplied positional selectors changed native collection or exited with usage error. The new full-only empty selector list preserves pytest discovery, warnings and fallback. Targeted explicit, source-only and mixed selection remains unchanged, and required collection/execution/coverage evidence still rejects empty runs while retaining genuine failures. The affected worker/snapshot/pytest suite passed **95 tests in 6.90 seconds**. This is local subprocess evidence, not Linux capsule acceptance. + +The [pytest testpaths reference](https://docs.pytest.org/en/stable/reference/reference.html#confval-testpaths), accessed 2026-09-14, confirms that configured discovery applies when no command-line directories, files or test identifiers are supplied. Full signed-candidate and public-installation acceptance remain pending for the resulting revision. + +Round-16 independent review found no additional actionable defect and passed **95 affected tests in 4.82 seconds**. Combined gates passed **2,562 smart tests in 155.88 seconds** and **2,562 full tests in 153.68 seconds**, retaining two existing lark deprecation warnings. Formatting, typing/lint (10.00/10), YAML, bundle imports, strict OpenSpec, planned Requirements evidence, development checksum/version verification and canonical publish pre-check passed. Fresh branch review and automatic CI signing follow these gates; native acceptance remains bound to the resulting signed revision. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T21:23:29.597335Z**, with **zero errors, 287 warnings and 68 informational findings**. No new advisories were introduced; all reported coverage percentages match the preceding review. Existing documented baseline/startup and local parent-coverage dispositions remain in force. Automatic CI signing and the resulting signed native corpus remain required after push. + +## Round-17 setup failures retain incomplete execution + +`PYTEST_SETUP_EXECUTION_RED.txt` records **one failure and four passes** before mixed setup failures stopped counting as completed test-body execution. The regression runs real pytest 9.1.1 with pytest-cov and the target Observer, then passes its retained observations through `run_portable_pytest` and the capsule member-response mapper. A setup failure alongside a healthy test now retains its genuine failed-phase finding and produces UNKNOWN/error evidence naming the unexecuted test and fixture-setup remedy. A real failed call, or teardown failure after an observed call, remains FAIL with completed call evidence; intentional native setup skips and xfails retain their existing policy. The affected discovery/worker/snapshot/target-pytest suites passed **100 tests in 4.19 seconds**, and Ruff checks and formatting passed. Combined repository gates and the final signed Linux acceptance matrix remain required. + +Independent review found no additional defect and separately passed all **five real-pytest phase regressions in 1.14 seconds**. Combined smart and full suites each passed **2,567 tests**, retaining two existing lark deprecation warnings. Smart duration was **126.10 seconds**; full duration was **121.24 seconds**. Formatting, typing/lint (10.00/10), YAML, bundle imports, strict OpenSpec, planned Requirements evidence, development checksum/version verification against dev and canonical publish pre-check passed. The unpublished reserved minor version remains 0.50.0; GitHub CI owns signatures and registry publication. + +The first round-17 fresh bug-hunt reported a new complexity blocker (CC 21) in the phase-regression test. A test-only refactor separates incomplete-setup and member-response assertions; the test now has CC 7 and its new assertion helpers have CC 7 and CC 8. Actual subprocess observations and all outcome assertions remain intact, with the failed-phase assertion strengthened to one exact test identity and message. The final affected suite passed **100 tests in 4.49 seconds**; Ruff checks/formatting and targeted pylint (10.00/10) passed. Production behavior did not change during this cleanup. A fresh staged bug-hunt remains required. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T21:51:39.623486Z**, with **zero errors, 287 warnings and 68 informational findings**. The new test-complexity finding was removed; no new advisory remains. The documented baseline/startup and local parent-coverage dispositions remain explicit. Automatic CI signing and final native acceptance follow the validated push. + +### 2026-09-15: Pylint namespace source-root fidelity + +The cp312 signed-candidate Poetry receipt retained six E0401/E0611 findings at `tests/utils/test_extras.py:7,9,10`. Its runtime inventory contains poetry-core and an editable `poetry.pth` bound to `/opt/specfact/snapshot/src`; Poetry and poetry-core are both project-owned. A private probe using exact pinned Poetry source demonstrated that Pylint instead cached `poetry` as `src/poetry/poetry.py`. Native Python resolved the namespace correctly. This is distinct from the mixed analyzer/project namespace limitation recorded above. + +Specification preceded regression tests and production edits. `PYLINT_NAMESPACE_ROOTS_RED.txt` records **4 failed, 5 passed** on the unchanged implementation. Three real-Pylint namespace cases (editable, explicitly declared, overlapping snapshot/editable roots) produced the spurious import findings; the missing-import case additionally retained those false positives. Native repository/CLI precedence and no-root controls already passed. + +The new target-only Pylint dispatcher derives defaults exclusively from already validated snapshot entries in the attached worker's `sys.path`. Deeper roots precede ancestors because Pylint chooses the first matching root. It creates an ordinary PyLinter and sets the fallback before native Run configuration and CLI parsing; explicit empty/native source-roots values therefore remain authoritative. It does not insert import paths, overlay guessed raw source on built packages, suppress import findings, or change the supervisor environment. The startup dispatch fixture now contains the new trusted dispatcher stub, retaining all existing tampering assertions. + +GREEN command: `SPECFACT_CLI_REPO=/private/tmp/specfact-473-core hatch run python -m pytest -q tests/unit/specfact_code_review/run/test_target_pylint.py tests/unit/specfact_code_review/run/test_target_bootstrap.py tests/unit/specfact_code_review/tools/test_pylint_runner.py`: **91 passed in 7.32 s**. The added parallel control runs real Pylint `--jobs=2`; its synthetic runtime lacks the built interpreter launcher, so the harness explicitly supplies the host multiprocessing executable. This proves ordinary PyLinter serialization and roots preservation, not the native capsule subprocess launcher. Formatting/Ruff and Pylint checks passed; focused typing reported zero errors/warnings. + +Host proof uses macOS Python 3.14.7, Pylint 4.0.8, Astroid 4.0.4 and pytest 9.1.1. The correction still requires the Linux candidate/public-installation corpus; local proof does not confer release acceptance. Pylint's documented implicit-namespace contract requires source-roots to avoid incorrect package names: https://pylint.readthedocs.io/en/stable/user_guide/usage/run.html#with-implicit-namespace-packages (Pylint 4.0.8 documentation, accessed 2026-09-15). + +The corpus guard followed a separate specification → tests → RED → implementation loop. `PYLINT_CORPUS_IMPORT_GUARD_RED.txt` records **12 failed, 3 passed** before the guard: all cold/warm E0401/E0611 combinations at the three verified Poetry import lines were silently accepted. The manifest now records those module/location pairs, and both cold and warm report acceptance check them. Three controls preserve other files, other lines and unrelated diagnostic classes; no report findings are removed. + +Combined dispatcher, bootstrap, Pylint parser and corpus guard suite: **125 passed in 7.05 s** (`tests/unit/test_external_capsule_corpus.py` plus the three focused files above). Bundle import boundaries, repository YAML validation, strict OpenSpec and changed-file Ruff/format checks passed. Targeted BasedPyright reported **0 errors, 0 warnings**. Production files plus the new worker tests scored **10.00/10** in Pylint. Expanding local Pylint to the two existing test modules reported five pre-existing protected-access warnings on unchanged lines (bootstrap line 294; corpus lines 195, 207, 211, 229); no new warning was introduced. New dispatcher complexity is at most 7. Independent review of the dispatcher found no production findings and independently passed 83 bootstrap/launch/worker tests. + + +Round-18 combined validation passed: smart and full suites each ran **2,592 tests**, with only the two existing lark deprecations. Durations were **107.31 seconds** and **108.90 seconds**, respectively. Independent corpus-guard review passed 34 tests and applied the stronger guard to both retained cp312 Poetry reports: each correctly rejected the six known import-resolution errors, while in-memory controls retaining eleven unrelated findings were accepted. The guard fails the corpus before writing acceptance and does not suppress report findings. The unpublished bundle remains 0.50.0 (registry baseline 0.49.85); its checksum was updated through canonical tooling, with signatures still owned by GitHub CI. Fresh review and new signed Linux corpus acceptance remain required. + +### Pylint dispatcher cache identity follow-up + +The planned Requirements adapter maps `scenario_id` to the canonical source requirement slug rather than individual scenario headings. The six namespace cases now use `pylint-preserves-attached-namespace-source-roots`; their granular behavior remains explicit in the intents and observables. + +A separate spec-first cache regression showed that changing only `target_pylint.py` left the existing cache directory reusable with otherwise fixed project, environment, Git and worker identities. `PYLINT_DISPATCH_CACHE_RED.txt` records the genuine failure: offline preparation did not raise the required cache-miss diagnostic after the dispatcher bytes changed. The test uses private copies of actual helper sources and the interpreter lock, exercises real cache-key calculation and selection, and stubs artifact loading only; it acquires no dependencies. The dispatcher now participates in the existing builder digest alongside the other trusted target helpers. An unchanged repeated call reuses the same cache; changed dispatcher bytes produce a distinct identity and offline miss. + +The previously reported smart and full suites each passed **2,592 tests before this cache-key addition**. Those prior results do not cover the new cache regression or final mapping correction; the final combined gates are coordinated separately. + +Cache follow-up GREEN: **146 passed in 7.99 s** across runtime-builder, target-Pylint, target-bootstrap, Pylint-parser and corpus tests. The canonical staged planned Requirements adapter passed **1 source, 0 failed, 0 skipped**. Ruff/format, strict OpenSpec, diff checks and targeted BasedPyright (**0 errors, 0 warnings**) passed; the new test has complexity 6. Local Pylint retained only two existing protected-access warnings on unchanged runtime-builder line 144 and test line 41. Final signing, fresh review, complete suites and Linux candidate replay remain root integration gates. + +Final cache follow-up validation: the complete suite passed **2,593 tests in 107.11 seconds**, with only the two existing lark deprecations. Configured lint passed (1,326 files already formatted; BasedPyright zero errors/warnings/notes; Ruff passed; Pylint 10.00/10). Repository YAML and bundle import-boundary checks passed, and all **28 contract tests** passed. The normal development signature/checksum and version-bump gate verified all seven manifests; publish preconditions passed with the existing `>=0.55.1,<1.0.0` compatibility intentionally retained. The current public registry remains 0.49.85; the unsigned 0.50.0 checksum was refreshed after the cache change, with publisher signatures and registry publication delegated to GitHub CI. Independent read-only review of the complete staged production patch found no defects. + +The final smart suite also passed **2,593 tests in 111.10 seconds**, with the same two existing lark deprecations. + +Fresh branch-wide SpecFact `--enforcement changed --bug-hunt` completed at **2026-09-14T23:07:40.175982Z**, exit 0, with **zero errors, 288 warnings and 69 info**. Relative to the preceding review, existing warnings are unchanged apart from source-line shifts and parent-process coverage: runtime_builder improves from 73.3% to 74.5%; target_bootstrap changes from 50.8% to 49.2% as its new dispatch branch executes in isolated subprocesses. Explicit narrow coverage exception: new `target_pylint.py:1` reports 0% in the parent coverage collector, while all ten real-Pylint subprocess controls execute it (editable/declared/overlapping roots, repository and CLI precedence including empty roots, genuine missing imports, built-root exclusion and parallel jobs). This measurement must not be represented as absent test execution or as native Linux acceptance; the isolated worker deliberately does not inherit host coverage injection. The existing startup/parent-coverage dispositions continue to apply. The one new informational length advisory is `test_runtime_builder.py:201`: retain its ordered unchanged/cache-reuse/changed-helper phases and offline assertions together, since extracting single-use setup would hide the causal cache regression without simplifying production. Independent review found no defect in that test or dispatcher. The new Linux signed-candidate corpus remains required. + +### PR #475: native pip package entrypoints + +After the specification was extended, `SETUP_CFG_INSTALL_RED.txt` recorded **5 failed, 3 passed** before changing the adapter. A tool-only `setup.cfg` previously appended an unbuildable `.` requirement; directory-shaped metadata markers were also wrongly admitted. The adapter now follows pip 26.2.1's native installable-directory predicate: a regular `pyproject.toml` or `setup.py` file permits root installation, while `setup.cfg` alone does not. Tool-only pyproject files remain eligible because native pip delegates their build metadata decisions. Requirements, constraints, extras and configuration discovery remain unchanged. + +GREEN: **257 adapter/discovery tests passed in 1.33 seconds**, including an actual offline pip dry run against configuration-only requirements and immutable source assertions. Independent replay passed the same 257 tests in 0.68 seconds and found no defects. Formatting, Ruff, diff and actual AI-bloat checks passed. Native pip source and the official local-project installation contract support this distinction: https://pip.pypa.io/en/stable/cli/pip_install/ (pip 26.2.1, accessed 2026-09-15). Pushed validation and resolution of comment 4010452407 remain pending combined review fixes. + +### PR #475: targeted pytest file and glob paths + +The canonical scenario preceded `TARGETED_TESTPATHS_RED.txt`, which records **9 failed, 3 passed**: eight real native-pytest selection variants and an escaping expanded symlink failed before the correction. Targeted discovery now follows pytest 9.1.1's recursive glob expansion, admits individual file roots, and falls back to the repository only when all configured expansions are empty. Matched roots retain containment and excluded-environment ancestry. Explicit source/test mixtures, filename patterns, ambiguity checks and full-review native collection remain unchanged. + +GREEN: **68 tests passed in 5.87 seconds**, including actual native collection, test-body calls and coverage in all eight source-only variants. Independent replay passed **68 tests in 5.90 seconds** with no findings. Formatting, Ruff, targeted typing (zero errors/warnings), and diff checks passed; new helper complexity is at most four. The existing candidate walker complexity and an unchanged protected-access test warning remain documented baseline. Comment 4010452417 remains open until the combined validated update is pushed. + +### PR #475: controller log and artifact trust boundaries + +The security review identified controller reopening of a builder-owned staging log. After the canonical scenario, `BUILD_LOG_RED.txt` recorded **10 failures** before remediation, using disposable sentinels only. The controller now creates an exclusive private regular log outside builder-writable mounts before execution, directs subprocess stdout/stderr to its existing descriptor, and never reopens or copies the former staging log. Native manager output and timeout partial output flow through the descriptor; public errors do not contain captured output. Launch and capture-file-creation failure controls were added. The twelve focused log tests passed; independent replay passed **12 tests in 0.96 seconds** and found no security defect. Full preparation cleanup is being checked separately before finalization. + +An independent adjacent audit demonstrated another pre-sealing escape using only temporary simulated-host files: a builder-created `artifact/native` symlink redirected native-library copies and permission changes before the final manifest rejected it. The canonical scenario preceded `BUILD_ARTIFACT_BOUNDARY_RED.txt` (**7 failed, 1 passed**). A metadata-only topology validator now runs immediately after builder exit, inside safe failure-log handling, before inventory reads or native enrichment. It rejects root/nested symlinks and special nodes without reading payload bytes; final sealing remains unchanged. The new boundary plus existing artifact tests passed **17 tests in 0.23 seconds**, with typing and formatting clean. A separate reviewer found no defects and confirmed actual native-copy sentinel preservation, ordinary sealing/offline reuse, and early rejection of root, inventory, native-directory and nested links and FIFO nodes. + +The combined logging, builder, driver, VCS, artifact-boundary and artifact scope passed **100 tests in 6.18 seconds** before the final downstream-failure log-cleanup follow-up. These host tests prove descriptor and path handling with safe local subprocesses; they do not substitute for a fresh Linux namespace corpus. Security comment 4010477055 remains open until the combined validated signed update is pushed. + +Root review caught premature success-log deletion before downstream preparation finished. `BUILD_LOG_DOWNSTREAM_RED.txt` records **four actual failures** for native inventory, sealing, publication and final descriptor validation. The trusted log now remains available through all downstream processing, descriptor validation and staging cleanup; only a fully successful preparation removes it. Failures preserve the original actionable diagnostic and append the private log path. The final focused integration passed **104 tests in 6.39 seconds**; formatting, Ruff and diff checks passed. + +### Independently verified namespace correction in native candidate e729fa3 + +Run [34907939755](https://github.com/nold-ai/specfact-cli-modules/actions/runs/34907939755) passed all three native ABI jobs for `e729fa3adec76b5ed152d140eae28842ff5a1b71`. Independent artifact checks confirmed tested merge `055eb4481dc53f4723564cd6022d48e89397caad` has the expected dev/candidate parents and identical tree `6af17c726016aa525d236c5ebd8d04bad392906b`. Provider ZIP digests matched for artifacts 10373772032, 10374191496 and 10374022590. Across all fifteen combinations, the receipts contain **168 host calls, 168 cold capsule calls, 168 warm capsule calls and 15 genuinely failing controlled-defect calls**. Applicable analyzers ran without UNKNOWN evidence; offline probes and runtime reuse identities matched. + +Each of the six Poetry cold/warm reports across all three ABIs now has **zero** of the six previous Pylint namespace import errors, while eleven unrelated findings remain. This directly verifies the source-root correction rather than suppressing arbitrary findings. These are historical component receipts: `e729fa3` does not contain the subsequent setup.cfg, targeted-path and security fixes. Candidate installation uses public baseline 0.49.85 and an explicit candidate overlay, so this is not public signed 0.50.0 acceptance. The detached-Hatch fixture remains labelled as reconstructed, and source immutability is supported by retained harness receipts rather than an independent retained-checkout replay. A fresh final candidate matrix remains required. + +Final combined full suite passed **2,637 tests in 111.57 seconds**, with only the two existing lark deprecations. Configured lint passed (1,328 files already formatted, BasedPyright zero errors/warnings/notes, Ruff passed and Pylint 10.00/10). YAML, bundle import boundaries, strict OpenSpec, the staged planned Requirements gate (one source, no failures/skips), checksum/version checks for all seven manifests and publish preconditions passed. Core compatibility remains intentionally `>=0.55.1,<1.0.0`; the public registry still reports 0.49.85. The existing final-cleanup review found no defects, and the artifact-boundary fix received independent read-only approval. + +The same pre-nested-fix tree passed the smart suite with **2,637 tests in 110.20 seconds** and the **28 contract tests**. Two late P1 comments on superseded PR #474 remain applicable to #475: nested Python resets caller environment and working directory, and a repeated temporary-filesystem mount hides private caller files. These compatibility fixes and their fresh evidence remain pending; the preceding full/smart results do not cover them. Read-only call-graph review confirmed all supervisor member launches use the sanitized first-entry command, while the generated Python wrapper is invoked only within target workers. + +### Late PR #474 findings inherited by #475: nested Python caller context + +The canonical scenario and design preceded focused regressions. `NESTED_PYTHON_CONTEXT_RED.txt` records **10 failures and one passing first-entry isolation control** on the unchanged implementation. Four failures execute real children using the emitted launcher environment/cwd policy and demonstrate lost application values, restored removed HOME, reset cwd and missing relative data; six expose the missing immutable-domain API. This policy replay is not a Linux bubblewrap execution claim. + +The nested generated Python wrapper now directly enters trusted native Python startup in the existing target namespace. First-entry workers still unconditionally establish mount/process/network isolation, mask supervisor paths, clear inherited environment and select snapshot cwd. A read-only bind of an existing trusted member file identifies the current graph by filesystem identity, independent of child environment variables; all five member domains are retained without new analyzer mounts. The existing explicit diagnostics for Python options disabling attachment remain. + +Focused GREEN: **99 passed in 8.84 seconds** across launcher, bootstrap, startup and real Pylint controls. This includes **five actual generated-wrapper/exec/native-startup subprocess cases**, one per member domain, with `env={}`, private cwd, relative files and attached project/member imports. Those tests use private copies of actual trusted helpers with relocated filesystem constants; they exercise real Python execution but do not prove Linux namespace mounts. + +The reconstructed Hatch test slice adds six real subprocess tests for inherited/explicit/minimal/empty environments, relative helper execution, and caller-owned temporary files in both default TMPDIR and explicit `/tmp`. Empty-environment dependency imports include Requests and pyodbc; the deliberately uninstalled detached source package is explicitly added by the caller, matching native behavior. Five unchanged stdlib test bodies passed on native Python **3.11.15** and **3.12.13** in a private supporting harness. The full reconstructed fixture, Python 3.13 and actual cold/offline-warm capsule execution remain required CI evidence. Neither reconstruction nor local probes validate the inaccessible original customer reproduction. Comments 4010422180 and 4010422190 remain open until the combined validated signed update is pushed. + +The final focused scope expanded actual generated-wrapper execution to **ten cases** (empty and explicitly overlaid environments across five domains) and passed **104 tests in 10.48 seconds**. Targeted typing reported zero errors/warnings; Ruff, formatting and Pylint (10.00/10) passed, with new production helper complexity at most four. Independent read-only review found no actionable defects in the complete nested-launch and fixture diff; independent replay is coordinated after the combined suite to avoid shared evidence-file races. Checked-in RED transcripts preserve failure content with trailing whitespace removed for repository diff hygiene. + +Final combined full-suite validation passed **2,658 tests in 115.87 seconds**, with only the two existing lark deprecations. Independent nested-scope replay passed **104 tests in 10.52 seconds**, including all ten actual wrapper/native-startup cases, and found no defects. Full lint passed (1,328 formatted files, typing zero errors/warnings/notes, Ruff clean, Pylint 10.00/10); YAML, import boundaries and strict OpenSpec passed. The staged planned Requirements gate passed one source with no failures/skips. Canonical unsigned checksum/version checks verified all seven manifests and publish preconditions passed with intentionally unchanged core compatibility. A fresh remote read still reports dev `976460c0541b685f5fae256faef4cec32fe9ba7e` and registry 0.49.85; 0.50.0 remains unpublished and CI owns its publisher signature. + +The smart suite also passed **2,658 tests in 114.60 seconds**; all **28 contract tests** passed. Fresh SpecFact bug-hunt review at **2026-09-15T00:34:09.608536Z** correctly blocked finalization with a temporary-file-wrapper type mismatch and a Pylint fatal error in the artifact-boundary test. It also reported new nesting/length and explicit subprocess-check advisories. These findings are being remediated before signing; this failed review is not passing delivery evidence. Startup contract and explicit launcher/test-fixture informational dispositions are narrowly documented in `REVIEW_EXCEPTIONS.md`, preserving dependency-free trusted startup and auditable isolation arguments. + +Review cleanup preserves behavior: the existing controller-owned stream is passed as its actual `IO[str]` file object, and the unchanged builder-input filename tuple moved to a named module constant. Log lifecycle and acquisition boundaries remain unchanged; **104 focused builder/log tests passed in 7.52 seconds**. Artifact traversal now separates immediate directory validation from the traversal stack, retaining metadata-only no-follow checks; **17 artifact tests passed in 0.90 seconds**. Canonical nesting checks report no findings for either validator helper or the mutation fixture. + +The Pylint failure was independently reproduced with **Pylint 4.0.8 / Astroid 4.0.4**: its pathlib checker treated a captured callable named `read_text` as an attribute expression and accessed `Name.attrname`. Renaming that test-only alias to `original_read_text` preserves its exact callable and guard while avoiding the checker defect. A combined before-run also reproduced six false import-group warnings in unchanged `test_runtime_artifacts.py`; the final combined invocation returns **exit 0 and an empty findings array**, with that file unmodified. The newly exposed encoding advisory and explicit subprocess-check forwarding were corrected. No analyzer suppression was added. Root read-only review of the mechanical production refinements found no defects; final combined suites and fresh review follow. + +After the review cleanup, final full and smart suites each passed **2,658 tests**, in **116.56 seconds** and **115.98 seconds** respectively, with only the two existing lark deprecations. Final configured lint again passed with zero typing errors/warnings/notes and Pylint 10.00/10. YAML, import boundaries, strict OpenSpec, staged planned Requirements and canonical checksum/version/publish preconditions passed. The publisher signature remains delegated to CI, and fresh Linux acceptance is still required for this final source. + +Final fresh branch-wide SpecFact `--enforcement changed --bug-hunt` at **2026-09-15T00:44:35.884497Z** returned **PASS_WITH_ADVISORY, exit 0, zero errors and zero blocking findings**, with **289 warnings and 70 informational findings**. Both prior blockers, the three new nesting/length warnings, subprocess-check warning and six false import-group warnings are absent. The remaining new startup-contract and native-wrapper informational advisories have exact dispositions in `REVIEW_EXCEPTIONS.md`; they do not waive missing execution or security evidence. Parent-coverage changes relative to the published candidate are: portable_worker.py: 78.9% to 79.6%; runtime_artifacts.py: 78.7% to 67.4%; runtime_build_driver.py: 51.4% to 49.1%; runtime_builder.py: 74.5% to 75.6%; target_bootstrap.py: 49.2% to 49.7%. The documented conditional Linux/isolated-process coverage boundary remains; these percentages are not represented as meeting the local 80% threshold. All 28 final contract tests also passed in 0.97 seconds. CI signing, a fresh signed candidate corpus and public release acceptance remain pending. + +### PR #475 finding 4011119813: controller capture and runtime hardlinks + +Specification and planned verification cases 473-7-13/14 were written before the regressions. Actual hardlink RED: 6 failed, 1 passing copied-manager-file control (`BUILD_HARDLINK_RED.txt`). Actual process-descriptor RED: 1 failed, 1 Linux-only test skipped on macOS (`BUILD_PIPE_RED.txt`). No native Linux exploit is claimed from these local results. Linux cross-bind-mount semantics constrain the review's exact hardlink mechanism; regular inherited diagnostic descriptors still require a controller-only capture boundary. The fix therefore uses pipe output from the builder plus independent-file validation at build, seal and attachment boundaries. + +Independent review found a select/read race if another reader drains the pipe after readiness. A deterministic transient `BlockingIOError` regression failed before the follow-up (`BUILD_PIPE_READINESS_RED.txt`: 1 failed). The controller now sets its pipe reader nonblocking and retries through the same absolute deadline. Independent final review found no remaining defects. Focused artifact validation passed 24 tests; expanded capture checks preserve 2 MiB binary output, early EOF timeout, write-failure cleanup, and partial logs. The Linux process-descriptor test is explicitly skipped on this macOS host and remains required in Linux CI. + +Commands for this follow-up (from the isolated delivery worktree, using the paired core environment): `hatch run format`, `hatch run lint` (includes typing), `hatch run yaml-lint`, `hatch run check-bundle-imports`, strict OpenSpec validation, `hatch run contract-test`, `hatch run test`, `hatch run smart-test`, and fresh explicit-file SpecFact review with `--enforcement changed --bug-hunt --json`. Local checksum validation compares against `origin/dev`; publisher signatures are obtained only through canonical GitHub CI. No Linux cross-mount hardlink exploit or original customer reproduction is claimed from local tests. + +Full follow-up suite: **2,670 passed, 1 Linux-only skip, 2 existing lark deprecation warnings in 115.77 s**. Contracts: **28 passed**. Independent reviewers found no remaining defects after the nonblocking pipe follow-up. Public signed and updated Linux candidate acceptance remain pending. + +Smart-test replay: **2,670 passed, 1 Linux-only skip, 2 existing lark deprecation warnings in 116.12 s**. RED transcripts preserve actual failures, with temporary path and trailing-whitespace normalization only. + +Final fresh bug-hunt review at 2026-09-15T01:59:46.244985Z: PASS_WITH_ADVISORY, exit 0. Review completed with 4 findings (1 blocking). The Linux-only skip remains explicit testing-error evidence; the existing descriptor-validator private access and explicit command/fixture structure are narrowly dispositioned in REVIEW_EXCEPTIONS.md. Introduced test-name/fixture warnings were corrected and the 28 applicable focused tests passed again (1 Linux-only skip). + +### PR #475 findings 4011304439 / 4011304448 + +Safe preparation reasons: actual final scoped RED **10 failed, 32 passed, 1 Linux-only skip** (`PREPARATION_DIAGNOSTIC_RED.txt`). The wrapper now preserves a stable ASCII controller code and exact signed numeric builder status, with its original exception cause and private log path. It does not publish arbitrary exception details or perform new log operations. An earlier exploratory local test set expected extra private diagnostic writes; that extra behavior was removed before production because the reviewer-requested safe public code is sufficient. Only the final scoped RED is release evidence. Independent review found no defects in the minimal formatter. + +Nested PYTHONPATH: actual RED **20 failed** in real native-versus-attached wrapper cases (`NESTED_PYTHONPATH_RED.txt`); caller-only imports and omitted/empty environment values failed. Focused passing evidence and updated Linux corpus remain required. Temporary paths and trailing whitespace in RED transcripts are normalized only; failures are retained verbatim otherwise. + +Independent review identified a callback introduced by the final caller-path insertion: a project `.pth` could replace `sys.path` with a list subclass whose slice setter ran after member validation. The actual nested-wrapper regression failed before correction (`NESTED_PATH_CONTAINER_RED.txt`: **1 failed**). Capturing the trusted list primitive and insertion slice before project hooks avoids invoking the subclass callback. Root review confirmed the final operation uses those captured values. Final focused launch/bootstrap validation passed **115 tests in 17.48 seconds**, with typing, Ruff, formatting and Pylint (10.00/10) clean. The safe-diagnostic scope passed **42 tests**, with the one Linux-only process-descriptor test skipped on macOS. The reconstructed Hatch slice now checks a caller-only relative import in a child and grandchild; local native and attached wrapper checks passed, while the updated Linux corpus remains required. + +Final combined R3 gates: full and smart suites each passed **2,697 tests**, with one Linux-only process-descriptor skip and two existing lark deprecation warnings (**124.65 s / 125.63 s**). All **28 contracts** passed. Formatting, typing, lint, YAML, bundle import boundaries and strict OpenSpec passed. Canonical checksum/version verification passed all seven manifests against unchanged dev `976460c0541b685f5fae256faef4cec32fe9ba7e`; the registry still reports 0.49.85, leaving planned minor 0.50.0 unpublished. GitHub CI remains the publisher-signature authority. Local tests do not establish current Linux or public signed installation acceptance. + +Fresh review initially invalidated its snapshot because the evidence document changed during analysis; this is discarded as UNKNOWN. The unchanged-tree replay at 2026-09-15T02:41:39.811705Z returned PASS_WITH_ADVISORY and exposed one new line-length warning, corrected by splitting the diagnostic string without changing its value. Remaining startup-contract, private-validator-access and explicit-fixture informational findings have existing exact dispositions in REVIEW_EXCEPTIONS.md. The Linux-only skip remains visible as a testing error pending real Linux validation. A final fresh review follows the formatting correction. + + +## Release PR477 pytest evidence corrections (2026-09-15) + +Review findings4019944740 and4019944751 were independently reproduced: mixed passing/skipped/XFAIL/XPASS tests and unrelated/low source coverage could return PASS. Specification scenarios preceded new tests and implementation. Adapter RED:17failed4passed; native policy RED:10failed3passed. Empty-reason XPASS additionally failed one native regression. Independent review then reproduced nested pytest-root misclassification and production coverage suppression under broad discovery roots:3adapter failures and3native-root failures were recorded before their fixes. The five RELEASE_*_RED.txt files preserve these runs. + +The correction records native pytest-cov's resolved threshold, pytest root and expected-failure marker presence; it preserves raw nodeids and selection. The adapter emits blocking non-pass outcomes and reuses per-source coverage checks with the80percent floor, higher effective native thresholds and no customer-wide initializer omission. Discovery roots do not establish production exclusions. Nested nodeids resolve against the contained native root. The existing discovery harness now uses its simulated snapshot as cwd, matching the real worker. + +Focused results:22adapter outcomes/coverage tests passed before root fixes;64adapter/worker tests passed. Final root/adapter/discovery tests:51passed. Native policy/root/empty-XPASS tests:17passed. An initial full run passed2730tests with1Linux-onlyskip but failed3old assertions because failure messages gained an unnecessary word; the original failure wording was restored. A test helper return annotation fixes an independently detected typing error. Final combined gates and current Linux acceptance remain required. + +Version0.50.1 preserves CI-produced immutable0.50.0 artifacts. Canonical GitHub CI will provide publisher signatures and updated registry artifacts. Findings remain unresolved pending signed pushed fixes and Linux acceptance; public installation and the original customer reproduction are not claimed. + + +Final local validation: 2,739 full-suite tests passed, with one Linux-only /proc skip and two existing lark deprecations, in 134.65 seconds. Formatting, typing (zero errors/warnings), lint (10.00/10), YAML/manifests, import boundaries and strict OpenSpec passed. The earlier 28-contract run passed; current contract/smart selection and fresh SpecFact review are recorded in the follow-up PR. A second independent read-only review found no remaining issues in the corrected coverage and outcome paths. Linux corpus evidence is still required before resolving PR findings. + +Final review after helper extraction: `specfact code review run --enforcement changed --bug-hunt --json` over the three changed runtime sources and three pytest test modules completed at 2026-09-15T21:01:01Z, exit 0, PASS_WITH_ADVISORY. It retained 106 findings (91 warnings, 14 informational, one error), including the documented deferred-import/target-hook boundaries and unchanged legacy runner findings. The error records the local Linux-only /proc test skip; this is not error-free review or Linux acceptance. The new complexity, unused-argument and missing public contract findings were corrected, and CrossHair was rerun in the dedicated environment. Report: `/private/tmp/specfact-477-review-final.json`. Smart suite: 2,739 passed, one platform skip, two existing deprecations (134.63 seconds); contracts: 28 passed. After the behavior-preserving helper extraction, 110 focused cases and lint/type checks passed. Required Linux validation remains outstanding. + +## PR #478 nested pytest outcome paths (2026-09-15 Europe/Berlin) + +Finding 4020270168 is valid: non-passing outcomes under a nested pytest root emitted the raw root-relative filename, so later snapshot filtering could lose the finding. A scenario was added before four regression cases. Actual RED: **4 failed, 25 deselected**, preserved in `PR478_PYTEST_PATH_RED.txt` and `/private/tmp/specfact-478-path-red.log`; two cases assert nested relative/absolute root attribution, and two assert containment before attribution. + +The adapter now resolves outcome files to snapshot-relative paths using the same contained native pytest-root resolver as source coverage. Raw observed node identifiers and the diagnostic test identity remain unchanged. Outside roots and escaping node paths return incomplete evidence instead of an incorrectly attributed test finding. No discovery-root production exemption was introduced. + +Focused command: `hatch run pytest -q tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py`. Final local result: **55 passed in 7.22 seconds** (`/private/tmp/specfact-478-path-green.log`). An intermediate implementation run failed because the existing test adapter replaces `Path` with a callable; using the equivalent `Path(".").resolve()` retained the harness and fixed that incompatibility. Ruff formatting/checks passed. This local macOS Python 3.14 result is not Linux capsule or public signed acceptance; combined gates and independent review remain required. + +Follow-up complete local lint includes formatting of all 1,331 files, zero type errors/warnings, Ruff clean and Pylint 10.00/10. Strict OpenSpec validation passed. Module signing and combined release gates are owned by the parent delivery task. + +Parent verification: independent diff review found no additional defect; 114 combined adapter/native-policy/discovery/worker cases passed in 8.68 seconds. Fresh bug-hunt review completed at 2026-09-15T21:21:52.324760Z with PASS_WITH_ADVISORY and exit 0 (94 retained findings, one local-platform-skip blocker). The sole changed-line warning is the existing documented deferred runner import boundary. Report `/private/tmp/specfact-478-path-review.json`. No Linux acceptance is inferred from this local review. + +## PR #478 native pytest startup failure (2026-09-15 Europe/Berlin) + +Finding 4020397305 is valid. Native pytest 9.1.1 with `--specfact-controlled-invalid-option` exits with usage code 4 before session finish, recording `pytest_root: null` and no test records. Replaying this real observation through the capsule request handler previously raised a root-resolution TypeError and returned the unknown member envelope without `target_execution`. The startup scenario preceded the regressions and production changes. Actual final RED: **6 failed, 29 deselected** (`PR478_PYTEST_STARTUP_RED.txt`, `/private/tmp/specfact-478-startup-red.log`). An initial test-only envelope harness incorrectly replaced Path.cwd; correcting that harness before production edits yielded the recorded failure on member identity instead. + +The adapter now validates observations before resolving outcome paths. Invalid executions with no outcome records return their diagnostic without inventing a pytest root. Usage exit 4 receives an actionable configuration/collection diagnostic referring to `target_execution`; native no-tests exit 5 retains its existing incomplete-execution semantics. Otherwise usable observations with explicit null, empty or non-string roots return `project_pytest_root_missing_or_invalid`. The capsule envelope test asserts that member identity, UNKNOWN/error status and the full original target execution record survive. + +Focused command: `hatch run pytest -q tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py tests/unit/specfact_code_review/run/test_portable_worker.py tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py`. Passing result: **103 passed in 7.37 seconds** (`/private/tmp/specfact-478-startup-green.log`). This macOS Python 3.14 result is not Linux capsule or public signed acceptance. Independent review, combined release gates and CI signatures remain required. + +Parent startup validation: 120 combined adapter/native/discovery/worker tests passed in 9.10 seconds. An independent read-only reviewer found no additional defects in the full startup correction and surrounding capsule envelope. Fresh bug-hunt review at 2026-09-15T21:45:21.681036Z returned PASS_WITH_ADVISORY, exit 0, with 95 retained findings and the documented Linux-only skip blocker. The new informational long-test finding has an explicit integration-proof disposition in REVIEW_EXCEPTIONS.md; no new production complexity regression was found. Report `/private/tmp/specfact-478-startup-review.json`. The prior path-only full/smart suites passed 2,743 tests each (147.38/149.80 seconds); they precede this startup correction and do not replace its forthcoming current-head Linux validation. + +### 2026-09-16 — PR478 installed source coverage RED + +Specification extended before tests and implementation to preserve native installed-package imports while requiring metadata ownership and byte-equivalent coverage attribution. Strict OpenSpec validation passed. At unchanged production HEAD `efe7303efb32ac31379906a38e3f960ec4e7a744`, `.venv/bin/python -m pytest tests/unit/specfact_code_review/run/test_installed_coverage.py -q` produced **11 failed, 2 passed** in 1.45s (log `/private/tmp/specfact-478-installed-coverage-red.log`). The native wheel-style installed-package pytest execution passed its actual test but snapshot-only measurement omitted its production source; the attribution assertion failed. A separate actual failing test retained native exit1. No collection/acquisition errors substitute for these behavioral failures. The test helper represents existing no-bridge behavior only when the new module is absent. + + +### 2026-09-16 — PR478 installed source attribution local GREEN + +`PR478_INSTALLED_COVERAGE_RED.txt` preserves the initial unmodified-production failure output. Independent review then reconstructed a flat-layout regular installation: real native pytest passed while Coverage enumerated the untouched snapshot copy at zero executed lines and measured the installed copy at100%. `PR478_INSTALLED_FLAT_RED.txt` records that additional failing-first case. The correction distinguishes actual executed snapshot lines from unexecuted enumeration and replaces only evaluation aliases; raw coverage remains unchanged. These are reconstructed fixtures, not the inaccessible original customer reproduction. + +The controller now reads sealed local-origin distribution RECORD ownership without importing customer packages, authorizes only owned installed coverage directories, and verifies exact source/installed bytes before and after execution. Generated or transformed selected source, ambiguous ownership or source paths, malformed RECORD data and unsafe directory ownership remain actionable incomplete evidence. Native built-package imports, failure exits, low coverage, and actual snapshot execution precedence remain intact. Snapshot discovery and owned-directory verification are cached within one preparation, avoiding repeated repository/package walks. + +Local focused command: `.venv/bin/python -m pytest tests/unit/specfact_code_review/run/test_installed_coverage.py tests/unit/specfact_code_review/run/test_portable_worker.py tests/unit/specfact_code_review/run/test_target_pytest.py -q` — **72 passed** in0.74s; log `/private/tmp/specfact-478-installed-coverage-focused.log`. Real native tests include installed imports with a build-generated initializer, a failing assertion, namespace ownership, relative and absolute measured paths, stale source and installed files, duplicate ownership/source paths, malformed hashes and escaped RECORD paths, controller-to-worker coverage arguments, raw report retention, and the final flat-layout source coverage evaluator. + +`hatch run format`, `hatch run lint` (including type checking, zero errors/warnings and pylint10.00), and strict OpenSpec validation passed. The Requirements compatibility adapter with the repository Requirements/code-review/codebase source roots, `--base-ref origin/dev --required-maturity planned`, passed with one source; implementation maturity remains explicitly not-yet-available. Its JSON and summary are `/private/tmp/specfact-478-installed-requirements.json` and `/private/tmp/specfact-478-installed-requirements.md`. Native Linux corpus and signed publication acceptance are still required; local results do not establish them. + + +### 2026-09-16 — PR478 native Coverage alias provenance + +Independent review reproduced a real false PASS when native `[tool.coverage.paths]` renamed measured installed paths into the snapshot: one selected byte-identical sibling authorized package measurement, while a different installed/source file lost its mismatch diagnostic after aliasing. `PR478_INSTALLED_ALIAS_RED.txt` records the new native failing-before assertion (one failed,24 deselected); the underlying two pytest tests actually passed. The controller now retains exact installed candidate paths, and the worker records their mappings through the installed Coverage implementation's own alias engine. No coverage pattern language is reimplemented or removed. Mismatched source remains incomplete when the native alias maps that installed candidate to its snapshot path; unrelated alias groups do not reject genuine snapshot execution. Valid byte-identical mappings recheck content before accepting either original or aliased rows. + +Focused validation now passes79 tests, including actual aliased native coverage, unchanged-byte acceptance, unrelated patterns, positive snapshot rows after source/installed mutation, and a missing native alias API that preserves collected test outcomes/root metadata. Metadata parsing imports no customer code; the two package-internal controller APIs use the controller's existing trusted icontract dependency for output invariants. The target worker imports no controller contract dependency. New complexity, parameter-count, test shadowing and unnecessary local-import findings were refactored; all new helper/test functions are below radon gradeC. Format/type/lint are passing. Full native Linux and public signed corpus evidence remains outstanding. + +The final origin probe also matches pytest-cov's native `topdir` context. A real Coverage alias test failed before this correction when customer code left the working directory elsewhere; `PR478_INSTALLED_ALIAS_CWD_RED.txt` preserves that failure. The worker now uses the native controller's topdir inside a restoring standard-library context manager, so exact mapping and caller-directory restoration both pass. Final focused validation is **80 passed**; no new helper/test function exceeds radon grade B. The request assembler is separate from `run_portable_pytest`, preserving that existing function's baseline cyclomatic complexity of 11, and the Observer keeps related origin state together rather than adding independent attributes. + + +### 2026-09-16 — complete measured-origin receipts and isolated native probes + +Full and smart suites exposed test-only Coverage global cache contamination: the synthetic probe constructed an uninitialized Coverage object in the same parent process across temporary roots. Both actual runs recorded2780 passed,1 failed,1 Linux-only skip. The probe now runs in a fresh subprocess, initializes through the public `get_data` API under pytest-cov's top directory, then changes to the customer directory before invoking the production receipt helper. It checks restoration inside that child. Production never clears or overrides native Coverage globals. + +Independent review then identified a package-renaming variant of the native alias issue. The actual reconstruction executed installed `capsule_owned/math.py` while reviewing different bytes at `src/original/math.py`; an identical selected sibling enabled package measurement. Native Coverage rewrote the installed path into the unmatched source path, whose empty suffix-candidate set previously allowed a false PASS. `PR478_INSTALLED_RENAMED_RED.txt` preserves the failing-first native regression (1 failed,32 deselected). Controller planning now retains every RECORD-owned Python origin in measured directories, once, and the worker records the native relocation of that complete origin set. A relocated destination without an independently verified source correspondence remains incomplete; missing receipts cannot authenticate an executed snapshot row. No package-renaming correspondence is guessed, no customer path configuration is removed, and the raw coverage report remains untouched. Broader final validation and Linux corpus remain pending. + +Latest focused validation after the complete-origin guard: **82 passed** in1.54s. Formatting, repository lint/type checks, strict OpenSpec validation, YAML lint, Requirements planned gate, and whitespace checks pass. This replaces neither pending final broad suites nor native Linux acceptance. + + +## PR478 native development environment alignment (2026-09-16) + +Native workflow35068293754 failed preparation with `project_analyzer_dependency_incompatible:pylint:isort; selected=9.0.1, required=!=5.13,<9,>=5`. The actual error and developer package inventory are retained in `PR478_NATIVE_ENVIRONMENT_FAILURE.txt` and `PR478_NATIVE_ENVIRONMENT_PACKAGES.txt`. Native development packages selected Pylint4.0.8 and basedpyright1.40.1; the signed cp311/cp312/cp313 toolchain declares Pylint4.0.7 and basedpyright1.39.10. Pylint4.0.7 itself declares isort<9. No hidden resolver constraint is added. + +The new spec preceded three failing cases in `PR478_DEV_ALIGNMENT_RED.txt`. The regression reads the repository declarations and signed toolchain and exercises production compatibility validation against the recorded native versions; it also retains rejection of the original incompatible versions. After explicitly declaring pylint==4.0.7 and basedpyright==1.39.10 in this repository's Hatch default environment, all six compatibility tests pass. The raw inventory remains evidence; tests keep their two observed versions inline so normal OpenSpec archival cannot break them. Customer declarations and production compatibility checks are unchanged. + +## Native index activation follow-up (2026-09-16) + +Native run [35070709935](https://github.com/nold-ai/specfact-cli-modules/actions/runs/35070709935) successfully prepared the reviewed project runtime (identity `sha256:a37e1bba96fabbbf7155fb672b886ddc40a84db1b77a2175d5ae67ad8e18d300`) with the declared analyzer pins. Original signature, formatting, YAML, import, lint and Requirements hooks passed, but review remained UNKNOWN: index snapshot discovery lost verified Hatch activation and an independent signed Semgrep member returned one structured error. The unchanged hooks correctly failed before contract execution; no successful receipt or delivery commit is claimed. + +The actual `hatch run python` checkout/index probe reproduced checkout `default` versus `project_environment_ambiguous:default,hatch-test` in the real `_cached_analysis_snapshot` path. The specification preceded regression tests; `PR478_INDEX_ACTIVATION_RED.txt` retains two failing behaviors and three passing controls. Controller-owned snapshot repository context now supplies only activation verification; snapshot metadata, dependency input hashes and source identity remain independent. Explicit project configuration wins, and removed or unrelated activation cannot select a different environment silently. The same real Hatch probe now resolves `default` in both checkout and index. + +Activation follow-up GREEN: 318 focused discovery, snapshot, VCS and attachment tests passed in 6.57 seconds, including both index and immutable revision inputs. The independently authored Semgrep diagnostic scenario preceded five real failing assertions (`PR478_SEMGREP_DIAGNOSTICS_RED.txt`), then 63 focused runner cases passed. The bounded formatter remains unpublished; the native hook executes the official signed 0.50.0 analyzer payload, so this change alone does not reveal that earlier run's lost raw error or establish its cause. + +Final local activation/diagnostic candidate validation: full suite 2,798 passed, one required Linux `/proc` test skipped on macOS, two pre-existing lark warnings, 129.89 seconds; smart suite the same outcomes, 150.66 seconds. Type and repository lint passed. The fresh follow-up five-file bug-hunt report `/private/tmp/specfact-478-followup-final-review.json` is PASS_WITH_ADVISORY with 31 AST-compared baseline warnings and zero errors, explicitly scoped in REVIEW_EXCEPTIONS.md. The earlier complete delivery-surface report retains the required Linux skip and cannot authorize delivery. Native unchanged-hook execution and final candidate/public signed corpus acceptance remain outstanding. + +## Native analysis follow-up (2026-09-16) + +[Run35073276747](https://github.com/nold-ai/specfact-cli-modules/actions/runs/35073276747) confirms automatic indexed runtime attachment now passes; Pylint and contracts complete. The hook still fails on Semgrep, basedpyright and actual target tests. Its separate diagnostic-only official Semgrep1.144 replay returned a structured syntax error at test_installed_coverage.py147: `match case:` reported `case was unexpected`. The parameter is renamed to `scenario`, retaining all six attribution cases and assertions; this accommodates the pinned parser without skipping the file, changing customer source or accepting parser errors. The raw replay is retained in the native artifact with source/configuration/capsule hashes and explicit startup differences; it is not recovered original stdout or acceptance. + +The same native observation records 99 failed phases caused by unavailable Git and 15 Semgrep fixture failures; a fixture's retained error names missing `uname` in the project Semgrep1.177 native executable. Basedpyright returned empty JSON and its cause is not established. These remain actual failures; no native hook receipt authorizes a delivery commit. + +## Native tools and implicit Hatch default (2026-09-16) + +The native run's real Git and uname failures above motivated explicit target capabilities, with specification before tests. `PR478_NATIVE_TOOLS_RED.txt` retains ten actual failures (five discovery behaviors and five absent capture APIs). Subsequent precise RED evidence covers a Git helper-only transitive library, missing local Git service helpers, and a builder-owned `bin` file collision before installation. The controller now captures fixed supported tools and all program/helper ELF dependencies before building, hashes every captured/generated file into cache and inventory, and installs only after project native-library inventory. The disposable builder cannot access the capture; an integration regression modifies system fixture bytes during building and confirms the original capture is installed. Destination collisions fail before any tool write. + +The independent Hatch default follow-up retains three actual failures in `PR478_IMPLICIT_HATCH_DEFAULT_RED.txt`; its 253 focused cases passed before integration. A real Hatch1.18 default export/activation probe now agrees across live and immutable snapshot discovery. The builtin default is recognized only within verified activation; explicit selection and snapshot dependency identity remain intact. + +Native-tools focused discovery/builder/native/snapshot validation: 314 passed in 1.99 seconds. Typing reports zero errors/warnings; new helper and test lint score10.00; every new helper/test complexity is at most9. `PR478_NATIVE_TOOLS_LINUX_SMOKE.json` records non-root UID1001 Ubuntu Linux execution with network disabled and reviewed source read-only. All ten commands passed: uname, sed, Git initialization/configuration, a real sed clean-filter changing SAFE to BLOCKED, commit, and depth-one file://clone using captured upload-pack. The capture contains50 files totaling38,128,554 bytes (36.36 MiB), including intentionally duplicated Git service executable bytes; no transfer or signed-capsule success is inferred. The smoke uses executable temporary storage inside its disposable container and a fixed Python interpreter link; it does not weaken any capsule boundary. + +`PR478_SEALED_SHELL_AUDIT.json` separately records static verification of all13 signed OCI layer hashes/sizes and final /bin/sh→dash composition, with no later replacement/deletion. Actual Git-filter success inside the signed capsule remains a required native hook/corpus check. Both smoke and static audit are labeled evidence, not protected review or release acceptance. The old signed worker can consume these descriptor additions and existing runtime-bin launchers without changing its code. + +The Node follow-up reproduced the actual distribution-name mismatch using BasedPyright1.39.10 wheel metadata and the production graph→mount→import finder path. `PR478_NODE_DOMAIN_RED.txt` retains two failures with two passing controls; separate evidence describes the wheel and observed native versions. The corrected catalog identifies nodejs-wheel-binaries, preserving the sealed Node entry or emitting an explicit version incompatibility. This repository now declares24.16.0 openly alongside its BasedPyright pin. The compatibility regression checks those declarations against all three signed ABI inventories and keeps the observed24.19.0 rejected. `PR478_NODE_POLICY_CACHE_RED.txt` additionally records genuine stale warm-cache reuse after changing only runtime_compatibility.py; that controller policy is now fingerprinted before lookup. Raw baseline native diagnostic confirmation remains separate from these verified causes and the final updated-snapshot gate. + +Combined focused follow-up validation passed402 cases in14.29seconds, including all three ABI declaration checks and policy-cache invalidation. Fresh combined developer bug-hunt after test cleanup reports PASS_WITH_ADVISORY with215warnings,55info and zero errors; exact existing-surface and conditional coverage dispositions are in REVIEW_EXCEPTIONS.md. Full type/lint/YAML/import and planned Requirements checks passed. The canonical dev-branch payload/version gate passed all7 manifests using origin/dev and its documented optional-public-key policy; this is not CI publisher-signature verification of the unsigned candidate. Native Linux unchanged-hook and public signed acceptance are still outstanding. + +Final full and smart suites each passed2841 tests with one required Linux/proc skip onmacOS and two pre-existing lark warnings (143.45s full;142.78s smart). The full command forwarded `--cov=specfact_code_review.run.runtime_builder --cov=specfact_code_review.run.target_pytest --cov-report=json:/private/tmp/specfact-478-combined-full-coverage.json` and retained the configured80% threshold. The scoped aggregate86.9674% passed; the raw data was copied to `/private/tmp/specfact-478-combined-full.coverage` before smart ran. Builder measures92.4444% with all introduced executable lines covered. Target pytest measures79.8851%; native-child-only mapping/startup lines remain outside parent data and are not rounded up to80%. Thirteen new validator boundary cases pass without production changes or manufactured RED evidence. Remaining coverage qualifications are stated precisely in REVIEW_EXCEPTIONS.md. + +## Native follow-up after exact snapshot run35081982712 (2026-09-16) + +The exact `d5b0c610d24b179a82b7acb5dac9c15aa1681dcc` candidate prepared successfully and its source tree remained unchanged, but the real unchanged hooks failed. Target pytest recorded877 passing calls and24 failed calls, with no collection or internal errors. Nine activation tests found that sanitized VCS lookup discarded the declared Git path; fifteen Semgrep fixture tests advanced past the earlier missing uname to a missing public certificate trust bundle. BasedPyright now ran successfully as a tool and reported the genuine missing import `specfact_cli.registry`, rather than the previous empty-JSON startup error. These are retained failures, not acceptance. + +The repository's Hatch dependencies omitted core despite a separate editable developer bootstrap. The new specification preceded `PR478_CORE_DECLARATION_RED.txt` (one genuine declaration failure) and the exact native diagnostic in `PR478_CORE_NATIVE_RED.json`. The published `specfact-cli==0.55.4` wheel was downloaded without installing it into the developer environment and imported from its extracted bytes in an isolated Python invocation. `PR478_PUBLISHED_CORE_API.json` records the hash, size and actual registry/public-key API checks. The repository now explicitly declares that immutable version; all11 focused compatibility cases pass. The unchanged optional developer bootstrap may still use its matching-version editable checkout; no arbitrary host environment is copied into targets. + +`PR478_ATTACHED_VCS_GIT_RED.txt` and its companion evidence retain seven real failures before verified attached-Git selection and69 focused passing cases after the correction. The equivalent six-branch if/elif coverage-fixture setup preserves all existing assertions while avoiding an independently reproduced Semgrep1.144 parser combination defect: tree-sitter rejects an empty-string argument and Pfff cannot parse match, leaving a misleading first parser error. This is fixture compatibility only; arbitrary customer syntax with the same engine limitation still yields explicit incomplete analysis rather than a false PASS. + +Signed public trust follows the same test-first sequence: `PR478_PUBLIC_TRUST_RED.txt` retains nine actual failures before capture/cache/inventory/default-path integration; the final focused trust suite passes11 cases, and six affected builder/capability suites pass91 cases with one existing Linux/proc skip on macOS. Synthetic builder fixtures explicitly replace the new capture boundary rather than pretending to contain a real signed capsule. Production capture validates a contained regular public PEM, rejects private material, and precedes untrusted building; the installed bytes and cache identity remain controller-owned. `PR478_PUBLIC_TRUST_NATIVE_PROBE.json` separately reproduces missing CA startup failure and successful exact-engine initialization after public CA installation in an offline Linux container. This is diagnostic evidence, not updated signed-capsule acceptance. Final integrated native hooks remain outstanding. + +The final integrated local follow-up review `/private/tmp/specfact-478-native-followup-final-review.json` is PASS_WITH_ADVISORY with215warnings,57informational findings and zero errors. Two introduced nested fixture setups were refactored without changing their cases/assertions; both affected files pass102 tests in11.19seconds, with targeted typing and Ruff passing. Independent read-only review found no remaining refactor defect. Existing measured coverage advisories and narrow boundary/readability dispositions remain in REVIEW_EXCEPTIONS.md. Full formatting, typing, lint, YAML, import and strict OpenSpec checks passed; all7 manifest payload/version checks passed under the documented optional-public-key developer policy. This remains local validation, not a successful native hook or signed release. + +The recorded PR478 RED text captures have trailing whitespace normalized only for repository diff hygiene; failure diagnostics, assertions and outcomes are unchanged. + +Final native-follow-up full and smart suites each pass2867 tests with one Linux/proc skip on macOS and two pre-existing lark warnings (138.69s full;136.88s smart). The full invocation preserves the configured80% threshold and exports scoped runtime_builder/target_pytest coverage to `/private/tmp/specfact-478-native-followup-coverage.json`, with raw data retained as `/private/tmp/specfact-478-native-followup.coverage`. This two-module aggregate is87.0646766%; builder measures92.5438596%, with every introduced executable line covered. Target pytest remains79.8850575%; native child mapping/startup lines158/198 are not counted in parent coverage. These are measured local results; unchanged Linux hooks and final signed external corpus acceptance remain mandatory. + +## Completed native review exposes two fixture prerequisites (2026-09-16) + +Run35090737789 completed its original unchanged review within the existing timeout (Block2 284.0seconds). Runtime attachment, BasedPyright, Pylint and contracts passed. Actual pytest collected1011tests, with1010passing calls and one failed call, without skips or collection/internal errors. The failed call attempted to write `.git/info/grafts` after template-free Git initialization omitted `.git/info`. Separate sealed Semgrep replay retained a syntax error on the valid keyword-only lambda in the builder logging fixture. `PR478_NATIVE_FIXTURE_FAILURES.json` binds these real failures to the verified artifact and source tree. The default-off timing diagnostic correctly declined replay because an original report existed; no timing-policy change follows. + +The graft fixture scenario precedes `PR478_TEMPLATE_FREE_GRAFTS_RED.txt`: ordinary Git templates passed while explicit `git init --template=` reproduced the same missing-parent failure. The fixture now creates only its required metadata parent before writing grafts. All identity, two-commit exported ancestry and absent exported graft assertions remain unchanged in both parameter cases. The full VCS test file passes56tests in8.90seconds; targeted typing reports zero errors/warnings and Ruff checks pass. No production Git isolation or source identity behavior changed. + +`PR478_LOGGING_PARSER_EVIDENCE.json` records the exact official Semgrep1.144.0 full-file parser failure and equivalent named-function passing result (error_file_count1→0; Pfff also passes). The tree-sitter-specific Common.Impossible failure remains recorded. The replacement retains the original keyword-only inputs, returned argv expression and every logging assertion, with AST comparison evidence. Its focused file passes27tests with one pre-existing Linux/proc-only skip on macOS, targeted typing zero errors/warnings and Ruff passing. This is pinned-engine fixture compatibility; unsupported valid customer syntax continues to produce incomplete analysis, and final unchanged Linux hooks remain required. + +Final fixture follow-up full and smart suites each pass2868tests with the same one Linux/proc-only skip on macOS and two pre-existing lark warnings (135.88s full;134.27s smart). Scoped manual `/private/tmp/specfact-478-native-fixtures-final-review.json` passes with two informational fixture-structure suggestions and zero warnings/errors. The explicit attached-Git and builder fixtures retain distinct isolation/resource assertions; their setup remains intentionally readable. An earlier scoped attempt omitted the standard source PYTHONPATH and produced a CrossHair import warning; the corrected canonical environment resolved it without source or gate changes. Strict OpenSpec and planned Requirements pass. Previous production line-coverage measurements remain applicable because this follow-up changes only tests and evidence. The completed but failed native receipt cannot authorize delivery; the new exact tree still requires unchanged Linux hooks and signed release acceptance. + +## Native coverage activation and reviewer evidence (2026-09-16) + +`PR478_NATIVE_COVERAGE_FAILURE.json` binds the latest real signed0.50.0 regression to native run35093180778, source459e5ba6 and its verified artifact. All1012 selected tests passed every recorded phase, while the injected valued `--cov` changed the configured source scope and activated the repository's otherwise dormant aggregate80% gate. The native aggregate was46.88243064729194%, so the unchanged hook correctly remained failed. An independent full configured-source measurement also failed the unchanged80% policy at47.69127470559879% despite2868 passing tests (one platform skip). Widening test selection or lowering the threshold is not the correction. + +The three coverage-fidelity scenarios were added before tests. `PR478_REVIEWER_COVERAGE_RECONSTRUCTED_RED.txt` retains six failures from a harness reconstructing the old argument injection; these are not six end-to-end old-worker reproductions. `PR478_TARGET_COVERAGE_WIRING_RED.txt` separately proves the actual production observer appended overriding arguments. The real native artifact above supplies the original execution evidence. `PR478_COVERAGE_LIFECYCLE_RED.txt` records two subsequent behavioral failures: missing coverage raised an uncaught NoDataError, and collect-only wrongly claimed an applied aggregate failure. `PR478_COVERAGE_BLOCKED_PRELOAD_RED.txt` records an explicitly blocked plugin being instrumented and a trusted preload breaking a strict customer warning policy. `PR478_COVERAGE_EFFECTIVE_SCOPE_RED.txt` proves missing effective CLI scope metadata. Corresponding production corrections followed each failing record. + +The new target-only helper uses pytest-cov's lifecycle and worker transfer. It leaves an active native collector untouched, exports private JSON separately, and labels native versus reviewer measurement. Its separate reviewer collector does not create a native aggregate-report gate; the unchanged controller still enforces max(80,effective threshold) for each reviewed source. Configured source/include/omit exclusions are preserved. The fixed preload carries pytest's assertion-free `PYTEST_DONT_REWRITE` marker so strict warning policy is preserved without changing customer assertion rewriting. Supported native API versions are explicit; missing data or unsupported contracts remain incomplete. + +Combined focused validation passes125tests in5.11seconds across target lifecycle/options/bootstrap/observer/controller suites. This includes real reviewer and native `-n2`, lost worker coverage receipt, configured subprocess collection, customer cwd changes, no-cov/no-cov-on-fail, native report write errors, collect-only, custom config/reset/source omission, byte-identical customer LCOV output, and both directions of aggregate-versus-per-file policy. The options suite introduced no production defect; its initial import/metadata expectation corrections were harness errors and are not claimed as RED proof. Bootstrap evidence retains7genuine failures plus1control, and controller evidence retains its21-failure and8-failure sequences. Ruff and targeted typing pass; new helper complexity is at most9. Independent review found no remaining lifecycle, bootstrap or controller findings. + +The repository explicitly declares pytest-xdist>=3.8.0 for these mandatory distributed tests; normal Hatch resolution installed3.8.0. PyPI3.8.0 declares Python>=3.9, including the supported3.11–3.13 classifiers (accessed2026-09-16). It is not falsely attributed to the existing signed analyzer lock. Final Linux ABI execution remains pending. + +Governance review supersedes the earlier assumption that a successful remote hook receipt is required before a normal developer commit. All original host pre-commit hooks remain mandatory. After their success, a normally signed developer commit can enter the canonical scratch CI signing workflow, import exact signed manifest bytes through another normal signed commit, and exercise the exact candidate observer in PR-to-dev CI. Failed immutable0.50.0 receipts are regression evidence only and authorize neither a bypass nor release acceptance. The configured thresholds, native exit statuses and final signed Linux/corpus gates remain unchanged. Broad gates and a fresh bug-hunt for this new candidate are still pending. + +Follow-up measurement strengthens the new helper through18 in-process boundary cases using real Coverage data and a real reviewer CovPlugin, not synthesized line evidence. Scoped helper coverage is97.04% under the unchanged80% gate; three unmeasured statements remain explicit. Existing native child tests are retained. A separate disposable environment executes seven unchanged harness assertions against actual pytest-cov7.0.0, pytest9.1.1, Coverage7.16.1 and xdist3.8.0, including both distributed modes, missing receipts and configured child collection; all pass. `PR478_COVERAGE_PLUGIN_COMPATIBILITY.json` retains exact commands, helper identity, streams and receipts. These local measurements do not replace signed Linux acceptance. + +The first consolidated full run exposed two existing nested-launch fixtures whose synthetic builtin inventory omitted the newly mandatory helper. Adding the actual helper file preserves all original assertions; the affected launcher/bootstrap suites pass130 integration cases. Eight additional direct-loader cases exercise original production code through real files and restore module/cache state. They cover27/28 loader statements (96.4286%); the defensive missing-spec-loader branch remains unmeasured. Existing real child-startup cases retain independent import-domain proof. + +The additional four-module coverage measurement remains a recorded failure:75.3571% aggregate versus unchanged80, including legacy bootstrap39.2157%, helper97.0414%, observer84.0426% and builder92.5439%. After direct-loader tests, separately measured bootstrap improves to53.3333% while the introduced loader function reaches96.4286%; the whole legacy module still does not satisfy80 and is not presented as passing. These optional scoped measurements are distinct from ordinary full-test commands and from the earlier whole-project47.6913% measurement. No threshold or customer option changed. + +The corrected ORIGINAL full and smart commands each pass2971tests with one genuine Linux/proc-only skip and two pre-existing lark warnings (148.72s full;155.97s smart). Their output is retained outside the checkout as `/private/tmp/specfact-478-coverage-final-full.log` and `-smart.log`. One interim manual review correctly rejected concurrent documentation/evidence changes with `worktree_snapshot_changed_during_analysis`; that report is failed evidence, not a code finding or acceptance. All source and evidence paths are now frozen for the final fresh review and original normal commit hooks. Their subsequent results are recorded outside the frozen checkout and in PR evidence before any release claim. + + +### PR478 single-file installed module follow-up + +Review finding4028556680 is valid: the package-directory guard rejected a uniquely RECORD-owned, byte-identical `standalone.py` installed directly in site-packages. The scenario was specified before tests. `PR478_SINGLE_MODULE_RED.txt` records two genuine failures (root and src-layout) and four preservation controls before changing the bridge; the root case reports `coverage_directory_ownership_unavailable`, while the src case reports `ambiguous_snapshot_source`. A separate native Coverage probe confirms that its module selector measures the imported installed file without measuring an imported unrelated module, avoiding the entire site-packages directory. + +The bridge separates authenticated file identity from optional package-directory measurement. It retains a native module selector only for a unique, valid top-level module and includes its exact path in origin receipts. It preserves duplicate-source and ownership rejection, RECORD hashes, source-byte equality, unchanged raw coverage and post-execution identity checks. The expanded bridge suite passes55 tests, including real imports for both source layouts and source/installed mutations after execution. Observer/collector wiring retains explicit native/configured scope and is validated separately before the follow-up is pushed. No finding resolution or current-head acceptance is claimed from these local results. + +The preceding signed head38402b96 was produced through all original source and signature-only commit hooks, exact canonical CI signing35123796656 and strict seven-manifest verification. Its final full/smart suites each pass2971 tests; the final affected review reports zero findings after the full review's documented advisories. Current candidate run35124077143 has passed cold/warm fixtures and the modules repository on all three supported Python ABIs and is still running the pinned external corpus. Its results belong to38402b96, not this uncommitted follow-up. + +The separate request/observer/collector wiring records11 genuine failing regressions and two passing native/configured-scope controls before implementation in `PR478_SINGLE_MODULE_WIRING_RED.txt`; the final affected three-file suite passes128 tests. Real native subprocess evidence measures the installed module and excludes an imported foreign module in reviewer-default mode, while configured and native app-only sources remain unchanged. Root/src bridge tests and post-execution mutation controls separately pass55 tests. Both focused groups pass typing and Ruff with no introduced Pylint warnings; independent review and final integrated gates follow before commit. + +Independent review reproduced one native selector ambiguity before commit: Coverage interprets a module name as a same-named working-directory folder when that folder exists. `PR478_SINGLE_MODULE_COLLISION_RED.txt` retains one actual failing diagnostic assertion and five real native/configured-scope controls. Reviewer-default collection now reports `project_pytest_coverage_module_selector_ambiguous` with a native source_pkgs remedy; it does not claim this collision is supported, expand the measured directory, change native configuration, or discard passing test observations. All32 helper tests pass after the correction; the request validator was split by responsibility to remove its introduced complexity warning. The earlier fresh review finished with advisory status before these edits; its former CC13 warning is not waived. Final review and integration gates remain required. + + +### Current candidate controlled-corpus fixture correction + +`PR478_CORPUS_CONTROL_RED.json` retains actual candidate35124077143 failure on signed head38402b96. All30 cold/warm executions across15 repository/ABI combinations finish with pytest PASS and no UNKNOWN evidence. Exactly nine controlled-defect reviews (Requests, Hatch and Flask on all three ABIs) retain the deliberate failing test and static type finding but lack coverage for the injected root file. The old fixture never imported or called its wrong-return function. Hatch and Flask also restrict measurement to native packages, while Requests' omit-only configuration does not enumerate unexecuted root files. The overall corpus and dependent quality jobs fail; these are not acceptance results. + +The controlled-corpus scenario preceded the correction. `PR478_CONTROLLED_PACKAGE_RED.txt` records13 actual failures and34 passing controls: generated native pytest fails the real-function assertion under both source_pkgs and omit-only configurations; unsafe destinations and invalid/missing pinned metadata are rejected by the new contract. The generated test now imports and calls the wrong-return function at an explicit pinned package path before asserting the integer result, retaining the intended runtime and static failures. Only the identified disposable copy is modified. Native coverage filters/options and the strict no-UNKNOWN, test-failure and type-failure validators are unchanged. Exclusive writes and contained ordinary parent directories prevent overwriting existing paths or following checkout symlinks. + +The final focused corpus suite passes47 tests, including actual executed function lines1/2 under both unchanged coverage configurations and unchanged original source identity. Ruff, typing and diff checks pass with no introduced Pylint warnings; new production complexity is at most9. These local generated-fixture results do not establish final pinned Linux acceptance. New RED transcript formatting removes only trailing whitespace; the original actual output remains under `/private/tmp`. All source, specifications and evidence are frozen together for independent review and final integrated verification before a new signed push. diff --git a/openspec/changes/code-review-16-portable-project-runtime/UNBORN_VCS_CONTEXT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/UNBORN_VCS_CONTEXT_RED.txt new file mode 100644 index 00000000..c8ae1afb --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/UNBORN_VCS_CONTEXT_RED.txt @@ -0,0 +1,276 @@ +Sanitized RED transcript: local worktree/temp paths and object addresses replaced; diagnostics and outcomes unchanged. +Command: PYTHONPATH=packages/specfact-code-review/src:src .venv/bin/python -m pytest tests/unit/specfact_code_review/run/test_runtime_vcs.py -k "unborn or invalid_git_metadata" -q + +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 41 items / 27 deselected / 14 selected + +tests/unit/specfact_code_review/run/test_runtime_vcs.py FFF........... [100%] + +=================================== FAILURES =================================== +_____ test_unborn_head_allows_discovery_inspection_and_private_copy[fresh] _____ + +tmp_path = PosixPath('/test_unborn_head_allows_discov0') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at
> +state = 'fresh' + + @pytest.mark.parametrize("state", ["fresh", "staged", "orphan"]) + def test_unborn_head_allows_discovery_inspection_and_private_copy(tmp_path: Path, monkeypatch, state: str) -> None: + root = tmp_path / "project" + if state == "orphan": + _repository(root) + _git(root, "tag", "unrelated-v1") + _git(root, "checkout", "--orphan", "new-branch") + else: + root.mkdir() + _git(root, "init", "-q", "--initial-branch=main") + (root / "pyproject.toml").write_text('[project]\nname="fixture"\n') + if state == "staged": + _git(root, "add", "pyproject.toml") +> plan = discover_project(root) + ^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:81: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:394: in discover_project + vcs=vcs_context(root), + ^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:65: in vcs_context + selected = _git(root, "rev-parse", "--verify", commit + "^{commit}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:57: in _git + return _git_bytes(root, *arguments).decode("utf-8").strip() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/test_unborn_head_allows_discov0/project') +input_data = None, shallow_file = None +arguments = ('rev-parse', '--verify', 'HEAD^{commit}') + + def _git_bytes(root: Path, *arguments: str, input_data: bytes | None = None, shallow_file: Path | None = None) -> bytes: + try: + result = subprocess.run( + [ + "git", + "-C", + str(root), + "-c", + "core.hooksPath=/dev/null", + *(["--shallow-file", str(shallow_file)] if shallow_file is not None else []), + *arguments, + ], + env={ + "PATH": os.defpath, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_NO_REPLACE_OBJECTS": "1", + "GIT_GRAFT_FILE": os.devnull, + "GIT_NO_LAZY_FETCH": "1", + "GIT_ALLOW_PROTOCOL": "", + "GIT_TERMINAL_PROMPT": "0", + }, + input=input_data, + capture_output=True, + check=False, + timeout=120, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError( + f"project_git_snapshot_failed:{arguments[0]}:{type(exc).__name__}; retry local metadata export" + ) from exc + if result.returncode: +> raise ProjectRuntimeError( + "project_git_snapshot_failed:" + + arguments[0] + + "; ensure required Git objects are available locally; metadata export disables network acquisition" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_git_snapshot_failed:rev-parse; ensure required Git objects are available locally; metadata export disables network acquisition + +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:48: ProjectRuntimeError +____ test_unborn_head_allows_discovery_inspection_and_private_copy[staged] _____ + +tmp_path = PosixPath('/test_unborn_head_allows_discov1') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at
> +state = 'staged' + + @pytest.mark.parametrize("state", ["fresh", "staged", "orphan"]) + def test_unborn_head_allows_discovery_inspection_and_private_copy(tmp_path: Path, monkeypatch, state: str) -> None: + root = tmp_path / "project" + if state == "orphan": + _repository(root) + _git(root, "tag", "unrelated-v1") + _git(root, "checkout", "--orphan", "new-branch") + else: + root.mkdir() + _git(root, "init", "-q", "--initial-branch=main") + (root / "pyproject.toml").write_text('[project]\nname="fixture"\n') + if state == "staged": + _git(root, "add", "pyproject.toml") +> plan = discover_project(root) + ^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:81: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:394: in discover_project + vcs=vcs_context(root), + ^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:65: in vcs_context + selected = _git(root, "rev-parse", "--verify", commit + "^{commit}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:57: in _git + return _git_bytes(root, *arguments).decode("utf-8").strip() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/test_unborn_head_allows_discov1/project') +input_data = None, shallow_file = None +arguments = ('rev-parse', '--verify', 'HEAD^{commit}') + + def _git_bytes(root: Path, *arguments: str, input_data: bytes | None = None, shallow_file: Path | None = None) -> bytes: + try: + result = subprocess.run( + [ + "git", + "-C", + str(root), + "-c", + "core.hooksPath=/dev/null", + *(["--shallow-file", str(shallow_file)] if shallow_file is not None else []), + *arguments, + ], + env={ + "PATH": os.defpath, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_NO_REPLACE_OBJECTS": "1", + "GIT_GRAFT_FILE": os.devnull, + "GIT_NO_LAZY_FETCH": "1", + "GIT_ALLOW_PROTOCOL": "", + "GIT_TERMINAL_PROMPT": "0", + }, + input=input_data, + capture_output=True, + check=False, + timeout=120, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError( + f"project_git_snapshot_failed:{arguments[0]}:{type(exc).__name__}; retry local metadata export" + ) from exc + if result.returncode: +> raise ProjectRuntimeError( + "project_git_snapshot_failed:" + + arguments[0] + + "; ensure required Git objects are available locally; metadata export disables network acquisition" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_git_snapshot_failed:rev-parse; ensure required Git objects are available locally; metadata export disables network acquisition + +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:48: ProjectRuntimeError +____ test_unborn_head_allows_discovery_inspection_and_private_copy[orphan] _____ + +tmp_path = PosixPath('/test_unborn_head_allows_discov2') +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at
> +state = 'orphan' + + @pytest.mark.parametrize("state", ["fresh", "staged", "orphan"]) + def test_unborn_head_allows_discovery_inspection_and_private_copy(tmp_path: Path, monkeypatch, state: str) -> None: + root = tmp_path / "project" + if state == "orphan": + _repository(root) + _git(root, "tag", "unrelated-v1") + _git(root, "checkout", "--orphan", "new-branch") + else: + root.mkdir() + _git(root, "init", "-q", "--initial-branch=main") + (root / "pyproject.toml").write_text('[project]\nname="fixture"\n') + if state == "staged": + _git(root, "add", "pyproject.toml") +> plan = discover_project(root) + ^^^^^^^^^^^^^^^^^^^^^^ + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:81: +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py:394: in discover_project + vcs=vcs_context(root), + ^^^^^^^^^^^^^^^^^ +../specfact-473-worktree/.venv/lib/python3.14/site-packages/icontract/_checkers.py:834: in wrapper + result = func(*args, **kwargs) + ^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:65: in vcs_context + selected = _git(root, "rev-parse", "--verify", commit + "^{commit}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:57: in _git + return _git_bytes(root, *arguments).decode("utf-8").strip() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + +root = PosixPath('/test_unborn_head_allows_discov2/project') +input_data = None, shallow_file = None +arguments = ('rev-parse', '--verify', 'HEAD^{commit}') + + def _git_bytes(root: Path, *arguments: str, input_data: bytes | None = None, shallow_file: Path | None = None) -> bytes: + try: + result = subprocess.run( + [ + "git", + "-C", + str(root), + "-c", + "core.hooksPath=/dev/null", + *(["--shallow-file", str(shallow_file)] if shallow_file is not None else []), + *arguments, + ], + env={ + "PATH": os.defpath, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_NO_REPLACE_OBJECTS": "1", + "GIT_GRAFT_FILE": os.devnull, + "GIT_NO_LAZY_FETCH": "1", + "GIT_ALLOW_PROTOCOL": "", + "GIT_TERMINAL_PROMPT": "0", + }, + input=input_data, + capture_output=True, + check=False, + timeout=120, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError( + f"project_git_snapshot_failed:{arguments[0]}:{type(exc).__name__}; retry local metadata export" + ) from exc + if result.returncode: +> raise ProjectRuntimeError( + "project_git_snapshot_failed:" + + arguments[0] + + "; ensure required Git objects are available locally; metadata export disables network acquisition" + ) +E specfact_code_review.run.runtime_models.ProjectRuntimeError: project_git_snapshot_failed:rev-parse; ensure required Git objects are available locally; metadata export disables network acquisition + +packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py:48: ProjectRuntimeError +----------------------------- Captured stderr call ----------------------------- +Switched to a new branch 'new-branch' +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unborn_head_allows_discovery_inspection_and_private_copy[fresh] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unborn_head_allows_discovery_inspection_and_private_copy[staged] +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_unborn_head_allows_discovery_inspection_and_private_copy[orphan] +================= 3 failed, 11 passed, 27 deselected in 1.34s ================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/UNCHANGED_POLICY_MANIFEST_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/UNCHANGED_POLICY_MANIFEST_RED.txt new file mode 100644 index 00000000..001ce631 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/UNCHANGED_POLICY_MANIFEST_RED.txt @@ -0,0 +1,583 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 151 items / 141 deselected / 10 selected + +tests/unit/specfact_code_review/run/test_scope.py FFFFFFFFFF [100%] + +=================================== FAILURES =================================== +_ test_index_unchanged_policy_manifest_preserves_selection[ruff-clean-index-existing] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma0/repo') +portable = False, changed = False, primary_path = 'ruff.toml' +primary_content = "extend='config/base.toml'\n" +reference_path = 'config/base.toml', reference_content = "extend='leaf.toml'\n" +leaf_path = 'config/leaf.toml', leaf_content = 'line-length=80\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.toml' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[ruff-clean-index-portable] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma1/repo') +portable = True, changed = False, primary_path = 'ruff.toml' +primary_content = "extend='config/base.toml'\n" +reference_path = 'config/base.toml', reference_content = "extend='leaf.toml'\n" +leaf_path = 'config/leaf.toml', leaf_content = 'line-length=80\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.toml' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[ruff-changed-source-existing] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma2/repo') +portable = False, changed = True, primary_path = 'ruff.toml' +primary_content = "extend='config/base.toml'\n" +reference_path = 'config/base.toml', reference_content = "extend='leaf.toml'\n" +leaf_path = 'config/leaf.toml', leaf_content = 'line-length=80\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.toml' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[ruff-changed-source-portable] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma3/repo') +portable = True, changed = True, primary_path = 'ruff.toml' +primary_content = "extend='config/base.toml'\n" +reference_path = 'config/base.toml', reference_content = "extend='leaf.toml'\n" +leaf_path = 'config/leaf.toml', leaf_content = 'line-length=80\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.toml' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[basedpyright-clean-index-existing] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma4/repo') +portable = False, changed = False, primary_path = 'pyrightconfig.json' +primary_content = '{"extends":"config/base.json"}\n' +reference_path = 'config/base.json' +reference_content = '{"extends":"leaf.json"}\n', leaf_path = 'config/leaf.json' +leaf_content = '{"typeCheckingMode":"basic"}\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.json' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[basedpyright-clean-index-portable] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma5/repo') +portable = True, changed = False, primary_path = 'pyrightconfig.json' +primary_content = '{"extends":"config/base.json"}\n' +reference_path = 'config/base.json' +reference_content = '{"extends":"leaf.json"}\n', leaf_path = 'config/leaf.json' +leaf_content = '{"typeCheckingMode":"basic"}\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.json' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[basedpyright-changed-source-existing] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma6/repo') +portable = False, changed = True, primary_path = 'pyrightconfig.json' +primary_content = '{"extends":"config/base.json"}\n' +reference_path = 'config/base.json' +reference_content = '{"extends":"leaf.json"}\n', leaf_path = 'config/leaf.json' +leaf_content = '{"typeCheckingMode":"basic"}\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.json' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +_ test_index_unchanged_policy_manifest_preserves_selection[basedpyright-changed-source-portable] _ + +git_repo = PosixPath('/test_index_unchanged_policy_ma7/repo') +portable = True, changed = True, primary_path = 'pyrightconfig.json' +primary_content = '{"extends":"config/base.json"}\n' +reference_path = 'config/base.json' +reference_content = '{"extends":"leaf.json"}\n', leaf_path = 'config/leaf.json' +leaf_content = '{"typeCheckingMode":"basic"}\n' + + @pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) + @pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) + @pytest.mark.parametrize( + ("primary_path", "primary_content", "reference_path", "reference_content", "leaf_path", "leaf_content"), + [ + ( + "ruff.toml", "extend='config/base.toml'\n", "config/base.toml", "extend='leaf.toml'\n", + "config/leaf.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/base.json"}\n', "config/base.json", + '{"extends":"leaf.json"}\n', "config/leaf.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + primary_path: str, + primary_content: str, + reference_path: str, + reference_content: str, + leaf_path: str, + leaf_content: str, + ) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ((primary_path, primary_content), (reference_path, reference_content), (leaf_path, leaf_content)): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (reference_path, leaf_path): + assert path in result.policy_paths +> assert result.input_manifest[path] == result.base_input_manifest[path] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/base.json' + +tests/unit/specfact_code_review/run/test_scope.py:827: KeyError +__ test_index_policy_manifest_retains_base_and_head_reference_closures[ruff] ___ + +git_repo = PosixPath('/test_index_policy_manifest_ret0/repo') +primary_path = 'ruff.toml', before = "extend='config/old.toml'\n" +after = "extend='config/new.toml'\n", old_path = 'config/old.toml' +new_path = 'config/new.toml', content = 'line-length=80\n' + + @pytest.mark.parametrize( + ("primary_path", "before", "after", "old_path", "new_path", "content"), + [ + ( + "ruff.toml", "extend='config/old.toml'\n", "extend='config/new.toml'\n", + "config/old.toml", "config/new.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/old.json"}\n', '{"extends":"config/new.json"}\n', + "config/old.json", "config/new.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_policy_manifest_retains_base_and_head_reference_closures( + git_repo: Path, primary_path: str, before: str, after: str, old_path: str, new_path: str, content: str + ) -> None: + (git_repo / "config").mkdir() + (git_repo / primary_path).write_text(before, encoding="utf-8") + for path in (old_path, new_path): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add alternative policy references") + (git_repo / primary_path).write_text(after, encoding="utf-8") + _git(git_repo, "add", primary_path) + + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index")) + try: + assert result.status == "PASS" + assert result.selected_paths == (primary_path,) + for path in (old_path, new_path): +> assert result.base_input_manifest[path].blob_sha == _git(git_repo, "rev-parse", f"HEAD:{path}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/old.toml' + +tests/unit/specfact_code_review/run/test_scope.py:864: KeyError +_ test_index_policy_manifest_retains_base_and_head_reference_closures[basedpyright] _ + +git_repo = PosixPath('/test_index_policy_manifest_ret1/repo') +primary_path = 'pyrightconfig.json', before = '{"extends":"config/old.json"}\n' +after = '{"extends":"config/new.json"}\n', old_path = 'config/old.json' +new_path = 'config/new.json', content = '{"typeCheckingMode":"basic"}\n' + + @pytest.mark.parametrize( + ("primary_path", "before", "after", "old_path", "new_path", "content"), + [ + ( + "ruff.toml", "extend='config/old.toml'\n", "extend='config/new.toml'\n", + "config/old.toml", "config/new.toml", "line-length=80\n", + ), + ( + "pyrightconfig.json", '{"extends":"config/old.json"}\n', '{"extends":"config/new.json"}\n', + "config/old.json", "config/new.json", '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], + ) + def test_index_policy_manifest_retains_base_and_head_reference_closures( + git_repo: Path, primary_path: str, before: str, after: str, old_path: str, new_path: str, content: str + ) -> None: + (git_repo / "config").mkdir() + (git_repo / primary_path).write_text(before, encoding="utf-8") + for path in (old_path, new_path): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add alternative policy references") + (git_repo / primary_path).write_text(after, encoding="utf-8") + _git(git_repo, "add", primary_path) + + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index")) + try: + assert result.status == "PASS" + assert result.selected_paths == (primary_path,) + for path in (old_path, new_path): +> assert result.base_input_manifest[path].blob_sha == _git(git_repo, "rev-parse", f"HEAD:{path}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'config/old.json' + +tests/unit/specfact_code_review/run/test_scope.py:864: KeyError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[ruff-clean-index-existing] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[ruff-clean-index-portable] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[ruff-changed-source-existing] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[ruff-changed-source-portable] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[basedpyright-clean-index-existing] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[basedpyright-clean-index-portable] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[basedpyright-changed-source-existing] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_unchanged_policy_manifest_preserves_selection[basedpyright-changed-source-portable] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_policy_manifest_retains_base_and_head_reference_closures[ruff] +FAILED tests/unit/specfact_code_review/run/test_scope.py::test_index_policy_manifest_retains_base_and_head_reference_closures[basedpyright] +====================== 10 failed, 141 deselected in 1.89s ====================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/UV_TABLE_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/UV_TABLE_RED.txt new file mode 100644 index 00000000..14928ab8 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/UV_TABLE_RED.txt @@ -0,0 +1,321 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 221 items / 213 deselected / 8 selected + +tests/unit/specfact_code_review/run/test_runtime_discovery.py FFFFFFFF [100%] + +=================================== FAILURES =================================== +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_0') +filename = 'pyproject.toml', section = 'tool.uv', value = '1', explicit = False + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_1') +filename = 'pyproject.toml', section = 'tool.uv', value = 'false' +explicit = False + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_2') +filename = 'pyproject.toml', section = 'tool.uv', value = '[]', explicit = False + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_3') +filename = 'pyproject.toml', section = 'tool.uv', value = '"invalid"' +explicit = False + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_4') +filename = 'pyproject.toml', section = 'tool.uv', value = '1', explicit = True + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_5') +filename = 'pyproject.toml', section = 'tool.uv', value = 'false' +explicit = True + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_6') +filename = 'pyproject.toml', section = 'tool.uv', value = '[]', explicit = True + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +_ test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.uv] _ + +tmp_path = PosixPath('/private/var/folders/ly/2wl4sfj179qfn8n46plbft6h0000gn/T/pytest-of-dom/pytest-316/test_consumed_metadata_tables_7') +filename = 'pyproject.toml', section = 'tool.uv', value = '"invalid"' +explicit = True + + @pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], + ) + @pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) + @pytest.mark.parametrize("explicit", [False, True]) + def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool + ) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') +> with pytest.raises(ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table")): + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E Failed: DID NOT RAISE ProjectRuntimeError + +tests/unit/specfact_code_review/run/test_runtime_discovery.py:393: Failed +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-1-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-false-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-[]-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[False-"invalid"-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-1-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-false-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-[]-pyproject.toml-tool.uv] +FAILED tests/unit/specfact_code_review/run/test_runtime_discovery.py::test_consumed_metadata_tables_have_actionable_shape_diagnostics[True-"invalid"-pyproject.toml-tool.uv] +====================== 8 failed, 213 deselected in 1.10s ======================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/VCS_SNAPSHOT_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/VCS_SNAPSHOT_RED.txt new file mode 100644 index 00000000..18edfc46 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/VCS_SNAPSHOT_RED.txt @@ -0,0 +1,24 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 3 items + +tests/unit/specfact_code_review/run/test_runtime_vcs.py ..F [100%] + +=================================== FAILURES =================================== +___________ test_immutable_snapshot_retains_selected_commit_context ____________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-91/test_immutable_snapshot_retain0') + + def test_immutable_snapshot_retains_selected_commit_context(tmp_path: Path) -> None: + from specfact_code_review.run import scope +> from specfact_code_review.run.portable_snapshot import discover_snapshot +E ImportError: cannot import name 'discover_snapshot' from 'specfact_code_review.run.portable_snapshot' (/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py) + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:44: ImportError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_immutable_snapshot_retains_selected_commit_context +========================= 1 failed, 2 passed in 0.41s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/VCS_VERSION_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/VCS_VERSION_RED.txt new file mode 100644 index 00000000..abd0b7b5 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/VCS_VERSION_RED.txt @@ -0,0 +1,47 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 2 items + +tests/unit/specfact_code_review/run/test_runtime_vcs.py FF [100%] + +=================================== FAILURES =================================== +_____ test_private_copy_has_correct_index_and_retains_source_modifications _____ + +tmp_path = PosixPath('/pytest-of-dom/pytest-87/test_private_copy_has_correct_0') + + def test_private_copy_has_correct_index_and_retains_source_modifications(tmp_path: Path) -> None: + root = tmp_path / 'project' + _repository(root) + copy_project(root, tmp_path / 'clean') +> assert _git(tmp_path / 'clean', 'status', '--porcelain') == '' +E AssertionError: assert 'D app.py\n?? app.py' == '' +E +E + D app.py +E + ?? app.py + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:26: AssertionError +_______ test_tag_change_invalidates_runtime_without_source_byte_changes ________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-87/test_tag_change_invalidates_ru0') + + def test_tag_change_invalidates_runtime_without_source_byte_changes(tmp_path: Path) -> None: + root = tmp_path / 'project' + _repository(root) + before = discover_project(root) + _git(root, 'tag', 'v1.0.0') + after = discover_project(root) + assert before.source_identity == after.source_identity +> assert before.identity != after.identity +E AssertionError: assert 'sha256:c25645a5681ace92334702141f2b5eceaabeb2e5c4aa00d7328031cdaca2edf4' != 'sha256:c25645a5681ace92334702141f2b5eceaabeb2e5c4aa00d7328031cdaca2edf4' +E + where 'sha256:c25645a5681ace92334702141f2b5eceaabeb2e5c4aa00d7328031cdaca2edf4' = ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-87/test_tag_...=(), extras=(), requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={}).identity +E + and 'sha256:c25645a5681ace92334702141f2b5eceaabeb2e5c4aa00d7328031cdaca2edf4' = ProjectPlan(root=PosixPath('/pytest-of-dom/pytest-87/test_tag_...=(), extras=(), requirements=(), constraints=(), source_roots=('.',), native_libraries=(), inputs={}, pytest_config={}).identity + +tests/unit/specfact_code_review/run/test_runtime_vcs.py:39: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_private_copy_has_correct_index_and_retains_source_modifications +FAILED tests/unit/specfact_code_review/run/test_runtime_vcs.py::test_tag_change_invalidates_runtime_without_source_byte_changes +============================== 2 failed in 0.20s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/WORKER_BOUNDARY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/WORKER_BOUNDARY_RED.txt new file mode 100644 index 00000000..fa7bbef3 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/WORKER_BOUNDARY_RED.txt @@ -0,0 +1,21 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 8 items + +tests/unit/specfact_code_review/run/test_portable_worker.py .......F [100%] + +=================================== FAILURES =================================== +___ test_target_worker_cannot_write_controller_output_or_share_pid_namespace ___ + + def test_target_worker_cannot_write_controller_output_or_share_pid_namespace() -> None: +> from specfact_code_review.run.target_launch import target_command +E ModuleNotFoundError: No module named 'specfact_code_review.run.target_launch' + +tests/unit/specfact_code_review/run/test_portable_worker.py:69: ModuleNotFoundError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_portable_worker.py::test_target_worker_cannot_write_controller_output_or_share_pid_namespace +========================= 1 failed, 7 passed in 0.10s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/WORKER_IDENTITY_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/WORKER_IDENTITY_RED.txt new file mode 100644 index 00000000..643b65f0 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/WORKER_IDENTITY_RED.txt @@ -0,0 +1,30 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 8 items + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py .......F [100%] + +=================================== FAILURES =================================== +_____________ test_descriptor_rejects_a_different_analyzer_worker ______________ + +tmp_path = PosixPath('/pytest-of-dom/pytest-34/test_descriptor_rejects_a_diff0') + + def test_descriptor_rejects_a_different_analyzer_worker(tmp_path: Path) -> None: + source = tmp_path / 'source' + source.mkdir() + artifact = tmp_path / 'artifact' + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id='linux-x86_64-cp312', worker_identity='sha256:' + 'a' * 64) + with pytest.raises(ProjectRuntimeError, match='worker_identity_mismatch'): +> load_runtime(prepared.descriptor_path, plan=plan, environment_id='linux-x86_64-cp312', worker_identity='sha256:' + 'b' * 64) +E TypeError: load_runtime() got an unexpected keyword argument 'worker_identity' + +tests/unit/specfact_code_review/run/test_runtime_artifacts.py:121: TypeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_runtime_artifacts.py::test_descriptor_rejects_a_different_analyzer_worker +========================= 1 failed, 7 passed in 0.12s ========================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/WORKER_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/WORKER_RED.txt new file mode 100644 index 00000000..4d8dc447 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/WORKER_RED.txt @@ -0,0 +1,20 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: /git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 0 items / 1 error + +==================================== ERRORS ==================================== +_ ERROR collecting tests/unit/specfact_code_review/run/test_portable_worker.py _ +ImportError while importing test module '/git/nold-ai/specfact-cli-modules-worktrees/feature/code-review-16-portable-project-runtime/tests/unit/specfact_code_review/run/test_portable_worker.py'. +Hint: make sure your test modules/packages have valid Python names. +Traceback: +tests/unit/specfact_code_review/run/test_portable_worker.py:7: in + from specfact_code_review.run.portable_worker import select_test_paths, preparation_failure_snapshot +E ModuleNotFoundError: No module named 'specfact_code_review.run.portable_worker' +=========================== short test summary info ============================ +ERROR tests/unit/specfact_code_review/run/test_portable_worker.py +!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! +=============================== 1 error in 0.11s =============================== diff --git a/openspec/changes/code-review-16-portable-project-runtime/XDIST_WORKER_DOMAIN_RED.txt b/openspec/changes/code-review-16-portable-project-runtime/XDIST_WORKER_DOMAIN_RED.txt new file mode 100644 index 00000000..3019404f --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/XDIST_WORKER_DOMAIN_RED.txt @@ -0,0 +1,52 @@ +============================= test session starts ============================== +platform darwin -- Python 3.14.7, pytest-9.1.1, pluggy-1.6.0 +rootdir: +configfile: pyproject.toml +plugins: mock-3.15.1, cov-7.1.0, asyncio-1.4.0, anyio-4.15.1 +asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function +collected 15 items + +tests/unit/specfact_code_review/run/test_target_launch.py ....F [ 33%] +tests/unit/specfact_code_review/run/test_target_bootstrap.py ....F [ 66%] +tests/unit/specfact_code_review/run/test_target_pytest.py ....F [100%] + +=================================== FAILURES =================================== +______ test_pytest_children_keep_private_coverage_and_their_member_domain ______ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1099d6e70> + + def test_pytest_children_keep_private_coverage_and_their_member_domain(monkeypatch) -> None: + from specfact_code_review.run.target_launch import target_command + + def environment(command): + return {command[index + 1]: command[index + 2] for index, arg in enumerate(command) if arg == '--setenv'} + + parent = environment(target_command('pytest-observe', [])) +> assert parent['COVERAGE_FILE'] == '/opt/specfact/tmp/.coverage' + ^^^^^^^^^^^^^^^^^^^^^^^ +E KeyError: 'COVERAGE_FILE' + +tests/unit/specfact_code_review/run/test_target_launch.py:67: KeyError +________ test_only_pytest_children_receive_the_pytest_dependency_domain ________ + +monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x109f13b60> + + def test_only_pytest_children_receive_the_pytest_dependency_domain(monkeypatch) -> None: +> from specfact_code_review.run.target_bootstrap import _python_execution_domain +E ImportError: cannot import name '_python_execution_domain' from 'specfact_code_review.run.target_bootstrap' (/packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py) + +tests/unit/specfact_code_review/run/test_target_bootstrap.py:66: ImportError +______________ test_observer_retains_xdist_worker_startup_failure ______________ + + def test_observer_retains_xdist_worker_startup_failure() -> None: + observer = target_pytest.Observer() +> observer.pytest_testnodedown(None, 'coverage storage is read-only') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +E AttributeError: 'Observer' object has no attribute 'pytest_testnodedown' + +tests/unit/specfact_code_review/run/test_target_pytest.py:53: AttributeError +=========================== short test summary info ============================ +FAILED tests/unit/specfact_code_review/run/test_target_launch.py::test_pytest_children_keep_private_coverage_and_their_member_domain +FAILED tests/unit/specfact_code_review/run/test_target_bootstrap.py::test_only_pytest_children_receive_the_pytest_dependency_domain +FAILED tests/unit/specfact_code_review/run/test_target_pytest.py::test_observer_retains_xdist_worker_startup_failure +========================= 3 failed, 12 passed in 0.10s ========================= diff --git a/openspec/changes/code-review-16-portable-project-runtime/design.md b/openspec/changes/code-review-16-portable-project-runtime/design.md new file mode 100644 index 00000000..fe89bccd --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/design.md @@ -0,0 +1,51 @@ +# Design: portable project runtime + +## Interface and data flow + +Keep existing review invocations. Add `specfact code review runtime inspect --json`, `runtime prepare --json`, `run --project-config PATH`, and `run --project-runtime PATH`. Inspect is read-only. Prepare is also invoked automatically for dependency-sensitive analysis. + +A package-manager-neutral project plan records manager/environment, Python constraints, dependency and configuration input hashes, selected extras/groups, source roots, pytest policy and declared native libraries. Explicit project configuration wins; verified active context precedes unambiguous repository discovery. Conflicts fail with one actionable diagnostic. Build backend declarations alone do not identify environment managers. Adapters preserve manager semantics and never merge every optional environment. + +Prepare in a disposable writable copy using a namespace-isolated builder and a verified capsule interpreter. Separate network dependency acquisition from offline analysis. Do not expose publisher credentials or import project code in the controller. Record manager versions, exact installed distributions, payload and input digests. Cache artifacts outside the source checkout, verify warm reuse, reject symlink/path escape and partially published artifacts. Preserve original manifests, locks and environment files. Native imports report missing shared libraries explicitly. + +The v2 descriptor supports local artifacts and binds inputs, platform/ABI and worker identities. It is local build provenance, never an invented publisher attestation. Retain the v1 reader unchanged. Dependency changes across base/head require independent preparations. Dirty/index sources bind their own content, not just HEAD. The host supervisor remains sealed; project dependencies and plugins execute only in disposable target workers. Ordinary package names are allowed in those workers but cannot shadow control entry points. Genuine tool/project constraints conflicts produce UNKNOWN, not changed project pins. + +## Pytest and report semantics + +Honor configuration precedence, plugin activation, import mode and source roots. Observe actual collection and execution; preserve selected policy and do not suppress controls silently. Unsupported options name the precise reason. Preparation failures retain independent static evidence and produce one root diagnostic referenced by dependent analyzer members. Genuine missing imports after successful preparation remain findings. Local v2 evidence is not eligible for protected pr_range promotion; the existing v1 trust path is unchanged. + +## Validation and delivery + +Freeze Requests, Hatch, Flask and Poetry commits from the accepted plan. Run real source/test slices under pip/Hatch/uv/Poetry on non-root Ubuntu 24.04 for CPython 3.11/3.12/3.13. Include a labelled reconstruction of #472, cold/offline-warm paths, ordinary GITHUB_ACTIONS, changed lock, corruption, native imports and controlled defects. Required applicable analyzer completion is independent of zero findings. The signed public release repeats the candidate matrix. Keep #472 open pending original customer reproduction. + +Run all canonical quality/signature/review/Requirements gates and fix PR findings. Limit corpus jobs to 90 minutes and record actual seconds/bytes. Roll back through reviewed revert and new signed publication; never mutate immutable published payloads. Archive only through openspec after merge and acceptance. + +Private VCS context is an explicit object export, not a repository clone. The selected commit, captured index tree, recorded tags and shallow boundary define its object closure. Git plumbing transfers only that closure into fresh metadata; unrelated refs, reflogs, unreachable objects, alternates and source-local configuration are not imported. This preserves SCM version inputs while binding cache reuse to the Git state build hooks can observe. + +Target startup installs analyzer-owned import resolution before project paths are processed by site initialization. Verified analyzer origins and namespace search locations must stay under the member installation. This governs ordinary import resolution and detects unexpected preloads; it does not claim that a Python import finder sandboxes arbitrary project code within the same interpreter. Project code remains confined to the isolated target worker, and project-owned graph dependencies retain their selected versions. + +### Pylint namespace source-root defaults + +The attached worker's validated snapshot import paths (explicit source roots and installed editable hooks) are the authority for Pylint fallback source roots. Pylint's native Run initializes an ordinary PyLinter with these defaults before its own repository configuration and CLI parsing. This avoids independently duplicating config selection, preserves explicit empty values, and keeps the standard picklable analyzer object for parallel execution. Defaults put nested roots before ancestors because Pylint resolves the first matching source root. Installed wheel paths and guessed `src/` paths are not added. + +The frozen Poetry corpus declares three independently verified import statement locations. Both cold and warm acceptance reject E0401/E0611 at these exact locations; unrelated findings remain admissible. This guard distinguishes completed execution from demonstrated import-resolution fidelity and will run again for the signed candidate and public release. + +### PR #475 configuration and targeted discovery fidelity + +The pip adapter uses native pip installable-directory markers (`pyproject.toml` or `setup.py` as regular files); `setup.cfg` remains a configuration input independently of package installation. Pytest targeted selection expands native configured test paths before inspecting matching Python files, preserving repository containment, excluded environments, recursion settings and the no-match repository fallback. Full reviews continue to delegate collection to pytest. + +Build diagnostics are a controller resource. Preparation opens a unique private regular log outside builder-writable mounts before execution and passes its descriptor for stdout/stderr capture. Native-manager diagnostics flow through that stream. Failure retention never reopens builder-owned paths; successful logs are closed and removed. The public diagnostic references the private log without embedding repository-controlled output. + +The builder artifact crosses the trust boundary only after metadata-only topology validation. Immediately after builder exit, the controller rejects any symlink or special node, including at the artifact root, before opening inventory JSON or enriching native libraries. Final sealing revalidates the enriched tree. This ordering prevents a builder-created native directory alias from redirecting host-side library copies or permission changes. + +### Nested Python execution context + +The supervisor always enters target workers through the sanitizing `target_command` namespace boundary. The generated project-runtime Python executable is used only inside those workers; its nested entry point reuses the inherited isolation and directly executes trusted native Python startup, preserving application environment additions/removals, cwd and private temporary files. No environment marker selects a less isolated initial launch. A read-only bind of a trusted built-in member file identifies the current dependency domain by file identity, surviving an explicitly empty child environment. Nested Python keeps that already-mounted member graph; it cannot add analyzer mounts. Required runtime startup variables remain controlled, and options that disable required attachment (`-I`, `-E`, `-S`) retain explicit diagnostics. This is a first-entry/nested call-path separation and dependency-domain indicator, not a new authorization framework. + +### PR #475 hardlink follow-up + +Build-output topology is an execution boundary, independent of the earlier source-symlink scope. Require single-link regular files during pre-enrichment validation and payload sealing/reuse. The driver copies installed distributions into the artifact, so package-manager hardlinks need not survive this boundary. Reject linked entries before reading their content; retain controller failure diagnostics privately. Stream builder stdout through a pipe with bounded reads and a controller-owned binary log; the builder never inherits the regular capture descriptor. Preserve timeout and downstream failure retention, and close the subprocess and capture resources on every exit. Tests reconstruct the filesystem condition using disposable log bytes and do not claim reproduction of the review's Linux `/proc`/bind-mount attack path. + +### PR #475 diagnostic and nested import-path follow-up + +Keep stable controller error codes and numeric exit status in public preparation diagnostics, alongside the existing private log path. Treat arbitrary exception text and paths as untrusted diagnostic content. Preserve existing log capture and retention without adding diagnostic writes or changing descriptor lifetime. Nested project PYTHONPATH is application configuration: retain it after the trusted startup directory without widening namespace mounts or changing analyzer import ownership. diff --git a/openspec/changes/code-review-16-portable-project-runtime/proposal.md b/openspec/changes/code-review-16-portable-project-runtime/proposal.md new file mode 100644 index 00000000..ca674d3b --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/proposal.md @@ -0,0 +1,29 @@ +# Portable project runtimes for capsule review + +## Why + +Bug #472 reports external Hatch capsule reviews without the consumer dependency graph. Static inspection confirms the ordinary-review handoff gap and broad import/plugin restrictions. Reported customer counts are not independently reproduced: the original PR returns HTTP 404 to the implementation identity. + +## What Changes + +Automatically discover and prepare pip/pip-tools, Hatch, uv and Poetry runtime context; add inspect/prepare commands and project-config/project-runtime review options. Introduce local content-addressed project-runtime-layer-v2 without changing v1 authority. Isolate project workers, preserve dependency pins and native-library evidence, and retain static findings when dependent evidence is incomplete. Validate pinned real external repos and signed public installation. + +## Capabilities + +### New Capabilities + +- `portable-project-runtime`: discovery, isolated preparation, descriptor validation, attachment and customer evidence. + +### Modified Capabilities + +- Code Review local and immutable scope execution, pytest inventory, import-domain preflight, and customer CI acceptance. + +## Impact + +Production changes remain in packages/specfact-code-review. New packaged runtime commands and contract resources require a minor bundle release (0.50.0 if available), signatures, registry/index.json and immutable resource identity updates. Update docs/modules/code-review.md at the existing modules.specfact.io reference permalink. Retain core compatibility unless actual contract tests establish a higher minimum. + +## Tracking + +User Story: https://github.com/nold-ai/specfact-cli-modules/issues/473 +Bug: https://github.com/nold-ai/specfact-cli-modules/issues/472 +Parent Feature #163, Epic #162; #472 is natively blocked by #473. Baseline context: #466, #459, #175. No open prerequisites. Implementation authorized 2026-09-14 Europe/Berlin on codex/feature-code-review-16-portable-project-runtime toward dev. diff --git a/openspec/changes/code-review-16-portable-project-runtime/requirements-evidence.yaml b/openspec/changes/code-review-16-portable-project-runtime/requirements-evidence.yaml new file mode 100644 index 00000000..ff9c79a3 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/requirements-evidence.yaml @@ -0,0 +1,539 @@ +schema_version: '2' +requirements: + openspec:code-review-16-portable-project-runtime:portable-project-runtime:discover-a-project-without-executing-its-code: + rationale: Discover a project without executing its code. The approved portable-runtime story requires independent + customer runtime evidence. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-1-1 + scenario_id: discover-a-project-without-executing-its-code + method: test + intent: Verify hatchling is only a build backend through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-1-2 + scenario_id: discover-a-project-without-executing-its-code + method: test + intent: Verify external hatch configuration through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-1-3 + scenario_id: discover-a-project-without-executing-its-code + method: test + intent: Verify conflicting environment evidence through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-1-4 + scenario_id: discover-a-project-without-executing-its-code + method: test + intent: Verify requirements constraints and local inputs through the focused regression suite and, for execution + claims, the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-1-5 + scenario_id: discover-a-project-without-executing-its-code + method: test + intent: Preserve verified checkout activation across actual immutable index and revision materialization. + observable: Snapshot dependency hashes remain distinct from live inputs; spoofed or unrelated activation is rejected, + explicit selection wins even when the active environment is absent, and missing environments are diagnosed. + - case_id: 473-1-6 + scenario_id: discover-a-project-without-executing-its-code + method: test + intent: Recognize verified implicit Hatch default activation across live, index and revision discovery. + observable: Default activation passes without an explicit default table; environment-only configuration still wins and unrelated activation remains rejected. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:prepare-an-isolated-reproducible-runtime: + rationale: Prepare an isolated reproducible runtime. The approved portable-runtime story requires independent + customer runtime evidence. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-2-1 + scenario_id: prepare-an-isolated-reproducible-runtime + method: test + intent: Verify cold preparation and offline warm reuse through the focused regression suite and, for execution + claims, the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-2-2 + scenario_id: prepare-an-isolated-reproducible-runtime + method: test + intent: Verify changed dependency inputs through the focused regression suite and, for execution claims, the + pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-2-3 + scenario_id: prepare-an-isolated-reproducible-runtime + method: test + intent: Verify corrupt or escaping artifact through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + ? openspec:code-review-16-portable-project-runtime:portable-project-runtime:separate-project-imports-from-the-sealed-supervisor + : rationale: Separate project imports from the sealed supervisor. The approved portable-runtime story requires + independent customer runtime evidence. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-3-1 + scenario_id: separate-project-imports-from-the-sealed-supervisor + method: test + intent: Verify common dependency collision through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-3-2 + scenario_id: separate-project-imports-from-the-sealed-supervisor + method: test + intent: Verify real incompatibility through the focused regression suite and, for execution claims, the pinned + Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:attach-context-to-every-review-scope: + rationale: Attach context to every review scope. The approved portable-runtime story requires independent customer + runtime evidence. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-4-1 + scenario_id: attach-context-to-every-review-scope + method: test + intent: Verify explicit-file customer review through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-4-2 + scenario_id: attach-context-to-every-review-scope + method: test + intent: Verify missing runtime preserves independent evidence through the focused regression suite and, for + execution claims, the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-4-3 + scenario_id: attach-context-to-every-review-scope + method: test + intent: Preserve bounded Semgrep structured error context without accepting incomplete analysis. + observable: Focused runner tests retain total count and bounded scalar type/code/message fields while excluding + arbitrary nested fields; both analyzer passes remain tool errors for nonempty error lists. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:observe-ordinary-customer-pytest-execution: + rationale: Observe ordinary customer pytest execution. The approved portable-runtime story requires independent + customer runtime evidence. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-5-1 + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Verify detached hatch reconstruction through the focused regression suite and, for execution claims, + the pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 473-5-2 + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Verify controlled regression through the focused regression suite and, for execution claims, the pinned + Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 477-pytest-1 + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Mixed PASS/SKIP/XFAIL/XPASS outcomes and empty expected-failure reasons yield blocking findings. + observable: Actual regressions in test_portable_pytest_evidence.py and test_portable_pytest_policy.py preserve + RED/GREEN evidence; Linux signed capsule acceptance remains required. + - case_id: 477-pytest-2 + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Missing and low per-source coverage fail with effective native thresholds and an 80 percent minimum. + observable: Actual regressions in test_portable_pytest_evidence.py and test_portable_pytest_policy.py preserve + RED/GREEN evidence; Linux signed capsule acceptance remains required. + - case_id: 477-pytest-3 + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Nested pytest roots resolve actual test files without exempting production under discovery roots. + observable: Actual regressions in test_portable_pytest_evidence.py and test_portable_pytest_policy.py preserve + RED/GREEN evidence; Linux signed capsule acceptance remains required. + - case_id: 478-pytest-paths + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Non-passing nested pytest nodeids identify snapshot-relative test files without changing raw evidence. + observable: Nested relative and absolute roots map findings to the reviewed test file; outside roots and + escaping node paths yield incomplete-evidence diagnostics without false file attribution. + - case_id: 478-pytest-startup + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Native pytest usage errors preserve the targeted member and recorded execution without a fabricated root. + observable: A real unknown-option exit four with null root retains target_execution in the capsule response; + malformed roots on otherwise usable observations produce actionable incomplete-evidence diagnostics. + - case_id: 478-installed-coverage + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Attribute native installed-project coverage only through sealed local distribution ownership and identical source bytes. + observable: Real installed-package pytest keeps native imports, actual defect exits, per-source coverage, and raw rows; generated code, ownership ambiguity, unsafe RECORD entries, and changed snapshots receive no guessed credit. + - case_id: 478-installed-flat-coverage + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Distinguish enumerated unexecuted flat-layout snapshot rows from actual installed execution. + observable: Native importlib pytest records zero executed snapshot lines and real installed lines; verified attribution replaces only the evaluation alias while preserving raw zero rows and true snapshot execution precedence. + - case_id: 478-installed-native-aliases + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Retain transformed-source uncertainty when native coverage aliases rewrite installed filenames. + observable: A real two-file alias reproduction cannot pass for different source bytes; identical mappings and unrelated native alias patterns remain supported, post-execution identity errors persist, and unavailable origin APIs preserve test records with an explicit diagnostic. Renamed package origins are recorded for the whole measured directory and cannot credit an unmatched reviewed source. + - case_id: 478-installed-single-module + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Attribute uniquely owned installed py_modules without granting coverage over unrelated site packages. + observable: Actual root and src-layout module imports retain verified 100 percent source attribution and untouched raw rows; duplicate source/ownership, changed bytes and malformed names reject credit. Native and configured source scopes remain authoritative while reviewer defaults use only the verified module selector. + - case_id: 478-pytest-coverage-fidelity + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Separate reviewer-only measurement from native coverage activation without reducing the per-file floor. + observable: Real subprocess tests preserve native reset, config, include/omit, report bytes, threshold, precision, disabling and failure outcomes; high aggregate coverage cannot hide low selected-file coverage. + - case_id: 478-pytest-coverage-workers + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Preserve real distributed and configured subprocess evidence under the supported plugin lifecycle. + observable: Real native and reviewer -n2 tests combine worker lines, missing worker receipts stay incomplete, configured subprocess lines are collected, and strict warning policy and customer cwd are preserved. + - case_id: 478-pytest-coverage-bootstrap + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Load only the verified target helper without widening supervisor or customer import paths. + observable: Eight startup and cache tests reject forged aliases, exercise first-entry and child bootstrap, and invalidate builder identity when helper bytes change. + - case_id: 478-pytest-coverage-policy + scenario_id: observe-ordinary-customer-pytest-execution + method: test + intent: Preserve native aggregate failures and incomplete execution with typed evidence. + observable: Controller tests retain real failure and collection evidence, reject malformed policy receipts and preserve exact actionable coverage diagnostics without treating arbitrary exit1 as an aggregate failure. + ? openspec:code-review-16-portable-project-runtime:portable-project-runtime:validate-external-repositories-through-released-installation + : rationale: Validate external repositories through released installation. The approved portable-runtime story + requires independent customer runtime evidence. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-6-1 + scenario_id: validate-external-repositories-through-released-installation + method: test + intent: Verify signed release acceptance through the focused regression suite and, for execution claims, the + pinned Linux capsule corpus. + observable: Actual test assertions and retained runtime/report evidence must demonstrate the stated scenario. + Local unit or bootstrap results do not constitute signed release acceptance. + - case_id: 478-controlled-package-execution + scenario_id: validate-external-repositories-through-released-installation + method: test + intent: Execute the deliberate type defect through ordinary package imports inside unchanged native measurement scope. + observable: Generated tests call the wrong-return function, actually fail, and produce measured function body lines under source_pkgs and omit-only configurations; pinned metadata, contained exclusive writes, original source identity and strict complete-evidence checks remain enforced. + ? openspec:code-review-16-portable-project-runtime:portable-project-runtime:native-environment-and-execution-evidence-fidelity + : rationale: Native environments, test phases, VCS versions and signed Python selection must match the actual + target project. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: openspec-source-spec + kind: config_file + locator: openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md + verification_cases: + - case_id: 473-7-1 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Select a concrete native Hatch environment and reject ambiguity. + observable: Focused regression assertions and actual Linux corpus execution must pass; local component results + do not confer signed release acceptance. + - case_id: 473-7-2 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Retain owned executables and actual pytest phase errors. + observable: Focused regression assertions and actual Linux corpus execution must pass; local component results + do not confer signed release acceptance. + - case_id: 473-7-3 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Reject host manager startup failures without test calls. + observable: Focused regression assertions and actual Linux corpus execution must pass; local component results + do not confer signed release acceptance. + - case_id: 473-7-4 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Bind VCS version inputs and immutable snapshot contexts. + observable: Focused regression assertions and actual Linux corpus execution must pass; local component results + do not confer signed release acceptance. + - case_id: 473-7-5 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Select a signed Python worker matching project constraints. + observable: Focused regression assertions and actual Linux corpus execution must pass; local component results + do not confer signed release acceptance. + - case_id: 473-7-6 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Allow localhost plugin coordination while external networking remains disabled. + observable: Focused regression assertions and actual Linux corpus execution must pass; local component results + do not confer signed release acceptance. + - case_id: 473-7-7 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Preserve tool-only setup.cfg configuration without attempting a root package installation, while retaining + native pip installable-directory behavior. + observable: Adapter regressions retain requirements and constraints, omit an unbuildable root, and keep pyproject.toml + or setup.py installation and extras controls passing. + - case_id: 473-7-8 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Match targeted source tests through configured pytest directories, literal files and glob paths. + observable: Real pytest-compatible cases select corresponding tests after path expansion, retain native no-match + fallback and reject escaping roots without changing explicit selection or full native discovery. + - case_id: 473-7-9 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Isolate controller diagnostic capture from builder-controlled filesystem paths. + observable: Disposable sentinel tests reject symlink and special-file redirection, retain private failure + and timeout diagnostics through a pre-created controller file descriptor, and close and remove successful + logs without exposing captured output. + - case_id: 473-7-10 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Validate untrusted builder artifact topology before controller-side reads or enrichment. + observable: Disposable sentinel regressions reject root, native-directory, inventory and nested-file symlinks + and special nodes before inventory access or native writes, while valid regular artifacts and final sealing + remain accepted. + - case_id: 473-7-11 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Preserve nested Python environment additions, overrides and removals, working directory and caller-private + temporary files. + observable: Real nested subprocess regressions retain caller context and relative file access; the reconstructed + Hatch corpus exercises the same behavior through host, cold capsule and offline warm capsule execution. + - case_id: 473-7-12 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Preserve the verified member dependency domain independently of mutable environment markers while + retaining initial worker isolation. + observable: Nested launch controls retain the mounted member domain under replacement environments; first-entry + commands always clear inherited supervisor state and establish private process, mount and network namespaces. + - case_id: 473-7-13 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Reject multiply linked runtime payloads and descriptors before content reads or attachment. + observable: Disposable hardlinks fail preparation, sealing and same-byte post-seal reuse before payload reads; + copied package-manager cache files remain accepted. + - case_id: 473-7-14 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Keep controller diagnostic file descriptors outside the build namespace. + observable: Actual child processes see pipe stdout rather than regular log storage; bounded capture preserves + failure, timeout and downstream diagnostics and closes resources. + - case_id: 473-7-15 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Preserve safe controller validation reasons in public preparation errors. + observable: Actual nonzero and signal exits retain numeric status; artifact symlink, special-node and hardlink + rejections expose their stable codes while arbitrary exception details stay private. + - case_id: 473-7-16 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Preserve caller import paths for nested Python without replacing trusted startup or analyzer ownership. + observable: Real nested wrappers match native caller imports for inherited and explicit absolute, relative, + empty-component, empty and omitted PYTHONPATH in project and analyzer domains; reconstructed Hatch validates + caller-only imports in its private temporary namespace. + - case_id: 473-7-17 + scenario_id: native-environment-and-execution-evidence-fidelity + method: test + intent: Preserve builder logging assertions and keyword-only fixture inputs using pinned-engine-compatible syntax. + observable: Real Semgrep 1.144.0 accepts the equivalent named fixture function and all builder logging tests pass; customer parser limitations remain explicit. + + + openspec:code-review-16-portable-project-runtime:portable-project-runtime:pylint-preserves-attached-namespace-source-roots: + rationale: Attached editable namespace imports must not be shadowed by Pylint's legacy file-root discovery. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: namespace-source-root-dispatch + kind: config_file + locator: packages/specfact-code-review/src/specfact_code_review/run/target_pylint.py + verification_cases: + - case_id: 473-8-1 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Run real Pylint against editable, declared and overlapping namespace import roots with a same-name + leaf. + observable: test_attached_namespace_root_prevents_leaf_shadowing passes its three parameter cases. + - case_id: 473-8-2 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Preserve actual missing imports after correcting namespace source roots. + observable: test_missing_import_is_preserved_with_attached_roots retains exactly the genuine E0401 finding. + - case_id: 473-8-3 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Keep native repository and command-line source-roots precedence, including an explicit empty value. + observable: test_native_config_overrides_runtime_fallback and test_native_cli_overrides_repository_roots pass. + - case_id: 473-8-4 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Avoid unconfigured source overlays and preserve ordinary Pylint multiprocessing serialization. + observable: test_built_runtime_does_not_infer_raw_source_root and test_parallel_pylint_retains_runtime_roots + pass. + - case_id: 473-8-5 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Reject E0401/E0611 at all three verified Poetry import statements in cold and warm acceptance. + observable: test_poetry_corpus_rejects_known_import_misresolution rejects all twelve phase/location/rule cases. + - case_id: 473-8-6 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Preserve unrelated upstream findings rather than imposing zero findings on untouched projects. + observable: test_poetry_corpus_preserves_unrelated_findings accepts other files, lines and diagnostic classes. + - case_id: 473-8-7 + scenario_id: pylint-preserves-attached-namespace-source-roots + method: test + intent: Invalidate cached runtime identity when only the trusted Pylint dispatcher changes. + observable: test_controller_policy_change_invalidates_offline_runtime_reuse[target_pylint.py] preserves unchanged reuse and + rejects changed bytes. + + openspec:code-review-16-portable-project-runtime:portable-project-runtime:declare-compatible-repository-development-analyzer-versions: + rationale: Repository development tooling must remain compatible with its signed review analyzer entries. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + touchpoints: + - id: declared-development-tools + kind: config_file + locator: pyproject.toml + verification_cases: + - case_id: 473-9-1 + scenario_id: declare-compatible-repository-development-analyzer-versions + method: test + intent: Compare declared development tool ranges, actual observed incompatible versions and every signed ABI through production compatibility validation. + observable: All three signed environments accept declared selections while the recorded incompatible native versions remain rejected; customer collision policy is unchanged. + + - case_id: 473-9-2 + scenario_id: declare-compatible-repository-development-analyzer-versions + method: test + intent: Keep the actual BasedPyright Node distribution sealed and explicitly reject conflicting project versions. + observable: Real graph, launch mounts and import finder tests preserve matching or absent Node dependencies and unrelated legacy-name packages; all three signed ABI declarations admit only the matching Node pin. + - case_id: 473-9-3 + scenario_id: declare-compatible-repository-development-analyzer-versions + method: test + intent: Prevent stale member graphs after a controller compatibility-policy change. + observable: Warm offline cache reuse succeeds before the policy file changes and fails afterwards with the same project and signed worker identity. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:declare-bounded-target-native-tools: + rationale: External tests require explicitly declared commands without importing arbitrary host environments. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + - nold-ai/specfact-cli-modules#472 + touchpoints: + - id: target-native-capabilities + kind: config_file + locator: packages/specfact-code-review/src/specfact_code_review/run/runtime_tools.py + verification_cases: + - case_id: 473-10-1 + scenario_id: declare-bounded-target-native-tools + method: test + intent: Preserve explicit configuration precedence and reject unsupported capabilities without PATH guessing. + observable: Discovery regressions accept supported declarations and explicit overrides, rejecting unknown names and invalid types. + - case_id: 473-10-2 + scenario_id: declare-bounded-target-native-tools + method: test + intent: Bind complete program, helper, launcher and transitive library bytes before the builder starts. + observable: Capture tests verify local Git services, changed helper libraries and offline cache identities; missing binaries and libraries fail precisely. + - case_id: 473-10-3 + scenario_id: declare-bounded-target-native-tools + method: test + intent: Install controller capture after project library inventory while rejecting all builder destination collisions. + observable: Builder integration preserves original captured bytes and separate project library resolution; collision tests reject command, directory and file conflicts before writing. + - case_id: 473-10-4 + scenario_id: declare-bounded-target-native-tools + method: test + intent: Execute real Git, uname and sed through the declared private launchers. + observable: Non-root offline Linux smoke executes Git clean filters and file transport successfully; final signed capsule acceptance remains separately required. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:sanitized-vcs-lookup-retains-declared-target-git: + rationale: Sanitized metadata export must retain its verified declared executable without trusting ambient paths. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + touchpoints: + - id: isolated-vcs-git + kind: config_file + locator: packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py + verification_cases: + - case_id: 473-11-1 + scenario_id: sanitized-vcs-lookup-retains-declared-target-git + method: test + intent: Execute real Git through an authenticated declared launcher while the system search path has no Git. + observable: Actual Git succeeds and seven isolation-control assertions retain configuration, hook, replacement and network restrictions; invalid capabilities fail closed. + - case_id: 473-11-2 + scenario_id: sanitized-vcs-lookup-retains-declared-target-git + method: test + intent: Reject hostile PATH entries and forged worker context without weakening host lookup. + observable: Host execution uses the fixed system path and copied context or tampered launcher cases are rejected. + - case_id: 473-11-3 + scenario_id: sanitized-vcs-lookup-retains-declared-target-git + method: test + intent: Exercise graft isolation with ordinary and explicitly empty Git templates. + observable: Both test_unbound_grafts_cannot_remove_selected_ancestry parameter cases retain identity, complete exported ancestry and absent exported grafts. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:the-repository-declares-its-runtime-registry-dependency: + rationale: A separate editable developer bootstrap does not declare dependencies for hermetic project preparation. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + touchpoints: + - id: declared-core-runtime + kind: config_file + locator: pyproject.toml + verification_cases: + - case_id: 473-12-1 + scenario_id: the-repository-declares-its-runtime-registry-dependency + method: test + intent: Require an immutable declared Hatch core dependency that provides the actual registry API used by reviewed code. + observable: test_declared_core_dependency_supplies_registry_handoff validates the declaration against installed distribution metadata and imports real registry APIs; the published wheel was independently verified. + openspec:code-review-16-portable-project-runtime:portable-project-runtime:target-workers-retain-only-signed-public-default-trust-data: + rationale: Offline dependencies must initialize native TLS without missing public defaults or importing host trust. + stakeholder_refs: + - nold-ai/specfact-cli-modules#473 + touchpoints: + - id: signed-public-trust + kind: config_file + locator: packages/specfact-code-review/src/specfact_code_review/run/runtime_trust.py + verification_cases: + - case_id: 473-13-1 + scenario_id: target-workers-retain-only-signed-public-default-trust-data + method: test + intent: Capture exact signed public bytes before building, verify native SSL loading and bind warm-cache reuse to their identity. + observable: Real certificate loading preserves required verification and hostname checks; unchanged offline reuse succeeds and changed signed trust misses the cache. + - case_id: 473-13-2 + scenario_id: target-workers-retain-only-signed-public-default-trust-data + method: test + intent: Reject missing, escaping, private, oversized or malformed trust and all untrusted builder destination collisions. + observable: Six invalid-source cases and two destination collisions fail closed; builder mutation cannot replace the controller's pre-build capture. diff --git a/openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md b/openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md new file mode 100644 index 00000000..1ac88170 --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/specs/portable-project-runtime/spec.md @@ -0,0 +1,856 @@ +# Portable project runtime + +## ADDED Requirements + +### Requirement: Discover a project without executing its code + +The system SHALL expose read-only runtime inspection with deterministic package-manager and input discovery. Explicit configuration SHALL win over active environment context and unambiguous repository discovery. Build backend metadata SHALL NOT imply an environment manager. Unsafe paths, ambiguous environments and unsupported Python constraints SHALL be actionable diagnostics. + +#### Scenario: Hatchling is only a build backend +- **GIVEN** a project using hatchling with uv.lock and no Hatch environments +- **WHEN** runtime inspection runs +- **THEN** uv is selected and no environment is created + +#### Scenario: External Hatch configuration +- **GIVEN** inherited and detached environments in hatch.toml and pyproject.toml +- **WHEN** a specific environment is selected +- **THEN** its dependencies, extras, source roots and configuration inputs are captured without merging unrelated environments + +#### Scenario: Conflicting environment evidence +- **GIVEN** both Poetry and uv lockfiles and no explicit selection +- **WHEN** runtime inspection runs +- **THEN** one diagnostic names the competing managers and the project-config remedy + +#### Scenario: Requirements constraints and local inputs +- **GIVEN** pip requirements with recursive requirement/constraint includes and repository-local editable dependencies +- **WHEN** inputs are discovered +- **THEN** their bytes are bound to the plan and escaping or cyclic includes are rejected + +#### Scenario: Verified activation survives an immutable index snapshot +- **GIVEN** a verified active repository environment and an independently materialized index or revision snapshot +- **WHEN** automatic runtime discovery reviews that snapshot +- **THEN** activation is verified against the controller-owned original repository while all dependency inputs, configuration and source identity come from the snapshot +- **AND** explicit project configuration wins, unrelated or spoofed activation is ignored, and an active environment absent from the selected snapshot is diagnosed rather than substituted + +#### Scenario: Verified implicit Hatch default activation +- **GIVEN** Hatch's implicit default environment exists while only custom environments are explicitly declared +- **AND** the running interpreter has verified repository-local default activation +- **WHEN** discovering the live project, an index snapshot or an immutable revision +- **THEN** default is selected without requiring an explicit default table, using dependency bytes from the selected source +- **AND** explicit environment-only configuration wins, unrelated activation remains invalid and absent named custom environments still fail + +### Requirement: Prepare an isolated reproducible runtime + +The system SHALL automatically prepare or reuse a private runtime using the selected manager. The system SHALL preserve source files, lockfiles and existing environments, isolate executable build hooks, record resolved dependencies and native components, verify cached content, and support offline warm reuse. Unlocked resolution SHALL be recorded without changing the customer's lockfiles. + +#### Scenario: Cold preparation and offline warm reuse +- **GIVEN** a supported project and an empty private cache +- **WHEN** prepare runs and then repeats offline +- **THEN** both return the same validated dependency artifact and no customer source changes + +#### Scenario: Changed dependency inputs +- **GIVEN** a prepared runtime +- **WHEN** a dependency, constraint, selected group or lock changes +- **THEN** the previous runtime is not reused as matching evidence + +#### Scenario: Corrupt or escaping artifact +- **GIVEN** modified cached bytes, an escaping symlink, or interrupted preparation +- **WHEN** runtime attachment or reuse runs +- **THEN** the artifact is rejected and cannot yield PASS + +### Requirement: Separate project imports from the sealed supervisor + +The system SHALL introduce project-runtime-layer-v2 for local runtime descriptors, preserve the v1 reader and protected trust rules, and run target imports/plugins in isolated workers. Customer dependencies SHALL NOT shadow supervisor entry points. Ordinary project dependency names SHALL NOT be rejected merely because an analyzer also depends on them. + +#### Scenario: Common dependency collision +- **GIVEN** a target depending on requests, httpx, icontract or pydantic +- **WHEN** v2 worker analysis runs +- **THEN** the project's recorded dependencies are used without importing them into the supervisor + +#### Scenario: Real incompatibility +- **GIVEN** incompatible tool/project constraints or a missing required native library +- **WHEN** preparation or worker preflight runs +- **THEN** the exact incompatible dependency/library is reported without silently substituting versions + +#### Scenario: Member-specific dependency closure +- **GIVEN** the signed analyzer environment contains packages outside a member's declared dependency graph +- **WHEN** a project worker or that analyzer member resolves imports +- **THEN** unrelated analyzer packages are unavailable, project-owned dependencies take precedence where compatible, and the selected member's dependency graph is recorded + + +### Requirement: Attach context to every review scope + +The system SHALL attach runtime evidence to explicit-file, full, worktree, index and range review. Base/head snapshots with different inputs SHALL receive different runtime bindings. Local provenance SHALL NOT become protected PR authority. Official installed customer modules SHALL work with GITHUB_ACTIONS=true. + +#### Scenario: Explicit-file customer review +- **GIVEN** an installed official module in an external repository +- **WHEN** changed Python files are reviewed without a PR context descriptor +- **THEN** runtime preparation happens automatically and dependent analyzers use it + +#### Scenario: Missing runtime preserves independent evidence +- **GIVEN** preparation fails +- **WHEN** the review runs +- **THEN** independent static members still execute and dependent members reference one actionable root diagnostic with incomplete evidence + +#### Scenario: Structured Semgrep failures retain bounded diagnostic context +- **GIVEN** either required Semgrep pass returns a nonempty structured errors list +- **WHEN** capsule review reports incomplete analysis +- **THEN** its tool error retains the total error count and at most the first three errors' scalar type, code and message fields, with strings bounded to 512 characters, JSON-escaped controls and a 4000-character total bound +- **AND** omitted errors are identified, arbitrary fields, nested objects and source snippets are excluded, and analysis remains UNKNOWN +- **AND** diagnostic messages may contain reviewed-source text under the existing bounded-stderr policy; no universal secret-redaction claim is made + +### Requirement: Observe ordinary customer pytest execution + +The system SHALL preserve pytest configuration, source paths, plugins and selection semantics, and record actual collection/execution. Unsupported controls SHALL name the exact option and affected evidence. Genuine findings SHALL remain after a valid runtime is attached. + +#### Scenario: Detached Hatch reconstruction +- **GIVEN** src layout, mixed source/test input, third-party/native imports, -p pytest_asyncio.plugin, importlib mode and pythonpath +- **WHEN** capsule review runs +- **THEN** applicable analyzers complete and pytest executes the selected tests + +#### Scenario: Controlled regression +- **GIVEN** a known failing assertion or genuine missing import in a disposable copy +- **WHEN** review runs +- **THEN** real findings and failing evidence remain visible + +#### Scenario: Portable pytest preserves non-passing outcomes + +- **GIVEN** a portable suite contains a passing call and a skipped, XFAIL, non-strict XPASS, or failed selected test +- **WHEN** the capsule evaluates the observed phases +- **THEN** it SHALL emit a blocking `TEST_OUTCOME_NOT_PASS` finding for every non-passing observed test outcome +- **AND** intentional skips SHALL remain terminal observations without inventing executed call phases +- **AND** genuine non-pass findings SHALL remain visible alongside incomplete execution diagnostics + +#### Scenario: Portable coverage validates each reviewed production source + +- **GIVEN** a portable suite completes and produces a coverage document +- **WHEN** the capsule evaluates reviewed production Python files +- **THEN** every applicable reviewed source SHALL have coverage evidence, and absent evidence SHALL mark analysis incomplete +- **AND** measured coverage below the greater of the established 80 percent floor and the effective project threshold SHALL emit blocking `TEST_COVERAGE_LOW` findings +- **AND** unrelated covered files SHALL NOT satisfy another source file's coverage requirement +- **AND** actual selected test modules, conventional test support directories, conftest files and type stubs SHALL NOT be treated as production coverage targets +- **AND** source initializers SHALL retain only the established empty-initializer exemption, without assuming SpecFact's own omit policy applies to customers + +#### Scenario: Native pytest roots do not redefine production sources + +- **GIVEN** pytest selects a nested configuration root or uses a package directory as its test-discovery root +- **WHEN** the capsule maps observed node identifiers and determines production coverage targets +- **THEN** it SHALL resolve raw node identifiers against pytest's actual root inside the source snapshot +- **AND** it SHALL reject an observed pytest root outside the snapshot with an explicit incomplete-evidence diagnostic +- **AND** a discovery root containing production modules SHALL NOT exempt those modules from coverage requirements + +#### Scenario: Nested pytest outcome findings identify snapshot files + +- **GIVEN** pytest reports a non-passing node identifier relative to a contained nested pytest root +- **WHEN** the portable adapter creates a review finding +- **THEN** the finding file SHALL identify the actual snapshot-relative test path while the native node identifier remains unchanged in observed evidence and diagnostic text +- **AND** absolute or escaping pytest roots outside the snapshot SHALL produce incomplete-evidence diagnostics before outcome paths are mapped +- **AND** an escaping observed node path SHALL NOT create a finding attributed outside the snapshot + +#### Scenario: Pytest startup failure preserves recorded execution + +- **GIVEN** native pytest exits with a usage error before session finish and records no pytest root or test outcomes +- **WHEN** the portable adapter evaluates the observation +- **THEN** it SHALL retain the targeted pytest member identity, the original target execution record and an actionable configuration diagnostic naming the native exit code +- **AND** it SHALL NOT invent a root or attempt outcome-path mapping for absent outcomes +- **AND** otherwise usable observations with null, empty or non-string roots SHALL return an explicit incomplete-evidence diagnostic instead of an uncaught adapter exception + +#### Scenario: Reviewer instrumentation does not activate a native aggregate coverage gate +- **GIVEN** the effective native pytest invocation does not enable pytest-cov and does not explicitly disable coverage +- **WHEN** the capsule obtains required reviewer coverage evidence +- **THEN** it SHALL use a separately identified reviewer-owned collector while preserving the native pytest exit and the configured coverage threshold value +- **AND** it SHALL enforce the existing per-reviewed-source floor of the greater of 80 percent and the effective configured threshold +- **AND** native Coverage source, source_pkgs, source_dirs, include and omit semantics SHALL remain authoritative; default scope may include only the snapshot, controller-verified installed directories and uniquely owned top-level Python module selectors when no such configuration is present +- **AND** excluded or missing reviewed sources SHALL remain incomplete rather than receive synthesized coverage. + +#### Scenario: Explicit native coverage policy retains its outcome and report destinations +- **GIVEN** native pytest configuration or arguments enable pytest-cov +- **WHEN** the capsule observes coverage +- **THEN** it SHALL preserve effective source, reset, configuration path, report destination, report-disable, threshold, precision, no-cov and no-cov-on-fail controls without appending overriding coverage options +- **AND** private review evidence SHALL be exported separately through the finished native Coverage object without replacing requested reports +- **AND** an actual native aggregate threshold failure SHALL remain a blocking coverage-policy finding with the observed total, threshold and precision rather than an unexplained missing-test failure +- **AND** explicit coverage disabling or failure-driven report suppression SHALL leave required reviewer evidence incomplete. + +#### Scenario: Reviewer collection preserves distributed and subprocess evidence +- **GIVEN** native pytest uses xdist or configured Coverage subprocess support +- **WHEN** reviewer-only instrumentation is active +- **THEN** the verified target-only plugin SHALL use the supported pytest-cov lifecycle and worker transfer without adding customer imports to the supervisor or double tracing an active native collector +- **AND** the report SHALL retain instrumentation provenance, effective run/report configuration, and actual collection/execution outcomes +- **AND** an unsupported plugin contract or missing worker coverage SHALL produce an actionable incomplete-evidence diagnostic. + +#### Scenario: Installed project source receives authenticated coverage attribution + +- **GIVEN** the native package manager installs a non-editable local project and its tests execute the installed package rather than the reviewed source path +- **WHEN** the sealed controller prepares targeted coverage +- **THEN** it SHALL preserve installed-package import precedence and use the sealed distribution's local origin and RECORD ownership to select installed coverage directories +- **AND** it SHALL attribute an actual installed coverage row to a reviewed file only when the full package-relative path has one snapshot match and both files have identical verified content before and after execution +- **AND** it SHALL retain the original native coverage paths and explicit attribution evidence without inventing executed lines or changing native pytest configuration +- **AND** native low coverage and failing test outcomes SHALL remain failing evidence + +#### Scenario: Installed coverage cannot credit unrelated or changed source + +- **GIVEN** installed source differs from the snapshot, has ambiguous source or distribution ownership, invalid RECORD hashes, unsafe paths, or shares its only coverage directory with a foreign distribution +- **WHEN** coverage is attributed to reviewed source +- **THEN** affected files SHALL receive an actionable incomplete-evidence diagnostic and SHALL NOT receive guessed coverage +- **AND** generated or transformed code SHALL retain its native runtime behavior without claiming line equivalence to different source +- **AND** a current snapshot change or post-execution content mismatch SHALL invalidate earlier attribution +- **AND** namespace ownership SHALL be checked without importing project packages into the controller +- **AND** genuine directly executed snapshot coverage SHALL take precedence over any installed alias, while raw installed evidence remains available; automatically enumerated zero-execution snapshot rows SHALL NOT mask byte-verified installed execution + +#### Scenario: Native coverage aliases cannot prove execution origin + +- **GIVEN** native Coverage path aliases rewrite installed-package paths to snapshot paths and a selected installed file differs from its reviewed source +- **WHEN** another owned selected file permits measuring that installed package directory +- **THEN** the worker SHALL retain the native Coverage mapping of installed candidate paths alongside untouched raw coverage +- **AND** the controller SHALL retain an actionable incomplete-origin diagnostic for the byte-different source even when its aliased report row has executed lines +- **AND** byte-identical mappings SHALL be reverified before accepting aliased rows +- **AND** unrelated alias groups SHALL NOT reject actual snapshot execution +- **AND** native origin receipts SHALL cover every owned Python file in measured installed directories, including package-renamed files with no matching snapshot suffix +- **AND** a rewritten source path without a controller-verified source correspondence SHALL remain incomplete instead of acquiring execution credit + +#### Scenario: Single-file installed modules retain verified coverage attribution + +- **GIVEN** a local non-editable distribution uniquely owns a top-level Python module through its sealed RECORD and the selected snapshot contains exactly one matching source with identical verified bytes +- **WHEN** its tests import the installed single-file module +- **THEN** coverage attribution SHALL preserve the installed import and map only actual byte-verified execution to the selected source, including a module under a unique source-layout directory +- **AND** reviewer-only default instrumentation may use the native module selector without measuring the entire site-packages directory; imported unrelated distributions SHALL NOT gain coverage credit +- **AND** explicitly configured or native-active coverage scope SHALL remain unchanged, with excluded source evidence incomplete +- **AND** ambiguous snapshot paths, distribution ownership, stale content, malformed module names and post-execution mutation SHALL remain incomplete rather than being guessed or credited +- **AND** if native module-selector semantics would instead select a same-named working-directory folder, reviewer-default collection SHALL report that ambiguity explicitly without expanding scope or altering native/configured collection + +### Requirement: Validate external repositories through released installation + +The system SHALL require a pinned Requests/pip, Hatch/Hatch, Flask/uv and Poetry/Poetry corpus on Ubuntu 24.04 CPython 3.11/3.12/3.13 for relevant changes and published releases. Tests SHALL reject empty or UNKNOWN required analysis, retain real findings, verify source immutability, and capture exact artifacts, test inventory, exit codes, duration and transfer cost. + +#### Scenario: Signed release acceptance +- **GIVEN** the published signed module and pinned upstream checkouts +- **WHEN** cold and offline-warm customer reviews run +- **THEN** actual applicable analysis and test execution complete without development overrides + +#### Scenario: Controlled corpus defects execute inside native measurement scope + +- **GIVEN** an identified disposable copy of a pinned corpus repository with unchanged native coverage configuration +- **WHEN** validating that a known defect still fails +- **THEN** the corpus SHALL place an annotated wrong-return function at an explicit contained package path/import recorded for that pinned entry and the generated test SHALL import and call that function before its deliberately failing assertion +- **AND** the reviewed function SHALL be within native package measurement scope rather than depending on enumeration of an unexecuted root file +- **AND** validation SHALL still require complete required analyzer evidence, the actual failing test and the actual static type defect; no coverage source, exclusion, threshold or outcome checks may be relaxed +- **AND** ordinary upstream checkout files and configuration SHALL remain unchanged; only the identified controlled copy contains the injected source and test + +### Requirement: Native environment and execution evidence fidelity +The runtime SHALL select a concrete environment from the package manager's native export, preserve installed executable entry points, and retain actual pytest setup, collection, and call failures. Corpus host baselines SHALL reject startup failures without executed tests. + +#### Scenario: Hatch internal matrix selection +- **GIVEN** a Hatch test matrix with one environment matching the selected Python ABI +- **WHEN** preparation selects the matrix +- **THEN** Hatch creates and locates that concrete environment using its positional environment argument +- **AND** multiple matching environments produce an ambiguity diagnostic before installation + +#### Scenario: Native executable required by test setup +- **GIVEN** an installed distribution that owns a console script or native executable +- **WHEN** a target worker starts tests +- **THEN** the executable is available in the isolated target runtime and included in its inventory +- **AND** its build-machine interpreter path is never retained + +#### Scenario: Test setup and host startup failures +- **GIVEN** pytest fails before a test call +- **WHEN** observations are recorded +- **THEN** the failed phase and exception text remain available with incomplete evidence +- **AND** a host command that never executes tests cannot satisfy corpus acceptance + +#### Scenario: VCS-derived build version and cache reuse +- **GIVEN** a project whose build derives its version from Git history or tags +- **WHEN** its runtime is prepared for a worktree or immutable review side +- **THEN** the private build receives sanitized Git metadata and an index matching the selected commit +- **AND** cache identity includes the selected commit, tag identities, and shallow-history boundary +- **AND** source modifications remain modifications rather than becoming a fabricated clean version + +#### Scenario: Project Python differs from the controller +- **GIVEN** a project selects Python 3.11 through explicit configuration or `.python-version`, while the controller uses Python 3.12 +- **WHEN** preparation or review starts +- **THEN** the signed Python 3.11 worker is selected and verified against the project's full Python version constraint +- **AND** base and head snapshots select their own compatible workers +- **AND** an unsupported pin or conflicting version constraint produces an explicit diagnostic + +#### Scenario: Offline plugin coordination +- **GIVEN** a pytest plugin uses localhost sockets to coordinate workers +- **WHEN** tests run in an offline target namespace +- **THEN** localhost resolves using a fixed private hosts file without granting external network access +- **AND** pytest internal errors retain their exact traceback as incomplete evidence + +#### Scenario: Nested review tests do not inherit controller scope +- **GIVEN** the development review controller uses cached-diff enforcement +- **WHEN** it executes repository tests that invoke review themselves +- **THEN** controller-only diff selection does not leak into those test processes +- **AND** tests can explicitly select their own diff scope + +#### Scenario: Source changes during full or explicit-file analysis +- **GIVEN** a local project runtime was prepared successfully +- **WHEN** source, dependency inputs, or VCS version metadata change during analysis +- **THEN** the final runtime binding is incomplete for every dependency-sensitive member +- **AND** unchanged artifact bytes alone cannot authorize a passing source binding + +#### Scenario: Hatch-declared pytest arguments +- **GIVEN** the selected native Hatch environment declares extra pytest arguments +- **WHEN** runtime preparation exports that environment +- **THEN** those arguments are retained in the runtime inventory and target pytest invocation +- **AND** explicit or disabled plugin options participate in duplicate-registration prevention + +#### Scenario: Isolated xdist workers retain pytest coordination +- **GIVEN** repository pytest configuration requests xdist workers and coverage +- **WHEN** child Python processes enter fresh target namespaces +- **THEN** they retain the pytest member's dependency domain and use private coverage storage +- **AND** unrelated analyzers do not inherit that pytest domain +- **AND** worker startup failures are retained in incomplete execution evidence + +#### Scenario: Analyzer package introspection respects member boundaries +- **WHEN** a member discovers its bundled checkers through filesystem-to-module lookup +- **THEN** its verified dependency closure is visible as a real import directory +- **AND** unrelated sealed packages are absent from that directory and its metadata inventory + +#### Scenario: Invalid and stale runtime state fails explicitly +- **WHEN** a supplied descriptor has invalid inventory types, preparation races with another publisher, or a pytest child fails before writing evidence +- **THEN** malformed descriptors and stale observations cannot become completed evidence +- **AND** a valid concurrent cache winner is verified and reused + +#### Scenario: Python startup controls cannot bypass runtime attachment +- **WHEN** a project invokes Python with an option that disables mandatory runtime bootstrap +- **THEN** execution rejects that exact option with a runtime diagnostic + +#### Scenario: Review selection and candidate policies survive normalization +- **WHEN** review focus is normalized or a portable range adds referenced policy files +- **THEN** explicit runtime paths and both candidate and target policy closures remain bound to the review + +#### Scenario: Source aliases cannot bypass build exclusions +- **WHEN** a source link resolves into an excluded file, directory, descendant, or Git configuration +- **THEN** discovery and copying reject it before any network-enabled build hook runs +- **AND** valid included file and directory links remain inside the copied source with their target bytes bound before building + +#### Scenario: Unlocked pip-tools and legacy package metadata +- **WHEN** a repository has requirements.in without a compiled requirements.txt, or static setup.cfg Python constraints +- **THEN** discovery imports those inputs and pip prepares the selected dependencies without writing into the checkout +- **AND** a compiled requirements.txt takes precedence when both pip-tools inputs exist + +#### Scenario: Optional test dependencies are selected without combining alternatives +- **WHEN** a project declares one test extra and no selected test group +- **THEN** automatic preparation selects that extra +- **AND** conflicting test extras or group/extra alternatives require explicit selection + +#### Scenario: Executable source changes invalidate reuse +- **WHEN** an included source file changes executable mode without changing bytes +- **THEN** source identity and runtime reuse change with it + +#### Scenario: Corpus acquisition cost is measured transparently +- **WHEN** the Linux corpus runs dependency acquisition and analysis commands +- **THEN** evidence records wall time, artifact bytes, and non-loopback host network byte deltas +- **AND** network counters are labelled as host-wide observations rather than exact package-payload transfer sizes + +#### Scenario: Recorded member dependency closure is complete +- **WHEN** a required distribution is missing from both project inventory and sealed analyzer inventory +- **THEN** preparation reports the member and missing distribution rather than silently omitting the dependency edge +- **AND** genuinely undeclared project imports remain ordinary analyzer findings after successful preparation + +#### Scenario: Analysis workers cannot read excluded checkout files +- **WHEN** an ordinary checkout contains excluded environment files or a local virtual environment +- **THEN** portable analyzers receive a verified private source copy with those exclusions applied +- **AND** selected included source bytes and configuration remain identical to the bound source snapshot + +#### Scenario: Cached enforcement retains staged VCS context +- **WHEN** a changed-enforcement review materializes the staged index of a dynamically versioned project +- **THEN** discovery receives its repository and index context so preparation preserves sanitized VCS facts + +#### Scenario: Partial pytest execution cannot hide collection or internal errors +- **WHEN** tests execute or fail alongside collection or internal errors +- **THEN** known failures remain visible and required test evidence remains incomplete + +#### Scenario: Runtime descriptors and native binaries match their execution domain +- **WHEN** a descriptor has invalid pytest argument types or an ELF artifact targets another machine architecture +- **THEN** validation rejects the artifact with a precise diagnostic before worker execution + +#### Scenario: Independent offline corpus proof +- **GIVEN** a cold external review has populated the verified cache +- **WHEN** the corpus repeats preparation and explicit attachment +- **THEN** both commands execute as a non-root user in a network namespace with no active non-loopback interfaces, and the evidence records this isolation separately from the offline option + +#### Scenario: Controlled defect evidence remains separate +- **GIVEN** an untouched upstream review and a disposable copy with injected defects +- **WHEN** the corpus analyzes both +- **THEN** their reports have distinct artifact names and the controlled report proves actual failing tests and static detection + +#### Scenario: Offline wrapper preserves ordinary process devices +- **GIVEN** warm validation runs inside a non-root user and network namespace +- **WHEN** the customer CLI or Git opens /dev/null +- **THEN** a private device filesystem supports ordinary subprocess execution without exposing host devices or external networking + +#### Scenario: Disabled pytest metadata autoload +- **GIVEN** project or selected environment options disable plugin autoload +- **WHEN** the worker constructs pytest arguments +- **THEN** it does not synthesize installed third-party entry points, preserves explicit plugin choices, and loads required coverage only once unless explicitly disabled + +#### Scenario: Directory-sensitive runtime identity +- **GIVEN** a cached runtime whose build can inspect directory existence and permissions +- **WHEN** an included directory is added, removed, or changes mode +- **THEN** source identity and cache reuse reflect that change, including the root directory mode + +#### Scenario: Pip lock and source requirements conflict +- **GIVEN** pylock.toml and either requirements.txt or requirements.in without explicit selection +- **WHEN** discovery runs +- **THEN** one diagnostic names all competing inputs and requests explicit configuration + +#### Scenario: Invalid INI remains an actionable preparation failure +- **GIVEN** malformed setup.cfg or pytest INI configuration +- **WHEN** discovery reads the configuration +- **THEN** ProjectRuntimeError identifies the affected file and review retains independent findings with incomplete runtime evidence + +#### Scenario: Protected offline test launcher +- **GIVEN** customer code has run before the offline validation step +- **WHEN** the corpus selects its namespace launcher +- **THEN** only the expected regular executable under a root-owned non-writable directory is accepted, with its bytes matching an administrator-owned digest record; writable, substituted, symlinked, or mismatched launchers are rejected before execution + +#### Scenario: Relative customer file selections enter the private source copy +- **WHEN** an ordinary review selects repository-relative source and test paths +- **THEN** the private analysis copy SHALL preserve those selections as paths within the copied repository +- **AND** equivalent absolute selections SHALL resolve to the same files +- **AND** a selection escaping the repository SHALL be rejected before analyzer execution. + +#### Scenario: Git HTTPS dependencies are acquired in the disposable builder +- **WHEN** dependencies use Git HTTPS transport, including a source tree without Git metadata +- **THEN** preparation SHALL provide the Git executable and HTTP/HTTPS transport helpers with their native dependencies and an explicit private Git exec path +- **AND** helper identities SHALL participate in cache reuse. + +#### Scenario: A selected pytest dependency violates an analyzer requirement +- **WHEN** a selected project distribution violates an active worker dependency specifier +- **THEN** preparation SHALL report the domain, dependency, selected version and required specifier before sealing the runtime +- **AND** independent static analysis SHALL remain available without silently replacing the project version. + +#### Scenario: Analyzer source mounts exclude Git history +- **WHEN** an ordinary worktree is copied for analyzer execution +- **THEN** its source mount SHALL exclude Git metadata and historical objects +- **AND** the separate disposable build copy SHALL retain sanitized VCS context for dynamic package versions. + +#### Scenario: Pytest configuration leaves discovery roots unspecified +- **WHEN** pytest testpaths is absent or empty +- **THEN** full review SHALL use the repository root as pytest's native default +- **AND** explicit source review SHALL discover corresponding root-level tests. + +#### Scenario: Git identity probing fails before cache lookup +- **WHEN** Git exec-path discovery fails, times out or cannot execute +- **THEN** preparation SHALL raise a precise ProjectRuntimeError with the original exception chained +- **AND** review SHALL retain the existing incomplete-runtime diagnostic path. + +#### Scenario: Hosted runner has writable optional-tool directories +- **WHEN** a hosted runner exposes a customer-writable `/opt` ancestor +- **THEN** the offline corpus launcher SHALL be provisioned outside that ancestor +- **AND** all launcher ownership and ancestor permission checks SHALL remain enforced. + +#### Scenario: Source package is named venv +- **WHEN** a repository contains a legitimate package directory named `venv` without a virtual-environment marker +- **THEN** source identity and both build/analysis copies SHALL retain that package +- **AND** actual environments identified by `pyvenv.cfg` SHALL remain excluded regardless of directory name +- **AND** symlink aliases SHALL not expose excluded environment contents. + +#### Scenario: Default test discovery encounters an existing environment +- **WHEN** repository-root test discovery encounters tests inside an excluded virtual environment +- **THEN** those installed dependency tests SHALL not create ambiguity or enter the customer test selection +- **AND** discovery SHALL prune excluded environment directories before traversal. + +#### Scenario: Runtime preparation fails before independent static analysis +- **WHEN** discovery or preparation fails and independent static members remain applicable +- **THEN** those members SHALL receive the same sanitized private source copy as successful preparation +- **AND** if that copy cannot be established safely, no analyzer SHALL receive the original source and required evidence SHALL remain incomplete. + +#### Scenario: Poetry constrains the effective Python runtime +- **WHEN** Poetry declares its Python constraint in tool.poetry.dependencies.python +- **THEN** discovery SHALL import that constraint and intersect it with PEP621 requires-python when both are declared +- **AND** unsupported manager-specific constraint syntax SHALL identify the exact declaration and remedy rather than be ignored or passed unmodified to a PEP440 interpreter selector. + +#### Scenario: Non-pip manager receives explicit pip dependency inputs +- **WHEN** explicit project configuration supplies requirements or constraints for uv, Hatch or Poetry +- **THEN** discovery and adapter dispatch SHALL reject unsupported fields with the selected manager and a remedy before installation +- **AND** unrelated exported requirements files SHALL not override native manager configuration. + +#### Scenario: Source-only pytest selection respects recursion exclusions +- **WHEN** matching test discovery traverses the repository root +- **THEN** pytest's default norecursedirs exclusions SHALL be retained when unspecified +- **AND** explicit norecursedirs configuration SHALL replace those defaults consistently with native pytest. + +#### Scenario: Pytest deselection preserves the executed selection inventory +- **GIVEN** repository pytest options select a subset with `-k` or `--deselect` +- **WHEN** the isolated worker collects and executes that selection +- **THEN** its required execution inventory contains the final selected items, deselected items do not become false unexecuted evidence, and actual selected failures or interrupted execution remain visible, including distributed execution. + +#### Scenario: Immutable portable snapshots retain safe tracked project links +- **GIVEN** an immutable Git snapshot contains tracked package or data symlinks needed by the project runtime +- **WHEN** portable discovery and preparation materialize that snapshot +- **THEN** safe in-snapshot links remain available with their original targets, while escaping or excluded-source aliases fail closed and existing governed-Python symlink restrictions remain enforced. + +#### Scenario: Invalid pytest path options preserve static fallback +- **GIVEN** a supported pytest configuration declares `pythonpath` or `testpaths` with a value other than a string or list of strings +- **WHEN** runtime discovery consumes that configuration +- **THEN** it returns a project-runtime diagnostic naming the option and source configuration, and ordinary review retains independent static evidence without an uncaught type error. + +#### Scenario: Staged snapshots retain their captured Git tree +- **GIVEN** an index review captures a staged tree that differs from HEAD +- **WHEN** its portable project runtime records VCS identity and prepares a private build copy +- **THEN** the descriptor and cache identity SHALL bind the captured tree separately from the HEAD commit +- **AND** the private Git index SHALL be populated from that captured tree while HEAD remains the selected commit +- **AND** staged-only paths and renames SHALL remain visible to Git-aware build hooks without reading the mutable live index. + +#### Scenario: Plain dependency preparation does not require unused Git capabilities +- **GIVEN** a plain project uses package-index dependencies without VCS context +- **WHEN** Git or its HTTP transport helpers are unavailable on the builder +- **THEN** preparation SHALL stage and fingerprint only available Git capabilities and SHALL permit ordinary dependency installation +- **AND** an actual acquisition requiring an unavailable Git capability SHALL retain the package manager failure and an actionable remedy rather than claim successful preparation. + +#### Scenario: Malformed pytest tables preserve runtime diagnostics +- **GIVEN** a pytest TOML file or pyproject declares its pytest or ini_options section as a scalar or array +- **WHEN** discovery selects that configuration +- **THEN** it SHALL reject the malformed table with the source filename and section path before converting or reading options +- **AND** ordinary review SHALL retain independent static findings and incomplete dependency-sensitive evidence. + +#### Scenario: Index policy discovery fails after snapshot materialization +- **GIVEN** both immutable index snapshots have been created +- **WHEN** changed-path, policy, or manifest discovery raises before ownership is returned +- **THEN** both temporary snapshot roots SHALL be removed and the existing scope failure diagnostic SHALL remain intact. + +#### Scenario: Private build Git metadata contains only identity-bound objects +- **GIVEN** a source repository also contains unrelated branches, remote refs, or unreachable objects +- **WHEN** portable preparation copies Git context +- **THEN** the private repository SHALL contain only the reachable object closure bound by the selected commit, captured index tree, declared tags, and shallow boundary +- **AND** selected ancestry, SCM tags, and detached staged objects SHALL remain available without unrelated refs or object storage. + +#### Scenario: Analyzer-owned dependencies precede project imports during startup +- **GIVEN** a member dependency graph records a package as analyzer-owned and the snapshot or target site-packages contains an import with the same name +- **WHEN** the member configures its runtime, including imports executed by target `.pth` files +- **THEN** the analyzer-owned package and its submodules SHALL resolve only from the verified member analyzer root before ordinary path lookup can select project code +- **AND** invalid or unavailable sealed origins SHALL fail closed rather than fall back to project code +- **AND** graph entries owned by the project and ordinary project-Python execution SHALL retain project import behavior. + +#### Scenario: Discovery validates consumed metadata tables before selection +- **GIVEN** pyproject or Hatch metadata declares a table consumed by discovery as a scalar or array +- **WHEN** discovery parses metadata, including with an explicit manager selection +- **THEN** it SHALL reject the malformed table before selection or constraint processing with a project-runtime diagnostic naming the source file and table path +- **AND** ordinary review SHALL preserve independent static findings and mark dependency-sensitive evidence incomplete. + +#### Scenario: Index manifests retain unchanged referenced policy evidence without widening selection +- **GIVEN** an index snapshot whose Ruff or basedpyright policy transitively references unchanged regular files +- **WHEN** index scope resolution constructs its input evidence +- **THEN** head and base input manifests SHALL retain the resolved policy closure identities in addition to selected changed paths +- **AND** selected_paths SHALL remain limited to changed governed inputs, preserving NOT_APPLICABLE/no_governed_impact for an unchanged index. + +#### Scenario: Bound Git metadata disappears during preparation +- **GIVEN** a runtime plan records nonempty Git provenance +- **WHEN** the bound repository loses its Git metadata after input verification +- **THEN** preparation SHALL fail before dependency acquisition instead of silently omitting that provenance +- **AND** intentionally metadata-free immutable source copies SHALL use their separately bound repository for Git export. + +#### Scenario: Targeted pytest evidence preserves parameter identities +- **GIVEN** selected pytest functions or class methods have parameter identifiers containing `::` or brackets +- **WHEN** targeted review reconciles collection, execution, JUnit, and process evidence +- **THEN** parameter text SHALL remain part of the test name rather than become a module or class qualifier +- **AND** complete passing selections SHALL reconcile as PASS while missing execution and conflicting outcomes remain incomplete evidence. + +#### Scenario: Python-version-only projects attach their declared runtime automatically + +- **GIVEN** a source repository declares its Python interpreter only through a root `.python-version` file, without packaging or dependency metadata +- **WHEN** an ordinary capsule review selects source files without explicit project-runtime options +- **THEN** review MUST discover and prepare the portable project runtime using the declared interpreter before dependency-sensitive analysis +- **AND** an unsupported declared interpreter MUST retain independent static analysis while marking runtime-dependent evidence incomplete with an actionable runtime diagnostic +- **AND** an otherwise equivalent source-only repository without `.python-version` or other project metadata MUST retain the existing standard-library review path + +#### Scenario: Quoted pytest paths retain native argument boundaries +- **GIVEN** repository INI-style pytest options contain quoted test paths, source paths, or file patterns with spaces +- **WHEN** portable runtime discovery and test selection import those options +- **THEN** string values follow pytest argument quoting rules and array values retain their declared boundaries +- **AND** full and source-only review select the same real tests as native pytest +- **AND** malformed quoting identifies the configuration option through an actionable runtime diagnostic + +#### Scenario: Executable project startup preserves the member import boundary +- **GIVEN** target site initialization processes executable `.pth` files, including ordinary editable-install and setuptools startup hooks +- **WHEN** an analyzer member finishes site initialization +- **THEN** its validated member finder SHALL regain first lookup precedence while legitimate additive project finders and rebased workspace paths remain available +- **AND** removal or mutation of protected finders or import machinery, or added paths outside the target and verified runtime roots, SHALL produce an explicit startup diagnostic before member execution +- **AND** project-Python startup SHALL retain native `.pth` behavior; these integrity checks SHALL NOT be described as a sandbox for arbitrary malicious code in the same interpreter. + +#### Scenario: uv manager signals require a valid table +- **GIVEN** pyproject metadata contains a scalar or array `tool.uv` value +- **WHEN** discovery validates manager signals, including with explicit manager selection +- **THEN** it SHALL reject the value before selection with the precise `project_config_invalid:pyproject.toml:tool.uv` table diagnostic + +#### Scenario: Analyzer import ownership follows its installed payload + +- **GIVEN** an analyzer distribution has missing or stale `top_level.txt` metadata, including names for build directories absent from its installed payload +- **WHEN** review constructs a member's sealed dependency graph +- **THEN** sealed import names MUST be derived from that distribution's recorded, present Python module, package, namespace, or native-extension payload, independently of the supervisor Python ABI +- **AND** a directory installed by another distribution MUST NOT establish ownership for a stale declared name +- **AND** a project-owned package sharing a stale name MUST remain importable in the project domain while actual analyzer imports retain strict origin enforcement +- **AND** unavailable distribution file inventory MUST produce an explicit analyzer inventory diagnostic instead of trusting unverified declarations +- **AND** changes to import-ownership logic MUST invalidate cached runtime preparation through the existing builder source identity + +#### Scenario: Portable range selection preserves absent Python paths +- **GIVEN** a portable range or index review selects Python paths added or deleted between its immutable snapshots +- **WHEN** one side has no surviving selected Python file +- **THEN** that side SHALL retain an empty Python analysis selection rather than expand to unrelated source files +- **AND** findings from unrelated unchanged files SHALL NOT become introduced or fixed because only one side analyzed them +- **AND** a nonempty metadata-only selection SHALL retain symmetric whole-source analysis, while an empty overall selection SHALL remain empty. + +#### Scenario: Containment applies to parsed pytest search paths + +- **GIVEN** repository pytest configuration supplies quoted `testpaths` or `pythonpath` values containing spaces +- **WHEN** portable runtime discovery validates repository paths +- **THEN** it MUST apply containment and symlink checks to the same shell-parsed values that pytest and portable test selection consume +- **AND** quoted parent traversal, absolute paths, and escaping symlink paths MUST fail before preparation or selection +- **AND** explicit review `source_roots` MUST NOT suppress validation of pytest's own `pythonpath` +- **AND** valid repository-relative paths containing spaces and native list values MUST remain unchanged in recorded configuration + +#### Scenario: Unborn Git repositories have no version context + +- **GIVEN** a newly initialized Git repository has a valid symbolic HEAD pointing to an absent branch and no first commit +- **WHEN** runtime discovery or inspection gathers optional VCS version context without requesting an explicit revision +- **THEN** it SHALL return empty VCS context and permit ordinary dependency discovery and preparation +- **AND** missing explicit revisions, detached or malformed HEAD state, and corrupt repositories SHALL remain precise diagnostics rather than being silently treated as unborn repositories + +#### Scenario: Runtime attachment preserves installed-package import precedence + +- **GIVEN** a project has a built package installed in its selected runtime and raw source under `src/`, with no declared pytest `pythonpath` or explicit review `source_roots` +- **WHEN** the capsule attaches the runtime and executes the project's tests +- **THEN** attachment SHALL NOT invent `src` or repository-root import overrides +- **AND** native pytest import/configuration behavior SHALL select the installed package, including build-generated package contents, while source-file analysis remains available +- **AND** explicitly configured review `source_roots`, pytest `pythonpath`, and installed editable hooks SHALL retain their declared import behavior +- **AND** native Python CLI current-directory and script-path behavior, and pytest's own test-module path handling, SHALL remain effective for uninstalled source-only repositories + +#### Scenario: Native dependency discovery supports sectionless ELF files + +- **GIVEN** a valid supported x86-64 ELF object retains bounded PT_LOAD and PT_DYNAMIC segments but has no section-header table +- **WHEN** project runtime preparation inventories its shared-library dependencies +- **THEN** it SHALL discover DT_NEEDED entries from the dynamic program segment and its mapped string table +- **AND** invalid segment ranges, invalid string-table mappings, unterminated dynamic entries or dependency names, and unsupported ELF encodings or platforms SHALL remain explicit diagnostics + +#### Scenario: Executable startup hooks cannot substitute analyzer dispatch + +- **GIVEN** a target runtime contains executable `.pth` startup hooks and a verified analyzer entry point +- **WHEN** target startup prepares the analyzer execution domain +- **THEN** it SHALL reject substitutions of the analyzer dispatch functions, their execution/code-loading helpers, or the verified built-in entry-point path before invoking analyzer code +- **AND** replacement of `runpy.run_module`, `runpy.run_path`, their internal execution helpers, or their function code SHALL produce one actionable startup-integrity diagnostic instead of a successful counterfeit analyzer run +- **AND** ordinary additive editable import hooks and path mappings SHALL remain supported +- **AND** this protection SHALL describe a startup-integrity boundary, not a general sandbox against arbitrary code sharing the analyzer interpreter + +- **AND** startup-integrity failures SHALL use a reserved non-analysis exit status, and analyzer result parsing SHALL retain that failure even when a startup hook writes valid analyzer-shaped JSON to stdout + +#### Scenario: Full reviews use native pytest discovery + +- **GIVEN** a full project review without explicit test selectors +- **WHEN** the target worker starts pytest +- **THEN** it SHALL delegate collection roots to pytest without converting configured `testpaths` or the repository root into positional selectors +- **AND** pytest's own configured path expansion, warnings and fallback when configured paths are absent, and positional selections in configured options SHALL remain effective +- **AND** explicit-file, mixed source/test, source-only, index and range review selections SHALL retain their existing explicit selection behavior +- **AND** required collection, execution and coverage evidence SHALL remain enforced; an empty selector list for native discovery SHALL NOT itself count as successful test execution + +#### Scenario: Setup failures retain incomplete test-body execution + +- **GIVEN** selected pytest tests include one setup failure and another test whose call phase executes +- **WHEN** the capsule evaluates the observed test phases +- **THEN** it SHALL retain the genuine setup failure finding and mark the missing test-body execution incomplete +- **AND** an observed failure outside the call phase SHALL NOT by itself prove that the test body executed +- **AND** failures during a completed call or its subsequent teardown SHALL retain genuine failure evidence without incorrectly claiming that the observed call never ran +- **AND** intentional pytest skips SHALL remain visible and preserve existing selection semantics + +#### Scenario: Targeted source reviews expand native pytest test paths + +- **GIVEN** repository pytest configuration selects test paths as directories, individual files, or glob patterns +- **WHEN** a targeted source-only or mixed source/test review resolves corresponding test files +- **THEN** candidate discovery SHALL expand configured glob paths and include matched individual Python files while retaining native configuration precedence and Python filename patterns +- **AND** when no configured path matches, corresponding-test discovery SHALL use the repository fallback rather than silently treating the configured pattern as a literal directory +- **AND** expanded paths SHALL remain inside the reviewed repository and preserve exclusion/recursion controls; escaping configured or matched paths SHALL remain explicit diagnostics +- **AND** explicit test selections, targeted ambiguity checks, full-review native discovery, and actual collection/execution/coverage requirements SHALL remain effective + +#### Scenario: Pip preparation distinguishes configuration from an installable root + +- **GIVEN** a Python repository contains tool configuration in `setup.cfg` but has neither a regular `pyproject.toml` nor `setup.py` file +- **WHEN** the pip adapter prepares the selected runtime +- **THEN** it SHALL install the selected requirements and constraints without appending the repository root as a package requirement +- **AND** when no dependency installation is needed it SHALL emit no pip install command +- **AND** it SHALL retain and import the repository's pytest/tool configuration independently of root-package installation +- **AND** a regular `pyproject.toml` or `setup.py` SHALL retain pip's native root-project installation behavior, including selected extras, without independently reimplementing backend metadata decisions + +#### Scenario: Build diagnostics cannot redirect controller filesystem access + +- **GIVEN** runtime preparation executes untrusted package-manager or build-hook code in its disposable namespace +- **WHEN** the controller captures or retains build diagnostics +- **THEN** it SHALL create a private regular log with an exclusive controller-owned file descriptor outside all builder-writable directory mounts before starting the builder +- **AND** builder and native-manager stdout/stderr SHALL reach the controller through a pipe; only the controller SHALL hold the regular log descriptor, without opening or copying a builder-chosen log path after execution +- **AND** symlink, directory, or FIFO substitutions at the former staging log path or a predictable failure-log path SHALL NOT cause host-file writes, reads, permission changes, or blocking opens +- **AND** failed and timed-out preparation SHALL retain the trusted private diagnostic log, close its descriptor, and remove disposable staging storage +- **AND** successful preparation SHALL close and remove its temporary diagnostic log +- **AND** public errors SHALL identify the private log without publishing raw build output or credentials + +#### Scenario: Builder artifacts are validated before controller enrichment + +- **GIVEN** the isolated builder has exited and left a candidate runtime artifact +- **WHEN** the controller is about to read inventory JSON, enumerate native extensions, copy or change permissions on libraries, or derive member inventories +- **THEN** it SHALL first validate that the artifact root is a nonsymlink directory and every descendant is a regular directory or regular file, without reading payload contents during this validation +- **AND** symlinks at the artifact root, inventory JSON, native directory or nested files, and FIFO or other special nodes SHALL fail preparation before any host-following read or write +- **AND** final sealing SHALL revalidate the complete tree after controller enrichment +- **AND** ordinary valid artifacts SHALL remain accepted + +#### Scenario: Nested project Python preserves caller context + +- **GIVEN** a project process already executing inside a verified isolated target worker +- **WHEN** it invokes the attached Python executable with inherited or explicitly replaced environment and a working directory +- **THEN** the nested process SHALL preserve application environment additions, overrides and removals, the caller-selected working directory, and files in the caller's private temporary filesystem +- **AND** the already-attached member dependency domain SHALL remain stable even when an explicit child environment removes runtime marker variables +- **AND** required runtime startup controls SHALL remain enforced and any unsupported execution control SHALL receive an explicit diagnostic +- **AND** nested execution SHALL retain isolation from supervisor output, control state, processes and external networking +- **AND** ordinary first-entry workers SHALL continue to clear the supervisor environment and establish the original isolation boundary +- **AND** an environment marker alone SHALL NOT grant permission to reuse target context or bypass first-entry isolation + +#### Scenario: Native verification fixtures preserve supported analyzer syntax +- **GIVEN** the pinned signed Semgrep engine rejects a valid Python fixture construct +- **WHEN** an equivalent fixture setup is used for this repository's native verification +- **THEN** every isolation assertion and callable parameter constraint remains intact +- **AND** the exact engine failure and passing replacement are retained as fixture compatibility evidence +- **AND** unsupported syntax in customer code still yields incomplete analysis rather than a suppressed error or a false PASS. + +### Requirement: Pylint preserves attached namespace source roots + +The portable Pylint worker SHALL use already verified snapshot import paths as fallback source roots before native Pylint configuration parsing. It SHALL NOT infer import roots merely from analyzed file locations, add undeclared raw source over an installed package, or suppress missing-import diagnostics. + +#### Scenario: Editable namespace contains a same-name leaf module +- **GIVEN** an attached editable runtime or explicit runtime source root exposes an implicit namespace package containing a same-name leaf module +- **WHEN** Pylint analyzes source and tests together +- **THEN** Pylint SHALL resolve imports using the verified namespace source root instead of treating the leaf module as the namespace +- **AND** genuinely unavailable imports SHALL retain their findings + +#### Scenario: Native Pylint configuration overrides fallback roots +- **GIVEN** verified runtime roots and explicit Pylint source-roots configuration exist +- **WHEN** the worker initializes Pylint +- **THEN** native repository configuration selection and CLI precedence SHALL override the fallback, including an explicitly empty source-roots value +- **AND** a built package without a declared snapshot import root or editable hook SHALL NOT acquire a raw source overlay + +#### Scenario: Corpus rejects known namespace import misresolution +- **GIVEN** the pinned Poetry corpus manifest identifies independently verified import statements +- **WHEN** either cold automatic review or warm descriptor attachment reports Pylint E0401/E0611 at one of those statements +- **THEN** corpus acceptance SHALL fail even when all analyzer members ran +- **AND** unrelated findings SHALL remain permitted and retained in the report + +#### Scenario: Pylint dispatcher changes invalidate runtime reuse +- **GIVEN** a cached runtime with unchanged project inputs and signed worker identity +- **WHEN** only the trusted Pylint dispatcher implementation changes +- **THEN** its content digest SHALL change the builder cache identity and offline preparation SHALL report a cache miss +- **AND** unchanged dispatcher bytes SHALL retain warm cache reuse + +#### Scenario: Runtime payloads cannot retain controller diagnostic hardlinks + +- **GIVEN** a builder leaves a regular artifact file sharing an inode with a controller diagnostic log or another file outside the artifact +- **WHEN** the controller validates build output, seals it, or verifies it for reuse +- **THEN** regular files with multiple hardlinks SHALL be rejected before their payload is read or the runtime is attached +- **AND** the controller SHALL retain its private failure log without publishing diagnostic bytes in the runtime or public error +- **AND** ordinary copied package files with a single link SHALL remain accepted, including when the package manager uses hardlinks inside its disposable environment +- **AND** a hardlink introduced after sealing SHALL invalidate attachment even when its bytes still match the recorded digest + +#### Scenario: Builder children cannot reopen controller log storage + +- **GIVEN** untrusted build hooks execute as descendants of the isolated build driver +- **WHEN** the driver or its children inspect or reopen their stdout/stderr through process descriptors +- **THEN** those descriptors SHALL identify pipes, never the controller's regular diagnostic file +- **AND** the controller SHALL stream diagnostics with bounded buffering, preserve partial timeout/failure output and close all capture descriptors and helpers +- **AND** normal successful and downstream-failure log retention semantics SHALL remain unchanged + +#### Scenario: Preparation failures retain safe controller reasons + +- **GIVEN** a builder exits unsuccessfully or controller artifact validation rejects its output +- **WHEN** preparation reports the failure to a review or runtime command +- **THEN** the public diagnostic SHALL retain the controller's stable failure code and a numeric builder exit status when available, alongside the private log path +- **AND** arbitrary exception text, selected paths and raw subprocess output SHALL NOT be copied into the public error +- **AND** existing private diagnostic capture and retention SHALL remain unchanged; reporting a controller code SHALL NOT require an extra filesystem operation + +#### Scenario: Nested Python preserves caller import paths + +- **GIVEN** a process already executing in an isolated target worker supplies inherited or explicit PYTHONPATH entries for customer modules +- **WHEN** it invokes the attached Python executable +- **THEN** the trusted startup directory SHALL remain first and caller import-path entries SHALL retain their order and normal empty/relative path semantics inside the existing namespace +- **AND** member analyzer imports SHALL remain sealed and host-only paths or supervisor state SHALL remain inaccessible +- **AND** empty or omitted caller PYTHONPATH SHALL not weaken trusted startup or member-domain verification + +### Requirement: Declare compatible repository development analyzer versions +This repository's Hatch development environment SHALL explicitly select its declared Pylint and basedpyright entry versions in its signed review toolchain. External customer dependency declarations and genuine runtime incompatibility checks SHALL remain unchanged. + +#### Scenario: Fresh developer resolution remains compatible +- **GIVEN** the signed analyzer entries and an observed newer incompatible native development resolution +- **WHEN** this repository selects its declared development analyzer versions +- **THEN** production runtime compatibility validation accepts the declared selection for every signed Python ABI +- **AND** the originally incompatible customer package versions remain rejected by the unchanged compatibility contract + +#### Scenario: BasedPyright preserves its actual sealed Node distribution +- **GIVEN** the signed BasedPyright dependency is `nodejs-wheel-binaries` and supplies `nodejs_wheel` +- **WHEN** project runtime compatibility and member import mounts are resolved +- **THEN** a matching or absent project distribution preserves the sealed Node dependency graph, mount and import origin +- **AND** an unequal project version produces an explicit BasedPyright incompatibility instead of a missing sealed import or silent substitution +- **AND** changing the controller compatibility policy invalidates offline cache reuse even when the selected signed worker identity is unchanged +- **AND** the obsolete `nodejs-wheel` distribution name does not impose a restriction on an unrelated customer package. + +### Requirement: Declare bounded target native tools + +Runtime preparation SHALL expose only explicitly declared supported native tools to target workers, preserving the sealed supervisor and the existing offline worker boundary. The initial supported capabilities are `git`, `uname`, and `sed`. Repository declarations use `[tool.specfact.code-review] native_tools`; an explicit project configuration overrides that declaration. + +#### Scenario: Declared native tools retain complete controller-owned identity +- **GIVEN** a repository declares supported native tools +- **WHEN** its runtime is prepared +- **THEN** the controller captures the supported executables, required Git helpers, generated launchers, and their complete ELF library closure before a disposable builder can run +- **AND** all captured bytes contribute to cache identity and recorded inventory +- **AND** the controller installs the capture only after the builder exits, rejecting tool destination collisions before writing any tool payload +- **AND** target workers use the private runtime PATH without adding host directories or copying an arbitrary host environment. + +#### Scenario: Unsupported or unavailable tools fail precisely +- **GIVEN** an unknown tool, a missing supported executable or helper, or an incompatible native binary +- **WHEN** discovery or preparation reaches that requirement +- **THEN** it reports the exact unsupported or unavailable capability without guessing another executable from PATH +- **AND** empty declarations add no tools. + +#### Scenario: Native tool inputs invalidate reuse and remain immutable during building +- **GIVEN** an existing prepared runtime and changed native tool or transitive library bytes +- **WHEN** preparation computes its cache key +- **THEN** offline reuse fails for the changed identity +- **AND** an isolated builder cannot replace captured controller-owned executables or libraries +- **AND** the sealed target shell required by Git is verified as part of the selected capsule identity rather than supplied by a customer artifact. + +### Requirement: Sanitized VCS lookup retains declared target Git + +The controller's VCS metadata operations SHALL preserve all existing no-global/system-configuration, no-hooks, no-replacements, no-lazy-fetch and protocol restrictions. Within an existing verified read-only target-worker context, they SHALL use the fixed attached Git launcher only when the descriptor declares Git and its controller-generated inventory hash matches contained regular launcher bytes. Host execution SHALL retain the fixed system search path. Ambient PATH entries or forged environment markers SHALL NOT select project executables, and invalid declared target capabilities SHALL fail closed with a precise diagnostic. + +#### Scenario: Verified target metadata export uses the declared launcher +- **GIVEN** an authenticated target-worker context with a declared and hash-verified Git launcher outside the system search path +- **WHEN** isolated VCS metadata export executes with a sanitized child environment +- **THEN** it uses that fixed absolute launcher while preserving all existing Git configuration and protocol restrictions +- **AND** missing or tampered capability bytes fail explicitly rather than selecting an ambient executable + +#### Scenario: Ambient environment cannot redirect host VCS export +- **GIVEN** ordinary host execution with hostile PATH entries or forged worker environment markers +- **WHEN** VCS metadata export runs +- **THEN** it retains fixed system lookup and never trusts those entries as attached project tools + +#### Scenario: Graft isolation is verified without ambient Git templates +- **GIVEN** Git initialized a repository without system templates or an existing `.git/info` directory +- **WHEN** the graft-isolation regression injects unbound ancestry metadata +- **THEN** its fixture explicitly creates the metadata parent before writing grafts +- **AND** the original assertions still prove source identity is unchanged, exported ancestry has both commits, and grafts are absent from the export +- **AND** ordinary template-backed initialization exercises the same assertions. + +### Requirement: Target workers retain only signed public default trust data + +Automatic runtime preparation SHALL preserve the signed worker's public certificate trust bundle at the conventional target path `/etc/ssl/certs/ca-certificates.crt`, without copying host trust stores, credentials or arbitrary host configuration. The controller SHALL capture verified contained regular certificate bytes before disposable building, bind their identity to cache reuse and runtime inventory, and install them after validating builder output. Builder-created destination or symlink collisions SHALL fail closed. Target analysis SHALL remain offline; certificate availability grants no network capability. + +#### Scenario: Offline dependency initialization can read public default certificates +- **GIVEN** an authenticated signed worker containing its public certificate bundle +- **WHEN** an isolated project dependency initializes its TLS context during offline analysis +- **THEN** the target's conventional certificate path contains the exact recorded signed public bytes +- **AND** builder modifications cannot replace those bytes or redirect the destination + +### Requirement: The repository declares its runtime registry dependency + +This repository's selected Hatch review environment SHALL declare an immutable published SpecFact CLI version providing the registry APIs that its reviewed code imports. Hermetic runtime preparation SHALL acquire that declared dependency through Hatch, without copying an editable host checkout or suppressing genuine missing imports. + +#### Scenario: Declared core dependency supplies the actual registry API +- **GIVEN** review code importing the SpecFact CLI module-discovery and installer APIs +- **WHEN** the repository's Hatch default dependencies are inspected and prepared +- **THEN** they explicitly admit the verified published core package and its actual registry API can be imported +- **AND** the separate developer editable-checkout bootstrap does not substitute for this declaration diff --git a/openspec/changes/code-review-16-portable-project-runtime/tasks.md b/openspec/changes/code-review-16-portable-project-runtime/tasks.md new file mode 100644 index 00000000..7de3f81d --- /dev/null +++ b/openspec/changes/code-review-16-portable-project-runtime/tasks.md @@ -0,0 +1,95 @@ +# Tasks + +## 1. Readiness and specifications +- [x] 1.1 Refresh hierarchy, branch and PR state; create dedicated worktree and linked User Story #473. +- [x] 1.2 Improve #472, add native parent/dependency metadata, record accepted scope and limits. +- [x] 1.3 Add proposal, design, mapped Given/When/Then specs and validation record. +- [x] 1.4 Validate OpenSpec strictly and capture failing tests before production edits. + +## 2. Runtime implementation +- [x] 2.1 Implement read-only discovery, explicit configuration, adapter contracts and CLI inspect/prepare. +- [x] 2.2 Implement isolated preparation, private content-addressed cache, inventory and v2 attachment validation. +- [x] 2.3 Separate worker import domains, diagnose tool/native conflicts and retain v1 trust semantics. +- [x] 2.4 Attach runtime and provenance to every scope, including independent changed base/head inputs. +- [x] 2.5 Support ordinary pytest/plugin controls and actionable incomplete-evidence reporting. + +## 3. External verification +- [x] 3.1 Add pinned external corpus, reconstructed Hatch failure, corruption and controlled defect regressions. +- [ ] 3.2 Execute non-root Linux 3.11/3.12/3.13 cold and offline-warm candidate matrix with ordinary GITHUB_ACTIONS. +- [ ] 3.3 Record actual test inventory, analyzer completeness, identities, source immutability, timing and bytes. + +## 4. Gates and delivery +- [x] 4.1 Run format, type-check, lint, yaml-lint, check-bundle-imports, contract/smart/full tests and Requirements evidence. +- [x] 4.2 Run fresh SpecFact review with --bug-hunt; fix all actionable findings and strict OpenSpec failures. +- [ ] 4.3 Bump minor version, sign immutable changed assets and update registry through canonical tooling. +- [ ] 4.4 Open PR to dev, fix incoming review findings, and complete protected merge/release integration. +- [ ] 4.5 Repeat public signed installation corpus; validate original customer reproduction before closing #472. +- [ ] 4.6 Close accepted story and archive via openspec archive after merge and acceptance. +- [ ] 4.7 Reconcile paired-core generated command documentation and registration scenarios against the final signed module revision; preserve independent Requirements fixture authority and record exact command/option parity evidence. + +## Native corpus namespace finding follow-up +- [x] Specify verified Pylint fallback source roots and native configuration precedence. +- [x] Record real namespace-shadowing RED, implement fallback defaults, and pass focused regression/dispatch tests. +- [ ] Confirm corrected Poetry import findings through the Linux signed candidate and public release corpus. + +## PR #475 review follow-up +- [x] Specify pip installable-directory markers and targeted pytest file/glob discovery before regression tests. +- [x] Record both real failures, implement the fixes and pass focused plus required combined gates. +- [x] Push verified Git ancestry with CI-produced publisher signatures, then reply and resolve findings 4010452407 and 4010452417. +- [x] Specify controller-owned build-log capture before the P1 security regression. +- [x] Validate safe failure/timeout log retention and success cleanup, then resolve security finding 4010477055 after the validated signed update is pushed. +- [x] Specify artifact topology validation before controller-side inventory and native-library enrichment. +- [x] Reproduce and reject artifact redirection before host reads or writes, with independent safety review and combined gates. + +## Late nested-process findings from superseded PR #474 +- [x] Specify and reproduce lost nested Python environment, working directory and private temporary files without weakening first-entry isolation. +- [x] Preserve verified runtime domain for nested children, including explicit replacement environments; run focused regressions and the reconstructed Hatch corpus. +- [x] Complete fresh combined gates and signed push to PR #475 before replying and resolving findings 4010422180 and 4010422190 on #474. + +## PR #475 diagnostic hardlink finding +- [x] Specify rejection of multiply linked runtime files before reads, sealing and reuse. +- [x] Record failing hardlink regressions, implement rejection and validate ordinary copied package compatibility. +- [ ] Complete independent review, required gates, CI signing, pushed validation and fresh native corpus before resolving 4011119813. + +## PR #475 diagnostic and nested import-path findings +- [x] Specify safe preparation reasons and preserved nested PYTHONPATH before regressions. +- [x] Record actual failing evidence, implement both corrections and pass focused and combined gates. +- [ ] Publish CI-signed fixes, verify current Linux acceptance and then resolve 4011304439 and 4011304448. + +## Release PR #477 pytest evidence findings +- [x] Record regressions for mixed passing/non-passing pytest outcomes and per-source coverage, including native threshold and test-only selection controls. +- [x] Preserve observed non-pass failures and validate reviewed production-source coverage without changing customer selection. +- [ ] Validate, CI-sign and publish an immutable correction, then resolve findings 4019944740 and 4019944751 after current-head acceptance. + +## PR #478 nested pytest outcome paths + +- [x] Specify snapshot-relative outcome finding paths and record failing regressions before implementation. +- [x] Reuse contained pytest-root validation for outcome findings and coverage, preserving native node identifiers. +- [ ] Complete independent review, combined gates, CI signing and current-head validation before resolving finding 4020270168. + +## PR #478 native pytest startup failure + +- [x] Specify and reproduce native usage-error observations with null pytest roots and lost capsule execution evidence. +- [x] Validate incomplete execution before outcome-path resolution, reject malformed roots explicitly and preserve native observations. +- [ ] Complete independent review, combined gates, CI signing and current-head validation before resolving finding 4020397305. + +## PR #478 installed project coverage attribution + +- [x] Record native regular-install coverage failure before implementing a metadata-owned, byte-verified installed-source bridge. +- [x] Preserve installed import precedence and raw coverage while verifying snapshot/RECORD ownership, content, directory boundaries, and attribution before and after execution. +- [x] Validate real native pytest coverage, defect detection, stale and ambiguous source, foreign namespace ownership, unsafe RECORD paths and tampering locally. +- [ ] Repeat the external capsule corpus in native CI and through the public signed release. + +## PR #478 native customer test tools + +- [x] Retain native failures, specify explicit supported tools, and record discovery/capture/helper/collision RED before fixes. +- [x] Capture controller-owned complete native closures and immutable identities; preserve project library isolation and existing worker interfaces. +- [x] Validate real offline Linux Git clean filters and local transport, with complete payload inventory and measured size. +- [x] Recognize verified implicit Hatch default activation while retaining explicit precedence and immutable snapshot inputs. +- [ ] Complete fresh exact-snapshot unchanged Linux hooks, candidate signed corpus and public signed acceptance. + +## Native coverage fidelity follow-up +- [x] Specify, reproduce and correct native coverage activation/source/report semantics while preserving reviewed-file thresholds. +- [x] Exercise real xdist/subprocess, explicit option fidelity, typed failure evidence and verified child bootstrap. +- [ ] Refresh broad repository gates and fresh bug-hunt for the coverage-fidelity candidate. +- [ ] Complete normal signed developer commits, canonical CI signing and exact candidate Linux/corpus acceptance. diff --git a/packages/specfact-code-review/module-package.yaml b/packages/specfact-code-review/module-package.yaml index 94080b1c..8fc15e78 100644 --- a/packages/specfact-code-review/module-package.yaml +++ b/packages/specfact-code-review/module-package.yaml @@ -1,5 +1,5 @@ name: nold-ai/specfact-code-review -version: 0.49.85 +version: 0.50.1 commands: - code tier: official @@ -30,5 +30,5 @@ description: Official SpecFact code review bundle package. category: codebase bundle_group_command: code integrity: - checksum: sha256:2ef123f8ed5672aaf21cde55c87c6e0040e8a38dd0b054d6ccff3f866db1d2ba - signature: xOHgngH9//BIu/nS8qzGgi28uca9NLGifaLcnRgCYYZE/sO7wkqWXLM/L4CvgZ2E1Q2ZsRzxd0MaP36buEWIDQ== + checksum: sha256:25bb1137bc0bb3254713e6001a767d6ac73d01f12868d8f29148b8280084b3ee + signature: +ZtHk940rQ8FS6Tbh67+rY2V9BAV2SR8S2qtJNt3EUT390N7ksDZFvxDAfwnnzK/vF8KF3ZTdorRSO7QhxAjAQ== diff --git a/packages/specfact-code-review/src/specfact_code_review/_review_utils.py b/packages/specfact-code-review/src/specfact_code_review/_review_utils.py index e0dae8bb..d0314d4b 100644 --- a/packages/specfact-code-review/src/specfact_code_review/_review_utils.py +++ b/packages/specfact-code-review/src/specfact_code_review/_review_utils.py @@ -3,6 +3,8 @@ from __future__ import annotations import os +from contextlib import contextmanager +from contextvars import ContextVar from pathlib import Path from typing import Literal @@ -68,6 +70,20 @@ def tool_error( ) +_DEVELOPMENT_RUNTIME: ContextVar[Path | None] = ContextVar("development_runtime", default=None) + + +@contextmanager +@require(lambda root: root.is_absolute()) +def development_runtime(root: Path): + """Bind the known development interpreter only while reviewing a staged snapshot.""" + token = _DEVELOPMENT_RUNTIME.set(root) + try: + yield + finally: + _DEVELOPMENT_RUNTIME.reset(token) + + _CAPSULE_TOOL_MODULES = { "radon": "radon", "pylint": "pylint", @@ -83,17 +99,29 @@ def tool_error( def analyzer_command(command: list[str]) -> list[str]: """Preserve isolated Python child imports and private Ruff cache in a capsule.""" if Path(__file__).parent != Path("/opt/specfact/builtin/specfact_code_review"): + development = _DEVELOPMENT_RUNTIME.get() + if development is not None and command[0] == "basedpyright": + return [*command, "--pythonpath", str(development / "bin/python")] return command if command[0] == "ruff": return [*command, "--cache-dir", "/opt/specfact/tmp/cache/ruff"] module = _CAPSULE_TOOL_MODULES.get(command[0]) if module is None: return command + target_worker = ( + command[0] in {"basedpyright", "pylint", "crosshair"} + and Path("/opt/specfact/project-runtime/project-runtime.json").is_file() + ) + if target_worker: + from specfact_code_review.run.target_launch import target_command + + return target_command(module, command[1:]) + bootstrap = "/opt/specfact/bootstrap/sealed_bootstrap.py" return [ "/opt/specfact/python/bin/python", "-I", "-S", - "/opt/specfact/bootstrap/sealed_bootstrap.py", + bootstrap, module, *command[1:], ] diff --git a/packages/specfact-code-review/src/specfact_code_review/review/commands.py b/packages/specfact-code-review/src/specfact_code_review/review/commands.py index c9c71e88..8a36e743 100644 --- a/packages/specfact-code-review/src/specfact_code_review/review/commands.py +++ b/packages/specfact-code-review/src/specfact_code_review/review/commands.py @@ -26,6 +26,7 @@ RunCommandError, run_command, ) +from specfact_code_review.run.runtime_commands import app as runtime_app app = typer.Typer(help="Code command extensions for structured review workflows.", no_args_is_help=True) @@ -108,6 +109,9 @@ class _ReviewRunCommandInputs: requirements_evidence: Path | None interactive: bool + project_config: Path | None = None + project_runtime: Path | None = None + def _friendly_run_command_error(exc: RunCommandError | ValueError | ViolationError) -> str: if isinstance( @@ -256,6 +260,8 @@ def _execute_review_run(inputs: _ReviewRunCommandInputs) -> None: preview_fixes=inputs.preview_fixes, with_mutation=inputs.with_mutation, requirements_evidence=inputs.requirements_evidence, + project_config=inputs.project_config, + project_runtime=inputs.project_runtime, ) except (ValueError, ViolationError) as exc: raise typer.BadParameter(_friendly_run_command_error(exc)) from exc @@ -287,6 +293,12 @@ def run( "--pr-context-file", help="Absolute, event-derived GitHub Actions PR/merge-queue context JSON.", ), + project_config: Path | None = typer.Option( + None, "--project-config", help="Portable project environment selection TOML." + ), + project_runtime: Path | None = typer.Option( + None, "--project-runtime", help="Prepared project-runtime.json descriptor." + ), include_tests: bool | None = typer.Option(None, "--include-tests"), exclude_tests: bool | None = typer.Option(None, "--exclude-tests"), focus: list[str] | None = typer.Option( @@ -366,11 +378,14 @@ def run( preview_fixes=preview_fixes, with_mutation=with_mutation, requirements_evidence=requirements_evidence, + project_config=project_config, + project_runtime=project_runtime, interactive=interactive, ) ) +review_app.add_typer(runtime_app, name="runtime") review_app.add_typer(ledger_app, name="ledger") review_app.add_typer(rules_app, name="rules") app.add_typer(review_app, name="review") diff --git a/packages/specfact-code-review/src/specfact_code_review/run/commands.py b/packages/specfact-code-review/src/specfact_code_review/run/commands.py index 20b4d0e7..52a2ac20 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/commands.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/commands.py @@ -95,6 +95,8 @@ class ReviewRunRequest: focus_facets: tuple[str, ...] = () review_focus: ReviewFocus | None = None requirements_evidence: Path | None = None + project_config: Path | None = None + project_runtime: Path | None = None @dataclass(frozen=True) @@ -110,6 +112,8 @@ class _ReviewLoopFlags: review_level: ReviewLevelFilter | None review_focus: ReviewFocus | None assurance_kind: LocalAssuranceKind = "explicit_files" + project_config: Path | None = None + project_runtime: Path | None = None def _changed_files_from_git_diff(*, include_tests: bool) -> list[Path]: @@ -518,6 +522,8 @@ def _emit_progress(description: str) -> None: review_level=flags.review_level, review_focus=flags.review_focus, assurance_kind=flags.assurance_kind, + project_config=flags.project_config, + project_runtime=flags.project_runtime, ), ) @@ -539,6 +545,8 @@ def _run_review_with_status( review_level=flags.review_level, review_focus=flags.review_focus, assurance_kind=flags.assurance_kind, + project_config=flags.project_config, + project_runtime=flags.project_runtime, ) report = _run_review_once(files, base) applied_simplification_findings: list[ReviewFinding] = [] @@ -571,6 +579,8 @@ def _run_review_once(files: list[Path], flags: _ReviewLoopFlags) -> ReviewReport review_level=flags.review_level, focus=flags.review_focus, assurance_kind=flags.assurance_kind, + project_config=flags.project_config, + project_runtime=flags.project_runtime, ) applied_simplification_findings: list[ReviewFinding] = [] if flags.fix: @@ -599,6 +609,8 @@ def _run_review_once(files: list[Path], flags: _ReviewLoopFlags) -> ReviewReport review_level=flags.review_level, focus=flags.review_focus, assurance_kind=flags.assurance_kind, + project_config=flags.project_config, + project_runtime=flags.project_runtime, ) report = _with_applied_simplification_findings(report, applied_simplification_findings) if flags.preview_fixes: @@ -744,6 +756,8 @@ def _get_optional_param(name: str, validator: Callable[[object], object], defaul focus_facets=focus_facets, review_focus=_review_focus_from_facets(focus_facets), requirements_evidence=requirements_evidence, + project_config=cast(Path | None, _get_optional_param("project_config", _as_optional_path)), + project_runtime=cast(Path | None, _get_optional_param("project_runtime", _as_optional_path)), ) # Reject any unexpected keyword arguments @@ -853,6 +867,7 @@ def _immutable_scope_report(request: ReviewRunRequest) -> ReviewReport: with_mutation=request.with_mutation, pr_context_file=request.pr_context_file, repository_slug=_repository_slug(Path.cwd()), + portable_project_runtime=request.pr_context_file is None, ) ) try: @@ -863,6 +878,8 @@ def _immutable_scope_report(request: ReviewRunRequest) -> ReviewReport: resolution, options=ReviewOptions( no_tests=request.no_tests, + project_config=request.project_config, + project_runtime=request.project_runtime, include_noise=request.include_noise, bug_hunt=request.bug_hunt, review_level=request.review_level, @@ -965,6 +982,8 @@ def _normalize_review_request(request: ReviewRunRequest) -> ReviewRunRequest: focus_facets=request.focus_facets, review_focus=_review_focus_from_facets(request.focus_facets), requirements_evidence=request.requirements_evidence, + project_config=request.project_config, + project_runtime=request.project_runtime, ) @@ -1142,6 +1161,35 @@ def _canonical_json_digest(value: dict[str, Any]) -> str: return f"sha256:{hashlib.sha256(canonical_json.encode('utf-8')).hexdigest()}" +def _resolve_worktree_files(request: ReviewRunRequest) -> list[Path]: + file_focus_facets = tuple(facet for facet in request.focus_facets if facet in {"source", "tests", "docs"}) + include_for_resolve = request.include_tests or bool(file_focus_facets) + legacy_scope: Literal["changed", "full"] | None = None + if request.scope in {"changed", "worktree"}: + legacy_scope = "changed" + elif request.scope == "full": + legacy_scope = "full" + resolved_files = resolve_legacy_files( + request.files, + LegacyFileSelectionRequest( + include_tests=include_for_resolve, + scope=legacy_scope, + path_filters=request.path_filters or [], + changed_discovery=_changed_files_from_git_diff, + full_discovery=_all_python_files_from_git, + ), + ) + resolved_files = _filter_files_by_focus(resolved_files, request.focus_facets) + if not resolved_files: + raise NoReviewableFilesError( + "No reviewable Python files matched the selected --focus facets." + if request.focus_facets + else "No Python files to review were provided or detected." + ) + + return resolved_files + + @beartype @require( lambda request_or_files: request_or_files is None or isinstance(request_or_files, (list, ReviewRunRequest)), @@ -1173,30 +1221,7 @@ def run_command( ) return _render_review_result(report, request) - file_focus_facets = tuple(facet for facet in request.focus_facets if facet in {"source", "tests", "docs"}) - include_for_resolve = request.include_tests or bool(file_focus_facets) - legacy_scope: Literal["changed", "full"] | None = None - if request.scope in {"changed", "worktree"}: - legacy_scope = "changed" - elif request.scope == "full": - legacy_scope = "full" - resolved_files = resolve_legacy_files( - request.files, - LegacyFileSelectionRequest( - include_tests=include_for_resolve, - scope=legacy_scope, - path_filters=request.path_filters or [], - changed_discovery=_changed_files_from_git_diff, - full_discovery=_all_python_files_from_git, - ), - ) - resolved_files = _filter_files_by_focus(resolved_files, request.focus_facets) - if not resolved_files: - raise NoReviewableFilesError( - "No reviewable Python files matched the selected --focus facets." - if request.focus_facets - else "No Python files to review were provided or detected." - ) + resolved_files = _resolve_worktree_files(request) requirements_evidence = ( _requirements_evidence_context(request.requirements_evidence) @@ -1207,6 +1232,8 @@ def run_command( resolved_files, _ReviewLoopFlags( no_tests=request.no_tests, + project_config=request.project_config, + project_runtime=request.project_runtime, include_noise=request.include_noise, fix=request.fix, preview_fixes=request.preview_fixes, diff --git a/packages/specfact-code-review/src/specfact_code_review/run/installed_coverage.py b/packages/specfact-code-review/src/specfact_code_review/run/installed_coverage.py new file mode 100644 index 00000000..406061ad --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/installed_coverage.py @@ -0,0 +1,401 @@ +"""Attribute measured installed code only to byte-identical, metadata-owned source.""" + +from __future__ import annotations + +import base64 +import csv +import hashlib +import json +import keyword +import os +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import Any +from urllib.parse import unquote, urlsplit + +from icontract import ensure + + +@dataclass(frozen=True) +class InstalledSource: + """A controller-verified correspondence, never a worker-supplied alias.""" + + source: Path + installed: Path + digest: str + distribution: str + + +@dataclass(frozen=True) +class CoverageBridge: + """Allowed measurement roots and independently verifiable source correspondences.""" + + directories: tuple[Path, ...] + mappings: tuple[InstalledSource, ...] + diagnostics: dict[str, str] + candidates: dict[str, tuple[str, ...]] + measured_origins: tuple[str, ...] = () + modules: tuple[str, ...] = () + + +@dataclass(frozen=True) +class DistributionRecord: + """Read-only distribution ownership derived from sealed installation metadata.""" + + name: str + local: bool + files: dict[str, str] + error: str = "" + + +def _regular(path: Path, root: Path) -> bool: + return ( + path.is_file() + and not path.is_symlink() + and path.resolve().is_relative_to(root.resolve()) + and path.stat().st_nlink == 1 + ) + + +def _digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _local_origin(metadata: Path) -> bool: + document = json.loads((metadata / "direct_url.json").read_text(encoding="utf-8")) + url = urlsplit(document.get("url", "")) + return ( + url.scheme == "file" + and not url.netloc + and unquote(url.path).rstrip("/") == "/opt/specfact/output/project" + and not document.get("dir_info", {}).get("editable", False) + ) + + +def _record_row(row: list[str], files: dict[str, str]) -> None: + """Retain only canonical Python ownership entries from one RECORD row.""" + if len(row) != 3: + raise ValueError("malformed RECORD row") + relative = PurePosixPath(row[0]) + if relative.as_posix() != row[0]: + raise ValueError("noncanonical RECORD path") + if relative.suffix != ".py": + return # Wheels also legitimately own ../bin entry points, not coverage source. + if relative.is_absolute() or ".." in relative.parts or "\\" in row[0]: + raise ValueError("unsafe Python RECORD path") + if row[0] in files: + raise ValueError("duplicate Python RECORD path") + files[row[0]] = row[1] + + +def _record(metadata: Path) -> DistributionRecord: + files: dict[str, str] = {} + error = "" + try: + local = _local_origin(metadata) + except (OSError, ValueError, TypeError, AttributeError): + local = False + try: + with (metadata / "RECORD").open(newline="", encoding="utf-8") as stream: + for row in csv.reader(stream): + _record_row(row, files) + except (OSError, ValueError, csv.Error) as exc: + error = str(exc) + return DistributionRecord(metadata.name, local, files, error) + + +def _verified_record(path: Path, encoded: str, site: Path) -> bool: + if not _regular(path, site): + return False + algorithm, separator, value = encoded.partition("=") + if not separator or algorithm not in {"sha256", "sha384", "sha512"}: + return False + actual = hashlib.new(algorithm, path.read_bytes()).digest() + return base64.urlsafe_b64encode(actual).decode().rstrip("=") == value + + +def _snapshot_index(snapshot: Path) -> dict[str, list[Path]]: + """Walk the snapshot once, retaining duplicates for fail-closed correspondence.""" + indexed: dict[str, list[Path]] = {} + for directory, directories, files in os.walk(snapshot, followlinks=False): + directories[:] = [ + name + for name in directories + if name not in {".git", ".venv", "venv", "__pycache__"} + and not (Path(directory) / name).is_symlink() + and not (Path(directory) / name / "pyvenv.cfg").is_file() + ] + for name in files: + if name.endswith(".py"): + indexed.setdefault(name, []).append(Path(directory) / name) + return indexed + + +def _source_candidates(snapshot: Path, relative: str, index: dict[str, list[Path]]) -> list[Path]: + """Require an entire recorded package path, never a basename-only match.""" + if len(PurePosixPath(relative).parts) < 2: + return index.get(relative, []) + return [ + candidate + for candidate in index.get(PurePosixPath(relative).name, []) + if candidate.relative_to(snapshot).as_posix().endswith("/" + relative) or candidate == snapshot / relative + ] + + +def _owned_directory( + installed: Path, site: Path, record: DistributionRecord, owners: dict[str, list[str]] +) -> Path | None: + """Choose the smallest native coverage directory without foreign Python owners.""" + directory = installed.parent + if directory == site: + return None + for candidate in directory.rglob("*.py"): + relative = candidate.relative_to(site).as_posix() + if owners.get(relative) != [record.name] or not _verified_record(candidate, record.files[relative], site): + return None + return directory + + +@dataclass +class _CoverageContext: + """Indexes shared by source planning within one immutable runtime attachment.""" + + snapshot: Path + site: Path + owners: dict[str, list[str]] + sources: dict[str, list[Path]] + directories: dict[tuple[Path, str], Path | None] + + +def _source_identity_error(source: Path, relative: str, record: DistributionRecord, context: _CoverageContext) -> str: + installed = context.site / relative + if context.owners.get(relative) != [record.name]: + return "ambiguous_distribution_ownership" + if _source_candidates(context.snapshot, relative, context.sources) != [source]: + return "ambiguous_snapshot_source" + if not _regular(source, context.snapshot) or not _verified_record(installed, record.files[relative], context.site): + return "invalid_record_content" + if _digest(source) != _digest(installed): + return "installed_source_differs" + return "" + + +def _source_mapping( + source: Path, record: DistributionRecord, context: _CoverageContext +) -> tuple[InstalledSource | None, Path | None, str]: + matches = [relative for relative in record.files if source.as_posix().endswith("/" + relative)] + if not matches: + return None, None, "" + if record.error or len(matches) != 1: + return None, None, "invalid_distribution_record" + relative = matches[0] + if error := _source_identity_error(source, relative, record, context): + return None, None, error + installed = context.site / relative + directory, error = _measurement_directory(installed, record, context) + if error: + return None, None, error + return InstalledSource(source, installed, _digest(source), record.name), directory, "" + + +def _measurement_directory( + installed: Path, record: DistributionRecord, context: _CoverageContext +) -> tuple[Path | None, str]: + """Keep valid module selectors separate from package-directory measurement.""" + if installed.parent == context.site: + if not installed.stem.isidentifier() or keyword.iskeyword(installed.stem): + return None, "invalid_top_level_module_name" + return None, "" + cache_key = (installed.parent, record.name) + if cache_key not in context.directories: + context.directories[cache_key] = _owned_directory(installed, context.site, record, context.owners) + directory = context.directories[cache_key] + return directory, "coverage_directory_ownership_unavailable" if directory is None else "" + + +def _distribution_context(snapshot: Path, site: Path) -> tuple[list[DistributionRecord], _CoverageContext]: + records = [_record(path) for path in sorted(site.glob("*.dist-info")) if path.is_dir() and not path.is_symlink()] + owners: dict[str, list[str]] = {} + for record in records: + for relative in record.files: + owners.setdefault(relative, []).append(record.name) + local = [record for record in records if record.local] + index = _snapshot_index(snapshot) if local else {} + return local, _CoverageContext(snapshot, site, owners, index, {}) + + +def _candidate_paths(source: Path, records: list[DistributionRecord], site: Path) -> tuple[str, ...]: + return tuple( + sorted( + { + str(site / relative) + for record in records + for relative in record.files + if source.as_posix().endswith("/" + relative) + } + ) + ) + + +def _measured_origins( + records: list[DistributionRecord], site: Path, directories: set[Path], modules: tuple[str, ...] +) -> tuple[str, ...]: + """Retain every measured owned origin, including generated or renamed packages.""" + return tuple( + sorted( + { + str(site / relative) + for record in records + for relative in record.files + if any(parent in directories for parent in (site / relative).parents) + } + | {str(site / f"{name}.py") for name in modules} + ) + ) + + +def _module_selectors(mappings: list[InstalledSource], site: Path) -> tuple[str, ...]: + return tuple(sorted({mapping.installed.stem for mapping in mappings if mapping.installed.parent == site})) + + +@ensure(lambda result, files: {row.source for row in result.mappings} <= {file.resolve() for file in files}) +def plan_installed_coverage(files: list[Path], *, snapshot: Path, site_packages: Path) -> CoverageBridge: + """Read sealed metadata without importing packages or changing native import paths.""" + snapshot, site = snapshot.resolve(), site_packages.resolve() + records, context = _distribution_context(snapshot, site) + sources = {file.resolve() for file in files if file.suffix == ".py"} + mappings, directories, diagnostics = [], set(), {} + for source in sorted(sources): + if not source.is_relative_to(snapshot): + continue + for record in records: + mapping, directory, error = _source_mapping(source, record, context) + if error: + diagnostics[str(source)] = error + if mapping is not None: + mappings.append(mapping) + if directory is not None: + directories.add(directory) + candidates = {str(source): _candidate_paths(source, records, site) for source in sources} + modules = _module_selectors(mappings, site) + return CoverageBridge( + tuple(sorted(directories)), + tuple(mappings), + diagnostics, + candidates, + _measured_origins(records, site, directories, modules), + modules, + ) + + +def _coverage_index(rows: dict[str, Any], snapshot: Path) -> dict[Path, list[tuple[str, Any]]]: + """Resolve each measured name once while retaining ambiguous aliases.""" + indexed: dict[Path, list[tuple[str, Any]]] = {} + for name, row in rows.items(): + indexed.setdefault((snapshot / name).resolve(), []).append((name, row)) + return indexed + + +def _executed(rows: list[tuple[str, Any]]) -> bool: + """Coverage may enumerate unexecuted source files; rows alone prove no execution.""" + return any(isinstance(row.get("executed_lines"), list) and row["executed_lines"] for _, row in rows) + + +def _coverage_rows(payload: dict[str, Any]) -> dict[str, Any]: + rows = payload.get("files", {}) + if not isinstance(rows, dict) or not all( + isinstance(name, str) and isinstance(row, dict) for name, row in rows.items() + ): + raise ValueError("invalid_installed_coverage_rows") + return rows + + +def _coverage_alias( + mapping: InstalledSource, indexed: dict[Path, list[tuple[str, Any]]], snapshot: Path +) -> tuple[Any, str]: + if ( + not _regular(mapping.source, snapshot) + or not _regular(mapping.installed, mapping.installed.parent) + or _digest(mapping.source) != mapping.digest + or _digest(mapping.installed) != mapping.digest + ): + return None, "source_identity_changed_after_execution" + if _executed(indexed.get(mapping.source, [])): + return None, "" + measured = indexed.get(mapping.installed, []) + if len(measured) != 1: + return None, "installed_coverage_missing_or_ambiguous" + return measured[0][1], "" + + +def _origin_diagnostics( + diagnostics: dict[str, str], + bridge: CoverageBridge, + indexed: dict[Path, list[tuple[str, Any]]], + relocations: dict[str, str] | None, + snapshot: Path, +) -> dict[str, str]: + retained = {} + for source, error in diagnostics.items(): + if not _executed(indexed.get(Path(source), [])): + retained[source] = error + continue + candidates = bridge.candidates.get(source, ()) + if relocations is None or any(path not in relocations for path in candidates): + retained[source] = error + ":coverage_origin_unavailable" + elif any((snapshot / relocations[path]).resolve() == Path(source) for path in candidates): + retained[source] = error + ":coverage_alias_origin_ambiguous" + return retained + + +def _unverified_origins(bridge: CoverageBridge, relocations: dict[str, str], snapshot: Path) -> set[tuple[Path, Path]]: + """Separate native relocation claims from independently verified source identities.""" + verified = {(mapping.installed, mapping.source) for mapping in bridge.mappings} + observed = {(Path(origin), (snapshot / relocations[origin]).resolve()) for origin in bridge.measured_origins} + return observed - verified + + +def _unverified_aliases( + bridge: CoverageBridge, + indexed: dict[Path, list[tuple[str, Any]]], + relocations: dict[str, str] | None, + snapshot: Path, +) -> dict[str, str]: + """Native renamed paths cannot bypass controller source correspondence checks.""" + if not bridge.measured_origins: + return {} + relocations = relocations or {} + executed = {Path(source) for source in bridge.candidates if _executed(indexed.get(Path(source), []))} + if any(origin not in relocations for origin in bridge.measured_origins): + return {str(source): "coverage_origin_unavailable" for source in executed} + return { + str(destination): "coverage_alias_origin_unverified" + for _origin, destination in _unverified_origins(bridge, relocations, snapshot) + if destination in executed + } + + +@ensure( + lambda result, bridge, payload: ( + set(result[0]["files"]) <= set(payload.get("files", {})) | {str(row.source) for row in bridge.mappings} + ) +) +def attribute_installed_coverage( + bridge: CoverageBridge, payload: dict[str, Any], *, snapshot: Path, relocations: dict[str, str] | None = None +) -> tuple[dict[str, Any], dict[str, str]]: + """Create an evaluation copy; retain the raw measured paths and percentages.""" + rows = _coverage_rows(payload) + indexed = _coverage_index(rows, snapshot) + attributed = dict(rows) + diagnostics = _origin_diagnostics(dict(bridge.diagnostics), bridge, indexed, relocations, snapshot) + for source, error in _unverified_aliases(bridge, indexed, relocations, snapshot).items(): + diagnostics.setdefault(source, error) + for mapping in bridge.mappings: + row, error = _coverage_alias(mapping, indexed, snapshot) + if error: + diagnostics[str(mapping.source)] = error + if row is not None: + for name, _original in indexed.get(mapping.source, []): + attributed.pop(name, None) + attributed[str(mapping.source)] = row + return {**payload, "files": attributed}, diagnostics diff --git a/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py b/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py new file mode 100644 index 00000000..3a450635 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/portable_snapshot.py @@ -0,0 +1,261 @@ +"""Attach a validated local runtime to review without changing v1 PR authority.""" + +from __future__ import annotations + +import json +import os +import tempfile +from dataclasses import dataclass, replace +from pathlib import Path +from typing import Any + +from icontract import require + +from specfact_code_review.run.portable_worker import DEPENDENT_MEMBERS, preparation_failure_snapshot, select_test_paths +from specfact_code_review.run.runtime_artifacts import load_runtime +from specfact_code_review.run.runtime_builder import copy_project, prepare_runtime +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_interpreter import project_worker +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError, document_digest +from specfact_code_review.run.runtime_sources import verify_inputs +from specfact_code_review.run.runtime_vcs import vcs_context + + +@require(lambda root: root.is_dir()) +def project_runtime_requested(root: Path, options: Any) -> bool: + """Recognize declared project environments while preserving stdlib-only fixtures.""" + return bool( + options.project_config + or options.project_runtime + or any( + (root / name).exists() + for name in ( + "pyproject.toml", + "hatch.toml", + "requirements.txt", + "requirements.in", + ".python-version", + "pylock.toml", + "setup.py", + "setup.cfg", + "uv.lock", + "poetry.lock", + ) + ) + ) + + +@require(lambda root: root.is_dir()) +def discover_snapshot(root: Path, *, config_path: Path | None, source_snapshot: Any = None) -> ProjectPlan: + """Attach selected VCS metadata without changing the immutable source tree.""" + repository = getattr(source_snapshot, "repository", None) + plan = discover_project(root, config_path=config_path, activation_root=repository) + if repository is None: + return plan + commit = getattr(source_snapshot, "vcs_commit", "") or source_snapshot.commit + if commit.startswith(("index:", "index-")): + raise ProjectRuntimeError("project_index_vcs_commit_missing: recapture the review index") + return replace( + plan, + vcs_repository=repository, + vcs=vcs_context(repository, commit, tree=getattr(source_snapshot, "tree", None)), + ) + + +@dataclass(frozen=True) +class ProjectSnapshotRequest: + """Source and selection context for one independently bound review side.""" + + snapshot_root: Path + files: list[Path] + options: Any + assurance_kind: str + source_snapshot: Any = None + + +@require(lambda request: request.snapshot_root.is_dir()) +def run_project_snapshot(runtime: Any, request: ProjectSnapshotRequest) -> tuple[Any, dict[str, Any]]: + """Bind every review side to its selected signed Python worker.""" + snapshot_root, files, options = request.snapshot_root, request.files, request.options + source_snapshot = request.source_snapshot + try: + plan = discover_snapshot(snapshot_root, config_path=options.project_config, source_snapshot=source_snapshot) + with project_worker(runtime, plan) as selected: + return _run_project_snapshot(selected, request, plan) + except (OSError, ValueError) as exc: + return _failed_project_snapshot( + runtime, snapshot_root=snapshot_root, files=files, options=options, reason=str(exc) + ) + + +def _failed_project_snapshot( + runtime: Any, *, snapshot_root: Path, files: list[Path], options: Any, reason: str +) -> tuple[Any, dict[str, Any]]: + from specfact_code_review.run.runner import CapsuleSnapshotSettings + + unavailable = preparation_failure_snapshot(reason) + request = ProjectSnapshotRequest(snapshot_root, files, options, "explicit_files") + try: + snapshot = _run_in_private_source( + runtime, request, CapsuleSnapshotSettings(portable_runtime=True, unavailable_members=unavailable) + ) + except (OSError, ValueError) as exc: + reason = f"project_source_copy_failed:{exc}; preparation={reason}" + snapshot = _blocked_source_snapshot(runtime, options, reason) + return snapshot, { + "status": "UNKNOWN", + "diagnostic": reason, + "remedy": ( + "Inspect project configuration with specfact code review runtime inspect --json, then run runtime prepare." + ), + } + + +def _blocked_source_snapshot(runtime: Any, options: Any, reason: str) -> Any: + """Record incomplete evidence without exposing an unsafe original source mount.""" + from specfact_code_review.run.runner import ( + CapsuleSnapshotSettings, + _run_capsule_snapshot, + default_pr_range_profile, + ) + + blocked: dict[str, dict[str, object]] = { + member: {"execution_state": "error", "evidence_outcome": "UNKNOWN", "diagnostic": reason} + for member in default_pr_range_profile().all_ids + } + with tempfile.TemporaryDirectory(prefix="specfact-unavailable-source-") as directory: + return _run_capsule_snapshot( + runtime, + snapshot_root=Path(directory), + files=[], + options=options, + settings=CapsuleSnapshotSettings(portable_runtime=True, unavailable_members=blocked), + ) + + +def _run_in_private_source(runtime: Any, request: ProjectSnapshotRequest, settings: Any) -> Any: + """Keep excluded host files out of every portable analyzer's source mount.""" + from specfact_code_review.run.runner import _run_capsule_snapshot + + with tempfile.TemporaryDirectory(prefix="specfact-project-source-") as directory: + source = Path(directory) / "source" + copy_project(request.snapshot_root, source, include_vcs=False) + files = [ + source / Path(os.path.abspath(request.snapshot_root / path)).relative_to(request.snapshot_root) + for path in request.files + ] + return _run_capsule_snapshot( + runtime, snapshot_root=source, files=files, options=request.options, settings=settings + ) + + +@require(lambda request: request.snapshot_root.is_dir()) +def _run_project_snapshot( + runtime: Any, request: ProjectSnapshotRequest, plan: ProjectPlan +) -> tuple[Any, dict[str, Any]]: + """Prepare once, execute applicable members, and retain independent evidence.""" + from specfact_code_review.run.runner import CapsuleSnapshotSettings + + snapshot_root, files, options = request.snapshot_root, request.files, request.options + assurance_kind = request.assurance_kind + try: + prepared = ( + load_runtime( + options.project_runtime, + plan=plan, + environment_id=runtime.environment_id, + worker_identity=runtime.identity, + ) + if options.project_runtime + else prepare_runtime(plan, runtime=runtime) + ) + except (OSError, ValueError) as exc: + return _failed_project_snapshot( + runtime, snapshot_root=snapshot_root, files=files, options=options, reason=str(exc) + ) + evidence = { + "status": "PASS", + "schema": "project-runtime-layer-v2", + "identity": prepared.identity, + "project_identity": plan.identity, + "environment_id": runtime.environment_id, + "authority": "local_build", + "protected_pr_eligible": False, + "source_roots": list(plan.source_roots), + } + arguments: dict[str, tuple[str, ...]] = { + "basedpyright": ("--pythonpath", "/opt/specfact/project-runtime/bin/python"), + } + conflicts = prepared.descriptor.get("inventory", {}).get("analyzer_conflicts", {}) + unavailable = { + member: {"execution_state": "error", "evidence_outcome": "UNKNOWN", "diagnostic": reason} + for member, reason in conflicts.items() + } + if not options.no_tests: + try: + selectors = select_test_paths(plan, files, full=assurance_kind == "full") + arguments["targeted-pytest-coverage"] = ("portable-pytest-v2", json.dumps({"selectors": selectors})) + except ProjectRuntimeError as exc: + unavailable["targeted-pytest-coverage"] = { + "execution_state": "error", + "evidence_outcome": "UNKNOWN", + "diagnostic": str(exc), + } + bound = replace( + runtime, identity=document_digest({"capsule": runtime.identity, "project_runtime": prepared.identity}) + ) + snapshot = _run_in_private_source( + bound, + request, + CapsuleSnapshotSettings( + project_runtime_root=prepared.root, + member_argv=arguments, + portable_runtime=True, + unavailable_members=unavailable, + ), + ) + try: + verify_inputs(plan) + load_runtime( + prepared.descriptor_path, plan=plan, environment_id=runtime.environment_id, worker_identity=runtime.identity + ) + except ProjectRuntimeError as exc: + for member in DEPENDENT_MEMBERS: + snapshot.evidence[member].update(execution_state="error", evidence_outcome="UNKNOWN", diagnostic=str(exc)) + evidence.update(status="UNKNOWN", diagnostic=str(exc)) + return snapshot, evidence + + +@require(lambda scope_evidence: scope_evidence.get("assurance_kind") != "pr_range") +def run_project_scope_pair(resolution: Any, *, runtime: Any, options: Any, scope_evidence: dict[str, Any]) -> Any: + """Resolve each immutable side separately; local provenance cannot authorize a PR.""" + from specfact_code_review.run.runner import _capsule_report, _classify_range_findings, _snapshot_python_files + + snapshots = (resolution.base_snapshot, resolution.head_snapshot) + metadata_only = bool(resolution.selected_paths) and not any( + Path(path).suffix in {".py", ".pyi"} for path in resolution.selected_paths + ) + results = [] + bindings = {} + for side, snapshot in zip(("base", "head"), snapshots, strict=True): + side_options = replace(options, project_runtime=None) if side == "base" else options + files = _snapshot_python_files(snapshot, resolution) + if metadata_only: + files = [snapshot.root / path for path in sorted(snapshot.contents) if Path(path).suffix in {".py", ".pyi"}] + result, binding = run_project_snapshot( + runtime, + ProjectSnapshotRequest( + snapshot_root=snapshot.root, + source_snapshot=snapshot, + files=files, + options=side_options, + assurance_kind="index" if scope_evidence.get("assurance_kind") == "index" else "range_preview", + ), + ) + results.append(result) + bindings[side] = binding + combined, findings = _classify_range_findings(resolution, results[0], results[1]) + evidence = {**scope_evidence, "project_runtime": bindings, "protected_pr_eligible": False} + if evidence.get("assurance_kind") in {"range_candidate", "pr_range"}: + evidence["assurance_kind"] = "range_preview" + return _capsule_report(combined, findings, options=options, scope_evidence=evidence) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py b/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py new file mode 100644 index 00000000..43c17e3f --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/portable_worker.py @@ -0,0 +1,381 @@ +"""Portable runtime scope selection and sealed-parent target-worker adapters.""" + +from __future__ import annotations + +import fnmatch +import glob +import json +import os +import shlex +import subprocess +from collections.abc import Callable, Iterator +from pathlib import Path +from typing import Any + +from icontract import ensure, require + +from specfact_code_review._review_utils import tool_error +from specfact_code_review.run.findings import ReviewFinding +from specfact_code_review.run.installed_coverage import ( + CoverageBridge, + attribute_installed_coverage, + plan_installed_coverage, +) +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError +from specfact_code_review.run.runtime_sources import is_excluded_source +from specfact_code_review.run.target_launch import target_command + + +DEPENDENT_MEMBERS = frozenset({"basedpyright", "pylint", "contracts", "targeted-pytest-coverage"}) +# Native pytest 9.1.1 defaults; explicit norecursedirs replaces this set. +DEFAULT_NORECURSEDIRS = ("*.egg", ".*", "_darcs", "build", "CVS", "dist", "node_modules", "venv", "{arch}") + + +def _strings(value: object) -> tuple[str, ...]: + if isinstance(value, str): + return tuple(shlex.split(value)) + if isinstance(value, list | tuple): + return tuple(str(item) for item in value) + return () + + +def _matching_source_tests(relative: str, candidates: set[str]) -> set[str]: + stem = Path(relative).stem + matches = {candidate for candidate in candidates if Path(candidate).name in {f"test_{stem}.py", f"{stem}_test.py"}} + if len(matches) > 1: + raise ProjectRuntimeError(f"project_test_selection_ambiguous:{relative}; include explicit test paths") + return matches + + +def _matches_directory_pattern(path: Path, pattern: str) -> bool: + """Match native pytest basename and relative/absolute directory patterns.""" + if os.sep != "/" and os.sep not in pattern: + pattern = pattern.replace("/", os.sep) + candidate = path.name + if os.sep in pattern: + candidate = str(path) + if path.is_absolute() and not os.path.isabs(pattern): + pattern = f"*{os.sep}{pattern}" + return fnmatch.fnmatch(candidate, pattern) + + +def _candidate_test_files(root: Path, norecursedirs: tuple[str, ...]) -> Iterator[Path]: + if is_excluded_source(root): + return + for directory, directories, files in os.walk(root, followlinks=False): + directories[:] = [ + name + for name in directories + if not is_excluded_source(Path(directory) / name) + and not any(_matches_directory_pattern(Path(directory) / name, pattern) for pattern in norecursedirs) + ] + for name in files: + if name.endswith(".py"): + yield Path(directory) / name + + +def _test_root_files(root: Path, norecursedirs: tuple[str, ...]) -> Iterator[Path]: + """Explicit file roots and directory roots use the same Python-file boundary.""" + if root.is_file(): + if root.suffix == ".py": + yield root + else: + yield from _candidate_test_files(root, norecursedirs) + + +def _contained_test_path(root: Path, relative: str) -> Path: + """Check literal patterns and expanded paths before walking candidate roots.""" + path = root / relative + if not path.resolve().is_relative_to(root.resolve()): + raise ProjectRuntimeError(f"project_test_path_escape:{relative}; keep testpaths inside the reviewed repository") + return path + + +def _expanded_test_roots(plan: ProjectPlan, roots: tuple[str, ...]) -> tuple[Path, ...]: + """Match native pytest's glob expansion and all-missing-path fallback.""" + expanded = set() + for pattern in roots: + _contained_test_path(plan.root, pattern) + for relative in glob.iglob(pattern, root_dir=plan.root, recursive=True): + expanded.add(_contained_test_path(plan.root, relative)) + return tuple(sorted(expanded)) or (plan.root,) + + +def _excluded_test_root(path: Path, project: Path) -> bool: + """Expanded file roots must retain excluded-directory ancestry.""" + return any(is_excluded_source(entry) for entry in (path, *path.parents) if entry.is_relative_to(project)) + + +def _test_candidates(plan: ProjectPlan, roots: tuple[str, ...], patterns: tuple[str, ...]) -> set[str]: + recursion = plan.pytest_config.get("norecursedirs", DEFAULT_NORECURSEDIRS) + norecursedirs = tuple(shlex.split(recursion)) if isinstance(recursion, str) else _strings(recursion) + return { + path.relative_to(plan.root).as_posix() + for root in _expanded_test_roots(plan, roots) + if not _excluded_test_root(root, plan.root) + for path in _test_root_files(root, norecursedirs) + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns) + } + + +@ensure(lambda result, full: (full or bool(result)) and len(result) == len(set(result))) +def select_test_paths(plan: ProjectPlan, files: list[Path], *, full: bool) -> tuple[str, ...]: + """Select real native test paths without rewriting the customer's pytest policy.""" + if full: + # Let target pytest resolve testpaths, fallback discovery and positional addopts. + return () + roots = _strings(plan.pytest_config.get("testpaths")) or (".",) + patterns = _strings(plan.pytest_config.get("python_files", ["test_*.py", "*_test.py"])) + candidates = _test_candidates(plan, roots, patterns) + selected = set() + for path in files: + relative = path.resolve().relative_to(plan.root).as_posix() + if any(fnmatch.fnmatch(path.name, pattern) for pattern in patterns): + selected.add(relative) + else: + selected.update(_matching_source_tests(relative, candidates)) + if not selected: + raise ProjectRuntimeError("project_test_selection_empty: include corresponding test files") + return tuple(sorted(selected)) + + +@ensure(lambda result: set(result) == DEPENDENT_MEMBERS) +def preparation_failure_snapshot(reason: str) -> dict[str, dict[str, Any]]: + """Reference one root cause without producing cascaded synthetic findings.""" + return { + member: { + "execution_state": "error", + "evidence_outcome": "UNKNOWN", + "diagnostic": "project_runtime_required", + "runtime_diagnostic": reason, + } + for member in DEPENDENT_MEMBERS + } + + +def _validate_collection_errors(observation: dict[str, Any]) -> None: + """Keep collection/plugin errors visible even when other tests fail.""" + if observation.get("collection_errors"): + raise ProjectRuntimeError( + "project_pytest_collection_error; --continue-on-collection-errors cannot complete collection; " + "inspect target_execution" + ) + if observation.get("internal_errors"): + raise ProjectRuntimeError( + "project_pytest_internal_error; inspect target_execution and required plugin configuration" + ) + + +def _missing_coverage_diagnostic(observation: dict[str, Any]) -> str: + """Preserve a specific worker remedy without coercing malformed diagnostic objects.""" + diagnostic = observation.get("coverage_diagnostic", "") + if not isinstance(diagnostic, str): + return "project_pytest_coverage_diagnostic_invalid; inspect target_execution coverage_diagnostic" + return diagnostic or "project_pytest_coverage_missing; enable pytest-cov and remove --no-cov if configured" + + +@require(lambda exit_code: isinstance(exit_code, int)) +def validate_observation(observation: dict[str, Any], exit_code: int) -> None: + """Reject incomplete execution without discarding native pytest selection policy.""" + _validate_collection_errors(observation) + if exit_code == 4: + raise ProjectRuntimeError( + "project_pytest_configuration_or_collection_failed:exit=4; inspect target_execution and pytest options" + ) + records = observation["records"] + collected = set(observation["collected"]) + executed = {row["nodeid"] for row in records if row["phase"] == "call"} + terminal = executed | {row["nodeid"] for row in records if row["outcome"] == "skipped"} + if observation["exit_code"] != exit_code or not collected or not executed: + raise ProjectRuntimeError( + f"project_pytest_execution_incomplete:exit={exit_code}; check collection/selection options" + ) + if missing := collected - terminal: + raise ProjectRuntimeError( + f"project_pytest_unexecuted:{','.join(sorted(missing))}; check fixture setup and -x/--maxfail options" + ) + if exit_code not in {0, 1}: + raise ProjectRuntimeError(f"project_pytest_configuration_or_collection_failed:exit={exit_code}") + if not observation.get("coverage", {}).get("files"): + raise ProjectRuntimeError(_missing_coverage_diagnostic(observation)) + + +def _nonpassing_observation_findings(records: list[dict[str, Any]], pytest_root: Path) -> list[ReviewFinding]: + findings = [] + for record in records: + has_xfail = bool(record.get("has_xfail") or record.get("wasxfail")) + if record["outcome"] != "passed" or has_xfail: + findings.append( + ReviewFinding( + category="testing", + severity="error", + tool="pytest", + rule="TEST_OUTCOME_NOT_PASS", + file=(pytest_root / str(record["nodeid"]).split("::", maxsplit=1)[0]) + .resolve() + .relative_to(Path(".").resolve()) + .as_posix(), + line=1, + message=( + f"Test {record['nodeid']} {record['outcome']} during {record['phase']}" + + (" with an xfail marker." if has_xfail else ".") + ), + fixable=False, + ) + ) + return findings + + +def _coverage_percentage(value: object) -> bool: + """Accept only finite native percentages, excluding Boolean integer subclasses.""" + return not isinstance(value, bool) and isinstance(value, int | float) and 0 <= value <= 100 + + +def _coverage_policy_fields(policy: dict[str, Any]) -> dict[str, bool]: + """Check the bounded native receipt's scalar types and percentage ranges.""" + precision = policy.get("precision") + return { + "mode": policy.get("mode") in ("native", "reviewer", "disabled"), + "threshold": _coverage_percentage(policy.get("threshold")), + "precision": isinstance(precision, int) and not isinstance(precision, bool) and 0 <= precision <= 9, + "measured_total": "measured_total" in policy + and (policy["measured_total"] is None or _coverage_percentage(policy["measured_total"])), + "native_threshold_failed": isinstance(policy.get("native_threshold_failed"), bool), + } + + +def _validate_coverage_policy(policy: object, exit_code: int) -> dict[str, Any]: + """Validate the native receipt without duplicating Coverage's rounding policy.""" + if not isinstance(policy, dict): + raise ValueError("expected coverage_policy object") + if invalid := [field for field, valid in _coverage_policy_fields(policy).items() if not valid]: + raise ValueError(f"invalid fields: {','.join(invalid)}") + if policy["native_threshold_failed"] and ( + policy["mode"] != "native" or policy["measured_total"] is None or exit_code != 1 + ): + raise ValueError("failed native threshold requires native mode, measured total and pytest exit 1") + return policy + + +def _coverage_policy_findings(observation: dict[str, Any], exit_code: int, anchor: Path) -> list[ReviewFinding]: + """Expose actual native aggregate failures separately from per-source review coverage.""" + if "coverage_policy" not in observation: + return [] + try: + policy = _validate_coverage_policy(observation["coverage_policy"], exit_code) + except ValueError as exc: + return [ + tool_error( + tool="pytest", + file_path=anchor, + message=f"project_pytest_coverage_policy_invalid:{exc}; inspect target_execution coverage_policy", + ) + ] + if not policy["native_threshold_failed"]: + return [] + return [ + ReviewFinding( + category="testing", + severity="error", + tool="pytest", + rule="TEST_COVERAGE_POLICY_FAILED", + file=str(anchor), + line=1, + message=( + f"Native aggregate coverage {policy['measured_total']}% failed its configured " + f"{policy['threshold']}% threshold (precision={policy['precision']}); " + "increase coverage to satisfy the repository's native policy." + ), + fixable=False, + ) + ] + + +def _installed_coverage_findings( + files: list[Path], + bridge: CoverageBridge, + observation: dict[str, Any], + evaluate: Callable[[list[Path], dict[str, Any]], list[ReviewFinding]], +) -> list[ReviewFinding]: + """Retain native evidence and turn unavailable attribution into a named diagnostic.""" + try: + coverage, attribution_errors = attribute_installed_coverage( + bridge, + observation["coverage"], + snapshot=Path(".").resolve(), + relocations=observation.get("coverage_relocations"), + ) + if bridge.mappings or attribution_errors: + observation["coverage_attribution"] = { + "mappings": [ + { + "source": str(row.source), + "installed": str(row.installed), + "sha256": row.digest, + "distribution": row.distribution, + } + for row in bridge.mappings + ], + "diagnostics": attribution_errors, + } + Path("/opt/specfact/tmp/pytest-observation.json").write_text(json.dumps(observation), encoding="utf-8") + return evaluate(files, {**observation, "coverage": coverage, "coverage_attribution_errors": attribution_errors}) + except (OSError, ValueError, TypeError, KeyError) as exc: + return [ + tool_error( + tool="pytest", + file_path=files[0] if files else Path("."), + message=f"project_pytest_installed_coverage_unavailable:{exc}", + ) + ] + + +def _portable_pytest_command(files: list[Path], encoded: str) -> tuple[CoverageBridge, list[str]]: + """Bind native measurement inputs to controller-verified installed ownership.""" + bridge = plan_installed_coverage( + files, snapshot=Path(".").resolve(), site_packages=Path("/opt/specfact/project-runtime/site-packages") + ) + request = json.loads(encoded) + request["coverage_directories"] = [str(path) for path in bridge.directories] + request["coverage_modules"] = list(bridge.modules) + request["coverage_candidates"] = sorted( + {path for paths in bridge.candidates.values() for path in paths} | set(bridge.measured_origins) + ) + command = target_command("pytest-observe", [json.dumps(request)]) + return bridge, command + + +@ensure(lambda result: all(isinstance(finding, ReviewFinding) for finding in result)) +def run_portable_pytest(files: list[Path], adapter_argv: tuple[str, ...]) -> list[ReviewFinding]: + """Parse actual child observations while leaving project plugins out of this process.""" + anchor = files[0] if files else Path(".") + if len(adapter_argv) != 2 or adapter_argv[0] != "portable-pytest-v2": + return [tool_error(tool="pytest", file_path=anchor, message="project_pytest_request_invalid")] + + from specfact_code_review.run.runner import evaluate_portable_pytest_coverage, resolve_portable_pytest_root + + try: + bridge, command = _portable_pytest_command(files, adapter_argv[1]) + Path("/opt/specfact/tmp/pytest-observation.json").unlink(missing_ok=True) + completed = subprocess.run(command, text=True, capture_output=True, check=False, timeout=1200) + observation = json.loads(Path("/opt/specfact/tmp/pytest-observation.json").read_text(encoding="utf-8")) + records = observation["records"] + policy_findings = _coverage_policy_findings(observation, completed.returncode, anchor) + validation_error = "" + try: + validate_observation(observation, completed.returncode) + except ProjectRuntimeError as exc: + validation_error = str(exc) + if validation_error and not records: + return [*policy_findings, tool_error(tool="pytest", file_path=anchor, message=validation_error)] + pytest_root = resolve_portable_pytest_root(observation) + findings = [*policy_findings, *_nonpassing_observation_findings(records, pytest_root)] + except (OSError, ValueError, KeyError, subprocess.SubprocessError) as exc: + return [tool_error(tool="pytest", file_path=anchor, message=str(exc))] + if validation_error: + findings.append(tool_error(tool="pytest", file_path=anchor, message=validation_error)) + else: + findings.extend(_installed_coverage_findings(files, bridge, observation, evaluate_portable_pytest_coverage)) + if completed.returncode == 1 and not findings: + return [tool_error(tool="pytest", file_path=anchor, message="project_pytest_failure_without_observed_test")] + return findings diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runner.py b/packages/specfact-code-review/src/specfact_code_review/run/runner.py index 53e664a4..bfa6fb88 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/runner.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/runner.py @@ -130,6 +130,8 @@ class ReviewOptions: review_level: Literal["error", "warning"] | None = None review_mode: ReviewEnforcementMode = "full" focus: ReviewFocus | None = None + project_config: Path | None = None + project_runtime: Path | None = None @dataclass(frozen=True) @@ -221,6 +223,7 @@ class CapsuleMemberExecutionRequest: adapter_argv: tuple[str, ...] = () complete_pytest_inventory: bool = False project_runtime_root: Path | None = None + portable_runtime: bool = False @dataclass(frozen=True) @@ -648,26 +651,37 @@ def _selected_module_payload() -> SelectedModulePayload: (parent for parent in source.parents if source == parent / _PACKAGE_ROOT / "run/runner.py"), None ) candidate_checkout = candidate_root is not None and (candidate_root / ".git").exists() - if os.environ.get("GITHUB_ACTIONS") == "true" and candidate_checkout: + if ( + os.environ.get("GITHUB_ACTIONS") == "true" + and os.environ.get("GITHUB_REPOSITORY") == "nold-ai/specfact-cli-modules" + and candidate_checkout + ): return _protected_candidate_payload() payload, reason = _official_installed_payload() return SelectedModulePayload(payload, reason) -def _prepare_capsule_runtime(*, project_runtime_identity: str = "not-applicable") -> tuple[CapsuleRuntime | None, str]: +def _capsule_lock_environment(environment_id: str | None) -> tuple[dict[str, object], str, dict[str, object]]: + lock_path = Path(__file__).parents[1] / "resources/contracts/pr-range-v1-toolchain-lock.json" + lock = cast(dict[str, object], json.loads(lock_path.read_text(encoding="utf-8"))) + environment_id = environment_id or _capsule_environment_id() + environments = cast(list[dict[str, object]], lock["environments"]) + environment = next( + item for item in environments if str(item.get("environment_id") or item.get("id")) == environment_id + ) + return lock, environment_id, environment + + +def _prepare_capsule_runtime( + *, project_runtime_identity: str = "not-applicable", environment_id: str | None = None +) -> tuple[CapsuleRuntime | None, str]: """Materialize and compose the signed runtime without host analyzer fallback.""" if platform.system() != "Linux" or platform.machine() not in {"x86_64", "AMD64"}: return None, "unsupported_controller_platform" capsule_root: Path | None = None try: - lock_path = Path(__file__).parents[1] / "resources/contracts/pr-range-v1-toolchain-lock.json" - lock = cast(dict[str, object], json.loads(lock_path.read_text(encoding="utf-8"))) - environment_id = _capsule_environment_id() - environments = cast(list[dict[str, object]], lock["environments"]) - environment = next( - item for item in environments if str(item.get("environment_id") or item.get("id")) == environment_id - ) + lock, environment_id, environment = _capsule_lock_environment(environment_id) storage_root = Path( os.environ.get( "SPECFACT_CODE_REVIEW_CAPSULE_CACHE", @@ -909,6 +923,10 @@ def _configured_member_findings( if member == "targeted-pytest-plugin-preflight": return _pytest_plugin_preflight_findings(adapter_argv) if member == "targeted-pytest-coverage": + if adapter_argv and adapter_argv[0] == "portable-pytest-v2": + from specfact_code_review.run.portable_worker import run_portable_pytest + + return run_portable_pytest(files, adapter_argv) findings, _coverage = ( _evaluate_complete_tdd_gate(files, adapter_argv) if complete_pytest_inventory else _evaluate_tdd_gate(files) ) @@ -1074,6 +1092,14 @@ def _capsule_process_request(request_path: Path) -> None: complete_pytest_inventory=complete_pytest_inventory, ), ) + observation_path = Path("/opt/specfact/tmp/pytest-observation.json") + if ( + member == "targeted-pytest-coverage" + and adapter_argv + and adapter_argv[0] == "portable-pytest-v2" + and observation_path.is_file() + ): + response["target_execution"] = json.loads(observation_path.read_text(encoding="utf-8")) except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError) as exc: response = { "member": "unknown", @@ -1141,6 +1167,7 @@ def _execute_capsule_member(request: CapsuleMemberExecutionRequest) -> dict[str, network="none", control_root=control_root, environment_id=request.runtime.environment_id, + import_domain="portable" if request.portable_runtime else "sealed", ) preflight = preflight_reserved_imports(context) if preflight.status != "PASS": @@ -1215,46 +1242,74 @@ def _dispatch_capsule_member( return _execute_capsule_member(request) +@dataclass(frozen=True) +class CapsuleSnapshotSettings: + """Execution-domain and policy bindings shared across one member roster.""" + + config_roots: tuple[Path, ...] = () + member_argv: dict[str, tuple[str, ...]] | None = None + project_runtime_root: Path | None = None + scope_paths: tuple[str, ...] | None = None + portable_runtime: bool = False + unavailable_members: dict[str, dict[str, object]] | None = None + + +def _complete_snapshot_pytest(member: str, settings: CapsuleSnapshotSettings) -> bool: + return ( + member == "targeted-pytest-coverage" + and settings.member_argv is not None + and member in settings.member_argv + and not settings.portable_runtime + ) + + +def _snapshot_relative_inputs(snapshot_root: Path, files: list[Path]) -> tuple[str, ...]: + resolved_snapshot = snapshot_root.resolve() + resolved_inputs = tuple(path.resolve() for path in files) + return tuple( + path.relative_to(resolved_snapshot).as_posix() if path.is_relative_to(resolved_snapshot) else path.as_posix() + for path in resolved_inputs + ) + + def _run_capsule_snapshot( runtime: CapsuleRuntime, *, snapshot_root: Path, files: list[Path], options: ReviewOptions, - config_roots: tuple[Path, ...] = (), - member_argv: dict[str, tuple[str, ...]] | None = None, - project_runtime_root: Path | None = None, - scope_paths: tuple[str, ...] | None = None, + settings: CapsuleSnapshotSettings | None = None, ) -> CapsuleSnapshotResult: + settings = settings or CapsuleSnapshotSettings() evidence: dict[str, dict[str, object]] = {} findings_by_member: dict[str, list[ReviewFinding]] = {} - resolved_snapshot = snapshot_root.resolve() - resolved_inputs = tuple(path.resolve() for path in files) - relative_inputs = tuple( - path.relative_to(resolved_snapshot).as_posix() if path.is_relative_to(resolved_snapshot) else path.as_posix() - for path in resolved_inputs + relative_inputs = _snapshot_relative_inputs(snapshot_root, files) + applicability = classify_snapshot_input_kinds( + relative_inputs if settings.scope_paths is None else settings.scope_paths ) - applicability = classify_snapshot_input_kinds(relative_inputs if scope_paths is None else scope_paths) for member in default_pr_range_profile().all_ids: - sealed_bugs_policy = member_argv is not None and "semgrep-bugs" in member_argv - raw = _dispatch_capsule_member( - CapsuleMemberExecutionRequest( - runtime=runtime, - member=member, - invocation_id=str(uuid4()), - snapshot_root=snapshot_root, - files=files, - options=options, - config_roots=config_roots, - adapter_argv=(member_argv or {}).get(member, ()), - complete_pytest_inventory=( - member == "targeted-pytest-coverage" and member_argv is not None and member in member_argv + sealed_bugs_policy = settings.member_argv is not None and "semgrep-bugs" in settings.member_argv + raw = (settings.unavailable_members or {}).get(member) + if raw is None: + raw = _dispatch_capsule_member( + CapsuleMemberExecutionRequest( + runtime=runtime, + member=member, + invocation_id=str(uuid4()), + snapshot_root=snapshot_root, + files=files, + options=options, + config_roots=settings.config_roots, + adapter_argv=(settings.member_argv or {}).get(member, ()), + complete_pytest_inventory=_complete_snapshot_pytest(member, settings), + project_runtime_root=( + settings.project_runtime_root if member in _PROJECT_RUNTIME_MEMBERS else None + ), + portable_runtime=settings.portable_runtime, ), - project_runtime_root=(project_runtime_root if member in _PROJECT_RUNTIME_MEMBERS else None), - ), - applicability=applicability, - sealed_bugs_policy=sealed_bugs_policy, - ) + applicability=applicability, + sealed_bugs_policy=sealed_bugs_policy, + ) raw_findings = raw.get("findings", []) findings = [ReviewFinding.model_validate(value) for value in cast(list[object], raw_findings)] findings_by_member[member] = findings @@ -1266,6 +1321,7 @@ def _run_capsule_snapshot( "sandbox_invocation": "fresh", "capsule_identity": runtime.identity, "environment_id": getattr(runtime, "environment_id", ""), + **({"target_execution": raw["target_execution"]} if "target_execution" in raw else {}), } return CapsuleSnapshotResult(evidence, findings_by_member) @@ -3888,7 +3944,8 @@ def _pytest_in_capsule() -> bool: def _pytest_env() -> dict[str, str]: - env = os.environ.copy() + env = _candidate_git_environment() + env.pop("SPECFACT_CODE_REVIEW_CHANGED_DIFF", None) if _pytest_in_capsule(): env.pop("PYTHONPATH", None) env["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1" @@ -4469,6 +4526,19 @@ class CachedAnalysisSnapshot: entries: dict[str, tuple[str, str]] directories: tuple[MaterializedDirectoryIdentity, ...] diff: CachedDiffIdentity + vcs_commit: str = "" + + @property + @ensure(lambda result: re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", result) is not None) + def tree(self) -> str: + """Expose the captured index tree through common snapshot discovery.""" + return self.diff.index_tree + + @property + @ensure(lambda result: result.startswith("index-")) + def commit(self) -> str: + """Carry the staged tree context through common snapshot discovery.""" + return "index-" + self.diff.index_tree @dataclass(frozen=True) @@ -5057,6 +5127,15 @@ def _cached_caller_prefix(repository: Path) -> str | None: return "" if prefix == "." else f"{prefix}/" +def _cached_head_commit(repository: Path) -> str: + """Capture a real HEAD commit while retaining unborn cached-analysis support.""" + result = _run_changed_line_git_command(["git", "-C", str(repository), "rev-parse", "--verify", "HEAD^{commit}"]) + if result is None or not result.stdout.endswith("\n"): + return "" + commit = result.stdout.removesuffix("\n") + return commit if re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", commit) else "" + + def _cached_analysis_snapshot(files: list[Path], destination: Path) -> CachedAnalysisSnapshot | None: """Bind selected paths and analyzer input to one immutable staged tree.""" repository_paths = _cached_repository_paths(files) @@ -5064,11 +5143,12 @@ def _cached_analysis_snapshot(files: list[Path], destination: Path) -> CachedAna return None repository, selected = repository_paths caller_prefix = _cached_caller_prefix(repository) + vcs_commit = _cached_head_commit(repository) base_tree = _cached_base_tree_identity(repository) tree_oid = _git_tree_identity(repository, "index") if caller_prefix is None or base_tree is None or tree_oid is None: return None - if _cached_base_tree_identity(repository) != base_tree: + if (_cached_base_tree_identity(repository), _cached_head_commit(repository)) != (base_tree, vcs_commit): return None entries = _cached_tree_entries(repository, tree_oid) selected_unsupported = entries is not None and any( @@ -5087,6 +5167,7 @@ def _cached_analysis_snapshot(files: list[Path], destination: Path) -> CachedAna entries=entries, directories=directories, diff=CachedDiffIdentity(base_tree=base_tree, index_tree=tree_oid, caller_prefix=caller_prefix), + vcs_commit=vcs_commit, ) @@ -6055,15 +6136,79 @@ def _coverage_findings( return findings, coverage_by_source +@ensure(lambda result: result.is_absolute() and result.is_relative_to(Path.cwd().resolve())) +def resolve_portable_pytest_root(observation: dict[str, Any]) -> Path: + """Resolve the native pytest root within the reviewed source snapshot.""" + recorded_root = observation.get("pytest_root", ".") + if not isinstance(recorded_root, str) or not recorded_root: + raise ValueError("project_pytest_root_missing_or_invalid; inspect target_execution") + snapshot = Path.cwd().resolve() + pytest_root = (snapshot / recorded_root).resolve() + if not pytest_root.is_relative_to(snapshot): + raise ValueError("project_pytest_root_outside_snapshot") + return pytest_root + + +def _portable_coverage_sources(files: list[Path], observation: dict[str, Any]) -> list[Path]: + snapshot = Path.cwd().resolve() + pytest_root = resolve_portable_pytest_root(observation) + test_files = { + (pytest_root / str(nodeid).split("::", maxsplit=1)[0]).resolve() + for nodeid in (*observation.get("collected", []), *observation.get("deselected", [])) + } + return [ + path + for path in files + if path.suffix == ".py" + and path.name != "conftest.py" + and path.resolve() not in test_files + and not _is_test_file(path.resolve().relative_to(snapshot)) + ] + + +@ensure(lambda result: all(isinstance(finding, ReviewFinding) for finding in result)) +def evaluate_portable_pytest_coverage(files: list[Path], observation: dict[str, Any]) -> list[ReviewFinding]: + """Apply the production-source gate to native target-worker coverage evidence.""" + anchor = files[0] if files else Path.cwd() + configured = observation.get("coverage_threshold") + threshold = _COVERAGE_THRESHOLD if configured is None else configured + if isinstance(threshold, bool) or not isinstance(threshold, int | float) or not 0 <= threshold <= 100: + return [tool_error(tool="pytest", file_path=anchor, message="project_pytest_coverage_threshold_invalid")] + try: + sources = _portable_coverage_sources(files, observation) + except ValueError as exc: + return [tool_error(tool="pytest", file_path=anchor, message=str(exc))] + attribution_errors = observation.get("coverage_attribution_errors", {}) + for source in sources: + if diagnostic := attribution_errors.get(str(source.resolve())): + return [ + tool_error( + tool="pytest", + file_path=source, + message=f"project_pytest_installed_coverage:{diagnostic}:{source}; " + "rebuild the runtime for this snapshot or declare the intended source environment", + ) + ] + findings, _coverage = _coverage_findings( + sources, + observation["coverage"], + allow_project_omitted_initializers=False, + threshold=max(_COVERAGE_THRESHOLD, threshold), + blocking_low_coverage=True, + ) + return findings + + def _pytest_junit_identities(observer: tuple[dict[str, object], ...]) -> dict[tuple[str, str], list[str]]: observed_nodes = tuple(dict.fromkeys(str(record.get("nodeid", "")) for record in observer)) junit_identities: dict[tuple[str, str], list[str]] = {} for nodeid in observed_nodes: - path, *qualifiers = nodeid.split("::") + address, parameter_open, parameters = nodeid.partition("[") + path, *qualifiers = address.split("::") if not qualifiers or not path.endswith(".py"): continue classname_parts = [path[:-3].replace("/", "."), *qualifiers[:-1]] - identity = (".".join(classname_parts), qualifiers[-1]) + identity = (".".join(classname_parts), qualifiers[-1] + parameter_open + parameters) junit_identities.setdefault(identity, []).append(nodeid) return junit_identities @@ -6292,6 +6437,8 @@ def _review_options_from_kwargs(options: ReviewOptions | None, overrides: dict[s "review_level", "review_mode", "focus", + "project_config", + "project_runtime", } unknown_keys = set(overrides) - allowed_keys if unknown_keys: @@ -6326,6 +6473,8 @@ def _review_options_from_kwargs(options: ReviewOptions | None, overrides: dict[s review_level=cast(Literal["error", "warning"] | None, review_level), review_mode=cast(ReviewEnforcementMode, review_mode), focus=cast(ReviewFocus | None, focus), + project_config=cast(Path | None, overrides.get("project_config")), + project_runtime=cast(Path | None, overrides.get("project_runtime")), ) @@ -6579,7 +6728,9 @@ def _run_cached_development_host_review( files=files, findings_by_member={}, ) - with chdir(cached_snapshot.root): + from specfact_code_review._review_utils import development_runtime + + with chdir(cached_snapshot.root), development_runtime(Path(sys.prefix)): host_report = run_review(cached_snapshot.files, replace(options, review_mode="full")) if not _materialized_tree_matches_entries( cached_snapshot.root, cached_snapshot.entries, cached_snapshot.directories @@ -6706,8 +6857,7 @@ def _run_local_capsule_snapshot( snapshot_root=snapshot_root, files=files, options=options, - config_roots=bindings.config_roots, - member_argv=bindings.member_argv, + settings=CapsuleSnapshotSettings(config_roots=bindings.config_roots, member_argv=bindings.member_argv), ) except (OSError, TypeError, ValueError, configparser.Error) as exc: return _local_pytest_failure_snapshot( @@ -6718,6 +6868,142 @@ def _run_local_capsule_snapshot( shutil.rmtree(root, ignore_errors=True) +@dataclass(frozen=True) +class _LocalCapsuleReviewContext: + files: list[Path] + options: ReviewOptions + scope_evidence: dict[str, object] + cached_snapshot: CachedAnalysisSnapshot | None + cached_diff: CachedDiffIdentity | None + worktree_identity: WorktreeReviewIdentity | Literal["not_repository"] + + +def _finalize_local_capsule_snapshot( + snapshot: CapsuleSnapshotResult, context: _LocalCapsuleReviewContext +) -> ReviewReport: + files, review_options, scope_evidence = context.files, context.options, context.scope_evidence + cached_snapshot, cached_diff = context.cached_snapshot, context.cached_diff + worktree_identity = context.worktree_identity + if _worktree_analysis_identity_changed(worktree_identity, files): + return _worktree_snapshot_unknown( + "worktree_snapshot_changed_during_analysis", + files=files, + options=review_options, + scope_evidence=scope_evidence, + ) + findings_by_member = snapshot.findings_by_member + if cached_snapshot is not None: + rebased_findings = _rebase_cached_snapshot_findings( + findings_by_member, + snapshot_root=cached_snapshot.root, + repository=cached_snapshot.repository, + ) + if rebased_findings is None: + return _with_capsule_enforcement( + _unknown_capsule_report( + "cached_capsule_finding_path_unavailable", + options=review_options, + scope_evidence=scope_evidence, + ), + review_options, + files=files, + findings_by_member={}, + cached_diff=cached_diff, + ) + findings_by_member = rebased_findings + enforced_report = _with_capsule_enforcement( + _capsule_report( + snapshot.evidence, + findings_by_member, + options=review_options, + scope_evidence=scope_evidence, + ), + review_options, + files=files, + findings_by_member=findings_by_member, + cached_diff=cached_diff, + ) + if _worktree_analysis_identity_changed(worktree_identity, files): + return _worktree_snapshot_unknown( + "worktree_snapshot_changed_during_analysis", + files=files, + options=review_options, + scope_evidence=scope_evidence, + ) + return enforced_report + + +def _run_local_capsule_context( + runtime: CapsuleRuntime, + files: list[Path], + review_options: ReviewOptions, + scope_evidence: dict[str, object], + assurance_kind: LocalAssuranceKind, +) -> ReviewReport: + with ExitStack() as stack: + snapshot_root = Path.cwd() + snapshot_files = files + cached_diff: CachedDiffIdentity | None = None + cached_snapshot: CachedAnalysisSnapshot | None = None + worktree_identity: WorktreeReviewIdentity | Literal["not_repository"] = "not_repository" + if _cached_review_requested(review_options): + temporary_root = Path(stack.enter_context(tempfile.TemporaryDirectory(prefix="specfact-index-"))) + cached_snapshot = _cached_analysis_snapshot(files, temporary_root) + if cached_snapshot is None: + return _with_capsule_enforcement( + _unknown_capsule_report( + "cached_snapshot_materialization_unavailable", + options=review_options, + scope_evidence=scope_evidence, + ), + review_options, + files=files, + findings_by_member={}, + ) + snapshot_root, snapshot_files = cached_snapshot.root, cached_snapshot.files + cached_diff = cached_snapshot.diff + elif review_options.review_mode == "changed": + captured_identity = _capture_worktree_analysis_identity(files) + if captured_identity is None: + return _worktree_snapshot_unknown( + "worktree_snapshot_identity_unavailable", + files=files, + options=review_options, + scope_evidence=scope_evidence, + ) + worktree_identity = captured_identity + from specfact_code_review.run.portable_snapshot import ( + ProjectSnapshotRequest, + project_runtime_requested, + run_project_snapshot, + ) + + if project_runtime_requested(snapshot_root, review_options): + snapshot, project_evidence = run_project_snapshot( + runtime, + ProjectSnapshotRequest( + snapshot_root=snapshot_root, + files=snapshot_files, + options=review_options, + assurance_kind=assurance_kind, + source_snapshot=cached_snapshot, + ), + ) + scope_evidence["project_runtime"] = project_evidence + else: + snapshot = _run_local_capsule_snapshot( + runtime, + snapshot_root=snapshot_root, + files=snapshot_files, + options=review_options, + assurance_kind=assurance_kind, + ) + context = _LocalCapsuleReviewContext( + files, review_options, scope_evidence, cached_snapshot, cached_diff, worktree_identity + ) + return _finalize_local_capsule_snapshot(snapshot, context) + + def run_capsule_review( files: list[Path], options: ReviewOptions | None = None, @@ -6744,92 +7030,7 @@ def run_capsule_review( findings_by_member={}, ) try: - with ExitStack() as stack: - snapshot_root = Path.cwd() - snapshot_files = files - cached_diff: CachedDiffIdentity | None = None - cached_snapshot: CachedAnalysisSnapshot | None = None - worktree_identity: WorktreeReviewIdentity | Literal["not_repository"] = "not_repository" - if _cached_review_requested(review_options): - temporary_root = Path(stack.enter_context(tempfile.TemporaryDirectory(prefix="specfact-index-"))) - cached_snapshot = _cached_analysis_snapshot(files, temporary_root) - if cached_snapshot is None: - return _with_capsule_enforcement( - _unknown_capsule_report( - "cached_snapshot_materialization_unavailable", - options=review_options, - scope_evidence=scope_evidence, - ), - review_options, - files=files, - findings_by_member={}, - ) - snapshot_root, snapshot_files = cached_snapshot.root, cached_snapshot.files - cached_diff = cached_snapshot.diff - elif review_options.review_mode == "changed": - captured_identity = _capture_worktree_analysis_identity(files) - if captured_identity is None: - return _worktree_snapshot_unknown( - "worktree_snapshot_identity_unavailable", - files=files, - options=review_options, - scope_evidence=scope_evidence, - ) - worktree_identity = captured_identity - snapshot = _run_local_capsule_snapshot( - runtime, - snapshot_root=snapshot_root, - files=snapshot_files, - options=review_options, - assurance_kind=assurance_kind, - ) - if _worktree_analysis_identity_changed(worktree_identity, files): - return _worktree_snapshot_unknown( - "worktree_snapshot_changed_during_analysis", - files=files, - options=review_options, - scope_evidence=scope_evidence, - ) - findings_by_member = snapshot.findings_by_member - if cached_snapshot is not None: - rebased_findings = _rebase_cached_snapshot_findings( - findings_by_member, - snapshot_root=cached_snapshot.root, - repository=cached_snapshot.repository, - ) - if rebased_findings is None: - return _with_capsule_enforcement( - _unknown_capsule_report( - "cached_capsule_finding_path_unavailable", - options=review_options, - scope_evidence=scope_evidence, - ), - review_options, - files=files, - findings_by_member={}, - cached_diff=cached_diff, - ) - findings_by_member = rebased_findings - enforced_report = _with_capsule_enforcement( - _capsule_report( - snapshot.evidence, - findings_by_member, - options=review_options, - scope_evidence=scope_evidence, - ), - review_options, - files=files, - findings_by_member=findings_by_member, - cached_diff=cached_diff, - ) - if _worktree_analysis_identity_changed(worktree_identity, files): - return _worktree_snapshot_unknown( - "worktree_snapshot_changed_during_analysis", - files=files, - options=review_options, - scope_evidence=scope_evidence, - ) - return enforced_report + return _run_local_capsule_context(runtime, files, review_options, scope_evidence, assurance_kind) finally: _cleanup_capsule_runtime(runtime) @@ -7520,10 +7721,12 @@ def _run_bound_snapshot_pair( snapshot_root=side.root, files=side.files, options=options, - config_roots=side.bindings.config_roots, - member_argv=side.bindings.member_argv, - project_runtime_root=project_runtime_root, - scope_paths=scope_paths, + settings=CapsuleSnapshotSettings( + config_roots=side.bindings.config_roots, + member_argv=side.bindings.member_argv, + project_runtime_root=project_runtime_root, + scope_paths=scope_paths, + ), ) for side in (base, head) ) @@ -7575,6 +7778,12 @@ def _run_immutable_scope_review_with_runtime( options=options, scope_evidence=scope_evidence, ) + from specfact_code_review.run.portable_snapshot import project_runtime_requested, run_project_scope_pair + + if project_runtime is None and ( + project_runtime_requested(base_snapshot.root, options) or project_runtime_requested(head_snapshot.root, options) + ): + return run_project_scope_pair(resolution, runtime=runtime, options=options, scope_evidence=scope_evidence) pytest_inventory, reason = _prepare_immutable_pytest_inventory( resolution, options, diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_adapters.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_adapters.py new file mode 100644 index 00000000..39366dbe --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_adapters.py @@ -0,0 +1,76 @@ +"""Native package-manager operations executed only inside the disposable builder.""" + +from __future__ import annotations + +from icontract import require + +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError + + +MANAGER_REQUIREMENTS = { + "pip": ("pip==26.2.1",), + "uv": ("uv==0.12.13",), + "hatch": ("hatch==1.18.0",), + "poetry": ("poetry==2.4.3",), +} + + +@require(lambda manager: len(manager) > 0) +def validate_adapter_inputs(manager: str, requirements: tuple[str, ...], constraints: tuple[str, ...]) -> None: + """Reject inputs that the native adapter cannot consume without changing semantics.""" + unsupported = [name for name, values in (("requirements", requirements), ("constraints", constraints)) if values] + if manager != "pip" and unsupported: + raise ProjectRuntimeError( + f"project_manager_inputs_unsupported:{manager}:{','.join(unsupported)}; " + "declare dependencies in the selected manager configuration or select manager=pip in --project-config" + ) + + +def _pip_commands(plan: ProjectPlan, python: str) -> tuple[tuple[str, ...], ...]: + arguments = [python, "-m", "pip", "install", "--disable-pip-version-check"] + for option, values in (("--group", plan.groups), ("-r", plan.requirements), ("-c", plan.constraints)): + for value in values: + arguments.extend((option, value)) + if any((plan.root / name).is_file() for name in ("pyproject.toml", "setup.py")): + arguments.append("." + ("[" + ",".join(plan.extras) + "]" if plan.extras else "")) + return (tuple(arguments),) if len(arguments) > 5 else () + + +def _uv_commands(plan: ProjectPlan, python: str) -> tuple[tuple[str, ...], ...]: + arguments = [python, "-m", "uv", "sync", "--active", "--no-editable"] + if "uv.lock" in plan.inputs: + arguments.append("--locked") + if plan.groups: + arguments.append("--no-default-groups") + for option, values in (("--group", plan.groups), ("--extra", plan.extras)): + for value in values: + arguments.extend((option, value)) + return (tuple(arguments),) + + +def _poetry_commands(plan: ProjectPlan, python: str) -> tuple[tuple[str, ...], ...]: + arguments = [python, "-m", "poetry", "install", "--no-interaction"] + if plan.groups: + arguments.extend(("--only", "main," + ",".join(plan.groups))) + for extra in plan.extras: + arguments.extend(("--extras", extra)) + return (tuple(arguments),) + + +def _hatch_commands(plan: ProjectPlan, python: str) -> tuple[tuple[str, ...], ...]: + if plan.groups or plan.extras: + raise ProjectRuntimeError( + "project_hatch_environment_required: select an environment declaring the requested groups/extras" + ) + return ((python, "-m", "hatch", "env", "create", plan.environment),) + + +@require(lambda python: len(python) > 0) +def install_commands(plan: ProjectPlan, *, python: str) -> tuple[tuple[str, ...], ...]: + """Preserve the selected manager's resolution semantics and existing lock.""" + adapters = {"pip": _pip_commands, "uv": _uv_commands, "poetry": _poetry_commands, "hatch": _hatch_commands} + adapter = adapters.get(plan.manager) + if adapter is None: + raise ProjectRuntimeError(f"project_manager_unsupported:{plan.manager}") + validate_adapter_inputs(plan.manager, plan.requirements, plan.constraints) + return adapter(plan, python) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py new file mode 100644 index 00000000..9207b5c3 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_artifacts.py @@ -0,0 +1,194 @@ +"""Seal and validate private local runtime artifacts without executing payloads.""" + +from __future__ import annotations + +import json +import os +import stat +from pathlib import Path +from typing import Any + +from beartype import beartype +from icontract import ensure, require + +from specfact_code_review.run.runtime_models import ( + PreparedRuntime, + ProjectPlan, + ProjectRuntimeError, + content_digest, + document_digest, +) + + +_DESCRIPTOR = "project-runtime.json" + + +def _reject_hardlink(metadata: os.stat_result, name: str) -> None: + """Require independent regular files before reading untrusted payload bytes.""" + if stat.S_ISREG(metadata.st_mode) and metadata.st_nlink != 1: + raise ProjectRuntimeError(f"project_runtime_hardlink_invalid:{name}; independent file required") + + +def _validate_build_directory(directory: Path, root: Path) -> list[Path]: + """Check immediate nodes without reading payloads, returning directories to visit.""" + children = [] + with os.scandir(directory) as entries: + for entry in entries: + metadata = entry.stat(follow_symlinks=False) + _reject_hardlink(metadata, Path(entry.path).relative_to(root).as_posix()) + mode = metadata.st_mode + if stat.S_ISDIR(mode): + children.append(Path(entry.path)) + continue + if not stat.S_ISREG(mode): + name = Path(entry.path).relative_to(root).as_posix() + raise ProjectRuntimeError(f"project_runtime_build_artifact_invalid:{name}; regular nodes required") + return children + + +@beartype +@require(lambda root: bool(root.name)) +def validate_build_artifact(root: Path) -> None: + """Reject indirection and special nodes before reading untrusted build output.""" + if not stat.S_ISDIR(root.lstat().st_mode): + raise ProjectRuntimeError("project_runtime_build_artifact_invalid: root must be a regular directory") + pending = [root] + while pending: + pending.extend(_validate_build_directory(pending.pop(), root)) + + +def _payload_manifest(root: Path) -> dict[str, dict[str, object]]: + entries = {} + if root.is_symlink() or not root.is_dir(): + raise ProjectRuntimeError("project_runtime_payload_invalid: root must be a regular directory") + for path in sorted(root.rglob("*")): + name = path.relative_to(root).as_posix() + if name in {_DESCRIPTOR, "." + _DESCRIPTOR + ".tmp"}: + continue + metadata = path.lstat() + _reject_hardlink(metadata, name) + mode = metadata.st_mode + if stat.S_ISLNK(mode): + raise ProjectRuntimeError(f"project_runtime_payload_symlink:{name}") + if stat.S_ISDIR(mode): + entries[name] = {"kind": "directory", "mode": stat.S_IMODE(mode)} + elif stat.S_ISREG(mode): + entries[name] = {"kind": "file", "mode": stat.S_IMODE(mode), "digest": content_digest(path.read_bytes())} + else: + raise ProjectRuntimeError(f"project_runtime_payload_special_file:{name}") + return entries + + +@ensure(lambda result: result.descriptor_path.is_file()) +def seal_runtime( + root: Path, + *, + plan: ProjectPlan, + environment_id: str, + worker_identity: str, + inventory: dict[str, Any] | None = None, +) -> PreparedRuntime: + """Publish a descriptor only after the complete local payload is available.""" + root = root.absolute() + descriptor: dict[str, Any] = { + "schema": "project-runtime-layer-v2", + "environment_id": environment_id, + "project_identity": plan.identity, + "project": plan.document(), + "worker_identity": worker_identity, + "provenance": {"authority": "local_build", "protected_pr_eligible": False}, + "inventory": inventory or {}, + "payload": _payload_manifest(root), + } + descriptor["identity"] = document_digest(descriptor) + target = root / _DESCRIPTOR + temporary = root / ("." + _DESCRIPTOR + ".tmp") + with temporary.open("x", encoding="utf-8") as stream: + json.dump(descriptor, stream, indent=2, sort_keys=True) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, target) + return PreparedRuntime(root, target, descriptor["identity"], descriptor) + + +def _validate_inventory(inventory: object) -> None: + if not isinstance(inventory, dict): + raise ProjectRuntimeError("project_runtime_inventory_invalid: expected an object") + arguments = inventory.get("pytest_arguments", []) + if not isinstance(arguments, list) or not all(isinstance(value, str) for value in arguments): + raise ProjectRuntimeError("project_runtime_inventory_invalid: pytest_arguments must be an array of strings") + conflicts = inventory.get("analyzer_conflicts", {}) + if not isinstance(conflicts, dict) or not all( + isinstance(key, str) and isinstance(value, str) for key, value in conflicts.items() + ): + raise ProjectRuntimeError("project_runtime_inventory_invalid: analyzer_conflicts must map strings") + graphs = inventory.get("member_graphs", {}) + if not isinstance(graphs, dict): + raise ProjectRuntimeError("project_runtime_inventory_invalid: member_graphs must be an object") + for graph in graphs.values(): + _validate_member_graph(graph) + + +def _validate_member_distribution(row: object) -> None: + if ( + not isinstance(row, dict) + or not isinstance(row.get("name"), str) + or not isinstance(row.get("origin"), str) + or row["origin"] not in {"project", "analyzer"} + ): + raise ProjectRuntimeError("project_runtime_inventory_invalid: malformed member distribution") + + +def _validate_member_graph(graph: object) -> None: + if ( + not isinstance(graph, dict) + or not isinstance(graph.get("sealed_imports"), list) + or not isinstance(graph.get("installed"), list) + ): + raise ProjectRuntimeError("project_runtime_inventory_invalid: malformed member graph") + if not all(isinstance(name, str) and name.isidentifier() for name in graph["sealed_imports"]): + raise ProjectRuntimeError("project_runtime_inventory_invalid: malformed member imports") + for row in graph["installed"]: + _validate_member_distribution(row) + + +def _read_descriptor(path: Path) -> dict[str, Any]: + if path.is_symlink() or not path.is_file() or path.name != _DESCRIPTOR: + raise ProjectRuntimeError("project_runtime_descriptor_invalid: expected project-runtime.json") + _reject_hardlink(path.lstat(), path.name) + try: + descriptor = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + raise ProjectRuntimeError(f"project_runtime_descriptor_invalid:{exc}") from exc + if not isinstance(descriptor, dict) or descriptor.get("schema") != "project-runtime-layer-v2": + raise ProjectRuntimeError("project_runtime_descriptor_schema_unsupported") + unsigned = {key: value for key, value in descriptor.items() if key != "identity"} + if descriptor.get("identity") != document_digest(unsigned): + raise ProjectRuntimeError("project_runtime_descriptor_identity_mismatch") + _validate_inventory(descriptor.get("inventory")) + return descriptor + + +def _verify_context(descriptor: dict[str, Any], plan: ProjectPlan, environment_id: str, worker_identity: str) -> None: + if descriptor.get("provenance") != {"authority": "local_build", "protected_pr_eligible": False}: + raise ProjectRuntimeError("project_runtime_descriptor_authority_invalid") + if descriptor.get("project_identity") != plan.identity or descriptor.get("project") != json.loads( + json.dumps(plan.document()) + ): + raise ProjectRuntimeError("project_runtime_inputs_mismatch: run runtime prepare again") + if descriptor.get("environment_id") != environment_id: + raise ProjectRuntimeError("project_runtime_environment_mismatch: rebuild for the selected capsule ABI") + if descriptor.get("worker_identity") != worker_identity: + raise ProjectRuntimeError("project_runtime_worker_identity_mismatch: rebuild for the selected analyzer worker") + + +@ensure(lambda result, plan: result.descriptor["project_identity"] == plan.identity) +def load_runtime(path: Path, *, plan: ProjectPlan, environment_id: str, worker_identity: str) -> PreparedRuntime: + """Verify all artifact bytes and current inputs before returning any mount root.""" + descriptor = _read_descriptor(path) + _verify_context(descriptor, plan, environment_id, worker_identity) + root = path.absolute().parent + if descriptor.get("payload") != _payload_manifest(root): + raise ProjectRuntimeError("project_runtime_payload_identity_mismatch: rebuild the corrupt artifact") + return PreparedRuntime(root, path.absolute(), str(descriptor["identity"]), descriptor) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py new file mode 100644 index 00000000..22d3a83e --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py @@ -0,0 +1,263 @@ +"""Standard-library-only build driver, executed inside a disposable namespace.""" + +from __future__ import annotations + +import json +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit, urlunsplit + + +ROOT = Path("/opt/specfact/output") + + +def clean_inventory(raw: dict) -> dict: + """Retain dependency graph facts, stripping URI credentials and arbitrary metadata.""" + + def redact(value: Any) -> Any: + if isinstance(value, str): + + def safe_url(match): + parsed = urlsplit(match.group(0)) + host = parsed.hostname or "" + if parsed.port: + host += f":{parsed.port}" + return urlunsplit((parsed.scheme, host, parsed.path, "", "")) + + return re.sub(r"(?:https?|git\+https)://[^\s]+", safe_url, value) + if isinstance(value, list): + return [redact(item) for item in value] + if isinstance(value, dict): + return {key: redact(item) for key, item in value.items()} + return value + + installed = [] + for distribution in raw.get("installed", []): + metadata = distribution.get("metadata", {}) + installed.append( + { + "metadata": { + key: metadata[key] + for key in ("name", "version", "requires_python", "requires_dist") + if key in metadata + }, + "requested": distribution.get("requested", False), + "direct_url": distribution.get("direct_url", {}), + } + ) + return redact( + { + "version": raw.get("version"), + "pip_version": raw.get("pip_version"), + "environment": raw.get("environment", {}), + "installed": installed, + } + ) + + +def _log_output(stdout: str | bytes | None, stderr: str | bytes | None) -> None: + """Forward native diagnostics to the controller's already-open private log.""" + for output in (stdout, stderr): + if output: + sys.stdout.write(output.decode("utf-8", errors="replace") if isinstance(output, bytes) else output) + sys.stdout.flush() + + +def _run(command: list[str], *, env: dict[str, str] | None = None) -> str: + try: + result = subprocess.run( + command, cwd=ROOT / "project", env=env, text=True, capture_output=True, check=False, timeout=3600 + ) + except subprocess.TimeoutExpired as exc: + _log_output(exc.stdout, exc.stderr) + raise + _log_output(result.stdout, result.stderr) + if result.returncode: + raise RuntimeError(f"project_manager_failed:{command[2:4]}:exit={result.returncode}") + return result.stdout.strip() + + +def select_hatch_environment(export: dict, requested: str, python: str) -> str: + """Select a concrete native export entry without expanding resolver semantics.""" + if requested in export: + selected_python = export[requested].get("python") + if selected_python and not (python == selected_python or python.startswith(str(selected_python) + ".")): + raise RuntimeError(f"project_python_incompatible:{requested}:requested={selected_python}; signed={python}") + return requested + candidates = sorted( + name + for name, config in export.items() + if name.startswith(requested + ".") + and (config.get("python") == python or python.startswith(str(config.get("python")) + ".")) + ) + if len(candidates) != 1: + raise RuntimeError( + f"project_environment_ambiguous:{requested}:{','.join(candidates)}; select a concrete Hatch environment" + ) + return candidates[0] + + +def _prepare_hatch(config: dict, tools_python: str, env: dict[str, str]) -> None: + env["HATCH_UV"] = str(Path(tools_python).with_name("uv")) + export = json.loads(_run([tools_python, "-m", "hatch", "env", "show", "--json"], env=env)) + selected = select_hatch_environment(export, str(config["environment"]), ".".join(map(str, sys.version_info[:3]))) + config["environment"] = selected + arguments = export[selected].get("extra-args", []) + if not isinstance(arguments, list) or not all(isinstance(value, str) for value in arguments): + raise RuntimeError("project_hatch_pytest_arguments_invalid:extra-args must be strings") + config["pytest_arguments"] = arguments + config["commands"] = [[tools_python, "-m", "hatch", "env", "create", selected]] + + +def _environment_path(config: dict[str, object], tools_python: str, env: dict[str, str]) -> Path: + if config["manager"] != "hatch": + return ROOT / "env" + result = _run([tools_python, "-m", "hatch", "env", "find", str(config["environment"])], env=env) + path = Path(result.splitlines()[-1]) + if not path.resolve().is_relative_to(ROOT): + raise RuntimeError("project_environment_escape") + return path + + +def _copy_site_packages(python: str, destination: Path) -> None: + paths = json.loads(_run([python, "-c", "import json,site; print(json.dumps(site.getsitepackages()))"])) + destination.mkdir() + for raw in paths: + path = Path(raw) + if not path.is_dir(): + continue + if not path.resolve().is_relative_to(ROOT): + raise RuntimeError("project_site_packages_escape") + shutil.copytree( + path, destination, dirs_exist_ok=True, symlinks=False, ignore=shutil.ignore_patterns("__pycache__", "*.pyc") + ) + # Editable paths refer to the disposable copy. Rebind only that exact source + # prefix; the changed bytes are subsequently included in the artifact identity. + for path in destination.rglob("*"): + if path.is_file() and (path.suffix == ".pth" or path.name.startswith("__editable__")): + text = path.read_text(encoding="utf-8") + path.write_text(text.replace(str(ROOT / "project"), "/opt/specfact/snapshot"), encoding="utf-8") + + +def copy_executables(environment: Path, artifact: Path, records: list[dict]) -> list[dict]: + """Copy only distribution-owned environment commands, never arbitrary host bins.""" + directory = artifact / "bin" + directory.mkdir(exist_ok=True) + copied = [] + for record in records: + source = Path(record["path"]) + if re.fullmatch(r"python(?:[23](?:\.\d+)*)?", source.name) or not source.is_file(): + continue + if source.parent != environment / "bin" or not source.resolve().is_relative_to(environment): + raise RuntimeError(f"project_executable_escape:{source.name}") + destination = directory / source.name + if destination.exists(): + raise RuntimeError(f"project_executable_collision:{source.name}") + content = source.read_bytes() + if content.startswith(b"\x7fELF"): + (artifact / "executables").mkdir(exist_ok=True) + binary = artifact / "executables" / source.name + binary.write_bytes(content) + binary.chmod(0o755) + root = "/opt/specfact/project-runtime" + destination.write_text( + "#!/opt/specfact/python/bin/python\nimport os,sys\n" + + f"root={root!r}\nbinary=root+'/executables/'+{source.name!r}\n" + + "loader=root+'/native/ld-linux-x86-64.so.2'\n" + + "argv=[loader,'--library-path',root+'/native',binary,*sys.argv[1:]] if os.pat" + "h.isfile(loader) else [binary,*sys.argv[1:]]\n" + "os.execv(argv[0],argv)\n", + encoding="utf-8", + ) + elif content.startswith(b"#!") and b"python" in content.split(b"\n", 1)[0]: + destination.write_bytes(b"#!/opt/specfact/project-runtime/bin/python\n" + content.split(b"\n", 1)[1]) + else: + raise RuntimeError( + f"project_executable_unsupported:{source.name}; declare its interpreter/runtime requirements" + ) + destination.chmod(0o755) + copied.append({"name": source.name, "distribution": record["distribution"]}) + return copied + + +def _executable_records(python: str) -> list[dict]: + script = ( + "import importlib.metadata,json,sys; from pathlib import Path; " + "directory=Path(sys.executable).parent; " + "print(json.dumps([{'path':str(path),'distribution':dist.metadata['Name']} " + "for dist in importlib.metadata.distributions() for file in (dist.files or []) " + "if (path:=Path(dist.locate_file(file)).absolute()).resolve().parent==directory.resolve()]))" + ) + records = json.loads(_run([python, "-c", script])) + for record in records: + record["path"] = str(Path(record["path"]).resolve()) + return records + + +def installer_environment(manager: str, inherited: dict[str, str]) -> dict[str, str]: + """Keep one manager's environment controls out of another manager's installer.""" + env = { + key: value + for key, value in inherited.items() + if key not in {"VIRTUAL_ENV", "PIP_PYTHON", "UV_PROJECT_ENVIRONMENT", "POETRY_VIRTUALENVS_CREATE"} + } + env.update( + HATCH_DATA_DIR=str(ROOT / "hatch-data"), + HATCH_CACHE_DIR=str(ROOT / "hatch-cache"), + PATH=str(ROOT / "tools/bin") + ":" + env.get("PATH", ""), + ) + controls = { + "pip": {"PIP_PYTHON": str(ROOT / "env/bin/python")}, + "uv": {"VIRTUAL_ENV": str(ROOT / "env"), "UV_PROJECT_ENVIRONMENT": str(ROOT / "env")}, + "poetry": {"VIRTUAL_ENV": str(ROOT / "env"), "POETRY_VIRTUALENVS_CREATE": "false"}, + "hatch": {}, + } + env.update(controls[manager]) + return env + + +def main() -> None: + config = json.loads((ROOT / "build.json").read_text(encoding="utf-8")) + interpreter = str(config["interpreter"]) + _run([interpreter, "-m", "venv", str(ROOT / "tools")]) + tools_python = str(ROOT / "tools/bin/python") + _run([tools_python, "-m", "pip", "install", "--disable-pip-version-check", *config["manager_requirements"]]) + _run([interpreter, "-m", "venv", str(ROOT / "env")]) + env = installer_environment(config["manager"], dict(os.environ)) + if config["manager"] == "hatch": + _prepare_hatch(config, tools_python, env) + for command in config["commands"]: + _run(command, env=env) + environment = _environment_path(config, tools_python, env) + python = str(environment / "bin/python") + inventory = _run([tools_python, "-m", "pip", "--python", python, "inspect", "--local"]) + artifact = ROOT / "artifact" + artifact.mkdir() + (artifact / "worker-config").mkdir() + (artifact / "worker-config/hosts").write_text("127.0.0.1 localhost\n::1 localhost\n", encoding="ascii") + _copy_site_packages(python, artifact / "site-packages") + facts = clean_inventory(json.loads(inventory)) + facts["resolved_environment"] = config["environment"] + facts["pytest_arguments"] = config.get("pytest_arguments", []) + facts["executables"] = copy_executables(environment, artifact, _executable_records(python)) + (artifact / "inventory.json").write_text(json.dumps(facts) + "\n", encoding="utf-8") + (artifact / "bin").mkdir(exist_ok=True) + launcher = artifact / "bin/python" + launcher.write_text( + "#!/opt/specfact/python/bin/python\nimport runpy\nrunpy.run_path('/opt/specfact" + "/builtin/specfact_code_review/run/target_launch.py', run_name='__main__')\n", + encoding="utf-8", + ) + launcher.chmod(0o755) + for name in ("uv.lock", "poetry.lock", "pylock.toml"): + path = ROOT / "project" / name + if path.is_file(): + (artifact / name).write_bytes(path.read_bytes()) + + +if __name__ == "__main__": + main() diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py new file mode 100644 index 00000000..88e540c3 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_builder.py @@ -0,0 +1,339 @@ +"""Prepare private dependency artifacts using the verified namespace launcher.""" + +from __future__ import annotations + +import errno +import json +import os +import re +import selectors +import shutil +import subprocess +import tempfile +import time +from pathlib import Path +from typing import IO, Any + +from icontract import ensure, require +from packaging.specifiers import SpecifierSet + +from specfact_code_review.run import sandbox +from specfact_code_review.run.runtime_adapters import MANAGER_REQUIREMENTS, install_commands +from specfact_code_review.run.runtime_artifacts import load_runtime, seal_runtime, validate_build_artifact +from specfact_code_review.run.runtime_compatibility import analyzer_dependency_conflicts +from specfact_code_review.run.runtime_domains import member_dependency_graphs +from specfact_code_review.run.runtime_git import git_identity, stage_git +from specfact_code_review.run.runtime_models import ( + PreparedRuntime, + ProjectPlan, + ProjectRuntimeError, + content_digest, + document_digest, +) +from specfact_code_review.run.runtime_native import inventory_native +from specfact_code_review.run.runtime_sources import ( + is_excluded_source, + source_identity, + source_link_target, + verify_inputs, +) +from specfact_code_review.run.runtime_tools import capture_native_tools, install_native_tools +from specfact_code_review.run.runtime_trust import capture_public_trust, install_public_trust +from specfact_code_review.run.runtime_vcs import copy_vcs_context + + +_BUILDER_FILES = ( + "runtime_build_driver.py", + "runtime_builder.py", + "runtime_artifacts.py", + "runtime_native.py", + "runtime_tools.py", + "runtime_trust.py", + "runtime_adapters.py", + "runtime_domains.py", + "runtime_compatibility.py", + "runtime_vcs.py", + "runtime_git.py", + "target_bootstrap.py", + "target_launch.py", + "target_pytest.py", + "target_coverage.py", + "installed_coverage.py", + "target_pylint.py", + "sitecustomize.py", +) +_BUILD_TIMEOUT_SECONDS = 4500 + + +def _stream_builder_output(process: subprocess.Popen[bytes], build_log: IO[bytes], deadline: float) -> None: + """Drain bounded binary chunks without exposing the destination file to children.""" + assert process.stdout is not None + os.set_blocking(process.stdout.fileno(), False) + with selectors.DefaultSelector() as selector: + selector.register(process.stdout, selectors.EVENT_READ) + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise subprocess.TimeoutExpired(process.args, _BUILD_TIMEOUT_SECONDS) + if not selector.select(remaining): + raise subprocess.TimeoutExpired(process.args, _BUILD_TIMEOUT_SECONDS) + try: + chunk = os.read(process.stdout.fileno(), 65536) + except BlockingIOError: + continue + if not chunk: + return + build_log.write(chunk) + build_log.flush() + + +def _run_logged_builder(command: list[str], descriptor: int, build_log: IO[bytes]) -> int: + """Keep the private log inode in the controller while supervising one child.""" + deadline = time.monotonic() + _BUILD_TIMEOUT_SECONDS + with subprocess.Popen(command, pass_fds=(descriptor,), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process: + try: + _stream_builder_output(process, build_log, deadline) + return process.wait(timeout=max(0.0, deadline - time.monotonic())) + finally: + if process.poll() is None: + process.kill() + process.wait() + + +@require(lambda source, destination: source.is_dir() and not destination.exists()) +def copy_project(source: Path, destination: Path, *, commit: str = "HEAD", include_vcs: bool = True) -> None: + """Copy inputs into private build storage without dereferencing source links.""" + + expected = source_identity(source) + + def ignored(directory: str, names: list[str]) -> set[str]: + excluded = {name for name in names if is_excluded_source(Path(directory) / name)} + for name in set(names) - excluded: + path = Path(directory) / name + if path.is_symlink(): + source_link_target(path, source) + return excluded + + # Never dereference aliases while reading the source. Validate the complete + # copied tree before a builder can access it, including concurrent link edits. + shutil.copytree(source, destination, ignore=ignored, symlinks=True) + for path in destination.rglob("*"): + if path.is_symlink(): + original = source / path.relative_to(destination) + target = source_link_target(original, source) + path.unlink() + path.symlink_to(os.path.relpath(destination / target.relative_to(source), path.parent)) + if source_identity(destination) != expected: + raise ProjectRuntimeError("project_runtime_source_changed_during_copy") + if include_vcs: + copy_vcs_context(source, destination, commit) + + +@ensure(lambda result: "--clearenv" in result and "--unshare-all" in result) +def builder_command(runtime: Any, *, staging: Path, executable: str) -> list[str]: + """Construct a private network-enabled acquisition namespace, not an analyzer launch.""" + command = [ + executable, + "--die-with-parent", + "--new-session", + "--unshare-all", + "--share-net", + "--ro-bind", + str(runtime.root), + "/", + "--bind", + str(staging), + "/opt/specfact/output", + "--proc", + "/proc", + "--dev", + "/dev", + "--tmpfs", + "/tmp", + "--clearenv", + "--setenv", + "HOME", + "/opt/specfact/output/home", + "--setenv", + "TMPDIR", + "/tmp", + "--setenv", + "PATH", + "/opt/specfact/output/builder-tools/bin:/opt/specfact/python/bin:/usr/bin:/bin", + "--setenv", + "GIT_EXEC_PATH", + "/opt/specfact/output/builder-tools/git-core", + "--setenv", + "GIT_CONFIG_NOSYSTEM", + "1", + "--setenv", + "SSL_CERT_FILE", + "/etc/ssl/certs/ca-certificates.crt", + "--chdir", + "/opt/specfact/output/project", + ] + for path in ("/etc/resolv.conf", "/etc/ssl/certs"): + if Path(path).exists(): + command.extend(("--ro-bind", path, path)) + command.extend((runtime.interpreter, "/opt/specfact/output/build_driver.py")) + return command + + +def _build(plan: ProjectPlan, runtime: Any, staging: Path, *, build_log: IO[bytes] | None = None) -> Path: + verify_inputs(plan) + copy_project(plan.root, staging / "project", include_vcs=False) + if source_identity(staging / "project") != plan.source_identity: + raise ProjectRuntimeError("project_runtime_source_changed_during_copy") + if plan.vcs: + copy_vcs_context( + plan.vcs_repository or plan.root, + staging / "project", + plan.vcs["commit"], + tree=plan.vcs.get("tree"), + bound_vcs=plan.vcs, + ) + stage_git(staging) + (staging / "home").mkdir() + driver = Path(__file__).with_name("runtime_build_driver.py") + shutil.copyfile(driver, staging / "build_driver.py") + config = { + "manager": plan.manager, + "environment": plan.environment, + "interpreter": runtime.interpreter, + "manager_requirements": MANAGER_REQUIREMENTS[plan.manager], + "commands": install_commands(plan, python="/opt/specfact/output/tools/bin/python"), + } + (staging / "build.json").write_text(json.dumps(config), encoding="utf-8") + descriptor = sandbox._verified_bubblewrap_descriptor(runtime.root, runtime.bubblewrap) + try: + command = builder_command(runtime, staging=staging, executable=f"/proc/self/fd/{descriptor}") + if build_log is not None: + returncode = _run_logged_builder(command, descriptor, build_log) + else: + returncode = subprocess.run( + command, + pass_fds=(descriptor,), + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + check=False, + timeout=_BUILD_TIMEOUT_SECONDS, + ).returncode + finally: + os.close(descriptor) + if returncode: + raise ProjectRuntimeError(f"project_runtime_prepare_failed:exit={returncode}") + if not (staging / "artifact/site-packages").is_dir(): + raise ProjectRuntimeError("project_runtime_prepare_incomplete") + return staging / "artifact" + + +def _validate_python(plan: ProjectPlan, environment: str) -> None: + lock = json.loads( + (Path(__file__).parents[1] / "resources/contracts/pr-range-v1-toolchain-lock.json").read_text(encoding="utf-8") + ) + versions = {row["environment_id"]: row["python_version"] for row in lock["environments"]} + version = versions.get(environment) + if version is None or (plan.requires_python and version not in SpecifierSet(plan.requires_python)): + raise ProjectRuntimeError(f"project_python_incompatible:{environment}:{plan.requires_python}") + if plan.python and not (version == plan.python or version.startswith(plan.python + ".")): + raise ProjectRuntimeError(f"project_python_incompatible:{version}:{plan.python}") + + +@require(lambda artifact, destination: artifact != destination) +def publish_artifact(artifact: Path, destination: Path) -> None: + """Allow a concurrent cache winner, which the caller must fully verify.""" + try: + os.rename(artifact, destination) + except OSError as exc: + if exc.errno not in {errno.EEXIST, errno.ENOTEMPTY}: + raise + + +def _public_build_failure_reason(error: BaseException) -> str: + """Expose stable controller codes without arbitrary exception details or paths.""" + if not isinstance(error, ProjectRuntimeError): + return type(error).__name__ + code, _, detail = str(error).partition(":") + code = code.partition(";")[0] + if re.fullmatch(r"project_[a-z0-9_]+", code) is None: + return type(error).__name__ + if code == "project_runtime_prepare_failed" and re.fullmatch(r"exit=-?[0-9]+", detail): + return f"{code}:{detail}" + return code + + +@ensure(lambda result, plan: result.descriptor["project_identity"] == plan.identity) +def prepare_runtime( + plan: ProjectPlan, + *, + runtime: Any, + cache_root: Path | None = None, + offline: bool = False, +) -> PreparedRuntime: + """Build or validate reusable local dependencies for the exact capsule ABI.""" + environment = str(runtime.environment_id) + _validate_python(plan, environment) + cache = cache_root or Path.home() / ".cache/specfact/code-review/project-runtimes-v2" + cache.mkdir(parents=True, exist_ok=True, mode=0o700) + if cache.is_symlink(): + raise ProjectRuntimeError("project_runtime_cache_symlink") + native_tools = capture_native_tools(plan.native_tools, capsule_root=getattr(runtime, "root", None)) + public_trust = capture_public_trust(getattr(runtime, "root", None)) + key = document_digest( + { + "project": plan.identity, + "environment": environment, + "worker": runtime.identity, + "manager": MANAGER_REQUIREMENTS[plan.manager], + "builder": {name: content_digest(Path(__file__).with_name(name).read_bytes()) for name in _BUILDER_FILES}, + "git": git_identity(), + "native_tools": native_tools.identity, + "public_trust": content_digest(public_trust), + } + )[7:] + destination = cache / key + descriptor_path = destination / "project-runtime.json" + if destination.exists(): + return load_runtime(descriptor_path, plan=plan, environment_id=environment, worker_identity=runtime.identity) + if offline: + raise ProjectRuntimeError("project_runtime_offline_cache_miss: run runtime prepare with network first") + with tempfile.TemporaryDirectory(prefix=".preparing-", dir=cache) as directory: + staging = Path(directory) + # The exclusive 0600 file is outside the builder's writable staging mount. + # Never reopen a path supplied by the builder, including for log retention. + with tempfile.NamedTemporaryFile( + mode="wb", prefix=f"failed-{key}-", suffix=".log", dir=cache, delete=False + ) as build_log: + try: + artifact = _build(plan, runtime, staging, build_log=build_log.file) + validate_build_artifact(artifact) + except (OSError, ValueError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError( + f"project_runtime_prepare_failed; private log: {build_log.name}; " + f"{_public_build_failure_reason(exc)}" + ) from exc + try: + verify_inputs(plan) + inventory = json.loads((artifact / "inventory.json").read_text(encoding="utf-8")) + inventory["native_libraries"] = inventory_native( + artifact, capsule_root=runtime.root, declared=plan.native_libraries, target_loader=True + ) + install_native_tools(native_tools, artifact) + inventory["native_tools"] = native_tools.inventory + inventory["public_trust"] = install_public_trust(public_trust, artifact) + inventory["analyzer_conflicts"] = analyzer_dependency_conflicts( + inventory, runtime.root / "opt/specfact/analyzers" + ) + inventory["member_graphs"] = member_dependency_graphs(inventory, runtime.root / "opt/specfact/analyzers") + seal_runtime( + artifact, plan=plan, environment_id=environment, worker_identity=runtime.identity, inventory=inventory + ) + publish_artifact(artifact, destination) + # A concurrent preparation must still pass full verification. + prepared = load_runtime( + descriptor_path, plan=plan, environment_id=environment, worker_identity=runtime.identity + ) + except (OSError, ValueError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError(f"{exc}; private log: {build_log.name}") from exc + Path(build_log.name).unlink() + return prepared diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_commands.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_commands.py new file mode 100644 index 00000000..d93a48db --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_commands.py @@ -0,0 +1,73 @@ +"""Customer-facing inspection and preparation of portable Python runtimes.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Annotated + +import typer +from icontract import require + +from specfact_code_review.run.runtime_builder import prepare_runtime +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_interpreter import select_environment +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +app = typer.Typer(help="Inspect and prepare an isolated runtime for this Python project.", no_args_is_help=True) + + +@app.command("inspect") +@require(lambda project_config: project_config is None or isinstance(project_config, Path)) +def inspect_runtime( + project_config: Annotated[Path | None, typer.Option("--project-config")] = None, + json_output: Annotated[bool, typer.Option("--json")] = False, +) -> None: + """Discover dependencies and configuration without installing or executing code.""" + try: + plan = discover_project(Path.cwd(), config_path=project_config) + except ProjectRuntimeError as exc: + raise typer.BadParameter(str(exc)) from exc + data = {**plan.document(), "identity": plan.identity} + typer.echo( + json.dumps(data, indent=2) + if json_output + else f"Project manager: {plan.manager}\nInput identity: {plan.identity}" + ) + + +@app.command("prepare") +@require(lambda project_config: project_config is None or isinstance(project_config, Path)) +def prepare_project( + project_config: Annotated[Path | None, typer.Option("--project-config")] = None, + json_output: Annotated[bool, typer.Option("--json")] = False, + offline: Annotated[bool, typer.Option("--offline")] = False, +) -> None: + """Prepare or verify a cached runtime using the signed capsule interpreter.""" + from specfact_code_review.run.runner import ( + _capsule_environment_id, + _cleanup_capsule_runtime, + _prepare_capsule_runtime, + ) + + runtime = None + try: + plan = discover_project(Path.cwd(), config_path=project_config) + selected = select_environment(plan, current=_capsule_environment_id()) + runtime, reason = _prepare_capsule_runtime(environment_id=selected) + if runtime is None: + raise ProjectRuntimeError(reason) + prepared = prepare_runtime(plan, runtime=runtime, offline=offline) + result = { + "descriptor": str(prepared.descriptor_path), + "identity": prepared.identity, + "project": plan.document(), + "authority": "local_build", + } + typer.echo(json.dumps(result, indent=2) if json_output else str(prepared.descriptor_path)) + except (OSError, ValueError) as exc: + raise typer.BadParameter(str(exc)) from exc + finally: + if runtime is not None: + _cleanup_capsule_runtime(runtime) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_compatibility.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_compatibility.py new file mode 100644 index 00000000..0e17872f --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_compatibility.py @@ -0,0 +1,78 @@ +"""Compare member dependency requirements with the project graph without importing it.""" + +from __future__ import annotations + +from importlib.metadata import Distribution, distributions +from pathlib import Path +from typing import Any + +from icontract import ensure +from packaging.markers import default_environment +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + + +MEMBER_DISTRIBUTIONS = { + "pylint": ("pylint", frozenset({"pylint", "astroid"})), + "basedpyright": ("basedpyright", frozenset({"basedpyright", "nodejs-wheel-binaries"})), + "contracts": ("crosshair-tool", frozenset({"crosshair-tool", "z3-solver"})), +} + + +def _requirements_for( + name: str, distribution: Distribution, project: dict[str, Any] | None, pinned: frozenset[str] +) -> list[str]: + if project and name not in pinned: + return list(project.get("requires_dist", []) or []) + return list(distribution.requires or []) + + +def _member_conflicts( + member: str, target: dict[str, Any], sealed: dict[str, Distribution], environment: dict[str, str] +) -> list[str]: + entry, pinned = MEMBER_DISTRIBUTIONS[member] + pending = [entry] + visited = set() + reasons = [] + while pending: + name = pending.pop() + if name in visited or name not in sealed: + continue + visited.add(name) + distribution = sealed[name] + project = target.get(name) + if project and name in pinned and project["version"] != distribution.version: + reasons.append(f"{name}: target={project['version']}, sealed entry={distribution.version}") + for raw in _requirements_for(name, distribution, project, pinned): + requirement = Requirement(raw) + if requirement.marker and not requirement.marker.evaluate(environment): + continue + dependency = str(canonicalize_name(requirement.name)) + actual = target.get(dependency) + if actual and not requirement.specifier.contains(actual["version"], prereleases=True): + reasons.append(f"{dependency}: target={actual['version']}, required={requirement.specifier} by {name}") + pending.append(dependency) + return reasons + + +@ensure(lambda result: set(result) <= set(MEMBER_DISTRIBUTIONS)) +def analyzer_dependency_conflicts(inventory: dict[str, Any], analyzer_root: Path) -> dict[str, str]: + """Name genuine per-member incompatibilities, allowing compatible target overrides.""" + target = { + str(canonicalize_name(row["metadata"]["name"])): row["metadata"] for row in inventory.get("installed", []) + } + sealed = {str(canonicalize_name(dist.metadata["Name"])): dist for dist in distributions(path=[str(analyzer_root)])} + environment = { + **default_environment(), + "sys_platform": "linux", + "platform_system": "Linux", + "platform_machine": "x86_64", + **inventory.get("environment", {}), + "extra": "", + } + conflicts = {} + for member in MEMBER_DISTRIBUTIONS: + reasons = _member_conflicts(member, target, sealed, environment) + if reasons: + conflicts[member] = "project_analyzer_dependency_incompatible:" + "; ".join(sorted(set(reasons))) + return conflicts diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py new file mode 100644 index 00000000..d0fb3fa8 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_discovery.py @@ -0,0 +1,447 @@ +"""Discover Python project inputs without importing or executing project code.""" + +from __future__ import annotations + +import configparser +import os +import shlex +import sys +import tomllib +from pathlib import Path +from typing import Any + +from icontract import require +from packaging.specifiers import InvalidSpecifier, SpecifierSet + +from specfact_code_review.run.runtime_adapters import validate_adapter_inputs +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError, content_digest +from specfact_code_review.run.runtime_sources import source_identity +from specfact_code_review.run.runtime_tools import validate_native_tools +from specfact_code_review.run.runtime_vcs import vcs_context + + +PROJECT_INPUT_NAMES = ( + "pyproject.toml", + "hatch.toml", + "uv.toml", + "uv.lock", + "poetry.lock", + "pylock.toml", + "setup.py", + "setup.cfg", + ".python-version", + "pytest.ini", + ".pytest.ini", + "pytest.toml", + ".pytest.toml", + "tox.ini", + "pyrightconfig.json", + "basedpyrightconfig.json", + "ruff.toml", + ".ruff.toml", + ".pylintrc", + "pylintrc", +) +_CONFIG_FIELDS = frozenset( + { + "manager", + "environment", + "python", + "groups", + "extras", + "requirements", + "constraints", + "source_roots", + "native_libraries", + "native_tools", + } +) +_MANAGERS = frozenset({"pip", "hatch", "uv", "poetry"}) +_PROJECT_TABLES = ( + "tool", + "project", + "dependency-groups", + "project.optional-dependencies", + "tool.specfact", + "tool.specfact.code-review", + "tool.uv", + "tool.hatch", + "tool.hatch.envs", + "tool.poetry", + "tool.poetry.dependencies", + "tool.poetry.group", + "tool.poetry.extras", +) + + +def _safe_input(root: Path, relative: str) -> Path: + path = Path(relative) + if path.is_absolute() or ".." in path.parts: + raise ProjectRuntimeError(f"project_input_escape:{relative}; use repository-relative inputs") + target = root / path + if any(parent.is_symlink() for parent in (target, *target.parents) if parent != root.parent): + raise ProjectRuntimeError(f"project_input_symlink:{relative}") + if not target.resolve().is_relative_to(root): + raise ProjectRuntimeError(f"project_input_escape:{relative}") + return target + + +def _toml(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + try: + return tomllib.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as exc: + raise ProjectRuntimeError(f"project_config_invalid:{path.name}:{exc}") from exc + + +def _metadata_tables(path: Path, tables: tuple[str, ...]) -> dict[str, Any]: + document = _toml(path) + for table in tables: + value = document + parts = table.split(".") + for index, part in enumerate(parts): + value = value.get(part, {}) + if not isinstance(value, dict): + section = ".".join(parts[: index + 1]) + raise ProjectRuntimeError(f"project_config_invalid:{path.name}:{section}; expected a table") + return document + + +def _validate_config_value(name: str, value: Any) -> None: + if name in {"manager", "environment", "python"}: + if not isinstance(value, str) or not value: + raise ProjectRuntimeError(f"project_config_invalid:{name} must be a nonempty string") + return + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise ProjectRuntimeError(f"project_config_invalid:{name} must be an array of strings") + + +def _config(path: Path | None) -> dict[str, Any]: + if path is None: + return {} + if path.is_symlink() or not path.is_file(): + raise ProjectRuntimeError("project_config_invalid: expected a regular TOML file") + values = _toml(path) + unknown = sorted(set(values) - _CONFIG_FIELDS) + if unknown: + raise ProjectRuntimeError(f"project_config_unknown_fields:{','.join(unknown)}") + for name, value in values.items(): + _validate_config_value(name, value) + return values + + +def _hatch_environment_exists(name: str, environments: dict[str, Any]) -> bool: + """Hatch always defines default, even without its explicit configuration table.""" + return name == "default" or name in environments + + +def _active_selection( + root: Path, project: dict[str, Any], hatch: dict[str, Any], *, snapshot: bool = False +) -> dict[str, str]: + """Trust manager activation only when it belongs to this running interpreter.""" + prefix = os.environ.get("VIRTUAL_ENV", "") + if not prefix or Path(prefix).resolve() != Path(sys.prefix).resolve(): + return {} + local_environment = any((root / name).resolve() == Path(prefix).resolve() for name in (".venv", "venv")) + if not local_environment or not (Path(prefix) / "pyvenv.cfg").is_file(): + return {} + selected = {} + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + active = os.environ.get("HATCH_ENV_ACTIVE", "") + if active and (snapshot or _hatch_environment_exists(active, environments)): + selected = {"manager": "hatch", "environment": active} + if os.environ.get("POETRY_ACTIVE") == "1": + if selected: + raise ProjectRuntimeError("project_active_environment_ambiguous:hatch,poetry; use --project-config") + selected = {"manager": "poetry"} + return selected + + +def _manager(root: Path, project: dict[str, Any], hatch: dict[str, Any], config: dict[str, Any]) -> str: + explicit = config.get("manager") + if explicit is not None: + if explicit not in _MANAGERS: + raise ProjectRuntimeError(f"project_manager_unsupported:{explicit}") + return str(explicit) + tools = project.get("tool", {}) + candidates = set() + if (root / "uv.lock").exists() or (root / "uv.toml").exists() or "uv" in tools: + candidates.add("uv") + if (root / "poetry.lock").exists() or "poetry" in tools: + candidates.add("poetry") + if hatch.get("envs") or tools.get("hatch", {}).get("envs"): + candidates.add("hatch") + if len(candidates) > 1: + raise ProjectRuntimeError( + f"project_manager_ambiguous:{','.join(sorted(candidates))}; select manager in --project-config" + ) + return next(iter(candidates), "pip") + + +def _requirement_include(line: str) -> str: + tokens = shlex.split(line, comments=True) + if not tokens: + return "" + flag = tokens[0] + if flag in {"-r", "--requirement", "-c", "--constraint"} and len(tokens) >= 2: + return tokens[1] + if flag.startswith(("--requirement=", "--constraint=")): + return flag.split("=", 1)[1] + if flag.startswith(("-r", "-c")) and len(flag) > 2: + return flag[2:] + return "" + + +def _requirements_inputs(root: Path, names: tuple[str, ...]) -> set[str]: + visited: set[str] = set() + + def visit(name: str, active: frozenset[str]) -> None: + path = _safe_input(root, name) + canonical = path.relative_to(root).as_posix() + if canonical in active: + raise ProjectRuntimeError(f"project_requirements_cycle:{canonical}") + if canonical in visited: + return + if not path.is_file(): + raise ProjectRuntimeError(f"project_input_missing:{canonical}") + visited.add(canonical) + if path.name == "pylock.toml" or (path.name.startswith("pylock.") and path.suffix == ".toml"): + return + logical_lines = path.read_text(encoding="utf-8").replace("\\\n", "").splitlines() + for line in logical_lines: + included = _requirement_include(line) + if not included: + continue + if "://" in included: + raise ProjectRuntimeError( + "project_requirements_remote_include_unsupported; provide a repository-local requirements include" + ) + child = Path(included) + if child.is_absolute(): + raise ProjectRuntimeError(f"project_input_escape:{included}") + relative = os.path.relpath(path.parent / child, root) + visit(relative, active | {canonical}) + + for name in names: + visit(name, frozenset()) + return visited + + +def _ini(path: Path) -> configparser.ConfigParser: + parser = configparser.ConfigParser(interpolation=None) + try: + parser.read(path, encoding="utf-8") + except (UnicodeError, configparser.Error) as exc: + raise ProjectRuntimeError(f"project_config_invalid:{path.name}:{exc}") from exc + return parser + + +def _validated_pytest_paths(configuration: dict[str, Any], source: str) -> dict[str, Any]: + for option in ("pythonpath", "testpaths", "python_files", "norecursedirs"): + value = configuration.get(option, "") + if isinstance(value, str): + try: + shlex.split(value) + except ValueError as exc: + raise ProjectRuntimeError( + f"project_pytest_config_invalid:{source}:{option}; invalid argument quoting" + ) from exc + continue + if isinstance(value, list) and all(isinstance(item, str) for item in value): + continue + raise ProjectRuntimeError( + f"project_pytest_config_invalid:{source}:{option}; expected a string or list of strings" + ) + return configuration + + +def _pytest_table(value: Any, source: str, section: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise ProjectRuntimeError(f"project_pytest_config_invalid:{source}:{section}; expected a table") + return value + + +def _pytest_config(root: Path, project: dict[str, Any]) -> dict[str, Any]: + for name in ("pytest.toml", ".pytest.toml"): + if (root / name).exists(): + return _validated_pytest_paths(_pytest_table(_toml(root / name).get("pytest", {}), name, "pytest"), name) + for name in ("pytest.ini", ".pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg"): + if name == "pyproject.toml": + pytest = _pytest_table(project.get("tool", {}).get("pytest", {}), name, "tool.pytest") + if pytest: + options = _pytest_table(pytest.get("ini_options", pytest), name, "tool.pytest.ini_options") + return _validated_pytest_paths(options, name) + continue + path = root / name + if not path.exists(): + continue + parser = _ini(path) + section = "tool:pytest" if name == "setup.cfg" else "pytest" + if parser.has_section(section): + return _validated_pytest_paths(dict(parser[section]), name) + return {} + + +def _source_roots(root: Path, config: dict[str, Any], pytest: dict[str, Any]) -> tuple[str, ...]: + # File discovery is independent: import paths must come from declared runtime configuration. + roots = config.get("source_roots", pytest.get("pythonpath", ())) + if isinstance(roots, str): + roots = shlex.split(roots) + result = tuple(dict.fromkeys(roots)) + for relative in result: + _safe_input(root, relative) + return result + + +def _selection( + project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], manager: str +) -> tuple[str, tuple[str, ...]]: + """Select only a single declared environment/test group when no override exists.""" + environment = values.get("environment", "default") + if manager == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + if len(environments) > 1: + raise ProjectRuntimeError( + f"project_environment_ambiguous:{','.join(sorted(environments))}; " + "select environment in --project-config" + ) + environment = next(iter(environments), "default") + if "groups" in values: + return environment, tuple(values["groups"]) + if manager == "hatch" or "extras" in values: + return environment, () + groups = project.get("dependency-groups", {}) + if manager == "poetry": + groups = project.get("tool", {}).get("poetry", {}).get("group", groups) + tests = sorted(set(groups) & {"test", "tests", "testing"}) + if len(tests) > 1: + raise ProjectRuntimeError(f"project_test_groups_ambiguous:{','.join(tests)}; select groups in --project-config") + return environment, tuple(tests) + + +def _default_requirements(root: Path) -> list[str]: + texts = [name for name in ("requirements.txt", "requirements.in") if (root / name).is_file()] + if (root / "pylock.toml").is_file() and texts: + raise ProjectRuntimeError( + f"project_requirements_ambiguous:pylock.toml,{','.join(texts)}; select requirements in --project-config" + ) + for name in ("pylock.toml", "requirements.txt", "requirements.in"): + if (root / name).is_file(): + return [name] + return [] + + +def _python_constraint(root: Path, project: dict[str, Any], manager: str) -> str: + declared = project.get("project", {}).get("requires-python") + if declared is None: + declared = _ini(root / "setup.cfg").get("options", "python_requires", fallback="") + poetry = project.get("tool", {}).get("poetry", {}).get("dependencies", {}).get("python") + if manager != "poetry" or poetry is None: + return str(declared) + remedy = ( + "project_python_constraint_unsupported:tool.poetry.dependencies.python=" + f"{poetry!r}; express the declaration as an equivalent PEP440 version range" + ) + if not isinstance(poetry, str): + raise ProjectRuntimeError(remedy) + try: + constraint = SpecifierSet(poetry) + except InvalidSpecifier as exc: + raise ProjectRuntimeError(remedy) from exc + return str(SpecifierSet(str(declared)) & constraint) + + +def _test_extras( + project: dict[str, Any], values: dict[str, Any], manager: str, groups: tuple[str, ...] +) -> tuple[str, ...]: + if "extras" in values: + return tuple(values["extras"]) + if manager == "hatch" or "groups" in values: + return () + declared = project.get("project", {}).get("optional-dependencies", {}) + if manager == "poetry": + declared = {**project.get("tool", {}).get("poetry", {}).get("extras", {}), **declared} + candidates = sorted(set(declared) & {"test", "tests", "testing"}) + if len(candidates) > 1 or (candidates and groups): + choices = [*("extra:" + name for name in candidates), *("group:" + name for name in groups)] + raise ProjectRuntimeError( + f"project_test_dependencies_ambiguous:{','.join(choices)}; select groups/extras in --project-config" + ) + return tuple(candidates) + + +def _project_selection( + root: Path, project: dict[str, Any], hatch: dict[str, Any], values: dict[str, Any], activation_root: Path | None +) -> dict[str, Any]: + """Combine explicit selection with independently verified snapshot activation.""" + activation_root = root if activation_root is None else activation_root.resolve() + selection = ( + {**_active_selection(activation_root, project, hatch, snapshot=activation_root != root), **values} + if "manager" not in values + else values + ) + if activation_root != root and selection.get("manager") == "hatch" and "environment" not in values: + environments = hatch.get("envs", project.get("tool", {}).get("hatch", {}).get("envs", {})) + active = selection.get("environment") + if active and not _hatch_environment_exists(active, environments): + raise ProjectRuntimeError( + f"project_active_environment_missing:{active}; select an environment in --project-config" + ) + return selection + + +@require(lambda root: root.is_dir()) +def discover_project( + root: Path, *, config_path: Path | None = None, activation_root: Path | None = None +) -> ProjectPlan: + """Inspect project manifests; never install dependencies or import setup code.""" + root = root.resolve() + values = _config(config_path) + inputs: dict[str, str] = {} + for name in PROJECT_INPUT_NAMES: + path = _safe_input(root, name) + if path.is_file(): + inputs[name] = content_digest(path.read_bytes()) + project = _metadata_tables(root / "pyproject.toml", _PROJECT_TABLES) + hatch = _metadata_tables(root / "hatch.toml", ("envs",)) + selection = _project_selection(root, project, hatch, values, activation_root) + manager = _manager(root, project, hatch, selection) + requirements = tuple(values.get("requirements", ())) + constraints = tuple(values.get("constraints", [])) + validate_adapter_inputs(manager, requirements, constraints) + if manager == "pip" and "requirements" not in values: + requirements = tuple(_default_requirements(root)) + for name in sorted(_requirements_inputs(root, requirements + constraints)): + inputs[name] = content_digest((root / name).read_bytes()) + pytest = _pytest_config(root, project) + for option in ("testpaths", "pythonpath"): + paths = pytest.get(option, []) + for path in shlex.split(paths) if isinstance(paths, str) else paths: + _safe_input(root, path) + environment, groups = _selection(project, hatch, selection, manager) + return ProjectPlan( + root=root, + source_identity=source_identity(root), + manager=manager, + environment=environment, + vcs=vcs_context(root), + python=values.get( + "python", (root / ".python-version").read_text().strip() if (root / ".python-version").is_file() else "" + ), + requires_python=_python_constraint(root, project, manager), + groups=groups, + extras=_test_extras(project, selection, manager, groups), + requirements=requirements, + constraints=constraints, + source_roots=_source_roots(root, values, pytest), + native_libraries=tuple(values.get("native_libraries", [])), + native_tools=validate_native_tools( + values.get( + "native_tools", + project.get("tool", {}).get("specfact", {}).get("code-review", {}).get("native_tools", []), + ) + ), + inputs=inputs, + pytest_config=pytest, + ) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_domains.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_domains.py new file mode 100644 index 00000000..3848ac79 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_domains.py @@ -0,0 +1,135 @@ +"""Record the exact dependency closure admitted to each target execution domain.""" + +from __future__ import annotations + +import re +from importlib.metadata import Distribution, distributions +from pathlib import Path +from typing import Any + +from icontract import ensure +from packaging.markers import default_environment +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + +from specfact_code_review.run.runtime_compatibility import MEMBER_DISTRIBUTIONS +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +_DOMAIN_ENTRIES = { + "pylint": ("pylint",), + "basedpyright": ("basedpyright",), + "crosshair": ("crosshair-tool",), + "pytest-observe": ("pytest", "pytest-cov"), +} +_PINNED = frozenset(name for _, names in MEMBER_DISTRIBUTIONS.values() for name in names) + + +def _runtime_module_name(filename: str) -> str: + """Recognize Python payload names without using the supervisor's native ABI.""" + if filename.endswith((".py", ".pyc")): + name = filename.rsplit(".", maxsplit=1)[0] + elif filename.endswith((".so", ".pyd")): + name, _, suffix = filename.partition(".") + if suffix not in {"so", "pyd", "abi3.so", "abi3.pyd"} and not re.fullmatch( + r"(?:cpython-\d+[a-z]*|cp\d+[a-z]*)-[\w-]+\.(?:so|pyd)", suffix + ): + return "" + else: + return "" + return name if name.isidentifier() else "" + + +def _top_level_imports(distribution: Distribution) -> set[str]: + """Bind ownership to present recorded modules, not stale build metadata.""" + files = distribution.files + if files is None: + raise ProjectRuntimeError(f"project_analyzer_import_inventory_missing:{distribution.metadata['Name']}") + names = set() + for entry in files: + module = _runtime_module_name(entry.name) + if not module or not all(part.isidentifier() for part in entry.parts[:-1]): + continue + if Path(str(distribution.locate_file(entry))).is_file(): + names.add(entry.parts[0] if len(entry.parts) > 1 else module) + return names + + +def _dependency_names( + requirements: list[str], + environment: dict[str, str], + *, + domain: str, + target: dict[str, Any], + sealed: dict[str, Distribution], +) -> list[str]: + names = [] + for raw in requirements: + requirement = Requirement(raw) + if requirement.marker is None or requirement.marker.evaluate(environment): + name = str(canonicalize_name(requirement.name)) + selected = _selected_distribution(name, target, sealed) + if selected is not None and not requirement.specifier.contains(selected[0]["version"], prereleases=True): + raise ProjectRuntimeError( + f"project_analyzer_dependency_incompatible:{domain}:{name}; " + f"selected={selected[0]['version']}, required={requirement.specifier}" + ) + names.append(name) + return names + + +def _selected_distribution( + name: str, target: dict[str, Any], sealed: dict[str, Distribution] +) -> tuple[dict[str, Any], list[str], set[str]] | None: + if name in target and name not in _PINNED: + project = target[name] + return ( + {"name": name, "version": project["version"], "origin": "project"}, + project.get("requires_dist", []) or [], + set(), + ) + distribution = sealed.get(name) + if distribution is None: + return None + return ( + {"name": name, "version": distribution.version, "origin": "analyzer"}, + list(distribution.requires or []), + _top_level_imports(distribution), + ) + + +def _domain_graph( + domain: str, target: dict[str, Any], sealed: dict[str, Distribution], environment: dict[str, str] +) -> dict[str, Any]: + pending = list(_DOMAIN_ENTRIES[domain]) + visited = set() + rows = [] + imports = set() + while pending: + name = pending.pop() + if name in visited: + continue + visited.add(name) + selected = _selected_distribution(name, target, sealed) + if selected is None: + raise ProjectRuntimeError( + f"project_analyzer_dependency_missing:{domain}:{name}; " + "rebuild the runtime with its complete declared dependencies" + ) + row, requirements, admitted_imports = selected + dependencies = _dependency_names(requirements, environment, domain=domain, target=target, sealed=sealed) + rows.append({**row, "dependencies": sorted(dependencies)}) + imports.update(admitted_imports) + pending.extend(dependencies) + return {"installed": sorted(rows, key=lambda row: row["name"]), "sealed_imports": sorted(imports)} + + +@ensure(lambda result: set(result) == set(_DOMAIN_ENTRIES)) +def member_dependency_graphs(inventory: dict[str, Any], analyzer_root: Path) -> dict[str, Any]: + """Admit required analyzer packages without exposing unrelated sealed dependencies.""" + target = { + str(canonicalize_name(row["metadata"]["name"])): row["metadata"] for row in inventory.get("installed", []) + } + sealed = {str(canonicalize_name(dist.metadata["Name"])): dist for dist in distributions(path=[str(analyzer_root)])} + environment = {**default_environment(), **inventory.get("environment", {}), "extra": ""} + return {domain: _domain_graph(domain, target, sealed, environment) for domain in _DOMAIN_ENTRIES} diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py new file mode 100644 index 00000000..831c87c4 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_git.py @@ -0,0 +1,90 @@ +"""Stage Git's HTTP transport and native closure for the disposable builder.""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +from icontract import ensure, require + +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest +from specfact_code_review.run.runtime_native import elf_dependencies, inventory_native + + +GIT = Path("/usr/bin/git") +BUILDER_ROOT = "/opt/specfact/output/builder-tools" +HTTP_HELPERS = ("git-remote-http", "git-remote-https") + + +def _programs() -> dict[str, Path]: + if not GIT.is_file(): + return {} + result = subprocess.run( + [str(GIT), "--exec-path"], env={"PATH": "/usr/bin:/bin"}, capture_output=True, text=True, check=True, timeout=10 + ) + directory = Path(result.stdout.strip()) + programs = {"bin/git": GIT} + for name in HTTP_HELPERS: + path = directory / name + if path.is_file(): + programs[f"git-core/{name}"] = path + return programs + + +@ensure(lambda result: all(value.startswith("sha256:") and len(value) == 71 for value in result.values())) +def git_identity() -> dict[str, str]: + """Bind available transport helpers as well as the main executable.""" + try: + if not GIT.is_file(): + return {} + # Inspection and offline cache lookup must remain usable on non-Linux hosts. + if not GIT.read_bytes().startswith(b"\x7fELF"): + return {"bin/git": content_digest(GIT.read_bytes())} + return {name: content_digest(path.read_bytes()) for name, path in _programs().items()} + except (OSError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError( + f"project_builder_git_identity_failed:{type(exc).__name__}; " + "verify Git and its transport helper installation" + ) from exc + + +@require(lambda staging: staging.is_dir()) +def stage_git(staging: Path) -> None: + """Give each Git program the matching private loader and library search path.""" + programs = _programs() + if not programs: + return + root = staging / "builder-tools" + needed = {"ld-linux-x86-64.so.2"} + for name, source in programs.items(): + if not source.read_bytes().startswith(b"\x7fELF"): + raise ProjectRuntimeError(f"project_builder_tool_not_elf:{name}") + target = root / name + target.parent.mkdir(parents=True, exist_ok=True) + executable = target.with_name(target.name + ".real") + shutil.copyfile(source, executable) + executable.chmod(0o755) + needed.update(elf_dependencies(source)) + target.write_text( + "#!/opt/specfact/python/bin/python\nimport os,sys\n" + f"root={BUILDER_ROOT!r}\n" + "os.environ['GIT_EXEC_PATH']=root+'/git-core'\n" + "loader=root+'/native/ld-linux-x86-64.so.2'\n" + f"os.execv(loader, [loader, '--library-path', root+'/native', root+'/{name}.real', *sys.argv[1:]])\n" + ) + target.chmod(0o755) + for name in HTTP_HELPERS: + if f"git-core/{name}" not in programs: + _stage_missing_helper(root / "git-core" / name, name) + inventory_native(root, capsule_root=staging / "no-capsule-libraries", declared=tuple(sorted(needed))) + + +def _stage_missing_helper(target: Path, name: str) -> None: + """Explain absent transport only when Git attempts to use that protocol.""" + diagnostic = f"project_builder_tool_missing:{name}; install Git HTTP transport helpers" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text( + f"#!/opt/specfact/python/bin/python\nimport sys\nprint({diagnostic!r}, file=sys.stderr)\nsys.exit(127)\n" + ) + target.chmod(0o755) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_interpreter.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_interpreter.py new file mode 100644 index 00000000..c42d5fb7 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_interpreter.py @@ -0,0 +1,63 @@ +"""Select a signed target Python from actual project constraints.""" + +from __future__ import annotations + +import json +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path +from typing import Any + +from icontract import ensure, require +from packaging.specifiers import SpecifierSet + +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError + + +@ensure(lambda result: bool(result) and all(name.startswith("linux-x86_64-cp") for name in result)) +def signed_versions() -> dict[str, str]: + lock = Path(__file__).parents[1] / "resources/contracts/pr-range-v1-toolchain-lock.json" + return {row["environment_id"]: row["python_version"] for row in json.loads(lock.read_text())["environments"]} + + +@ensure(lambda result: result.startswith("linux-x86_64-cp")) +def select_environment(plan: ProjectPlan, *, current: str) -> str: + """Prefer a compatible current worker; otherwise require an unambiguous match.""" + constraint = SpecifierSet(plan.requires_python) + matching = [ + name + for name, version in signed_versions().items() + if version in constraint + and (not plan.python or version == plan.python or version.startswith(plan.python + ".")) + ] + if current in matching: + return current + if len(matching) == 1: + return matching[0] + if not matching: + raise ProjectRuntimeError( + f"project_python_incompatible:pin={plan.python}; " + f"requires-python={plan.requires_python}; supported={signed_versions()}" + ) + raise ProjectRuntimeError(f"project_python_ambiguous:{','.join(matching)}; select python in --project-config") + + +@contextmanager +@require(lambda runtime: bool(runtime.environment_id)) +def project_worker(runtime: Any, plan: ProjectPlan) -> Iterator[Any]: + """Materialize and clean up a different signed ABI only when the project needs it.""" + selected = select_environment(plan, current=runtime.environment_id) + if selected == runtime.environment_id: + yield runtime + return + from specfact_code_review.run.runner import _cleanup_capsule_runtime, _prepare_capsule_runtime + + alternate, reason = _prepare_capsule_runtime(environment_id=selected) + if alternate is None: + raise ProjectRuntimeError(reason) + try: + if alternate.environment_id != selected: + raise ProjectRuntimeError("project_worker_environment_mismatch") + yield alternate + finally: + _cleanup_capsule_runtime(alternate) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_models.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_models.py new file mode 100644 index 00000000..f7a19b82 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_models.py @@ -0,0 +1,73 @@ +"""Portable project inputs and local runtime evidence contracts.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import Any + +from icontract import ensure + + +class ProjectRuntimeError(ValueError): + """An actionable preparation or runtime identity failure.""" + + +@ensure(lambda result: result.startswith("sha256:") and len(result) == 71) +def content_digest(value: bytes) -> str: + """Return the content identity used by local runtime artifacts.""" + return "sha256:" + hashlib.sha256(value).hexdigest() + + +@ensure(lambda result: result.startswith("sha256:") and len(result) == 71) +def document_digest(value: object) -> str: + """Hash a canonical JSON document independently of file formatting.""" + return content_digest(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()) + + +@dataclass(frozen=True) +class ProjectPlan: + """Read-only discovery result; executable manager semantics remain adapter-owned.""" + + root: Path + manager: str + environment: str = "default" + python: str = "" + requires_python: str = "" + source_identity: str = "" + vcs_repository: Path | None = None + vcs: dict[str, str] = field(default_factory=dict) + groups: tuple[str, ...] = () + extras: tuple[str, ...] = () + requirements: tuple[str, ...] = () + constraints: tuple[str, ...] = () + source_roots: tuple[str, ...] = () + native_libraries: tuple[str, ...] = () + native_tools: tuple[str, ...] = () + inputs: dict[str, str] = field(default_factory=dict) + pytest_config: dict[str, Any] = field(default_factory=dict) + + @ensure(lambda result: "root" not in result) + def document(self) -> dict[str, Any]: + """Return relocation-independent inputs suitable for cache identity.""" + result = asdict(self) + result.pop("root") + result.pop("vcs_repository") + return result + + @property + @ensure(lambda result: result.startswith("sha256:") and len(result) == 71) + def identity(self) -> str: + return document_digest(self.document()) + + +@dataclass(frozen=True) +class PreparedRuntime: + """Validated local artifact and the provenance available to the controller.""" + + root: Path + descriptor_path: Path + identity: str + descriptor: dict[str, Any] diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py new file mode 100644 index 00000000..9a9a75e5 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_native.py @@ -0,0 +1,224 @@ +"""Inventory ELF dependencies without executing ldd or customer extension code.""" + +from __future__ import annotations + +import re +import shutil +import struct +from pathlib import Path +from typing import Any + +from icontract import ensure + +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest + + +SYSTEM_LIBRARY_ROOTS = (Path("/usr/lib/x86_64-linux-gnu"), Path("/lib/x86_64-linux-gnu")) + + +def _elf_sections(payload: bytes, name: str) -> list[tuple[int, ...]]: + if len(payload) < 64 or payload[4:6] != b"\x02\x01": + raise ProjectRuntimeError(f"project_native_elf_unsupported:{name}") + machine = struct.unpack_from(" len(payload): + raise ProjectRuntimeError(f"project_native_elf_invalid:{name}") + return [struct.unpack_from(" str: + end = table.find(b"\0", offset) + if end < 0: + raise ProjectRuntimeError("project_native_elf_invalid:unterminated dependency") + try: + return table[offset:end].decode("ascii") + except UnicodeDecodeError as exc: + raise ProjectRuntimeError("project_native_elf_invalid:non-ASCII dependency") from exc + + +def _needed_names(payload: bytes, section: tuple[int, ...], sections: list[tuple[int, ...]]) -> list[str]: + start, length, strings_index = section[4], section[5], section[6] + if strings_index >= len(sections) or start + length > len(payload) or length % 16: + raise ProjectRuntimeError("project_native_elf_invalid:dynamic section bounds") + strings = sections[strings_index] + table = payload[strings[4] : strings[4] + strings[5]] + if len(table) != strings[5]: + raise ProjectRuntimeError("project_native_elf_invalid:string table bounds") + names = [] + for position in range(start, start + length, 16): + tag, value = struct.unpack_from(" None: + if segment[0] not in (1, 2): # PT_LOAD, PT_DYNAMIC + return + start, address, length, memory = segment[2], segment[3], segment[5], segment[6] + if start + length > payload_size or length > memory or address + memory > 2**64: + raise ProjectRuntimeError("project_native_elf_invalid:program segment bounds") + + +def _elf_segments(payload: bytes) -> list[tuple[int, ...]]: + offset = struct.unpack_from(" len(payload): + raise ProjectRuntimeError("project_native_elf_invalid:program header bounds") + segments = [struct.unpack_from(" list[tuple[int, int]]: + start, length = segment[2], segment[5] + if length % 16: + raise ProjectRuntimeError("project_native_elf_invalid:dynamic segment size") + entries = [] + for position in range(start, start + length, 16): + tag, value = struct.unpack_from(" int: + values = [value for tag, value in entries if tag == requested] + if len(values) != 1: + raise ProjectRuntimeError("project_native_elf_invalid:dynamic string table metadata") + return values[0] + + +def _segment_string_table(payload: bytes, segments: list[tuple[int, ...]], entries: list[tuple[int, int]]) -> bytes: + address = _dynamic_value(entries, 5) # DT_STRTAB + length = _dynamic_value(entries, 10) # DT_STRSZ + offsets = { + segment[2] + address - segment[3] + for segment in segments + if segment[0] == 1 and segment[3] <= address and address + length <= segment[3] + segment[5] + } + if len(offsets) != 1: + raise ProjectRuntimeError("project_native_elf_invalid:dynamic string table mapping") + start = offsets.pop() + return payload[start : start + length] + + +def _segment_needed_names(payload: bytes) -> list[str]: + segments = _elf_segments(payload) + dynamic = [segment for segment in segments if segment[0] == 2] + if not dynamic: + return [] + if len(dynamic) != 1: + raise ProjectRuntimeError("project_native_elf_invalid:multiple dynamic segments") + entries = _dynamic_entries(payload, dynamic[0]) + needed = [value for tag, value in entries if tag == 1] + if not needed: + return [] + table = _segment_string_table(payload, segments, entries) + return [_dependency_name(table, offset) for offset in needed] + + +@ensure(lambda result: result == tuple(sorted(set(result)))) +def elf_dependencies(path: Path) -> tuple[str, ...]: + """Read ELF64 DT_NEEDED names from sections or sectionless program segments.""" + payload = path.read_bytes() + if not payload.startswith(b"\x7fELF"): + return () + sections = _elf_sections(payload, path.name) + names = [] if sections else _segment_needed_names(payload) + for section in sections: + if section[1] == 6: # SHT_DYNAMIC + names.extend(_needed_names(payload, section, sections)) + return tuple(sorted(set(names))) + + +def _system_library(name: str, system_roots: tuple[Path, ...]) -> Path: + candidates = [root / name for root in system_roots if (root / name).is_file()] + if not candidates: + raise ProjectRuntimeError( + f"project_native_library_missing:{name}; install the declared system library on the builder" + ) + source = candidates[0].resolve() + if not any(source.is_relative_to(root.resolve()) for root in system_roots): + raise ProjectRuntimeError(f"project_native_library_escape:{name}") + if not source.read_bytes().startswith(b"\x7fELF"): + raise ProjectRuntimeError(f"project_native_library_not_elf:{name}") + return source + + +def _native_record(path: Path, artifact: Path, origin: str) -> dict[str, Any]: + return { + "path": path.relative_to(artifact).as_posix(), + "sha256": content_digest(path.read_bytes()), + "needed": elf_dependencies(path), + "origin": origin, + } + + +def _library_closure( + artifact: Path, pending: set[str], available: set[str], system_roots: tuple[Path, ...] +) -> list[dict[str, Any]]: + seen = set() + records = [] + while pending: + name = min(pending) + pending.remove(name) + if name in seen: + continue + seen.add(name) + if not re.fullmatch(r"[A-Za-z0-9_.+-]+\.so(?:\.[A-Za-z0-9_.+-]+)*", name): + raise ProjectRuntimeError(f"project_native_library_invalid:{name}; declare ELF shared-library names") + if name in available: + continue + source = _system_library(name, system_roots) + pending.update(elf_dependencies(source)) + target = artifact / "native" / name + target.parent.mkdir(exist_ok=True) + shutil.copyfile(source, target) + target.chmod(0o755 if name == "ld-linux-x86-64.so.2" else 0o644) + records.append({**_native_record(target, artifact, "system-library"), "source": str(source)}) + return records + + +def _native_extensions(artifact: Path) -> list[Path]: + return sorted( + {path for pattern in ("*.so*", "executables/*") for path in artifact.rglob(pattern) if path.is_file()} + ) + + +@ensure(lambda result: all(row["sha256"].startswith("sha256:") for row in result)) +def inventory_native( + artifact: Path, + *, + capsule_root: Path, + declared: tuple[str, ...], + system_roots: tuple[Path, ...] = SYSTEM_LIBRARY_ROOTS, + target_loader: bool = False, +) -> list[dict[str, Any]]: + """Copy required native closures into target storage; preserve the signed supervisor.""" + extensions = _native_extensions(artifact) + capsule = {path.name for path in capsule_root.rglob("*.so*") if path.is_file()} + bundled = {path.name for path in extensions} + records = [_native_record(extension, artifact, "project") for extension in extensions] + pending = set(declared) + for record in records: + pending.update(record["needed"]) + available = capsule | bundled + if target_loader and pending: + # Newer native libraries must use a matching loader/libc domain, never + # replace libc underneath the running signed supervisor. + pending.add("ld-linux-x86-64.so.2") + available = bundled | {name for name in capsule if name.startswith("libpython")} + return records + _library_closure(artifact, pending, available, system_roots) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py new file mode 100644 index 00000000..be3526c4 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_sources.py @@ -0,0 +1,94 @@ +"""Identify the exact source inputs copied into a disposable project build.""" + +from __future__ import annotations + +import os +import stat +from pathlib import Path + +from icontract import ensure, require + +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError, content_digest, document_digest +from specfact_code_review.run.runtime_vcs import vcs_context + + +IGNORED_INPUTS = frozenset( + { + ".git", + ".venv", + ".tox", + ".specfact", + "__pycache__", + ".pytest_cache", + ".mypy_cache", + ".ruff_cache", + "node_modules", + ".env", + ".coverage", + } +) + + +@require(lambda path: bool(path.name)) +def is_excluded_source(path: Path) -> bool: + """Distinguish actual environments from ordinary packages with common names.""" + return path.name in IGNORED_INPUTS or (path.is_dir() and (path / "pyvenv.cfg").is_file()) + + +@ensure(lambda result, root: result.is_relative_to(root)) +def source_link_target(path: Path, root: Path) -> Path: + """Apply the same exclusion boundary to aliases and ordinary source paths.""" + relative = path.relative_to(root).as_posix() + try: + target = path.resolve(strict=True) + except (OSError, RuntimeError) as exc: + raise ProjectRuntimeError(f"project_source_symlink_invalid:{relative}") from exc + if not target.is_relative_to(root) or target == root or path.is_relative_to(target): + raise ProjectRuntimeError(f"project_source_symlink_escape:{relative}") + if any( + is_excluded_source(entry) for entry in (target, *target.parents) if entry != root and entry.is_relative_to(root) + ): + raise ProjectRuntimeError(f"project_source_symlink_excluded:{relative}") + return target + + +def _source_entry(path: Path, root: Path) -> str: + relative = path.relative_to(root).as_posix() + if path.is_symlink(): + return "link:" + source_link_target(path, root).relative_to(root).as_posix() + if path.is_file(): + return content_digest(path.read_bytes()) + f":mode={stat.S_IMODE(path.lstat().st_mode)}" + if not path.is_dir(): + raise ProjectRuntimeError(f"project_source_special_file:{relative}") + return f"directory:mode={stat.S_IMODE(path.lstat().st_mode)}" + + +@ensure(lambda result: result.startswith("sha256:") and len(result) == 71) +def source_identity(root: Path) -> str: + """Bind locally built packages and workspace members to their actual bytes.""" + entries = {".": _source_entry(root, root)} + for directory, directories, files in os.walk(root, followlinks=False): + directories[:] = sorted(name for name in directories if not is_excluded_source(Path(directory) / name)) + names = [*directories, *(name for name in sorted(files) if not is_excluded_source(Path(directory) / name))] + for name in names: + path = Path(directory) / name + identity = _source_entry(path, root) + if identity is not None: + entries[path.relative_to(root).as_posix()] = identity + return document_digest(entries) + + +@require(lambda plan: plan.root.is_dir()) +def verify_inputs(plan: ProjectPlan) -> None: + """Verify the selected input set without rediscovering a different environment.""" + for name, expected in plan.inputs.items(): + path = plan.root / name + if path.is_symlink() or not path.is_file() or content_digest(path.read_bytes()) != expected: + raise ProjectRuntimeError(f"project_runtime_inputs_changed_during_build:{name}") + if ( + vcs_context(plan.vcs_repository or plan.root, plan.vcs.get("commit", "HEAD"), tree=plan.vcs.get("tree")) + != plan.vcs + ): + raise ProjectRuntimeError("project_runtime_vcs_changed_during_build") + if source_identity(plan.root) != plan.source_identity: + raise ProjectRuntimeError("project_runtime_source_changed_during_build") diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_tools.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_tools.py new file mode 100644 index 00000000..db358327 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_tools.py @@ -0,0 +1,163 @@ +"""Capture explicitly requested system capabilities outside disposable builders.""" + +from __future__ import annotations + +import tempfile +from dataclasses import dataclass +from pathlib import Path + +from icontract import ensure + +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest, document_digest +from specfact_code_review.run.runtime_native import SYSTEM_LIBRARY_ROOTS, elf_dependencies, inventory_native + + +PROGRAM_ROOT = Path("/usr/bin") +GIT_HELPER_ROOTS = (Path("/usr/lib/git-core"), Path("/usr/libexec/git-core")) +GIT_HELPER_NAMES = ( + "git-remote-http", + "git-remote-https", + "git-upload-pack", + "git-receive-pack", + "git-upload-archive", +) +SUPPORTED_TOOLS = frozenset({"git", "uname", "sed"}) +_RUNTIME_ROOT = "/opt/specfact/project-runtime/native-tools" + + +@dataclass(frozen=True) +class CapturedTools: + """Controller memory owns every byte before the untrusted builder starts.""" + + files: dict[str, bytes] + inventory: dict[str, dict[str, str]] + + @property + @ensure(lambda result: result.startswith("sha256:") and len(result) == 71) + def identity(self) -> str: + """Bind launchers, programs, libraries and sealed external requirements.""" + return document_digest(self.inventory) + + +@ensure(lambda result: result == tuple(sorted(set(result)))) +def validate_native_tools(names: object) -> tuple[str, ...]: + """Reject unsupported capabilities without searching ambient executable paths.""" + if not isinstance(names, (list, tuple)) or not all(isinstance(name, str) for name in names): + raise ProjectRuntimeError("project_config_invalid:native_tools must be an array of strings") + unknown = set(names) - SUPPORTED_TOOLS + if unknown: + raise ProjectRuntimeError( + f"project_native_tool_unsupported:{','.join(sorted(unknown))}; supported: git,sed,uname" + ) + return tuple(sorted(set(names))) + + +def _programs(names: tuple[str, ...]) -> dict[str, Path]: + programs = {name: PROGRAM_ROOT / name for name in names} + if "git" in names: + for name in GIT_HELPER_NAMES: + found = {root.joinpath(name).resolve() for root in GIT_HELPER_ROOTS if (root / name).is_file()} + if len(found) != 1: + raise ProjectRuntimeError(f"project_native_tool_missing:{name}; install one Git helper installation") + programs[f"git-core/{name}"] = found.pop() + return programs + + +def _launcher(name: str) -> bytes: + git_environment = "os.environ['GIT_EXEC_PATH']=root+'/git-core'\nos.environ['GIT_CONFIG_NOSYSTEM']='1'\n" + text = ( + "#!/opt/specfact/python/bin/python\nimport os,sys\n" + f"root={_RUNTIME_ROOT!r}\n" + + (git_environment if name == "git" or name.startswith("git-core/") else "") + + "loader=root+'/native/ld-linux-x86-64.so.2'\n" + + f"os.execv(loader,[loader,'--library-path',root+'/native',root+'/executables/{name}',*sys.argv[1:]])\n" + ) + return text.encode() + + +def _capture_program(root: Path, name: str, source: Path) -> dict[str, str]: + if not source.is_file(): + raise ProjectRuntimeError(f"project_native_tool_missing:{name}; install the declared system tool") + payload = source.read_bytes() + if not payload.startswith(b"\x7fELF"): + raise ProjectRuntimeError(f"project_native_tool_not_elf:{name}; Linux x86-64 tooling required") + target = root / "executables" / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(payload) + elf_dependencies(target) # Validate architecture before publishing captured bytes. + return {"source": str(source.resolve()), "sha256": content_digest(payload)} + + +def _shell_requirement(capsule_root: Path | None) -> dict[str, str]: + if capsule_root is None: + raise ProjectRuntimeError("project_native_tool_shell_missing:git requires the sealed /bin/sh") + shell = (capsule_root / "bin/sh").resolve() + if not shell.is_relative_to(capsule_root.resolve()) or not shell.is_file() or not shell.stat().st_mode & 0o111: + raise ProjectRuntimeError("project_native_tool_shell_missing:git requires the sealed /bin/sh") + return {"source": "/bin/sh", "sha256": content_digest(shell.read_bytes())} + + +def _capture_payload(root: Path, programs: dict[str, Path]) -> CapturedTools: + inventory = {} + for name, source in programs.items(): + inventory[f"native-tools/executables/{name}"] = _capture_program(root, name, source) + needed = {"ld-linux-x86-64.so.2"} + for name in programs: + needed.update(elf_dependencies(root / "executables" / name)) + libraries = inventory_native( + root, capsule_root=root / "no-capsule", declared=tuple(sorted(needed)), system_roots=SYSTEM_LIBRARY_ROOTS + ) + for row in libraries: + key = "native-tools/" + row["path"] + inventory.setdefault(key, {"source": row.get("source", "controller-capture"), "sha256": row["sha256"]}) + files = { + "native-tools/" + path.relative_to(root).as_posix(): path.read_bytes() + for path in root.rglob("*") + if path.is_file() + } + for name in programs: + key = "native-tools/" + name if name.startswith("git-core/") else "bin/" + name + files[key] = _launcher(name) + inventory[key] = {"source": "generated-controller-launcher", "sha256": content_digest(files[key])} + return CapturedTools(files, inventory) + + +@ensure( + lambda result: all( + content_digest(payload) == result.inventory[name]["sha256"] for name, payload in result.files.items() + ) +) +def capture_native_tools(names: tuple[str, ...], *, capsule_root: Path | None = None) -> CapturedTools: + """Snapshot bounded tools and complete ELF closures before cache lookup or builds.""" + selected = validate_native_tools(names) + if not selected: + return CapturedTools({}, {}) + programs = _programs(selected) + with tempfile.TemporaryDirectory(prefix="specfact-native-tools-") as directory: + captured = _capture_payload(Path(directory), programs) + if "git" in selected: + captured.inventory["@capsule/bin/sh"] = _shell_requirement(capsule_root) + return captured + + +def _reject_destinations(capture: CapturedTools, artifact: Path) -> None: + destinations = [artifact / "native-tools", *(artifact / name for name in capture.files if name.startswith("bin/"))] + binary_directory = artifact / "bin" + invalid_bin = binary_directory.is_symlink() or (binary_directory.exists() and not binary_directory.is_dir()) + if invalid_bin or any(path.exists() or path.is_symlink() for path in destinations): + raise ProjectRuntimeError("project_native_tool_collision: builder output occupies a declared tool destination") + + +@ensure( + lambda capture, artifact: all((artifact / name).read_bytes() == payload for name, payload in capture.files.items()) +) +def install_native_tools(capture: CapturedTools, artifact: Path) -> None: + """Install only captured bytes after rejecting all builder-owned destinations.""" + if not capture.files: + return + _reject_destinations(capture, artifact) + for name, payload in capture.files.items(): + target = artifact / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(payload) + target.chmod(0o755) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_trust.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_trust.py new file mode 100644 index 00000000..4a999728 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_trust.py @@ -0,0 +1,61 @@ +"""Preserve signed public trust data without exposing host trust or credentials.""" + +from __future__ import annotations + +import os +import ssl +import stat +from pathlib import Path + +from icontract import ensure + +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest + + +SOURCE = "opt/specfact/analyzers/certifi/cacert.pem" +DESTINATION = "worker-config/ssl/certs/ca-certificates.crt" +_MAX_BUNDLE = 2_000_000 + + +@ensure(lambda result: bool(result) and len(result) <= _MAX_BUNDLE) +def capture_public_trust(capsule_root: Path | None) -> bytes: + """Capture validated public certificates from the already authenticated worker.""" + try: + if capsule_root is None: + raise ValueError("signed worker root missing") + source = capsule_root / SOURCE + if not source.resolve().is_relative_to(capsule_root.resolve()): + raise ValueError("certificate path leaves signed worker") + descriptor = os.open(source, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, "rb") as stream: + if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode): + raise ValueError("certificate bundle is not regular") + payload = stream.read(_MAX_BUNDLE + 1) + if len(payload) > _MAX_BUNDLE or b"PRIVATE KEY" in payload: + raise ValueError("certificate bundle is oversized or contains private material") + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.load_verify_locations(cadata=payload.decode("ascii")) + if not context.cert_store_stats()["x509_ca"]: + raise ValueError("certificate bundle has no public trust anchors") + return payload + except (OSError, ValueError) as exc: + raise ProjectRuntimeError( + "project_runtime_public_trust_invalid; reinstall the authenticated worker certificate bundle" + ) from exc + + +@ensure(lambda result: result["sha256"].startswith("sha256:")) +def install_public_trust(payload: bytes, artifact: Path) -> dict[str, str]: + """Install controller-captured bytes after untrusted output validation.""" + directory = artifact + for name in Path(DESTINATION).parts[:-1]: + directory /= name + if directory.is_symlink() or (directory.exists() and not directory.is_dir()): + raise ProjectRuntimeError("project_runtime_public_trust_collision; builder changed certificate parents") + directory.mkdir(exist_ok=True) + destination = artifact / DESTINATION + if destination.exists() or destination.is_symlink(): + raise ProjectRuntimeError("project_runtime_public_trust_collision; builder supplied certificate destination") + with destination.open("xb") as stream: + stream.write(payload) + return {"source": SOURCE, "path": DESTINATION, "sha256": content_digest(payload)} diff --git a/packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py b/packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py new file mode 100644 index 00000000..8a5c2fad --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/runtime_vcs.py @@ -0,0 +1,197 @@ +"""Sanitized VCS context for version-aware private project builds.""" + +from __future__ import annotations + +import json +import os +import shutil +import stat +import subprocess +import tempfile +from pathlib import Path + +from icontract import require + +from specfact_code_review.run import target_launch +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest + + +def _attached_git() -> str: + """Select only the controller-recorded launcher inside the verified worker mount.""" + project = target_launch.PROJECT + launcher = project / "bin/git" + try: + descriptor = json.loads((project / "project-runtime.json").read_text(encoding="utf-8")) + declared = descriptor["project"]["native_tools"] + recorded = descriptor["inventory"]["native_tools"]["bin/git"] + if not isinstance(declared, list) or "git" not in declared: + raise ValueError("undeclared capability") + if not stat.S_ISREG(launcher.lstat().st_mode) or not launcher.resolve().is_relative_to(project.resolve()): + raise ValueError("launcher must be a contained regular file") + if recorded["source"] != "generated-controller-launcher" or recorded["sha256"] != content_digest( + launcher.read_bytes() + ): + raise ValueError("launcher identity mismatch") + except (OSError, ValueError, KeyError, TypeError) as exc: + raise ProjectRuntimeError( + "project_git_attached_tool_invalid; declare native_tools=['git'] and prepare the runtime again" + ) from exc + return str(launcher) + + +def _git_executable() -> str: + """Host lookup remains fixed; an inherited worker uses its attested capability.""" + if not target_launch.CONTEXT.exists(): + return "git" + try: + target_launch.execution_domain() + except RuntimeError as exc: + raise ProjectRuntimeError( + "project_git_attached_context_invalid; launch VCS inspection inside a verified target worker" + ) from exc + return _attached_git() + + +def _git_result( + root: Path, *arguments: str, input_data: bytes | None = None, shallow_file: Path | None = None +) -> subprocess.CompletedProcess[bytes]: + try: + return subprocess.run( + [ + _git_executable(), + "-C", + str(root), + "-c", + "core.hooksPath=/dev/null", + *(["--shallow-file", str(shallow_file)] if shallow_file is not None else []), + *arguments, + ], + env={ + "PATH": os.defpath, + "GIT_CONFIG_NOSYSTEM": "1", + "GIT_CONFIG_GLOBAL": "/dev/null", + "GIT_NO_REPLACE_OBJECTS": "1", + "GIT_GRAFT_FILE": os.devnull, + "GIT_NO_LAZY_FETCH": "1", + "GIT_ALLOW_PROTOCOL": "", + "GIT_TERMINAL_PROMPT": "0", + }, + input=input_data, + capture_output=True, + check=False, + timeout=120, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise ProjectRuntimeError( + f"project_git_snapshot_failed:{arguments[0]}:{type(exc).__name__}; retry local metadata export" + ) from exc + + +def _git_bytes(root: Path, *arguments: str, input_data: bytes | None = None, shallow_file: Path | None = None) -> bytes: + result = _git_result(root, *arguments, input_data=input_data, shallow_file=shallow_file) + if result.returncode: + raise ProjectRuntimeError( + "project_git_snapshot_failed:" + + arguments[0] + + "; ensure required Git objects are available locally; metadata export disables network acquisition" + ) + return result.stdout + + +def _git(root: Path, *arguments: str) -> str: + return _git_bytes(root, *arguments).decode("utf-8").strip() + + +def _unborn_head(root: Path) -> bool: + reference = _git_result(root, "symbolic-ref", "--quiet", "HEAD") + if reference.returncode: + return False + branch = reference.stdout.decode("utf-8").strip() + return ( + branch.startswith("refs/heads/") + and _git_result(root, "show-ref", "--verify", "--quiet", branch).returncode == 1 + ) + + +@require(lambda root: root.is_dir()) +def vcs_context(root: Path, commit: str = "HEAD", *, tree: str | None = None) -> dict[str, str]: + """Bind commits, tags and shallow boundaries without importing user Git config.""" + if not (root / ".git").exists(): + return {} + try: + selected = _git(root, "rev-parse", "--verify", commit + "^{commit}") + except ProjectRuntimeError as exc: + if exc.__cause__ is None and commit == "HEAD" and tree is None and _unborn_head(root): + return {} + raise + shallow = _git(root, "rev-parse", "--git-path", "shallow") + boundary = Path(shallow) + if not boundary.is_absolute(): + boundary = root / boundary + return { + "commit": selected, + "tree": _git(root, "rev-parse", "--verify", (tree or selected) + "^{tree}"), + "tags": _git(root, "for-each-ref", "--format=%(refname) %(objectname)", "refs/tags"), + "shallow": boundary.read_text(encoding="ascii") if boundary.is_file() else "", + } + + +def _copy_bound_objects(source: Path, private: Path, selected: dict[str, str]) -> None: + tags = dict(line.rsplit(" ", 1) for line in selected["tags"].splitlines()) + roots = {selected["commit"], selected["tree"], *tags.values()} + objects = _git_bytes( + source, + "rev-list", + "--objects", + "--no-object-names", + "--stdin", + input_data=("\n".join(sorted(roots)) + "\n").encode("ascii"), + shallow_file=private / ".git/shallow", + ) + pack_prefix = private / ".git/objects/pack/pack" + _git_bytes( + source, + "pack-objects", + "--no-reuse-delta", + str(pack_prefix), + input_data=objects, + shallow_file=private / ".git/shallow", + ) + actual = _git(private, "cat-file", "--batch-all-objects", "--batch-check=%(objectname)") + if set(actual.splitlines()) != set(objects.decode("ascii").splitlines()): + raise ProjectRuntimeError("project_git_snapshot_object_closure_mismatch") + for name, identity in tags.items(): + _git(private, "update-ref", name, identity) + + +@require(lambda source, destination: source.is_dir() and destination.is_dir()) +def copy_vcs_context( + source: Path, + destination: Path, + commit: str = "HEAD", + *, + tree: str | None = None, + bound_vcs: dict[str, str] | None = None, +) -> None: + """Transfer only identity-bound history and staged objects into fresh metadata.""" + if not (source / ".git").exists(): + if bound_vcs: + raise ProjectRuntimeError( + "project_git_snapshot_origin_missing; restore captured repository metadata and retry" + ) + return + selected = dict(bound_vcs) if bound_vcs is not None else vcs_context(source, commit, tree=tree) + if not selected: + return + with tempfile.TemporaryDirectory(prefix="specfact-build-git-", dir=destination.parent) as directory: + private = Path(directory) / "repository" + object_format = _git(source, "rev-parse", "--show-object-format") + _git(source, "init", "--quiet", "--template=", f"--object-format={object_format}", str(private)) + _git(private, "config", "--local", "core.hooksPath", "/dev/null") + (private / ".git/shallow").write_text(selected["shallow"], encoding="ascii") + _copy_bound_objects(source, private, selected) + if not selected["shallow"]: + (private / ".git/shallow").unlink() + (private / ".git/HEAD").write_text(selected["commit"] + "\n", encoding="ascii") + _git(private, "read-tree", selected["tree"]) + shutil.move(str(private / ".git"), destination / ".git") diff --git a/packages/specfact-code-review/src/specfact_code_review/run/sandbox.py b/packages/specfact-code-review/src/specfact_code_review/run/sandbox.py index 070be79c..f0faf068 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/sandbox.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/sandbox.py @@ -57,6 +57,7 @@ class SnapshotInvocationContext: control_root: Path | None environment_id: str = "" reserved_import_prefixes: tuple[str, ...] = _DEFAULT_RESERVED_IMPORT_PREFIXES + import_domain: Literal["sealed", "portable"] = "sealed" @property def digest(self) -> str: @@ -67,6 +68,7 @@ def digest(self) -> str: "config_roots": [str(path) for path in self.config_roots], "control_root": None if self.control_root is None else str(self.control_root), "interpreter": self.interpreter, + "import_domain": self.import_domain, "environment_id": self.environment_id, "member": self.member, "network": self.network, @@ -623,6 +625,10 @@ def _signed_reserved_import_prefixes(environment_id: str) -> frozenset[str]: def preflight_reserved_imports(context: SnapshotInvocationContext) -> PreflightResult: """Reject top-level module, stub, package, or namespace-package collisions.""" + if context.import_domain == "portable": + # The sealed bootstrap excludes the snapshot from supervisor startup. + # Target imports occur only in a separate offline child interpreter. + return PreflightResult("PASS") try: prefixes = _signed_reserved_import_prefixes(context.environment_id) | frozenset( context.reserved_import_prefixes diff --git a/packages/specfact-code-review/src/specfact_code_review/run/scope.py b/packages/specfact-code-review/src/specfact_code_review/run/scope.py index ba576c63..6a2da081 100644 --- a/packages/specfact-code-review/src/specfact_code_review/run/scope.py +++ b/packages/specfact-code-review/src/specfact_code_review/run/scope.py @@ -15,14 +15,17 @@ import subprocess import tempfile import tomllib -from collections.abc import Callable, Iterable, Sequence -from contextlib import ExitStack -from dataclasses import dataclass, field, replace +from collections.abc import Callable, Iterable, Iterator, Sequence +from contextlib import ExitStack, contextmanager +from dataclasses import asdict, dataclass, field, replace +from functools import partial from pathlib import Path, PurePosixPath from typing import Literal, cast from icontract import ensure, require +from specfact_code_review.run.runtime_sources import source_link_target + ScopeKind = Literal["worktree", "index", "range", "full", "explicit_files"] ScopeStatus = Literal["PASS", "FAIL", "UNKNOWN", "NOT_APPLICABLE"] @@ -368,6 +371,7 @@ class ScopeRequest: pr_context_file: Path | None = None repository_slug: str | None = None project_runtime_source_lock_paths: tuple[Path, ...] = () + portable_project_runtime: bool = False @dataclass(frozen=True) @@ -379,6 +383,8 @@ class Snapshot: tree: str contents: dict[str, bytes] entries: dict[str, TreeEntry] + repository: Path | None = None + vcs_commit: str = "" @require(lambda relative_path: bool(str(relative_path)), "relative path must not be empty") @ensure(lambda result: isinstance(result, bytes)) @@ -626,27 +632,53 @@ def _safe_snapshot_path(root: Path, relative: str) -> Path: return root.joinpath(*path.parts) -def _materialize_tree(repository: Path, revision: str, *, snapshot_identity: str) -> Snapshot: +def _materialize_project_links(root: Path, entries: dict[str, TreeEntry], contents: dict[str, bytes]) -> None: + """Recreate tracked links only after writing blobs, then validate the entire graph.""" + links = [entry for entry in entries.values() if entry.object_type == "blob" and entry.git_mode == "120000"] + try: + for entry in links: + destination = _safe_snapshot_path(root, entry.path) + if any(parent.is_symlink() for parent in destination.parents if parent.is_relative_to(root)): + raise GitResolutionError(f"Git link has a symlink parent: {entry.path}") + destination.parent.mkdir(parents=True, exist_ok=True) + destination.symlink_to(os.fsdecode(contents[entry.path])) + for entry in links: + source_link_target(_safe_snapshot_path(root, entry.path), root) + except (OSError, ValueError) as exc: + raise GitResolutionError(f"Unsafe project link in Git snapshot: {exc}") from exc + + +def _materialize_tree( + repository: Path, revision: str, *, snapshot_identity: str, preserve_links: bool = False +) -> Snapshot: tree = _resolve_tree(repository, revision) - root = Path(tempfile.mkdtemp(prefix=f"specfact-review-{snapshot_identity[:12]}-")) - contents: dict[str, bytes] = {} - entries = {entry.path: entry for entry in _tree_entries(repository, revision)} - for entry in entries.values(): - if entry.object_type != "blob": - continue - payload = _git_bytes(repository, ["cat-file", "blob", entry.object_id]) - contents[entry.path] = payload - if entry.git_mode not in _REGULAR_GIT_MODES: - continue - destination = _safe_snapshot_path(root, entry.path) - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(payload) - destination.chmod(0o755 if entry.git_mode == "100755" else 0o644) - return Snapshot(root=root, commit=snapshot_identity, tree=tree, contents=contents, entries=entries) + root = Path(tempfile.mkdtemp(prefix=f"specfact-review-{snapshot_identity[:12]}-")).resolve() + with ExitStack() as cleanup: + cleanup.callback(shutil.rmtree, root, ignore_errors=True) + contents: dict[str, bytes] = {} + entries = {entry.path: entry for entry in _tree_entries(repository, revision)} + for entry in entries.values(): + if entry.object_type != "blob": + continue + payload = _git_bytes(repository, ["cat-file", "blob", entry.object_id]) + contents[entry.path] = payload + if entry.git_mode not in _REGULAR_GIT_MODES: + continue + destination = _safe_snapshot_path(root, entry.path) + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(payload) + destination.chmod(0o755 if entry.git_mode == "100755" else 0o644) + if preserve_links: + _materialize_project_links(root, entries, contents) + snapshot = Snapshot( + root=root, commit=snapshot_identity, tree=tree, contents=contents, entries=entries, repository=repository + ) + cleanup.pop_all() + return snapshot -def _materialize_commit(repository: Path, commit: str) -> Snapshot: - return _materialize_tree(repository, commit, snapshot_identity=commit) +def _materialize_commit(repository: Path, commit: str, *, preserve_links: bool = False) -> Snapshot: + return _materialize_tree(repository, commit, snapshot_identity=commit, preserve_links=preserve_links) def _is_test_path(path: Path) -> bool: @@ -2085,6 +2117,38 @@ def _context_failure(reason: str, diagnostics: str, resolved: _ResolvedRange) -> ) +def _portable_source_locks(base: Snapshot, head: Snapshot) -> frozenset[str]: + from specfact_code_review.run.runtime_discovery import PROJECT_INPUT_NAMES + + return frozenset( + path + for path in set(base.contents) | set(head.contents) + if Path(path).name in PROJECT_INPUT_NAMES + or Path(path).suffix == ".lock" + or Path(path).name.startswith(("requirements", "constraints")) + ) + + +def _range_policy_paths(root: Path) -> frozenset[str]: + with ExitStack() as cleanup: + ruff_policy = resolve_ruff_policy(root, expected_version="0.15.12") + if ruff_policy.bundle_root is not None: + cleanup.callback(shutil.rmtree, ruff_policy.bundle_root, ignore_errors=True) + if ruff_policy.status != "PASS": + raise PolicyResolutionError(ruff_policy.reason) + basedpyright_policy = resolve_basedpyright_policy(root, expected_version="1.39.10") + if basedpyright_policy.bundle_root is not None: + cleanup.callback(shutil.rmtree, basedpyright_policy.bundle_root, ignore_errors=True) + if basedpyright_policy.status != "PASS": + raise PolicyResolutionError(basedpyright_policy.reason) + return frozenset((*ruff_policy.closure_paths, *basedpyright_policy.reference_paths)) + + +def _range_policy_closure(request: ScopeRequest, target: Path, head: Path) -> frozenset[str]: + paths = _range_policy_paths(target) + return paths | _range_policy_paths(head) if request.portable_project_runtime else paths + + def _materialized_range(request: ScopeRequest) -> _ResolvedRange | ScopeResolution: claimed_context = _load_claimed_context(request) base = _resolve_commit(request.repository, cast(str, request.base_ref)) @@ -2099,28 +2163,24 @@ def _materialized_range(request: ScopeRequest) -> _ResolvedRange | ScopeResoluti candidate_identities=identities, ) merge_base = candidates[0] + materialize = partial(_materialize_commit, request.repository, preserve_links=request.portable_project_runtime) with ExitStack() as cleanup: - base_snapshot = _materialize_commit(request.repository, merge_base) + base_snapshot = materialize(merge_base) cleanup.callback(shutil.rmtree, base_snapshot.root, ignore_errors=True) - head_snapshot = _materialize_commit(request.repository, head) + head_snapshot = materialize(head) cleanup.callback(shutil.rmtree, head_snapshot.root, ignore_errors=True) source_locks = frozenset(PurePosixPath(path).as_posix() for path in request.project_runtime_source_lock_paths) source_locks |= _context_source_lock_paths(claimed_context) - target_snapshot = base_snapshot if base == merge_base else _materialize_commit(request.repository, base) + if request.portable_project_runtime and claimed_context is None: + source_locks |= _portable_source_locks(base_snapshot, head_snapshot) + target_snapshot = base_snapshot if base == merge_base else materialize(base) if target_snapshot is not base_snapshot: cleanup.callback(shutil.rmtree, target_snapshot.root, ignore_errors=True) - project_runtime_source_locks = _snapshot_source_lock_identities(target_snapshot, source_locks) - ruff_policy = resolve_ruff_policy(target_snapshot.root, expected_version="0.15.12") - if ruff_policy.bundle_root is not None: - cleanup.callback(shutil.rmtree, ruff_policy.bundle_root, ignore_errors=True) - if ruff_policy.status != "PASS": - raise PolicyResolutionError(ruff_policy.reason) - basedpyright_policy = resolve_basedpyright_policy(target_snapshot.root, expected_version="1.39.10") - if basedpyright_policy.bundle_root is not None: - cleanup.callback(shutil.rmtree, basedpyright_policy.bundle_root, ignore_errors=True) - if basedpyright_policy.status != "PASS": - raise PolicyResolutionError(basedpyright_policy.reason) - additional_policy_paths = frozenset((*ruff_policy.closure_paths, *basedpyright_policy.reference_paths)) + identity_locks = source_locks + if request.portable_project_runtime and claimed_context is None: + identity_locks = frozenset(path for path in source_locks if path in target_snapshot.contents) + project_runtime_source_locks = _snapshot_source_lock_identities(target_snapshot, identity_locks) + additional_policy_paths = _range_policy_closure(request, target_snapshot.root, head_snapshot.root) selected = tuple( path for path in _range_paths(request.repository, merge_base, head) @@ -2144,16 +2204,6 @@ def _materialized_range(request: ScopeRequest) -> _ResolvedRange | ScopeResoluti statuses, additional_policy_paths, ) - rename_projection = [ - { - "blob_sha": item.blob_sha, - "disposition": item.disposition, - "git_mode": item.git_mode, - "new_path": item.new_path, - "old_path": item.old_path, - } - for item in renames - ] result = _ResolvedRange( candidates, base, @@ -2165,7 +2215,7 @@ def _materialized_range(request: ScopeRequest) -> _ResolvedRange | ScopeResoluti project_runtime_source_locks, statuses, renames, - _canonical_json_digest(rename_projection), + _canonical_json_digest([asdict(item) for item in renames]), _source_manifest_digest(base_snapshot), _source_manifest_digest(head_snapshot), policy_bundle, @@ -2176,10 +2226,6 @@ def _materialized_range(request: ScopeRequest) -> _ResolvedRange | ScopeResoluti ) if target_snapshot is not base_snapshot: shutil.rmtree(target_snapshot.root, ignore_errors=True) - if ruff_policy.bundle_root is not None: - shutil.rmtree(ruff_policy.bundle_root, ignore_errors=True) - if basedpyright_policy.bundle_root is not None: - shutil.rmtree(basedpyright_policy.bundle_root, ignore_errors=True) cleanup.pop_all() return result @@ -2239,7 +2285,8 @@ def _range_result(request: ScopeRequest, resolved: _ResolvedRange) -> ScopeResol head_manifest, claimed_context=claimed_context, ) - if any(path in resolved.source_lock_paths for path in resolved.selected_paths): + portable = request.portable_project_runtime and claimed_context is None + if not portable and any(path in resolved.source_lock_paths for path in resolved.selected_paths): return _range_resolution( context, status="UNKNOWN", @@ -2247,7 +2294,7 @@ def _range_result(request: ScopeRequest, resolved: _ResolvedRange) -> ScopeResol ci_exit_code=1, diagnostics="Candidate source-lock bytes are governed evidence and cannot build the project-runtime layer.", ) - if resolved.candidate_policy_paths: + if resolved.candidate_policy_paths and not portable: return _range_resolution( context, status="UNKNOWN", @@ -2526,7 +2573,7 @@ def _index_unknown( def _resolve_index(request: ScopeRequest) -> ScopeResolution: try: - context = _materialized_index_context(request.repository) + context = _materialized_index_context(request.repository, preserve_links=request.portable_project_runtime) except GitResolutionError as exc: return _index_unknown("git_resolution_failed", str(exc)) if isinstance(context, ScopeResolution): @@ -2541,10 +2588,12 @@ def _resolve_index(request: ScopeRequest) -> ScopeResolution: return _resolved_index(context) -def _materialized_index_context(repository: Path) -> _IndexResolutionContext | ScopeResolution: +def _materialized_index_context( + repository: Path, *, preserve_links: bool = False +) -> _IndexResolutionContext | ScopeResolution: capture = _capture_index(repository) try: - return _materialized_index_context_from_capture(repository, capture) + return _materialized_index_context_from_capture(repository, capture, preserve_links=preserve_links) finally: shutil.rmtree(capture.path.parent, ignore_errors=True) @@ -2678,8 +2727,23 @@ def _snapshot_input_manifest(snapshot: Snapshot, paths: Iterable[str]) -> dict[s return {path: identity for path in paths if (identity := _input_identity(snapshot, path)) is not None} +@contextmanager +def _materialized_index_snapshots( + repository: Path, tree: str, identity: str, *, preserve_links: bool +) -> Iterator[tuple[Snapshot, Snapshot]]: + with ExitStack() as cleanup: + head = _resolve_commit(repository, "HEAD") + base = _materialize_commit(repository, head, preserve_links=preserve_links) + cleanup.callback(shutil.rmtree, base.root, ignore_errors=True) + index = _materialize_tree(repository, tree, snapshot_identity=identity, preserve_links=preserve_links) + index = replace(index, vcs_commit=head) + cleanup.callback(shutil.rmtree, index.root, ignore_errors=True) + yield base, index + cleanup.pop_all() + + def _materialized_index_context_from_capture( - repository: Path, capture: _IndexCapture + repository: Path, capture: _IndexCapture, *, preserve_links: bool = False ) -> _IndexResolutionContext | ScopeResolution: _after_index_capture() stage_entries = _index_stage_entries(repository, capture) @@ -2688,81 +2752,87 @@ def _materialized_index_context_from_capture( tree = _write_index_tree(repository, capture) tree_entries = {entry.path: entry for entry in _tree_entries(repository, tree)} metadata = _index_metadata(stage_entries, _index_flag_tags(repository, capture), tree_entries) - head_commit = _resolve_commit(repository, "HEAD") - base_snapshot = _materialize_commit(repository, head_commit) - index_snapshot = _materialize_tree(repository, tree, snapshot_identity=f"index-{capture.digest[7:]}") - changed_paths = set(_range_paths(repository, head_commit, tree)) - changed_paths.update(path for path, item in metadata.items() if item.intent_to_add) - base_policy_paths, _ = _resolved_index_policy_paths(base_snapshot.root) - discovered_policy_paths = _discovered_index_policy_paths(index_snapshot) - preliminary_policy_paths = base_policy_paths | discovered_policy_paths - preliminary_candidates = changed_paths | set( - _unsafe_snapshot_policy_paths(index_snapshot, discovered_policy_paths) - | _missing_snapshot_policy_paths(index_snapshot, discovered_policy_paths) - ) - preliminary_paths = tuple( - path - for path in sorted(preliminary_candidates) - if _governed_path( - path, - frozenset(), - base=base_snapshot, - head=index_snapshot, - additional_policy_paths=preliminary_policy_paths, + with _materialized_index_snapshots( + repository, tree, f"index-{capture.digest[7:]}", preserve_links=preserve_links + ) as (base_snapshot, index_snapshot): + head_commit = base_snapshot.commit + changed_paths = set(_range_paths(repository, head_commit, tree)) + changed_paths.update(path for path, item in metadata.items() if item.intent_to_add) + base_policy_paths, _ = _resolved_index_policy_paths(base_snapshot.root) + discovered_policy_paths = _discovered_index_policy_paths(index_snapshot) + preliminary_policy_paths = base_policy_paths | discovered_policy_paths + preliminary_candidates = changed_paths | set( + _unsafe_snapshot_policy_paths(index_snapshot, discovered_policy_paths) + | _missing_snapshot_policy_paths(index_snapshot, discovered_policy_paths) ) - ) - preliminary_manifest = _snapshot_input_manifest(index_snapshot, preliminary_paths) - preliminary_base_manifest = _snapshot_input_manifest(base_snapshot, preliminary_paths) - preliminary_context = _IndexResolutionContext( - base_snapshot, - index_snapshot, - preliminary_paths, - metadata, - tree, - preliminary_base_manifest, - preliminary_manifest, - tuple( + preliminary_paths = tuple( path - for path in preliminary_paths - if _is_policy_path( + for path in sorted(preliminary_candidates) + if _governed_path( path, frozenset(), base=base_snapshot, head=index_snapshot, additional_policy_paths=preliminary_policy_paths, ) - ), - ) - if _unsafe_index_path(preliminary_context) is not None: - return preliminary_context - - additional_policy_paths, policy_error = _resolved_index_policy_paths(index_snapshot.root) - if policy_error: - shutil.rmtree(base_snapshot.root, ignore_errors=True) - shutil.rmtree(index_snapshot.root, ignore_errors=True) - return _index_unknown("policy_parse_failure", policy_error) + ) + preliminary_context = _IndexResolutionContext( + base_snapshot, + index_snapshot, + preliminary_paths, + metadata, + tree, + _snapshot_input_manifest(base_snapshot, preliminary_paths), + _snapshot_input_manifest(index_snapshot, preliminary_paths), + tuple( + path + for path in preliminary_paths + if _is_policy_path( + path, + frozenset(), + base=base_snapshot, + head=index_snapshot, + additional_policy_paths=preliminary_policy_paths, + ) + ), + ) + if _unsafe_index_path(preliminary_context) is not None: + return preliminary_context + + additional_policy_paths, policy_error = _resolved_index_policy_paths(index_snapshot.root) + if policy_error: + shutil.rmtree(base_snapshot.root, ignore_errors=True) + shutil.rmtree(index_snapshot.root, ignore_errors=True) + return _index_unknown("policy_parse_failure", policy_error) + return _index_context_with_policy_evidence( + preliminary_context, changed_paths, base_policy_paths, additional_policy_paths + ) + + +def _index_context_with_policy_evidence( + context: _IndexResolutionContext, + changed_paths: set[str], + base_policy_paths: frozenset[str], + additional_policy_paths: frozenset[str], +) -> _IndexResolutionContext: selected_paths = tuple( path for path in sorted(changed_paths) if _governed_path( path, frozenset(), - base=base_snapshot, - head=index_snapshot, + base=context.base_snapshot, + head=context.index_snapshot, additional_policy_paths=additional_policy_paths, ) ) - manifest = _snapshot_input_manifest(index_snapshot, selected_paths) - base_manifest = _snapshot_input_manifest(base_snapshot, selected_paths) - return _IndexResolutionContext( - base_snapshot, - index_snapshot, - selected_paths, - metadata, - tree, - base_manifest, - manifest, - tuple(sorted(additional_policy_paths)), + manifest_paths = sorted(set(selected_paths) | base_policy_paths | additional_policy_paths) + return replace( + context, + selected_paths=selected_paths, + base_manifest=_snapshot_input_manifest(context.base_snapshot, manifest_paths), + manifest=_snapshot_input_manifest(context.index_snapshot, manifest_paths), + policy_paths=tuple(sorted(additional_policy_paths)), ) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/sitecustomize.py b/packages/specfact-code-review/src/specfact_code_review/run/sitecustomize.py new file mode 100644 index 00000000..e515fe09 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/sitecustomize.py @@ -0,0 +1,17 @@ +"""Fail-closed startup for native Python CLI calls inside the target namespace.""" + +from __future__ import annotations + +import os +import runpy +from pathlib import Path + + +if os.environ.get("SPECFACT_PROJECT_PYTHON") == "1": + try: + bootstrap = runpy.run_path(str(Path(__file__).with_name("target_bootstrap.py"))) + bootstrap["_configure_runtime"](bootstrap["python_execution_domain"]()) + except BaseException: + # Python normally swallows sitecustomize failures and executes -c anyway. + # Exit this isolated child before any requested project code can run. + os._exit(78) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py b/packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py new file mode 100644 index 00000000..06af00db --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/target_bootstrap.py @@ -0,0 +1,319 @@ +"""Isolated target-child startup; only standard-library imports precede target setup.""" + +from __future__ import annotations + +import builtins +import importlib.machinery +import importlib.metadata +import importlib.util +import io +import json +import os +import pkgutil +import re +import runpy +import site +import sys +import sysconfig +from collections.abc import Mapping +from pathlib import Path +from types import ModuleType + + +PROJECT = Path("/opt/specfact/project-runtime") +SNAPSHOT = Path("/opt/specfact/snapshot") +ANALYZERS = Path("/opt/specfact/config/member-analyzers") +BUILTIN = Path("/opt/specfact/builtin") +CONTEXT = Path("/opt/specfact/config/python-context") +_OWNED_COVERAGE_MODULES: dict[str, ModuleType] = {} +_TOOL_IMPORTS = { + "pylint": {"pylint", "astroid"}, + "crosshair": {"crosshair", "z3"}, + "basedpyright": {"basedpyright", "nodejs_wheel"}, +} + + +def _verified_analyzer_spec(fullname: str, spec): + """Confine module origins and every namespace portion to the verified mount.""" + locations = [] if spec is None else list(spec.submodule_search_locations or ()) + if spec is not None and spec.origin: + locations.append(spec.origin) + root = ANALYZERS.resolve() + if not locations or any(not Path(location).resolve().is_relative_to(root) for location in locations): + raise ImportError(f"project_worker_analyzer_origin_mismatch:{fullname}") + return spec + + +class AnalyzerFinder: + """Pin analyzer entry-point packages while allowing target-owned dependencies.""" + + def __init__(self, names: set[str]) -> None: + self.names = names + + def find_spec(self, fullname: str, path=None, target=None): + del target + if fullname.split(".")[0] not in self.names: + return None + spec = importlib.machinery.PathFinder.find_spec(fullname, path or [str(ANALYZERS)]) + return _verified_analyzer_spec(fullname, spec) + + def validate_loaded(self) -> None: + """Reject cached project modules instead of silently replacing their objects.""" + for fullname, module in tuple(sys.modules.items()): + if fullname.split(".")[0] in self.names: + _verified_analyzer_spec(fullname, getattr(module, "__spec__", None)) + + +class DomainFinder(AnalyzerFinder, importlib.metadata.DistributionFinder): + """Pin this member's recorded analyzer-owned dependency closure.""" + + def __init__(self, graph: dict) -> None: + super().__init__(set(graph["sealed_imports"])) + self.distributions = {row["name"] for row in graph["installed"] if row["origin"] == "analyzer"} + + def find_distributions(self, context=None): + context = context or importlib.metadata.DistributionFinder.Context() + # Explicit target inventories must never enumerate analyzer plugins. + if tuple(context.path) != tuple(sys.path): + return iter(()) + requested = re.sub(r"[-_.]+", "-", context.name).lower() if context.name else None + return ( + distribution + for distribution in importlib.metadata.distributions(path=[str(ANALYZERS)]) + if (name := re.sub(r"[-_.]+", "-", distribution.metadata["Name"]).lower()) in self.distributions + and (requested is None or requested == name) + ) + + +def _stdlib_paths() -> list[str]: + """Exclude target and analyzer paths before rebuilding an execution domain.""" + stdlib = Path(sysconfig.get_path("stdlib")) + allowed = { + stdlib, + stdlib / "lib-dynload", + Path(sys.base_prefix) / "lib" / f"python{sys.version_info.major}{sys.version_info.minor}.zip", + } + return [path for path in sys.path if path and Path(path) in allowed] + + +def _dispatch_callable_state(value) -> tuple: + """Detect replacement and in-place changes to dispatch function implementations.""" + return ( + id(value), + getattr(value, "__code__", None), + getattr(value, "__defaults__", None), + tuple(sorted((getattr(value, "__kwdefaults__", None) or {}).items())), + ) + + +def _dispatch_namespace_state(namespace: Mapping[str, object]) -> tuple: + """Retain each namespace member's identity and callable implementation.""" + return tuple((name, _dispatch_callable_state(value)) for name, value in namespace.items()) + + +def _protected_dispatch_state() -> tuple: + """Bind runpy's dispatch namespace and the code-loading primitives it consumes.""" + namespace = _dispatch_namespace_state(vars(runpy)) + classes = ( + importlib.machinery.SourceFileLoader, + importlib.machinery.SourcelessFileLoader, + *(value for value in vars(runpy).values() if isinstance(value, type)), + ) + class_state = tuple((id(base), _dispatch_namespace_state(vars(base))) for cls in classes for base in cls.__mro__) + primitives = ( + builtins.exec, + builtins.compile, + io.open_code, + importlib.util.find_spec, + importlib.util.find_spec.__globals__.get("_find_spec"), + pkgutil.get_importer, + pkgutil.read_code, + os.fsdecode, + os.path.abspath, + ) + modules = tuple(id(sys.modules.get(name)) for name in ("runpy", "pkgutil", "io", "importlib.util")) + return namespace, class_state, tuple(_dispatch_callable_state(value) for value in primitives), modules + + +def _protected_import_state(finder: AnalyzerFinder) -> tuple: + """Snapshot the lookup machinery and graph that startup hooks must preserve.""" + return ( + builtins.__import__, + importlib.machinery.PathFinder.find_spec, + _verified_analyzer_spec, + finder.find_spec, + finder.validate_loaded, + getattr(finder, "find_distributions", None), + frozenset(finder.names), + frozenset(getattr(finder, "distributions", ())), + id(sys.modules), + PROJECT.resolve(), + SNAPSHOT.resolve(), + ANALYZERS.resolve(), + BUILTIN.resolve(), + _protected_dispatch_state(), + ) + + +def _validate_target_paths(allowed: tuple[Path, ...]) -> None: + """Retain rebased editable roots without admitting unrelated startup paths.""" + for path in sys.path: + if not isinstance(path, str) or not any(Path(path).resolve().is_relative_to(root) for root in allowed): + raise RuntimeError( + "project_worker_startup_path_escape; inspect target .pth files and " + "rebuild editable installs against the copied project" + ) + + +def _configure_member_site(finder: AnalyzerFinder, stdlib_paths: list[str]) -> None: + """Preserve additive editable hooks and reestablish member lookup precedence.""" + state = _protected_import_state + expected = state(finder) + protected = {id(entry) for entry in sys.meta_path} + allowed = tuple(Path(path).resolve() for path in [*stdlib_paths, PROJECT, SNAPSHOT, ANALYZERS]) + site.addsitedir(str(PROJECT / "site-packages")) + try: + unchanged = state(finder) == expected and protected <= {id(entry) for entry in sys.meta_path} + except (AttributeError, TypeError): + unchanged = False + if not unchanged: + raise RuntimeError( + "project_worker_startup_import_state_changed; target .pth hooks must preserve protected import machinery" + ) + sys.meta_path[:] = [finder, *(entry for entry in sys.meta_path if entry is not finder)] + _validate_target_paths(allowed) + finder.validate_loaded() + + +def _load_pytest_coverage() -> ModuleType: + """Load only the fixed builtin helper, rejecting cached customer aliases.""" + alias = "_specfact_target_coverage" + helper = Path(__file__).with_name("target_coverage.py") + if helper.is_symlink() or not helper.is_file(): + raise ImportError("project_pytest_coverage_helper_unavailable") + if alias in sys.modules: + loaded = sys.modules[alias] + owned = _OWNED_COVERAGE_MODULES.get(alias) + origin = getattr(getattr(loaded, "__spec__", None), "origin", None) + filename = getattr(loaded, "__file__", None) + if loaded is not owned or not origin or not filename: + raise ImportError("project_pytest_coverage_helper_origin_mismatch") + if Path(origin).resolve() != helper.resolve() or Path(filename).resolve() != helper.resolve(): + raise ImportError("project_pytest_coverage_helper_origin_mismatch") + return loaded + spec = importlib.util.spec_from_file_location(alias, helper) + if spec is None or spec.loader is None: + raise ImportError("project_pytest_coverage_helper_unavailable") + loaded = importlib.util.module_from_spec(spec) + sys.modules[alias] = loaded + try: + spec.loader.exec_module(loaded) + except BaseException: + if sys.modules.get(alias) is loaded: + del sys.modules[alias] + raise + _OWNED_COVERAGE_MODULES[alias] = loaded + return loaded + + +def _configure_runtime(module: str) -> None: + """Initialize only the target worker; project Python gets no analyzer fallbacks.""" + caller_paths = _caller_pythonpath() + set_paths, prepend = list.__setitem__, slice(0, 0) + descriptor = json.loads((PROJECT / "project-runtime.json").read_text(encoding="utf-8")) + stdlib_paths = _stdlib_paths() + sys.path[:] = stdlib_paths + finder = AnalyzerFinder(set(_TOOL_IMPORTS.get(module, set()))) + if module != "project-python": + finder = DomainFinder(descriptor["inventory"]["member_graphs"][module]) + finder.names.update(_TOOL_IMPORTS.get(module, set())) + sys.meta_path.insert(0, finder) + finder.validate_loaded() + roots = descriptor["project"]["source_roots"] + sys.path.extend(str(SNAPSHOT / root) for root in roots) + if module == "project-python": + site.addsitedir(str(PROJECT / "site-packages")) + else: + _configure_member_site(finder, stdlib_paths) + sys.path.append(str(ANALYZERS)) + set_paths(sys.path, prepend, caller_paths) + sys.executable = str(PROJECT / "bin/python") + if module == "pytest-observe": + _load_pytest_coverage() + + +def _caller_pythonpath() -> list[str]: + """Consume caller configuration before hooks; defer paths until member sealing.""" + encoded = os.environ.pop("_SPECFACT_CALLER_PYTHONPATH", None) + if encoded is None: + return [] + pythonpath = json.loads(encoded) + if pythonpath is None: + os.environ.pop("PYTHONPATH", None) + return [] + os.environ["PYTHONPATH"] = pythonpath + return [os.path.abspath(path) for path in pythonpath.split(os.pathsep)] if pythonpath else [] + + +def python_execution_domain() -> str: + """Keep subprocesses in the recorded domain even with an empty environment.""" + launcher = runpy.run_path( + str(Path(__file__).with_name("target_launch.py")), init_globals={"CONTEXT": CONTEXT, "BUILTIN": BUILTIN} + ) + return launcher["execution_domain"]() + + +def _validate_python_arguments(arguments: list[str]) -> None: + skip_value = False + for argument in arguments: + if skip_value: + skip_value = False + continue + if argument in {"-c", "-m", "--"} or not argument.startswith("-") or argument == "-": + break + if argument.startswith(("-W", "-X")): + skip_value = argument in {"-W", "-X"} + continue + unsupported = next((option for option in "IES" if option in argument[1:]), None) + if not argument.startswith("--") and unsupported: + raise RuntimeError( + f"project_python_option_unsupported:-{unsupported}; this option disables required runtime attachment" + ) + + +def _project_python_command(arguments: list[str]) -> tuple[list[str], dict[str, str]]: + """Use the native Python CLI, preserving -c/-m/-u and future imports exactly.""" + _validate_python_arguments(arguments) + environment = dict(os.environ) + environment["_SPECFACT_CALLER_PYTHONPATH"] = json.dumps(environment.get("PYTHONPATH")) + environment.update(PYTHONPATH=str(BUILTIN / "specfact_code_review/run"), SPECFACT_PROJECT_PYTHON="1") + launcher = runpy.run_path(str(Path(__file__).with_name("target_launch.py"))) + return launcher["interpreter_command"](["-s", *arguments]), environment + + +def main() -> None: + """Enter an isolated analyzer or hand native Python arguments to its interpreter.""" + module = sys.argv.pop(1) + if module.startswith("-") or module.endswith(".py") or module == "python-argv": + arguments = sys.argv[1:] if module == "python-argv" else [module, *sys.argv[1:]] + command, environment = _project_python_command(arguments) + os.execve(command[0], command, environment) + run_module, run_path = runpy.run_module, runpy.run_path + observer_path = str(BUILTIN / "specfact_code_review/run/target_pytest.py") + pylint_path = str(BUILTIN / "specfact_code_review/run/target_pylint.py") + snapshot_root = SNAPSHOT + try: + _configure_runtime(module) + except (RuntimeError, ImportError) as exc: + sys.stderr.write(f"{exc}\n") + raise SystemExit(78) from exc + if module == "pytest-observe": + run_path(observer_path, run_name="__main__") + elif module == "pylint": + run_path(pylint_path, init_globals={"SNAPSHOT_ROOT": snapshot_root}, run_name="__main__") + else: + run_module(module, run_name="__main__", alter_sys=True) + + +if __name__ == "__main__": + main() diff --git a/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py b/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py new file mode 100644 index 00000000..42c4424d --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/target_coverage.py @@ -0,0 +1,268 @@ +"""Target-only collection keeps reviewer instrumentation separate from native policy. + +PYTEST_DONT_REWRITE: the verified bootstrap preloads this assertion-free plugin; +customer assertion rewriting remains unchanged. +""" + +from __future__ import annotations + +import contextlib +import copy +import importlib.metadata +import json +import keyword +import os +from pathlib import Path +from types import SimpleNamespace + + +_REQUEST = "SPECFACT_REVIEW_COVERAGE_REQUEST" +_SESSION = SimpleNamespace(observation=None) +_SUPPORTED = {"7.0.0", "7.1.0"} +_FILTERS = ("source", "source_pkgs", "source_dirs", "include", "omit") + + +def configure(*, snapshot: Path, output: Path, directories: list[str], modules: list[str] | None = None) -> None: + """Publish controller-validated measurement locations to this pytest process tree.""" + os.environ[_REQUEST] = json.dumps( + {"snapshot": str(snapshot), "output": str(output), "directories": directories, "modules": modules or []} + ) + + +def _request() -> dict: + request = json.loads(os.environ[_REQUEST]) + for name in ("snapshot", "output"): + if not isinstance(request.get(name), str) or not Path(request[name]).is_absolute(): + raise ValueError("project_pytest_coverage_request_invalid") + directories = request.get("directories") + if not isinstance(directories, list) or not all( + isinstance(path, str) and Path(path).is_absolute() for path in directories + ): + raise ValueError("project_pytest_coverage_request_invalid") + request["modules"] = _module_names(request.get("modules", [])) + return request + + +def _module_names(modules) -> list[str]: + """Validate named coverage selectors independently of filesystem locations.""" + if not isinstance(modules, list) or not all( + isinstance(name, str) and name.isidentifier() and not keyword.iskeyword(name) for name in modules + ): + raise ValueError("project_pytest_coverage_request_invalid") + return modules + + +def _coverage_options(native): + run = {key: native.get_option(f"run:{key}") for key in _FILTERS} + run.update(branch=native.get_option("run:branch"), paths=native.get_option("paths")) + report = { + "fail_under": native.get_option("report:fail_under"), + "precision": native.get_option("report:precision"), + "include": native.get_option("report:include"), + "omit": native.get_option("report:omit"), + } + return run, report + + +def _configuration(options): + import coverage + from coverage.exceptions import CoverageException + + try: + native = coverage.Coverage(config_file=getattr(options, "cov_config", True)) + except CoverageException as exc: + raise ValueError(f"{type(exc).__name__}:{exc}") from exc + run, report = _coverage_options(native) + threshold = getattr(options, "cov_fail_under", None) + precision = getattr(options, "cov_precision", None) + return ( + run, + report, + report["fail_under"] if threshold is None else threshold, + report["precision"] if precision is None else precision, + ) + + +def _reviewer_sources(request, configured): + if any(configured.values()): + return [True] + collisions = [name for name in request["modules"] if Path(name).is_dir()] + if collisions: + raise ValueError( + f"project_pytest_coverage_module_selector_ambiguous:{','.join(collisions)}; " + "a same-named directory exists at pytest startup; configure an explicit coverage source_pkgs " + "selection to disambiguate installed modules" + ) + return [request["snapshot"], *request["directories"], *request["modules"]] + + +def _reviewer(options, manager, request, configured): + from pytest_cov.plugin import CovPlugin + + class ReviewerCoverage(CovPlugin): + """Use native measurement/worker transfer without creating a native report gate.""" + + def _should_report(self): + # No native coverage was requested. The controller enforces per-file policy. + return False + + owned = copy.copy(options) + owned.cov_source = _reviewer_sources(request, configured) + owned.cov_report = {} + return ReviewerCoverage(owned, manager) + + +def pytest_load_initial_conftests(early_config, parser, args): + """Native pytest-cov's tryfirst hook resolves explicit activation before this hook.""" + del parser, args + if _REQUEST not in os.environ: + return + state = SimpleNamespace( + request=None, + plugin=None, + mode="disabled", + threshold=0, + precision=0, + configuration={}, + diagnostic="", + session=None, + ) + _SESSION.observation = state + try: + _prepare(early_config) + except (AttributeError, ImportError, KeyError, OSError, RuntimeError, TypeError, ValueError) as exc: + state.diagnostic = f"project_pytest_coverage_instrumentation_unavailable:{type(exc).__name__}:{exc}" + + +def _state() -> SimpleNamespace: + state = _SESSION.observation + if state is None: + raise ValueError("project_pytest_coverage_plugin_not_activated") + return state + + +def _prepare(config): + state = _state() + version = importlib.metadata.version("pytest-cov") + if version not in _SUPPORTED: + raise ValueError(f"unsupported_pytest_cov_contract:{version}; supported=7.0.0,7.1.0") + request = _request() + state.request = request + options = config.known_args_namespace + run, report, threshold, precision = _configuration(options) + state.threshold, state.precision = threshold, precision + state.configuration = { + "run": run, + "report": report, + "cov_config": getattr(options, "cov_config", None), + "cov_report": getattr(options, "cov_report", {}), + "cov_source": getattr(options, "cov_source", None), + } + if getattr(options, "no_cov", False): + state.diagnostic = "project_pytest_coverage_disabled:--no-cov; required reviewer evidence is unavailable" + return + if not hasattr(options, "cov_source") or any( + config.pluginmanager.is_blocked(name) for name in ("pytest_cov", "pytest_cov.plugin") + ): + state.diagnostic = ( + "project_pytest_coverage_disabled:-p no:pytest_cov; required reviewer evidence is unavailable" + ) + return + native = config.pluginmanager.getplugin("_cov") + if native is not None: + state.mode, state.plugin = "native", native + return + state.mode = "reviewer" + state.plugin = _reviewer(options, config.pluginmanager, request, {key: run[key] for key in _FILTERS}) + config.pluginmanager.register(state.plugin, "_specfact_reviewer_cov") + + +def pytest_sessionfinish(session, exitstatus): + del exitstatus + if _SESSION.observation is not None: + _SESSION.observation.session = session + + +def active_plugin(): + """Return only the collector actually used for this execution.""" + return None if _SESSION.observation is None else _SESSION.observation.plugin + + +def _policy() -> dict: + state = _state() + from coverage.results import should_fail_under + + plugin = state.plugin + total = None if plugin is None else plugin.cov_total + failed = ( + state.mode == "native" + and plugin is not None + and total is not None + and state.session is not None + and int(state.session.exitstatus) == 1 + and not plugin.options.collectonly + and plugin._should_report() + and should_fail_under(total, state.threshold, state.precision) + ) + return { + "mode": state.mode, + "threshold": state.threshold, + "precision": state.precision, + "measured_total": total, + "native_threshold_failed": bool(failed), + } + + +def _export() -> dict: + state = _state() + plugin = state.plugin + if plugin is None: + return {} + if state.session is None or hasattr(state.session.config, "workerinput"): + raise ValueError("project_pytest_coverage_controller_observation_missing") + if plugin.failed and plugin.options.no_cov_on_fail: + raise ValueError( + "project_pytest_coverage_suppressed:--no-cov-on-fail; required reviewer evidence is unavailable" + ) + controller = plugin.cov_controller + run, report = _coverage_options(controller.cov) + state.configuration["effective"] = { + "run": run, + "report": report, + "threshold": plugin.options.cov_fail_under, + "precision": plugin.options.cov_precision, + } + if getattr(controller, "failed_workers", ()): + raise ValueError("project_pytest_coverage_worker_missing; inspect xdist worker coverage receipts") + output = Path(state.request["output"]) + # Preserve native report destinations. This private export has its own authority. + from coverage.exceptions import CoverageException + + try: + with contextlib.chdir(controller.topdir): + total = controller.cov.json_report(outfile=str(output)) + except CoverageException as exc: + raise ValueError(f"{type(exc).__name__}:{exc}") from exc + if state.mode == "reviewer": + plugin.cov_total = total + return json.loads(output.read_text(encoding="utf-8")) + + +def evidence() -> dict: + """Export separately labelled evidence after native pytest reporting has completed.""" + if _SESSION.observation is None: + return {"coverage": {}, "coverage_diagnostic": "project_pytest_coverage_plugin_not_activated"} + state = _state() + payload = {} + if not state.diagnostic: + try: + payload = _export() + except (AttributeError, KeyError, OSError, RuntimeError, TypeError, ValueError) as exc: + state.diagnostic = f"project_pytest_coverage_evidence_unavailable:{exc}" + return { + "coverage": payload, + "coverage_policy": _policy(), + "coverage_threshold": state.threshold, + "coverage_configuration": state.configuration, + "coverage_diagnostic": state.diagnostic, + } diff --git a/packages/specfact-code-review/src/specfact_code_review/run/target_launch.py b/packages/specfact-code-review/src/specfact_code_review/run/target_launch.py new file mode 100644 index 00000000..3c4d612e --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/target_launch.py @@ -0,0 +1,138 @@ +"""Launch target imports with no access to the sealed parent's writable results.""" + +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path + + +PROJECT = Path("/opt/specfact/project-runtime") +BUILTIN = Path(globals().get("BUILTIN", "/opt/specfact/builtin")) +CONTEXT = Path(globals().get("CONTEXT", "/opt/specfact/config/python-context")) +DOMAIN_FILES = { + "project-python": "specfact_code_review/run/target_bootstrap.py", + "pytest-observe": "specfact_code_review/run/target_pytest.py", + "pylint": "specfact_code_review/run/target_pylint.py", + "basedpyright": "specfact_code_review/tools/basedpyright_runner.py", + "crosshair": "specfact_code_review/tools/contract_runner.py", +} +SEALED = Path("/opt/specfact/analyzers") +MEMBER = Path("/opt/specfact/config/member-analyzers") + + +def member_mounts(module: str) -> list[str]: + """Expose a real import directory containing only the verified member closure.""" + if module == "python-argv": + module = "project-python" + mounts = ["--tmpfs", str(MEMBER)] + if module == "project-python": + return mounts + descriptor = json.loads((PROJECT / "project-runtime.json").read_text(encoding="utf-8")) + graph = descriptor["inventory"]["member_graphs"][module] + imports = set(graph["sealed_imports"]) + distributions = {row["name"] for row in graph["installed"] if row["origin"] == "analyzer"} + for entry in sorted(SEALED.iterdir()): + if entry.name.endswith(".dist-info"): + admitted = ( + re.sub(r"[-_.]+", "-", entry.name.removesuffix(".dist-info").rsplit("-", 1)[0]).lower() in distributions + ) + else: + admitted = entry.name.split(".", maxsplit=1)[0] in imports + if admitted: + mounts.extend(["--ro-bind", str(entry), str(MEMBER / entry.name)]) + return mounts + + +def execution_domain() -> str: + """Read the inherited member identity; environment variables cannot change it.""" + for module, relative in DOMAIN_FILES.items(): + try: + if CONTEXT.samefile(BUILTIN / relative): + return module + except OSError: + continue + raise RuntimeError("project_python_context_missing_or_invalid; launch Python inside an attached target worker") + + +def interpreter_command(arguments: list[str]) -> list[str]: + """Enter the target native loader domain while keeping the signed Python binary.""" + python = "/opt/specfact/python/bin/python" + loader = PROJECT / "native/ld-linux-x86-64.so.2" + if loader.is_file(): + return [str(loader), "--library-path", str(loader.parent), python, *arguments] + return [python, *arguments] + + +def target_command(module: str, arguments: list[str]) -> list[str]: + """Separate PID/mount/network domains before any customer Python executes.""" + return [ + "/opt/specfact/bin/bwrap-static", + "--die-with-parent", + "--new-session", + "--unshare-all", + "--ro-bind", + "/", + "/", + "--tmpfs", + "/opt/specfact/output", + "--tmpfs", + "/opt/specfact/config", + "--tmpfs", + "/opt/specfact/control", + "--tmpfs", + "/tmp", + "--ro-bind", + "/opt/specfact/project-runtime/worker-config", + "/etc", + "--bind", + "/opt/specfact/tmp", + "/opt/specfact/tmp", + "--proc", + "/proc", + "--dev", + "/dev", + *member_mounts(module), + "--ro-bind", + str(BUILTIN / DOMAIN_FILES.get(module, DOMAIN_FILES["project-python"])), + str(CONTEXT), + "--clearenv", + "--setenv", + "HOME", + "/opt/specfact/tmp/home", + "--setenv", + "TMPDIR", + "/opt/specfact/tmp", + "--setenv", + "PATH", + "/opt/specfact/project-runtime/bin:/opt/specfact/python/bin:/opt/specfact/analyzers/bin", + "--setenv", + "COVERAGE_FILE", + "/opt/specfact/tmp/.coverage", + "--setenv", + "PYTEST_DISABLE_PLUGIN_AUTOLOAD", + "1", + "--setenv", + "SPECFACT_TARGET_PYTEST", + "1" if module == "pytest-observe" else "0", + "--chdir", + "/opt/specfact/snapshot", + *interpreter_command( + ["-I", "-S", "/opt/specfact/builtin/specfact_code_review/run/target_bootstrap.py", module, *arguments] + ), + ] + + +def main() -> None: + # This generated executable is called only from an already isolated worker. + # First-entry callers always use target_command and cannot select this via env. + command = interpreter_command( + ["-I", "-S", str(BUILTIN / DOMAIN_FILES["project-python"]), "python-argv", *sys.argv[1:]] + ) + os.execv(command[0], command) + + +if __name__ == "__main__": + main() diff --git a/packages/specfact-code-review/src/specfact_code_review/run/target_pylint.py b/packages/specfact-code-review/src/specfact_code_review/run/target_pylint.py new file mode 100644 index 00000000..aa27e86c --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/target_pylint.py @@ -0,0 +1,37 @@ +"""Run Pylint with verified runtime roots as defaults beneath native configuration.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from pylint.lint import PyLinter, Run + + +SNAPSHOT_ROOT = Path(globals().get("SNAPSHOT_ROOT", "/opt/specfact/snapshot")) + + +def _runtime_source_roots() -> list[str]: + """Use only attached snapshot imports, including validated editable .pth roots.""" + snapshot = SNAPSHOT_ROOT.resolve() + roots = {Path(path).resolve() for path in sys.path if path} + attached = [root for root in roots if root.is_relative_to(snapshot) and root.is_dir()] + # Pylint selects the first matching ancestor; prefer the specific import root. + return [str(root) for root in sorted(attached, key=lambda root: (-len(root.parts), str(root)))] + + +def _runtime_linter(*args, **kwargs) -> PyLinter: + """Set defaults before Run applies native config selection and CLI overrides.""" + linter = PyLinter(*args, **kwargs) + linter.set_option("source-roots", _runtime_source_roots()) + return linter + + +class _RuntimeRun(Run): + """Keep the native runner and ordinary picklable PyLinter instance.""" + + LinterClass = staticmethod(_runtime_linter) + + +if __name__ == "__main__": + _RuntimeRun(sys.argv[1:]) diff --git a/packages/specfact-code-review/src/specfact_code_review/run/target_pytest.py b/packages/specfact-code-review/src/specfact_code_review/run/target_pytest.py new file mode 100644 index 00000000..a8d796a6 --- /dev/null +++ b/packages/specfact-code-review/src/specfact_code_review/run/target_pytest.py @@ -0,0 +1,274 @@ +"""Observe customer pytest in its target worker, never in the controller.""" + +from __future__ import annotations + +import contextlib +import glob +import importlib.metadata +import json +import keyword +import os +import runpy +import shlex +import sys +from pathlib import Path +from typing import Any + + +ROOT = Path("/opt/specfact/project-runtime") +SNAPSHOT_ROOT = Path("/opt/specfact/snapshot") + + +def _explicit_plugins(config: dict[str, Any]) -> set[str]: + raw = config.get("addopts", []) + tokens = shlex.split(raw) if isinstance(raw, str) else list(raw) + explicit = {str(tokens[index + 1]) for index, token in enumerate(tokens[:-1]) if token == "-p"} + explicit.update(str(token)[2:] for token in tokens if str(token).startswith("-p") and len(str(token)) > 2) + return explicit + + +def _entry_plugins(distribution: importlib.metadata.Distribution, explicit: set[str]) -> list[str]: + plugins = [] + for entry in distribution.entry_points: + if entry.group != "pytest11": + continue + names = {entry.name, entry.value.split(":")[0]} + if names & explicit or {f"no:{name}" for name in names} & explicit: + continue + plugins.extend(("-p", entry.name)) + return plugins + + +def _plugins(config: dict[str, Any]) -> list[str]: + explicit = _explicit_plugins(config) + plugins = [] + raw = config.get("addopts", []) + options = shlex.split(raw) if isinstance(raw, str) else list(raw) + autoload = "--disable-plugin-autoload" not in options + for distribution in importlib.metadata.distributions(path=[str(ROOT / "site-packages")]): + if autoload: + plugins.extend(_entry_plugins(distribution, explicit)) + coverage_choices = {"pytest_cov", "pytest_cov.plugin", "no:pytest_cov", "no:pytest_cov.plugin"} + if "pytest_cov" not in plugins and not coverage_choices & explicit: + plugins.extend(("-p", "pytest_cov")) + return plugins + + +def _effective_pytest_config(descriptor: dict[str, Any]) -> dict[str, Any]: + config = dict(descriptor["project"]["pytest_config"]) + options = config.get("addopts", []) + config["addopts"] = [ + *(shlex.split(options) if isinstance(options, str) else options), + *descriptor["inventory"].get("pytest_arguments", []), + ] + return config + + +def _test_support_roots(config) -> list[str]: + """Retain configured test directories without excluding the whole snapshot.""" + snapshot = SNAPSHOT_ROOT.resolve() + roots = set() + for pattern in config.getini("testpaths"): + for relative in glob.iglob(pattern, root_dir=config.rootpath, recursive=True): + path = (config.rootpath / relative).resolve() + if path.is_dir() and path != snapshot and path.is_relative_to(snapshot): + roots.add(path.relative_to(snapshot).as_posix()) + return sorted(roots) + + +class Observer: + """Retain phase-specific facts without claiming setup failures executed test calls.""" + + def __init__(self) -> None: + self.records = [] + self.collected = set() + self.deselected = set() + self.deselection_inventory = "complete" + self.worker_collected = set() + self.collection_errors = [] + self.internal_errors = [] + self.coverage_threshold = None + self.coverage_origin = {"candidates": [], "relocations": {}, "diagnostic": ""} + self.test_roots = [] + self.pytest_root = None + + def pytest_sessionfinish(self, session, exitstatus): + del exitstatus + helper = sys.modules.get("_specfact_target_coverage") + coverage_plugin = helper.active_plugin() if helper is not None else None + if coverage_plugin is None: + coverage_plugin = session.config.pluginmanager.getplugin("_cov") + self.coverage_threshold = getattr(getattr(coverage_plugin, "options", None), "cov_fail_under", None) + self.coverage_origin["relocations"], self.coverage_origin["diagnostic"] = _native_coverage_relocations( + coverage_plugin, self.coverage_origin["candidates"] + ) + self.test_roots = _test_support_roots(session.config) + root = session.config.rootpath.resolve() + snapshot = SNAPSHOT_ROOT.resolve() + self.pytest_root = root.relative_to(snapshot).as_posix() if root.is_relative_to(snapshot) else str(root) + + def pytest_internalerror(self, excrepr, excinfo): + del excinfo + self.internal_errors.append(str(excrepr)) + + def pytest_testnodedown(self, node, error): + del node + if error and str(error) not in self.internal_errors: + self.internal_errors.append(str(error)) + + def pytest_itemcollected(self, item): + self.collected.add(item.nodeid) + + def pytest_deselected(self, items): + removed = {item.nodeid for item in items} + self.deselected.update(removed) + self.collected.difference_update(removed) + + def pytest_collection_finish(self, session): + selected = {item.nodeid for item in session.items} | self.worker_collected + self.deselected.update(self.collected - selected) + self.collected = selected + + def pytest_xdist_node_collection_finished(self, node, ids): + del node + self.deselection_inventory = "controller_observed" + self.worker_collected.update(ids) + self.collected.update(ids) + + def pytest_collectreport(self, report): + if report.failed: + self.collection_errors.append({"nodeid": report.nodeid, "detail": report.longreprtext}) + + def pytest_runtest_logreport(self, report): + self.records.append( + { + "nodeid": report.nodeid, + "phase": report.when, + "outcome": report.outcome, + "wasxfail": str(getattr(report, "wasxfail", "")), + "has_xfail": hasattr(report, "wasxfail"), + "detail": report.longreprtext if report.failed else "", + } + ) + + +def _native_coverage_relocations(plugin, candidates): + """Ask native Coverage for origin mapping without losing actual test records.""" + if not candidates: + return {}, "" + try: + with contextlib.chdir(plugin.cov_controller.topdir): + coverage = plugin.cov_controller.cov + aliases = coverage._make_aliases() + return {path: aliases.map(path) for path in candidates}, "" + except (AttributeError, TypeError, ValueError, OSError) as exc: + return {}, f"coverage_origin_unavailable:{type(exc).__name__}" + + +def _installed_coverage_candidates(request: dict) -> list[str]: + site = (ROOT / "site-packages").resolve() + candidates = request.get("coverage_candidates", []) + if not isinstance(candidates, list) or not all( + isinstance(path, str) and Path(path).is_absolute() and Path(path).resolve().is_relative_to(site) + for path in candidates + ): + raise ValueError("project_pytest_coverage_candidate_invalid") + return candidates + + +def _installed_coverage_directories(request: dict) -> list[str]: + """Accept only controller-selected directories inside the attached runtime.""" + site = ROOT / "site-packages" + directories = request.get("coverage_directories", []) + if not isinstance(directories, list) or not all( + isinstance(path, str) + and Path(path).is_dir() + and Path(path).resolve().is_relative_to(site.resolve()) + and Path(path).resolve() != site.resolve() + for path in directories + ): + raise ValueError("project_pytest_installed_coverage_directory_invalid") + return directories + + +def _installed_coverage_modules(request: dict) -> list[str]: + """Limit named selectors to regular top-level modules in the attached runtime.""" + site = (ROOT / "site-packages").resolve() + modules = request.get("coverage_modules", []) + if not isinstance(modules, list) or not all( + isinstance(name, str) + and name.isidentifier() + and not keyword.iskeyword(name) + and (site / f"{name}.py").is_file() + and not (site / f"{name}.py").is_symlink() + for name in modules + ): + raise ValueError("project_pytest_installed_coverage_module_invalid") + return modules + + +def main() -> None: + import pytest + + pytest.hookimpl(optionalhook=True)(Observer.pytest_xdist_node_collection_finished) + pytest.hookimpl(optionalhook=True)(Observer.pytest_testnodedown) + request = json.loads(sys.argv[1]) + descriptor = json.loads((ROOT / "project-runtime.json").read_text(encoding="utf-8")) + output = Path("/opt/specfact/tmp/pytest-observation.json") + observer = Observer() + observer.coverage_origin["candidates"] = _installed_coverage_candidates(request) + + os.environ["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] = "1" + coverage_output = Path("/opt/specfact/tmp/coverage.json") + os.environ["COVERAGE_FILE"] = "/opt/specfact/tmp/.coverage" + helper = sys.modules.get("_specfact_target_coverage") + if helper is None: + bootstrap = runpy.run_path(str(Path(__file__).with_name("target_bootstrap.py"))) + helper = bootstrap["_load_pytest_coverage"]() + helper.configure( + snapshot=SNAPSHOT_ROOT, + output=coverage_output, + directories=_installed_coverage_directories(request), + modules=_installed_coverage_modules(request), + ) + args = [ + "-p", + "_specfact_target_coverage", + *_plugins(_effective_pytest_config(descriptor)), + *descriptor["inventory"].get("pytest_arguments", []), + "-o", + "cache_dir=/opt/specfact/tmp/pytest-cache", + "--basetemp=/opt/specfact/tmp/pytest", + *request["selectors"], + ] + code = pytest.main(args, plugins=[observer]) + measurement = helper.evidence() + output.write_text( + json.dumps( + { + "exit_code": int(code), + "collected": sorted(observer.collected), + "deselected": sorted(observer.deselected), + "deselection_inventory": observer.deselection_inventory, + "records": observer.records, + "collection_errors": observer.collection_errors, + "internal_errors": observer.internal_errors, + "pytest_version": pytest.__version__, + "coverage_version": importlib.metadata.version("coverage"), + "pytest_cov_version": importlib.metadata.version("pytest-cov"), + **measurement, + "coverage_relocations": observer.coverage_origin["relocations"], + "coverage_origin_diagnostic": observer.coverage_origin["diagnostic"], + "test_roots": observer.test_roots, + "pytest_root": observer.pytest_root, + "argv": args, + "configured_addopts": descriptor["project"]["pytest_config"].get("addopts", []), + } + ), + encoding="utf-8", + ) + raise SystemExit(int(code)) + + +if __name__ == "__main__": + main() diff --git a/packages/specfact-code-review/src/specfact_code_review/tools/pylint_runner.py b/packages/specfact-code-review/src/specfact_code_review/tools/pylint_runner.py index e047aa40..d5e0eff1 100644 --- a/packages/specfact-code-review/src/specfact_code_review/tools/pylint_runner.py +++ b/packages/specfact-code-review/src/specfact_code_review/tools/pylint_runner.py @@ -97,6 +97,8 @@ def _finding_from_item(item: object, *, allowed_paths: set[str]) -> ReviewFindin def _payload_from_output(stdout: str, *, stderr: str, returncode: int | None) -> list[object]: + if returncode == 78: + raise ValueError(f"pylint worker startup failed; {returncode=}, stderr={stderr[:4096]!r}") stripped = stdout.strip() if not stripped: out = stdout diff --git a/packages/specfact-code-review/src/specfact_code_review/tools/semgrep_runner.py b/packages/specfact-code-review/src/specfact_code_review/tools/semgrep_runner.py index 3d90236f..dfacd0ac 100644 --- a/packages/specfact-code-review/src/specfact_code_review/tools/semgrep_runner.py +++ b/packages/specfact-code-review/src/specfact_code_review/tools/semgrep_runner.py @@ -386,6 +386,36 @@ def _snip_stderr_tail(stderr: str) -> str: return "…" + err_raw[-_SEMGREP_STDERR_SNIP_MAX:] +def _bounded_semgrep_scalar(value: object) -> object: + """Bound escaped scalar text so nine fields fit the diagnostic budget.""" + + if not isinstance(value, str): + return value if len(json.dumps(value)) <= 384 else "[scalar exceeds diagnostic limit]" + value = value[:512] + if len(json.dumps(value, ensure_ascii=True)) <= 384: + return value + lower, upper = 0, len(value) + while lower < upper: + middle = (lower + upper + 1) // 2 + if len(json.dumps(value[:middle] + "...", ensure_ascii=True)) <= 384: + lower = middle + else: + upper = middle - 1 + return value[:lower] + "..." + + +def _semgrep_error_fields(error: object) -> dict[str, object]: + """Retain diagnostic fields only; never serialize nested source metadata.""" + + if not isinstance(error, dict): + return {} + fields: dict[str, object] = {} + for key in ("type", "code", "message"): + if key in error and isinstance(error[key], (str, int, float, bool, type(None))): + fields[key] = _bounded_semgrep_scalar(error[key]) + return fields + + def _validate_semgrep_completion(result: subprocess.CompletedProcess[str], payload: dict[str, object]) -> None: """Reject process and structured execution errors before accepting results.""" @@ -396,7 +426,13 @@ def _validate_semgrep_completion(result: subprocess.CompletedProcess[str], paylo if not isinstance(errors, list): raise ValueError("semgrep structured errors must be a list") if errors: - raise ValueError(f"semgrep returned structured errors (count={len(errors)})") + # Like bounded stderr, native messages may include reviewed-source text. + # Do not serialize arbitrary error fields or claim secret redaction. + details = json.dumps([_semgrep_error_fields(error) for error in errors[:3]], separators=(",", ":")) + raise ValueError( + f"semgrep returned structured errors (count={len(errors)}, omitted={max(0, len(errors) - 3)}); " + f"details={details}" + ) def _load_semgrep_payload( diff --git a/pyproject.toml b/pyproject.toml index 2e709ab2..f8baca61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,23 +12,29 @@ dependencies = [] [project.optional-dependencies] dev = [] +[tool.specfact.code-review] +native_tools = ["git", "uname", "sed"] + [tool.hatch.envs.default] type = "virtual" path = ".venv" dependencies = [ + "specfact-cli==0.55.4", "beartype>=0.22.0", "json5>=0.9.28", "icontract>=2.7.1", "packaging>=24.0", "pytest>=8.4.2", "pytest-cov>=7.0.0", + "pytest-xdist>=3.8.0", "pytest-mock>=3.15.1", "pytest-asyncio>=1.2.0", - "basedpyright>=1.32.1", + "basedpyright==1.39.10", + "nodejs-wheel-binaries==24.16.0", "radon>=6.0.1", "ruff>=0.14.2", "semgrep>=1.144.0", - "pylint>=4.0.2", + "pylint==4.0.7", "pre-commit>=3.7.0", "typer>=0.20.0", "pyyaml>=6.0.3", diff --git a/registry/index.json b/registry/index.json index 439faee9..ee4da40d 100644 --- a/registry/index.json +++ b/registry/index.json @@ -78,9 +78,9 @@ }, { "id": "nold-ai/specfact-code-review", - "latest_version": "0.49.85", - "download_url": "modules/specfact-code-review-0.49.85.tar.gz", - "checksum_sha256": "3e3ee8898c58f696f6dec7ca82134d9a8178356202f4338743995e4e6486387f", + "latest_version": "0.50.1", + "download_url": "modules/specfact-code-review-0.50.1.tar.gz", + "checksum_sha256": "61489b8b3e9596082f738d093987fe0427815b2a6148cfae678a996878d117e7", "core_compatibility": ">=0.55.1,<1.0.0", "tier": "official", "publisher": { diff --git a/registry/modules/specfact-code-review-0.50.0.tar.gz b/registry/modules/specfact-code-review-0.50.0.tar.gz new file mode 100644 index 00000000..8f27b048 Binary files /dev/null and b/registry/modules/specfact-code-review-0.50.0.tar.gz differ diff --git a/registry/modules/specfact-code-review-0.50.0.tar.gz.sha256 b/registry/modules/specfact-code-review-0.50.0.tar.gz.sha256 new file mode 100644 index 00000000..de9ab9c6 --- /dev/null +++ b/registry/modules/specfact-code-review-0.50.0.tar.gz.sha256 @@ -0,0 +1 @@ +a757a9122be8da6c3c1ea63354a5614be7a3532bffdf0a98aef37934fa09c060 diff --git a/registry/modules/specfact-code-review-0.50.1.tar.gz b/registry/modules/specfact-code-review-0.50.1.tar.gz new file mode 100644 index 00000000..3ed6418f Binary files /dev/null and b/registry/modules/specfact-code-review-0.50.1.tar.gz differ diff --git a/registry/modules/specfact-code-review-0.50.1.tar.gz.sha256 b/registry/modules/specfact-code-review-0.50.1.tar.gz.sha256 new file mode 100644 index 00000000..a0bb8a91 --- /dev/null +++ b/registry/modules/specfact-code-review-0.50.1.tar.gz.sha256 @@ -0,0 +1 @@ +61489b8b3e9596082f738d093987fe0427815b2a6148cfae678a996878d117e7 diff --git a/registry/signatures/specfact-code-review-0.50.0.tar.sig b/registry/signatures/specfact-code-review-0.50.0.tar.sig new file mode 100644 index 00000000..019783bc --- /dev/null +++ b/registry/signatures/specfact-code-review-0.50.0.tar.sig @@ -0,0 +1 @@ +4MTwnnk7uKPZMpQKaqHkAMezz2nBFWr9KuKdUMpgI9/lBvY+gaK9u5Z6WdWbxEKoLdYZcrTXMxWSqTGhATSWAw== diff --git a/registry/signatures/specfact-code-review-0.50.1.tar.sig b/registry/signatures/specfact-code-review-0.50.1.tar.sig new file mode 100644 index 00000000..f406427f --- /dev/null +++ b/registry/signatures/specfact-code-review-0.50.1.tar.sig @@ -0,0 +1 @@ ++ZtHk940rQ8FS6Tbh67+rY2V9BAV2SR8S2qtJNt3EUT390N7ksDZFvxDAfwnnzK/vF8KF3ZTdorRSO7QhxAjAQ== diff --git a/scripts/external_capsule_corpus.py b/scripts/external_capsule_corpus.py new file mode 100644 index 00000000..e1ba9b51 --- /dev/null +++ b/scripts/external_capsule_corpus.py @@ -0,0 +1,531 @@ +"""Run the pinned external corpus through the public review/runtime interface.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import keyword +import os +import platform +import runpy +import shutil +import stat +import subprocess +import sys +import time +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + + +EXPECTED_ANALYZERS = frozenset( + { + "ruff", + "radon", + "semgrep-clean", + "ai-bloat-ast", + "ast-clean-code", + "basedpyright", + "pylint", + "contracts", + "semgrep-bugs", + "targeted-pytest-coverage", + } +) + + +MANIFEST = Path(__file__).resolve().parents[1] / "tests/fixtures/portable-runtime/corpus.json" + + +def _require_test_execution(rows: list[dict[str, Any]]) -> None: + execution = next((row.get("target_execution", {}) for row in rows if row["id"] == "targeted-pytest-coverage"), {}) + if not execution.get("collected") or not any(row["phase"] == "call" for row in execution.get("records", [])): + raise ValueError("external capsule has no actual test execution") + if not execution.get("coverage", {}).get("files"): + raise ValueError("external capsule has no actual coverage") + + +def assert_completed_report(report: dict[str, Any]) -> None: + """Allow real findings while requiring completed applicable evidence.""" + if report.get("has_unknown_required_evidence") or report.get("assurance_status") not in {"PASS", "FAIL"}: + raise ValueError("external capsule analysis incomplete") + rows = report.get("analyzer_evidence", []) + if any(row.get("evidence_outcome") == "UNKNOWN" for row in rows): + raise ValueError("external capsule member incomplete") + _require_test_execution(rows) + if len(rows) != len(EXPECTED_ANALYZERS) or {row["id"] for row in rows} != EXPECTED_ANALYZERS: + raise ValueError("external capsule analyzer roster incomplete or duplicated") + if any(row.get("evidence_outcome") not in {"PASS", "FAIL"} for row in rows): + raise ValueError("external capsule applicable analyzer did not complete") + + +def network_counters(source: Path = Path("/proc/net/dev")) -> dict[str, int] | None: + """Measure Linux host-interface bytes without inspecting network payloads.""" + if not source.is_file(): + return None + totals = {"received_bytes": 0, "sent_bytes": 0} + for line in source.read_text(encoding="ascii").splitlines()[2:]: + interface, raw = line.split(":", 1) + if interface.strip() == "lo": + continue + fields = raw.split() + totals["received_bytes"] += int(fields[0]) + totals["sent_bytes"] += int(fields[8]) + return totals + + +def network_delta(before: dict[str, int] | None, after: dict[str, int] | None) -> dict[str, Any] | None: + """Report host-wide traffic, explicitly not exact dependency download sizes.""" + if before is None or after is None: + return None + return { + **{name: after[name] - value for name, value in before.items()}, + "scope": "host_non_loopback_interface_counters", + } + + +OFFLINE_ROOT = Path("/specfact-corpus-offline") + + +def _check_offline_path(path: Path, *, directory: bool = False) -> None: + metadata = path.lstat() + expected_type = stat.S_ISDIR if directory else stat.S_ISREG + if metadata.st_uid != 0 or metadata.st_mode & 0o022 or not expected_type(metadata.st_mode): + raise ValueError(f"offline_launcher_untrusted:{path.name}") + + +def _verified_offline_launcher(configured: str) -> Path: + launcher = OFFLINE_ROOT / "bwrap" + if Path(configured) != launcher: + raise ValueError("offline_launcher_untrusted:unexpected_path") + try: + for directory in (OFFLINE_ROOT, *OFFLINE_ROOT.parents): + _check_offline_path(directory, directory=True) + identity = OFFLINE_ROOT / "bwrap.sha256" + _check_offline_path(launcher) + _check_offline_path(identity) + expected = identity.read_text(encoding="ascii").strip() + actual = hashlib.sha256(launcher.read_bytes()).hexdigest() + if expected != actual or not os.access(launcher, os.X_OK): + raise ValueError("offline_launcher_untrusted:identity_or_mode") + except OSError as exc: + raise ValueError("offline_launcher_untrusted:unavailable") from exc + return launcher + + +def offline_command(argv: list[str]) -> list[str]: + """Deny external networking independently of the customer's offline flag.""" + launcher = os.environ.get("SPECFACT_CORPUS_OFFLINE_LAUNCHER") + if not launcher: + raise ValueError("offline corpus requires an administrator-provisioned namespace launcher") + launcher = _verified_offline_launcher(launcher) + probe = ( + "import fcntl,os,socket,struct,sys; probe_socket=socket.socket(); " + "valid=os.getuid()!=0 and os.readlink('/proc/self/ns/net')!=sys.argv[1] " + "and all(name=='lo' or not (struct.unpack_from('H',fcntl.ioctl(probe_socket.fileno()," + "0x8913,struct.pack('256s',name.encode())),16)[0]&1) for _,name in socket.if_nameindex()); " + "probe_socket.close(); " + "valid or sys.exit(77); " + "print('SPECFACT_OFFLINE_NETWORK_VERIFIED',file=sys.stderr,flush=True); " + "os.execvp(sys.argv[2],sys.argv[2:])" + ) + return [ + str(launcher), + "--unshare-user", + "--unshare-net", + "--uid", + str(os.getuid()), + "--gid", + str(os.getgid()), + "--bind", + "/", + "/", + "--dev", + "/dev", + "--die-with-parent", + "--", + sys.executable, + "-c", + probe, + os.readlink("/proc/self/ns/net"), + *argv, + ] + + +def execute(argv: list[str], *, cwd: Path, evidence: Path, name: str, allowed: tuple[int, ...] = (0,)) -> str: + started = time.monotonic() + network_before = network_counters() + offline = name in {"offline-prepare", "warm-attach"} + command = offline_command(argv) if offline else argv + result = subprocess.run(command, cwd=cwd, text=True, capture_output=True, check=False, timeout=4500) + evidence.mkdir(parents=True, exist_ok=True) + (evidence / f"{name}.stdout").write_text(result.stdout, encoding="utf-8") + (evidence / f"{name}.stderr").write_text(result.stderr, encoding="utf-8") + (evidence / f"{name}.json").write_text( + json.dumps( + { + "argv": argv, + "execution_argv": command, + "offline_network_verified": offline and "SPECFACT_OFFLINE_NETWORK_VERIFIED" in result.stderr, + "exit": result.returncode, + "seconds": time.monotonic() - started, + "network": network_delta(network_before, network_counters()), + "python": sys.version, + "platform": platform.platform(), + "uid": os.getuid(), + }, + indent=2, + ), + encoding="utf-8", + ) + if offline and "SPECFACT_OFFLINE_NETWORK_VERIFIED" not in result.stderr: + raise ValueError(f"{name} did not verify network isolation; see {evidence}") + if result.returncode not in allowed: + raise ValueError(f"{name} failed with exit {result.returncode}; see {evidence}") + return result.stdout + + +def materialize_reconstruction(source: Path, destination: Path) -> None: + """Create executable external fixtures without collecting them in the module suite.""" + shutil.copytree(source, destination) + for template in destination.rglob("*.py.in"): + template.rename(template.with_suffix("")) + + +def checkout(entry: dict[str, Any], root: Path, evidence: Path) -> None: + if entry.get("fixture"): + root.parent.mkdir(parents=True, exist_ok=True) + materialize_reconstruction(MANIFEST.parent / entry["fixture"], root) + identity = hashlib.sha256(json.dumps(tracked_identity(root), sort_keys=True).encode()).hexdigest() + entry["commit"] = "reconstructed-sha256:" + identity + evidence.mkdir(parents=True, exist_ok=True) + (evidence / "fixture-origin.json").write_text( + json.dumps( + {"kind": "reconstruction", "original_customer_reproduction": False, "source_identity": identity} + ), + encoding="utf-8", + ) + return + root.mkdir(parents=True) + execute(["git", "init", "--quiet"], cwd=root, evidence=evidence, name="git-init") + execute(["git", "fetch", "--depth=1", entry["url"], entry["commit"]], cwd=root, evidence=evidence, name="git-fetch") + execute(["git", "checkout", "--detach", "--quiet", "FETCH_HEAD"], cwd=root, evidence=evidence, name="git-checkout") + actual = execute(["git", "rev-parse", "HEAD"], cwd=root, evidence=evidence, name="git-head").strip() + if actual != entry["commit"]: + raise ValueError("corpus checkout identity mismatch") + + +def _worktree_entry_identity(path: Path) -> str | None: + if path.is_symlink(): + payload = os.readlink(path).encode() + elif path.is_file(): + payload = path.read_bytes() + elif path.is_dir(): + payload = b"directory" + else: + return None + return hashlib.sha256(payload + str(path.lstat().st_mode).encode()).hexdigest() + + +def tracked_identity(root: Path) -> dict[str, str]: + identities: dict[str, str] = {} + root_identity = _worktree_entry_identity(root) + if root_identity is not None: + identities["."] = root_identity + for directory, directories, files in os.walk(root, followlinks=False): + directories[:] = sorted(name for name in directories if name != ".git") + for name in sorted([*directories, *files]): + path = Path(directory) / name + if name == ".git": + continue + identity = _worktree_entry_identity(path) + if identity is not None: + identities[path.relative_to(root).as_posix()] = identity + return identities + + +def json_document(output: str) -> dict[str, Any]: + # The core CLI may surround command output with a banner and timing footer. + decoder = json.JSONDecoder() + for index, character in enumerate(output): + if character == "{": + try: + value, _ = decoder.raw_decode(output[index:]) + except ValueError: + continue + if isinstance(value, dict): + return value + raise ValueError("runtime command emitted no JSON document") + + +def review( + root: Path, evidence: Path, config: Path, paths: list[str], *, descriptor: str = "", name: str = "" +) -> dict[str, Any]: + name = name or ("warm-attach" if descriptor else "cold-auto") + output = evidence / f"{name}-report.json" + argv = [ + "specfact", + "code", + "review", + "run", + *paths, + "--project-config", + str(config), + "--json", + "--out", + str(output), + "--bug-hunt", + ] + if descriptor: + argv += ["--project-runtime", descriptor] + execute(argv, cwd=root, evidence=evidence, name=name, allowed=(0, 1, 2)) + report = json.loads(output.read_text(encoding="utf-8")) + assert_completed_report(report) + return report + + +def assert_host_execution(result: Path) -> None: + """Reject manager startup errors and suites that never enter a test call.""" + if not result.is_file(): + raise ValueError("host has no actual test execution report") + cases = ET.parse(result).getroot().iter("testcase") + if not any(case.find("error") is None and case.find("skipped") is None for case in cases): + raise ValueError("host has no actual test execution") + + +def host_test(entry: dict[str, Any], root: Path, evidence: Path, workspace: Path) -> None: + """Record native-manager test execution in a separate disposable checkout.""" + host = workspace / "host-checkouts" / entry["name"] + shutil.copytree(root, host, symlinks=True) + environment = workspace / "host-environments" / entry["name"] + execute([sys.executable, "-m", "venv", str(environment)], cwd=host, evidence=evidence, name="host-venv") + python = str(environment / "bin/python") + manager = entry["manager"] + versions = {"pip": "pip==26.2.1", "hatch": "hatch==1.18.0", "uv": "uv==0.12.13", "poetry": "poetry==2.4.3"} + execute([python, "-m", "pip", "install", versions[manager]], cwd=host, evidence=evidence, name="host-manager") + if manager == "pip": + install = [ + python, + "-m", + "pip", + "install", + ".", + *[arg for group in entry["groups"] for arg in ("--group", group)], + ] + run = [python, "-m", "pytest"] + elif manager == "uv": + install = [ + python, + "-m", + "uv", + "sync", + "--locked", + "--no-default-groups", + *[arg for group in entry["groups"] for arg in ("--group", group)], + ] + run = [str(host / ".venv/bin/python"), "-m", "pytest"] + elif manager == "hatch": + export = json.loads( + execute( + ["env", f"HATCH_UV={environment / 'bin/uv'}", python, "-m", "hatch", "env", "show", "--json"], + cwd=host, + evidence=evidence, + name="host-environments", + ) + ) + driver = ( + MANIFEST.parents[3] / "packages/specfact-code-review/src/specfact_code_review/run/runtime_build_driver.py" + ) + selected = runpy.run_path(str(driver))["select_hatch_environment"]( + export, entry["environment"], f"{sys.version_info.major}.{sys.version_info.minor}" + ) + install = [python, "-m", "hatch", "env", "create", selected] + run = [python, "-m", "hatch", "-e", selected, "run", "python", "-m", "pytest"] + else: + install = [python, "-m", "poetry", "install", "--no-interaction", "--only", "main," + ",".join(entry["groups"])] + run = [python, "-m", "poetry", "run", "python", "-m", "pytest"] + execute(install, cwd=host, evidence=evidence, name="host-install") + execute( + [ + *run, + *entry.get("host_pytest_args", []), + f"--junitxml={evidence / 'host-junit.xml'}", + *[path for path in entry["paths"] if path.startswith("tests/")], + ], + cwd=host, + evidence=evidence, + name="host-tests", + allowed=(0, 1), + ) + assert_host_execution(evidence / "host-junit.xml") + + +def _controlled_module(entry: dict[str, Any]) -> tuple[str, str]: + """Require one explicit canonical package path and matching Python import.""" + definition = entry.get("controlled_defect") + if not isinstance(definition, dict): + raise ValueError("controlled fixture path/import metadata missing") + relative, module = definition.get("source"), definition.get("import") + if not isinstance(module, str) or not all( + part.isidentifier() and not keyword.iskeyword(part) for part in module.split(".") + ): + raise ValueError("controlled fixture import invalid") + return _controlled_source(relative, module), module + + +def _controlled_source(relative: object, module: str) -> str: + """Keep the declared source path canonical and consistent with its package import.""" + if not isinstance(relative, str): + raise ValueError("controlled fixture path invalid") + path = Path(relative) + suffix = module.replace(".", "/") + ".py" + if path.is_absolute() or path.as_posix() != relative or ".." in path.parts or "\\" in relative: + raise ValueError("controlled fixture path must be canonical and relative") + if not (relative == suffix or relative.endswith("/" + suffix)): + raise ValueError("controlled fixture path/import mismatch") + return relative + + +def _controlled_destination(root: Path, relative: str) -> Path: + """Allow only absent files beneath existing ordinary copied package directories.""" + parent = root + for part in (".", *Path(relative).parts[:-1]): + parent /= part + if parent.is_symlink() or not parent.is_dir(): + raise ValueError(f"controlled fixture path has unsafe parent:{relative}") + destination = root / relative + if destination.exists() or destination.is_symlink(): + raise ValueError(f"controlled fixture destination already exists:{relative}") + return destination + + +def controlled_defect(root: Path, evidence: Path, config: Path, workspace: Path, *, entry: dict[str, Any]) -> None: + """Require actual test failure and static defect detection in an identified copy.""" + relative, module = _controlled_module(entry) + test_path = "tests/test_specfact_controlled.py" + _controlled_destination(root, relative) + _controlled_destination(root, test_path) + copy = workspace / "controlled" / root.name + if copy.parent.is_symlink() or copy.exists() or copy.is_symlink(): + raise ValueError("controlled fixture destination is unsafe or already exists") + shutil.copytree(root, copy, symlinks=True) + source, test = _controlled_destination(copy, relative), _controlled_destination(copy, test_path) + with source.open("x", encoding="utf-8") as stream: + stream.write('def controlled() -> int:\n return "injected wrong type"\n') + with test.open("x", encoding="utf-8") as stream: + stream.write( + f"from {module} import controlled\n\n" + 'def test_specfact_controlled_failure():\n assert controlled() == 42, "SPECFACT_CONTROLLED_473"\n' + ) + report = review( + copy, + evidence, + config, + [relative, test_path], + name="controlled-defect", + ) + findings = report.get("findings", []) + if not any(row.get("rule") == "TEST_OUTCOME_NOT_PASS" for row in findings): + raise ValueError("controlled failing test was not detected") + if not any( + row.get("tool") == "basedpyright" and row.get("file", "").endswith("specfact_controlled.py") for row in findings + ): + raise ValueError("controlled type defect was not detected") + + +def _assert_verified_imports(report: dict[str, Any], entry: dict[str, Any]) -> None: + """Reject known namespace regressions without requiring unrelated findings to vanish.""" + expected = {(row["file"], row["line"]) for row in entry.get("verified_imports", [])} + for finding in report.get("findings", []): + path = finding.get("file", "").removeprefix("/opt/specfact/snapshot/").removeprefix("./") + if ( + finding.get("tool") == "pylint" + and finding.get("rule") in {"E0401", "E0611"} + and (path, finding.get("line")) in expected + ): + raise ValueError(f"external capsule misresolved verified import at {path}:{finding['line']}") + + +def run_entry(entry: dict[str, Any], workspace: Path) -> None: + evidence = workspace / "evidence" / entry["name"] + root = workspace / "checkouts" / entry["name"] + checkout(entry, root, evidence) + before = tracked_identity(root) + host_test(entry, root, evidence, workspace) + config = evidence / "project-runtime.toml" + config.write_text( + f"manager={json.dumps(entry['manager'])}\n" + f"environment={json.dumps(entry['environment'])}\n" + f"groups={json.dumps(entry['groups'])}\n" + f"native_libraries={json.dumps(entry.get('native_libraries', []))}\n", + encoding="utf-8", + ) + execute( + ["specfact", "code", "review", "runtime", "inspect", "--project-config", str(config), "--json"], + cwd=root, + evidence=evidence, + name="inspect", + ) + # Cold run exercises automatic preparation; the offline call must reuse it. + _assert_verified_imports(review(root, evidence, config, entry["paths"]), entry) + raw = execute( + ["specfact", "code", "review", "runtime", "prepare", "--project-config", str(config), "--offline", "--json"], + cwd=root, + evidence=evidence, + name="offline-prepare", + ) + prepared = json_document(raw) + descriptor = Path(prepared["descriptor"]) + data = json.loads(descriptor.read_text(encoding="utf-8")) + (evidence / "runtime-identity.json").write_text(json.dumps(data, indent=2), encoding="utf-8") + (evidence / "runtime-bytes.json").write_text( + json.dumps({"artifact_bytes": sum(p.stat().st_size for p in descriptor.parent.rglob("*") if p.is_file())}), + encoding="utf-8", + ) + _assert_verified_imports(review(root, evidence, config, entry["paths"], descriptor=str(descriptor)), entry) + controlled_defect(root, evidence, config, workspace, entry=entry) + if tracked_identity(root) != before: + raise ValueError("capsule modified upstream source or environment inputs") + (evidence / "acceptance.json").write_text( + json.dumps( + { + "status": "PASS", + "commit": entry["commit"], + "mode": os.environ.get("MODE", "local-candidate"), + "source_unchanged": True, + }, + indent=2, + ), + encoding="utf-8", + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workspace", type=Path, required=True) + parser.add_argument("--repository", choices=["requests", "hatch", "flask", "poetry", "hatch-detached"]) + args = parser.parse_args() + if platform.system() != "Linux" or platform.machine() != "x86_64" or os.getuid() == 0: + raise ValueError("corpus requires non-root Linux x86-64") + manifest = json.loads(MANIFEST.read_text(encoding="utf-8")) + if f"{sys.version_info.major}.{sys.version_info.minor}" not in manifest["python"]: + raise ValueError("unsupported corpus Python ABI") + failures = [] + for entry in [*manifest["repositories"], *manifest.get("reconstructions", [])]: + if args.repository and args.repository != entry["name"]: + continue + try: + run_entry(entry, args.workspace.resolve()) + except (OSError, ValueError, subprocess.SubprocessError) as exc: + failures.append({"repository": entry["name"], "diagnostic": str(exc)}) + summary = args.workspace / "evidence" / "corpus-summary.json" + summary.parent.mkdir(parents=True, exist_ok=True) + summary.write_text( + json.dumps({"status": "FAIL" if failures else "PASS", "failures": failures}, indent=2), encoding="utf-8" + ) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/fixtures/portable-runtime/corpus.json b/tests/fixtures/portable-runtime/corpus.json new file mode 100644 index 00000000..ce316753 --- /dev/null +++ b/tests/fixtures/portable-runtime/corpus.json @@ -0,0 +1,124 @@ +{ + "schema": "specfact-external-python-corpus-v1", + "platform": "ubuntu-24.04-linux-x86_64", + "python": [ + "3.11", + "3.12", + "3.13" + ], + "job_timeout_minutes": 90, + "repositories": [ + { + "name": "requests", + "url": "https://github.com/psf/requests.git", + "commit": "dae7ef63b4df6eded86637f251fc4e3a06c3b479", + "manager": "pip", + "groups": [ + "test" + ], + "environment": "default", + "paths": [ + "src/requests/structures.py", + "tests/test_structures.py" + ], + "controlled_defect": { + "source": "src/requests/specfact_controlled.py", + "import": "requests.specfact_controlled" + } + }, + { + "name": "hatch", + "url": "https://github.com/pypa/hatch.git", + "commit": "d5f7bfe813dd4d81520def23b43f5d46aad1899c", + "manager": "hatch", + "groups": [], + "environment": "hatch-test", + "paths": [ + "src/hatch/utils/structures.py", + "tests/utils/test_structures.py" + ], + "controlled_defect": { + "source": "src/hatch/specfact_controlled.py", + "import": "hatch.specfact_controlled" + } + }, + { + "name": "flask", + "url": "https://github.com/pallets/flask.git", + "commit": "d73fa1cdcbd8b1465c151db8924ba58b1dd14e35", + "manager": "uv", + "groups": [ + "tests" + ], + "environment": "default", + "paths": [ + "src/flask/config.py", + "tests/test_config.py" + ], + "controlled_defect": { + "source": "src/flask/specfact_controlled.py", + "import": "flask.specfact_controlled" + } + }, + { + "name": "poetry", + "url": "https://github.com/python-poetry/poetry.git", + "commit": "be56ff07db06e9b82574648433ca228e4cac549b", + "manager": "poetry", + "groups": [ + "test" + ], + "environment": "default", + "paths": [ + "src/poetry/utils/extras.py", + "tests/utils/test_extras.py" + ], + "verified_imports": [ + { + "file": "tests/utils/test_extras.py", + "line": 7, + "module": "poetry.core.packages.package" + }, + { + "file": "tests/utils/test_extras.py", + "line": 9, + "module": "poetry.factory" + }, + { + "file": "tests/utils/test_extras.py", + "line": 10, + "module": "poetry.utils.extras" + } + ], + "controlled_defect": { + "source": "src/poetry/specfact_controlled.py", + "import": "poetry.specfact_controlled" + } + } + ], + "reconstructions": [ + { + "name": "hatch-detached", + "fixture": "hatch-detached", + "manager": "hatch", + "environment": "review", + "groups": [], + "native_libraries": [ + "libodbc.so.2" + ], + "paths": [ + "src/portable_customer/core.py", + "tests/test_core.py" + ], + "host_pytest_args": [ + "-p", + "no:asyncio" + ], + "host_note": "Disable only the asyncio entry-point alias; pytest_asyncio.plugin remains explicitly loaded by the unchanged fixture addopts.", + "controlled_defect": { + "source": "src/portable_customer/specfact_controlled.py", + "import": "portable_customer.specfact_controlled" + } + } + ] +} diff --git a/tests/fixtures/portable-runtime/hatch-detached/README.md b/tests/fixtures/portable-runtime/hatch-detached/README.md new file mode 100644 index 00000000..dc351c09 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/README.md @@ -0,0 +1,22 @@ +# Reconstructed #472 conditions + +This controlled fixture models the reported detached Hatch environment, `src/` layout, +additional source root, explicit asyncio plugin, importlib mode, third-party dependency, +and native `pyodbc` import. It is not the original customer repository or a claim that +its underlying PR has been reproduced. No network request or database connection occurs. + +Select Hatch environment `review` and declare `libodbc.so.2` in the external runtime +configuration. The deliberately incompatible default pytest constraint must not leak +into the detached review environment. Validation runs both the native host tests and +capsule review, with the configuration stored outside the fixture source. + +The same test slice also launches real nested `sys.executable` processes. It checks +inherited environment values, explicit additions/overrides/removals (including `HOME`), +a minimal environment without runtime markers, attached Python/native imports with +`env={}`, relative helpers and data under pytest's `tmp_path`, and caller-owned open +temporary files in both the default location and explicit `/tmp`. The detached fixture +does not install its source package, so the empty-environment child explicitly adds the +source directory before importing it, just as a native Python caller must. Children must +share the caller's working directory and temporary filesystem while retaining the capsule's existing +isolation. These assertions run unchanged in host, cold capsule, and offline warm +capsule validation; local host execution alone does not establish capsule acceptance. diff --git a/tests/fixtures/portable-runtime/hatch-detached/hatch.toml b/tests/fixtures/portable-runtime/hatch-detached/hatch.toml new file mode 100644 index 00000000..d0a16e66 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/hatch.toml @@ -0,0 +1,7 @@ +# Reconstruction of reported conditions; this is not the inaccessible customer repository. +[envs.default] +dependencies = ["pytest<8"] + +[envs.review] +detached = true +dependencies = ["pytest>=9,<10", "pytest-asyncio>=1,<2", "requests>=2.32,<3", "pyodbc==5.3.0"] diff --git a/tests/fixtures/portable-runtime/hatch-detached/pyproject.toml b/tests/fixtures/portable-runtime/hatch-detached/pyproject.toml new file mode 100644 index 00000000..f961711e --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "specfact-472-reconstructed-fixture" +version = "0.0.0" +requires-python = ">=3.11,<3.14" + +[tool.pytest.ini_options] +addopts = "-ra -v --import-mode=importlib -p pytest_asyncio.plugin" +pythonpath = ["src", "tools", "."] +testpaths = ["tests"] +asyncio_mode = "auto" diff --git a/tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/__init__.py.in b/tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/__init__.py.in new file mode 100644 index 00000000..539b71d6 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/__init__.py.in @@ -0,0 +1 @@ +"""Reconstructed external project for runtime acceptance.""" diff --git a/tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/core.py.in b/tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/core.py.in new file mode 100644 index 00000000..737a5a14 --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/src/portable_customer/core.py.in @@ -0,0 +1,14 @@ +"""Exercise third-party Python and native-extension imports without network access.""" + +import pyodbc +import requests + + +def prepared_method() -> str: + """Prepare a request locally; never send it.""" + return requests.Request("GET", "https://example.invalid").prepare().method + + +def installed_drivers() -> list[str]: + """Load the native ODBC manager without connecting to a database.""" + return pyodbc.drivers() diff --git a/tests/fixtures/portable-runtime/hatch-detached/tests/test_core.py.in b/tests/fixtures/portable-runtime/hatch-detached/tests/test_core.py.in new file mode 100644 index 00000000..dda44e8c --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/tests/test_core.py.in @@ -0,0 +1,143 @@ +"""Require actual dependency imports and ordinary nested Python process semantics.""" + +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from fixture_value import VALUE +from portable_customer.core import installed_drivers, prepared_method + + +async def test_runtime_dependencies() -> None: + assert prepared_method() == "GET" + assert isinstance(installed_drivers(), list) + assert VALUE == 42 + + +def test_nested_python_inherits_environment(monkeypatch) -> None: + monkeypatch.setenv("PORTABLE_INHERITED", "parent-added") + result = subprocess.run( + [sys.executable, "-c", "import os; print(os.environ['PORTABLE_INHERITED'])"], + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert result.stdout.strip() == "parent-added" + + +def test_nested_python_honors_explicit_environment(monkeypatch) -> None: + monkeypatch.setenv("PORTABLE_OVERRIDE", "parent") + monkeypatch.setenv("PORTABLE_REMOVED", "parent-only") + environment = dict(os.environ) + environment.update(PORTABLE_ADDED="child-added", PORTABLE_OVERRIDE="child", HOME="/portable-child-home") + del environment["PORTABLE_REMOVED"] + script = ( + "import json,os; print(json.dumps([os.getenv(name) for name in " + "('PORTABLE_ADDED','PORTABLE_OVERRIDE','PORTABLE_REMOVED','HOME')]))" + ) + result = subprocess.run( + [sys.executable, "-c", script], + env=environment, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == ["child-added", "child", None, "/portable-child-home"] + assert os.environ["PORTABLE_OVERRIDE"] == "parent" + assert os.environ["PORTABLE_REMOVED"] == "parent-only" + + +def test_nested_python_accepts_minimal_environment(monkeypatch) -> None: + monkeypatch.setenv("PORTABLE_REMOVED", "parent-only") + result = subprocess.run( + [ + sys.executable, + "-c", + "import json,os; print(json.dumps([os.getenv('PORTABLE_ADDED'),os.getenv('PORTABLE_REMOVED'),os.getenv('HOME')]))", + ], + env={"PORTABLE_ADDED": "minimal"}, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == ["minimal", None, None] + + +def test_nested_python_empty_environment_keeps_attached_imports() -> None: + source = Path(__file__).resolve().parents[1] / "src" + script = ( + "import json,sys; sys.path.insert(0, sys.argv[1]); " + "from portable_customer.core import installed_drivers, prepared_method; " + "print(json.dumps([prepared_method(), isinstance(installed_drivers(), list)]))" + ) + result = subprocess.run( + [sys.executable, "-c", script, str(source)], + env={}, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == ["GET", True] + + +def test_nested_python_preserves_cwd_and_relative_files(tmp_path: Path) -> None: + (tmp_path / "payload.txt").write_text("caller-data", encoding="utf-8") + (tmp_path / "helper.py").write_text( + "import json\nfrom pathlib import Path\n" + "print(json.dumps([str(Path.cwd()), Path('payload.txt').read_text(encoding='utf-8')]))\n", + encoding="utf-8", + ) + result = subprocess.run( + [sys.executable, "helper.py"], + cwd=tmp_path, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert json.loads(result.stdout) == [str(tmp_path.resolve()), "caller-data"] + + +def test_nested_python_shares_caller_owned_temporary_file() -> None: + for directory in (None, "/tmp"): + with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", dir=directory) as stream: + stream.write("parent-data") + stream.flush() + script = ( + "import sys; from pathlib import Path; path=Path(sys.argv[1]); " + "assert path.read_text(encoding='utf-8') == 'parent-data'; " + "path.write_text('child-data', encoding='utf-8')" + ) + subprocess.run([sys.executable, "-c", script, stream.name], check=True, timeout=30) + stream.seek(0) + assert stream.read() == "child-data" + + +def test_nested_python_preserves_explicit_pythonpath(tmp_path: Path) -> None: + extra = tmp_path / "extra" + extra.mkdir() + (extra / "caller_only.py").write_text("VALUE = 'caller-path'\n", encoding="utf-8") + child = "import caller_only; print(caller_only.VALUE)" + script = ( + "import caller_only,os,subprocess,sys; " + "assert caller_only.VALUE == 'caller-path'; " + "assert os.environ['PYTHONPATH'] == 'extra'; " + "subprocess.run([sys.executable, '-c', sys.argv[1]], check=True, timeout=30)" + ) + result = subprocess.run( + [sys.executable, "-c", script, child], + cwd=tmp_path, + env={"PYTHONPATH": "extra"}, + capture_output=True, + text=True, + check=True, + timeout=30, + ) + assert result.stdout.strip() == "caller-path" diff --git a/tests/fixtures/portable-runtime/hatch-detached/tools/fixture_value.py.in b/tests/fixtures/portable-runtime/hatch-detached/tools/fixture_value.py.in new file mode 100644 index 00000000..cb05742b --- /dev/null +++ b/tests/fixtures/portable-runtime/hatch-detached/tools/fixture_value.py.in @@ -0,0 +1,3 @@ +"""Additional source root imported through the repository's pytest configuration.""" + +VALUE = 42 diff --git a/tests/unit/specfact_code_review/run/test_commands.py b/tests/unit/specfact_code_review/run/test_commands.py index e49e913a..5ec636c1 100644 --- a/tests/unit/specfact_code_review/run/test_commands.py +++ b/tests/unit/specfact_code_review/run/test_commands.py @@ -1793,3 +1793,15 @@ def test_run_command_rejects_missing_files() -> None: assert result.exit_code == 2 assert "not found" in result.output.lower() + + +def test_normalized_simplify_request_preserves_project_runtime_paths() -> None: + request = run_commands.ReviewRunRequest( + files=[], + focus_facets=("simplify",), + project_config=Path("project.toml"), + project_runtime=Path("project-runtime.json"), + ) + normalized = run_commands._normalize_review_request(request) + assert normalized.project_config == request.project_config + assert normalized.project_runtime == request.project_runtime diff --git a/tests/unit/specfact_code_review/run/test_installed_coverage.py b/tests/unit/specfact_code_review/run/test_installed_coverage.py new file mode 100644 index 00000000..bc79ce69 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_installed_coverage.py @@ -0,0 +1,691 @@ +"""Real native coverage must retain installed imports and authenticate source attribution.""" + +import base64 +import csv +import hashlib +import importlib +import importlib.util +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from specfact_code_review.run import installed_coverage, portable_worker, target_pytest +from specfact_code_review.run.runner import evaluate_portable_pytest_coverage +from specfact_code_review.run.target_pytest import _installed_coverage_candidates + + +def _record_hash(path: Path) -> str: + return "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(path.read_bytes()).digest()).decode().rstrip("=") + + +def _distribution(site: Path, name: str, files: list[Path], *, local: bool = True) -> Path: + metadata = site / f"{name}-1.0.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text(f"Metadata-Version: 2.1\nName: {name}\nVersion: 1.0\n") + (metadata / "direct_url.json").write_text( + json.dumps({"url": "file:///opt/specfact/output/project" if local else "https://example.invalid/wheel.whl"}) + ) + with (metadata / "RECORD").open("w", newline="") as stream: + writer = csv.writer(stream) + writer.writerows((str(path.relative_to(site)), _record_hash(path), path.stat().st_size) for path in files) + return metadata + + +@pytest.fixture(name="installed_project") +def installed_project_fixture(tmp_path: Path) -> SimpleNamespace: + snapshot, site = tmp_path / "snapshot", tmp_path / "runtime/site-packages" + source, installed = snapshot / "src/capsule_owned/math.py", site / "capsule_owned/math.py" + source.parent.mkdir(parents=True) + installed.parent.mkdir(parents=True) + text = "def add(left, right):\n return left + right\n" + source.write_text(text) + installed.write_text(text) + initializer = installed.parent / "__init__.py" + initializer.write_text('BUILT = "installed"\n') + metadata = _distribution(site, "capsule-owned", [installed, initializer]) + tests = snapshot / "tests" + tests.mkdir() + (tests / "test_math.py").write_text( + "import capsule_owned\nfrom capsule_owned.math import add\n" + "def test_add():\n assert capsule_owned.BUILT == 'installed'\n assert add(2, 3) == 5\n" + ) + return SimpleNamespace(snapshot=snapshot, site=site, source=source, installed=installed, metadata=metadata) + + +def _bridge(project: SimpleNamespace) -> Any: + module = "specfact_code_review.run.installed_coverage" + if importlib.util.find_spec(module) is None: + # Pre-change behavior: only the snapshot is measured and no installed aliases exist. + return SimpleNamespace(directories=(), mappings=(), diagnostics={}) + return importlib.import_module(module).plan_installed_coverage( + [project.source], snapshot=project.snapshot, site_packages=project.site + ) + + +def _normalize(project, bridge, payload): + module = "specfact_code_review.run.installed_coverage" + if importlib.util.find_spec(module) is None: + return payload, {} + return importlib.import_module(module).attribute_installed_coverage( + bridge, payload, snapshot=project.snapshot, relocations={path: path for path in bridge.measured_origins} + ) + + +def _native_coverage(project, bridge, tmp_path, *, import_mode="prepend"): + report = tmp_path / "coverage.json" + command = [ + sys.executable, + "-P", + "-m", + "pytest", + "-q", + "-p", + "pytest_cov", + "--disable-warnings", + f"--import-mode={import_mode}", + f"--cov={project.snapshot}", + *[f"--cov={path}" for path in bridge.directories], + *[f"--cov={name}" for name in getattr(bridge, "modules", ())], + f"--cov-report=json:{report}", + "tests/test_math.py", + ] + result = subprocess.run( + command, + cwd=project.snapshot, + text=True, + capture_output=True, + check=False, + timeout=60, + env={ + **os.environ, + "PYTHONPATH": str(project.site), + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "COVERAGE_FILE": str(tmp_path / ".coverage"), + }, + ) + assert report.is_file(), result.stdout + result.stderr + return result, json.loads(report.read_text()) + + +def test_native_installed_package_coverage_is_attributed_without_changing_imports(installed_project, tmp_path): + project = installed_project + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + before = json.dumps(raw, sort_keys=True) + attributed, diagnostics = _normalize(project, bridge, raw) + row = attributed["files"].get(str(project.source)) + assert row is not None, "native installed production source is absent from snapshot-only coverage" + assert row["summary"]["percent_covered"] == 100 + assert not diagnostics + assert json.dumps(raw, sort_keys=True) == before + assert any(str(project.installed) == str((project.snapshot / path).resolve()) for path in raw["files"]) + + +def test_actual_defect_stays_failed_after_installed_attribution(installed_project, tmp_path): + project = installed_project + (project.snapshot / "tests/test_math.py").write_text( + "from capsule_owned.math import add\ndef test_wrong():\n assert add(2, 3) == 6\n" + ) + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path) + attributed, diagnostics = _normalize(project, bridge, raw) + assert result.returncode == 1 + assert str(project.source) in attributed["files"] + assert not diagnostics + + +def _invalidate_installed_attribution(project, scenario): + if scenario == "changed": + project.source.write_text("def add(left, right):\n return left - right\n") + return + if scenario == "duplicate-source": + duplicate = project.snapshot / "another/capsule_owned/math.py" + duplicate.parent.mkdir(parents=True) + duplicate.write_bytes(project.source.read_bytes()) + return + if scenario == "duplicate-owner": + _distribution(project.site, "other", [project.installed]) + return + if scenario == "record-hash": + record = project.metadata / "RECORD" + record.write_text(record.read_text().replace(_record_hash(project.installed), "sha256=broken")) + return + if scenario == "record-escape": + with (project.metadata / "RECORD").open("a") as stream: + stream.write("../outside.py,sha256=broken,1\n") + return + assert scenario == "foreign-namespace" + foreign = project.installed.parent / "foreign.py" + foreign.write_text("VALUE = 1\n") + _distribution(project.site, "foreign", [foreign], local=False) + + +@pytest.mark.parametrize( + "scenario", ["changed", "duplicate-source", "duplicate-owner", "record-hash", "record-escape", "foreign-namespace"] +) +def test_unsafe_installed_attribution_is_incomplete(installed_project, scenario): + project = installed_project + _invalidate_installed_attribution(project, scenario) + bridge = _bridge(project) + assert not bridge.mappings + assert str(project.source) in bridge.diagnostics + + +def test_nonlocal_distribution_does_not_claim_identical_snapshot(installed_project): + project = installed_project + (project.metadata / "direct_url.json").write_text(json.dumps({"url": "https://example.invalid/owned.whl"})) + assert not _bridge(project).mappings + + +@pytest.mark.parametrize("changed", ["source", "installed"]) +def test_content_change_after_planning_invalidates_coverage(installed_project, changed): + project = installed_project + bridge = _bridge(project) + assert bridge.mappings + getattr(project, changed).write_text("CHANGED = True\n") + payload = {"files": {str(project.installed): {"summary": {"percent_covered": 100}}}} + attributed, diagnostics = _normalize(project, bridge, payload) + assert str(project.source) not in attributed["files"] + assert str(project.source) in diagnostics + + +def test_missing_measured_row_cannot_be_manufactured(installed_project): + project = installed_project + bridge = _bridge(project) + assert bridge.mappings + attributed, diagnostics = _normalize(project, bridge, {"files": {}}) + assert not attributed["files"] + assert str(project.source) in diagnostics + + +def test_direct_snapshot_coverage_takes_precedence(installed_project): + project = installed_project + bridge = _bridge(project) + payload = { + "files": { + str(project.source): {"summary": {"percent_covered": 10}, "executed_lines": [1]}, + str(project.installed): {"summary": {"percent_covered": 100}}, + } + } + attributed, diagnostics = _normalize(project, bridge, payload) + assert attributed["files"][str(project.source)]["summary"]["percent_covered"] == 10 + assert not diagnostics + + +@pytest.mark.parametrize("relative", [False, True]) +def test_measured_path_forms_and_low_coverage_preserved(installed_project, relative): + project = installed_project + bridge = _bridge(project) + name = os.path.relpath(project.installed, project.snapshot) if relative else str(project.installed) + raw = {"files": {name: {"summary": {"percent_covered": 12.5}}}} + attributed, diagnostics = _normalize(project, bridge, raw) + assert attributed["files"][str(project.source)]["summary"]["percent_covered"] == 12.5 + assert not diagnostics + + +def _passed_observation(project): + return { + "exit_code": 0, + "collected": ["tests/test_math.py::test_add"], + "records": [{"nodeid": "tests/test_math.py::test_add", "phase": "call", "outcome": "passed"}], + "coverage": {"files": {str(project.installed): {"summary": {"percent_covered": 100}}}}, + } + + +def test_controller_wires_owned_directories_and_preserves_raw_observation(installed_project, tmp_path, monkeypatch): + + project = installed_project + monkeypatch.chdir(project.snapshot) + observation = tmp_path / "observation.json" + planned = installed_coverage.plan_installed_coverage( + [project.source], snapshot=project.snapshot, site_packages=project.site + ) + monkeypatch.setattr(portable_worker, "plan_installed_coverage", lambda *args, **kwargs: planned) + monkeypatch.setattr( + portable_worker, + "Path", + lambda path: observation if path == "/opt/specfact/tmp/pytest-observation.json" else Path(path), + ) + sent = {} + + def command(_member, arguments): + sent.update(json.loads(arguments[0])) + return ["native-child"] + + def execute(*_args, **_kwargs): + observation.write_text(json.dumps(_passed_observation(project))) + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(portable_worker, "target_command", command) + monkeypatch.setattr(portable_worker.subprocess, "run", execute) + findings = portable_worker.run_portable_pytest( + [project.source], ("portable-pytest-v2", '{"selectors":["tests/test_math.py"]}') + ) + assert not findings + assert sent["coverage_directories"] == [str(project.installed.parent)] + assert set(sent["coverage_candidates"]) == {str(path.resolve()) for path in project.site.rglob("*.py")} + assert sent["selectors"] == ["tests/test_math.py"] + retained = json.loads(observation.read_text()) + assert list(retained["coverage"]["files"]) == [str(project.installed)] + assert retained["coverage_attribution"]["mappings"][0]["source"] == str(project.source) + + +def test_owned_namespace_leaf_can_measure_without_importing_package(installed_project): + project = installed_project + (project.installed.parent / "__init__.py").unlink() + _distribution(project.site, "namespace-marker", [], local=False) + # Remove the absent initializer RECORD row; a PEP420 package needs no initializer. + record = project.metadata / "RECORD" + record.write_text("\n".join(line for line in record.read_text().splitlines() if "__init__.py" not in line) + "\n") + assert "capsule_owned" not in sys.modules + bridge = _bridge(project) + assert len(bridge.mappings) == 1 + assert "capsule_owned" not in sys.modules + + +def test_owned_package_directory_is_verified_once_for_multiple_sources(installed_project, monkeypatch): + + project = installed_project + other_source = project.source.with_name("other.py") + other_installed = project.installed.with_name("other.py") + other_source.write_text("VALUE = 2\n") + other_installed.write_bytes(other_source.read_bytes()) + with (project.metadata / "RECORD").open("a") as stream: + stream.write(f"capsule_owned/other.py,{_record_hash(other_installed)},{other_installed.stat().st_size}\n") + original = installed_coverage._owned_directory + calls = [] + + def count(*args): + calls.append(args) + return original(*args) + + monkeypatch.setattr(installed_coverage, "_owned_directory", count) + bridge = installed_coverage.plan_installed_coverage( + [project.source, other_source], snapshot=project.snapshot, site_packages=project.site + ) + assert len(bridge.mappings) == 2 + assert len(calls) == 1 + + +@pytest.mark.parametrize("bad", [None, ["bad"], {1: {}}, {"path": None}]) +def test_bad_native_coverage_is_an_actionable_tool_error(installed_project, bad): + + project = installed_project + bridge = _bridge(project) + findings = portable_worker._installed_coverage_findings( + [project.source], bridge, {"coverage": {"files": bad}}, evaluate_portable_pytest_coverage + ) + assert findings[0].category == "tool_error" + assert "project_pytest_installed_coverage_unavailable" in findings[0].message + + +def test_native_worker_accepts_only_contained_coverage_directories(installed_project, monkeypatch): + + project = installed_project + monkeypatch.setattr(target_pytest, "ROOT", project.site.parent) + request = {"coverage_directories": [str(project.installed.parent)]} + assert target_pytest._installed_coverage_directories(request) == [str(project.installed.parent)] + for path in (project.site, project.snapshot, project.installed): + with pytest.raises(ValueError, match="installed_coverage_directory_invalid"): + target_pytest._installed_coverage_directories({"coverage_directories": [str(path)]}) + + +@pytest.mark.parametrize("payload", [{}, {"coverage_candidates": []}], ids=["default", "empty"]) +def test_native_coverage_candidates_allow_empty_selection( + installed_project: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, payload: dict[str, Any] +) -> None: + """A worker without installed coverage candidates needs no inferred paths.""" + monkeypatch.setattr(target_pytest, "ROOT", installed_project.site.parent) + assert not _installed_coverage_candidates(payload) + + +def test_native_coverage_candidates_preserve_attached_absolute_path( + installed_project: SimpleNamespace, monkeypatch: pytest.MonkeyPatch +) -> None: + """Preserve the exact contained path selected by the controller.""" + monkeypatch.setattr(target_pytest, "ROOT", installed_project.site.parent) + candidates = [str(installed_project.installed.resolve())] + assert _installed_coverage_candidates({"coverage_candidates": candidates}) == candidates + + +@pytest.mark.parametrize( + "candidates", + [None, "module.py", {"path": "module.py"}, ("module.py",), [None], [42], [True], ["capsule_owned/math.py"]], + ids=["null", "string", "mapping", "tuple", "null-item", "integer-item", "boolean-item", "relative-path"], +) +def test_native_coverage_candidates_reject_malformed_inputs( + installed_project: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, candidates: Any +) -> None: + """Reject malformed types and relative paths before constructing coverage mappings.""" + monkeypatch.setattr(target_pytest, "ROOT", installed_project.site.parent) + with pytest.raises(ValueError, match=r"^project_pytest_coverage_candidate_invalid$"): + _installed_coverage_candidates({"coverage_candidates": candidates}) + + +@pytest.mark.parametrize("via_symlink", [False, True], ids=["outside-path", "symlink-escape"]) +def test_native_coverage_candidates_reject_paths_outside_attached_runtime( + installed_project: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, via_symlink: bool +) -> None: + """An absolute spelling inside site-packages cannot authorize an escaping symlink.""" + project = installed_project + monkeypatch.setattr(target_pytest, "ROOT", project.site.parent) + candidate = project.source.resolve() + if via_symlink: + candidate = project.site / "escaped.py" + candidate.symlink_to(project.source.resolve()) + with pytest.raises(ValueError, match=r"^project_pytest_coverage_candidate_invalid$"): + _installed_coverage_candidates({"coverage_candidates": [str(candidate)]}) + + +def test_unexecuted_flat_snapshot_row_does_not_mask_real_installed_coverage(installed_project, tmp_path, monkeypatch): + project = installed_project + flat = project.snapshot / "capsule_owned" + project.source.parent.rename(flat) + project.source = flat / "math.py" + (flat / "__init__.py").write_bytes((project.installed.parent / "__init__.py").read_bytes()) + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path, import_mode="importlib") + assert result.returncode == 0, result.stdout + result.stderr + snapshot_row = raw["files"]["capsule_owned/math.py"] + assert snapshot_row["executed_lines"] == [] + assert snapshot_row["summary"]["percent_covered"] == 0 + attributed, diagnostics = _normalize(project, bridge, raw) + assert attributed["files"][str(project.source)]["summary"]["percent_covered"] == 100 + assert not diagnostics + + monkeypatch.chdir(project.snapshot) + assert not evaluate_portable_pytest_coverage([project.source], {"coverage": attributed}) + assert raw["files"]["capsule_owned/math.py"]["summary"]["percent_covered"] == 0 + + +@pytest.fixture(name="aliased_project") +def aliased_project_fixture(installed_project): + project = installed_project + project.good = project.source.with_name("good.py") + project.installed_good = project.installed.with_name("good.py") + project.good.write_text("VALUE = 7\n") + project.installed_good.write_bytes(project.good.read_bytes()) + with (project.metadata / "RECORD").open("a") as stream: + stream.write(f"capsule_owned/good.py,{_record_hash(project.installed_good)},10\n") + (project.snapshot / "pyproject.toml").write_text('[tool.coverage.paths]\nsource=["src", "*/site-packages"]\n') + test = project.snapshot / "tests/test_math.py" + test.write_text( + test.read_text() + "from capsule_owned.good import VALUE\ndef test_good():\n assert VALUE == 7\n" + ) + return project + + +def _native_relocations(project): + script = """ +import json, os, runpy, sys +from types import SimpleNamespace +import coverage +module, topdir, customer_cwd, candidates = sys.argv[1:] +native = coverage.Coverage(config_file=os.path.join(topdir, "pyproject.toml")) +native.get_data() # Initialize the same fresh-process path cache as pytest-cov. +plugin = SimpleNamespace(cov_controller=SimpleNamespace(cov=native, topdir=topdir)) +relocate = runpy.run_path(module)["_native_coverage_relocations"] +os.chdir(customer_cwd) +receipt = relocate(plugin, json.loads(candidates)) +assert os.getcwd() == customer_cwd +print(json.dumps(receipt)) +""" + result = subprocess.run( + [ + sys.executable, + "-P", + "-c", + script, + target_pytest.__file__, + str(project.snapshot), + str(Path.cwd()), + json.dumps([str(path.resolve()) for path in project.site.rglob("*.py")]), + ], + cwd=project.snapshot, + text=True, + capture_output=True, + check=True, + timeout=60, + ) + relocated, diagnostic = json.loads(result.stdout) + assert not diagnostic + return relocated + + +def test_native_coverage_alias_cannot_credit_transformed_source(aliased_project, tmp_path, monkeypatch): + project = aliased_project + monkeypatch.chdir(project.snapshot) + project.source.write_text("def add(left, right):\n return left - right\n") + bridge = installed_coverage.plan_installed_coverage( + [project.source, project.good], snapshot=project.snapshot, site_packages=project.site + ) + assert bridge.diagnostics[str(project.source)] == "installed_source_differs" + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + assert raw["files"]["src/capsule_owned/math.py"]["executed_lines"] + attributed, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations=_native_relocations(project) + ) + assert "coverage_alias_origin_ambiguous" in diagnostics[str(project.source)] + assert str(project.good) not in diagnostics + findings = evaluate_portable_pytest_coverage( + [project.source, project.good], {"coverage": attributed, "coverage_attribution_errors": diagnostics} + ) + assert findings and findings[0].category == "tool_error" + + +def test_native_alias_accepts_identical_installed_source(aliased_project, tmp_path, monkeypatch): + project = aliased_project + monkeypatch.chdir(project.snapshot) + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + attributed, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations=_native_relocations(project) + ) + assert not diagnostics + assert not evaluate_portable_pytest_coverage([project.source], {"coverage": attributed}) + + +def test_unrelated_native_alias_does_not_reject_direct_snapshot_execution(aliased_project, monkeypatch): + project = aliased_project + monkeypatch.chdir(project.snapshot) + project.source.write_text("def add(left, right):\n return left - right\n") + (project.snapshot / "pyproject.toml").write_text('[tool.coverage.paths]\nsource=["src", "*/build/src"]\n') + bridge = _bridge(project) + assert bridge.diagnostics[str(project.source)] == "installed_source_differs" + relocations = _native_relocations(project) + assert relocations[str(project.installed)] == str(project.installed) + raw = {"files": {str(project.source): {"executed_lines": [1, 2], "summary": {"percent_covered": 100}}}} + _, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations=relocations + ) + assert not diagnostics + + +def test_native_alias_cannot_clear_post_execution_identity_change(aliased_project, monkeypatch): + project = aliased_project + monkeypatch.chdir(project.snapshot) + bridge = _bridge(project) + project.source.write_text("CHANGED = True\n") + raw = {"files": {str(project.source): {"executed_lines": [1, 2], "summary": {"percent_covered": 100}}}} + _, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations=_native_relocations(project) + ) + assert "source_identity_changed_after_execution" in diagnostics[str(project.source)] + + +@pytest.mark.parametrize("changed", ["source", "installed"]) +def test_direct_snapshot_row_cannot_erase_post_plan_identity_change(installed_project, changed): + project = installed_project + bridge = _bridge(project) + getattr(project, changed).write_text("CHANGED = True\n") + raw = {"files": {str(project.source): {"executed_lines": [1], "summary": {"percent_covered": 100}}}} + _, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations={str(project.installed): str(project.installed)} + ) + assert diagnostics[str(project.source)] == "source_identity_changed_after_execution" + + +def test_missing_native_alias_api_retains_observation_and_origin_diagnostic(monkeypatch, tmp_path): + monkeypatch.setattr(target_pytest, "SNAPSHOT_ROOT", tmp_path) + observer = target_pytest.Observer() + observer.coverage_origin["candidates"] = ["/opt/specfact/project-runtime/site-packages/pkg/app.py"] + observer.records = [{"nodeid": "test_app.py::test_app", "phase": "call", "outcome": "passed"}] + plugin = SimpleNamespace( + cov_controller=SimpleNamespace(cov=object(), topdir=str(tmp_path)), options=SimpleNamespace(cov_fail_under=80) + ) + config = SimpleNamespace( + rootpath=tmp_path, getini=lambda _name: [], pluginmanager=SimpleNamespace(getplugin=lambda _name: plugin) + ) + observer.pytest_sessionfinish(SimpleNamespace(config=config), 0) + assert observer.records[0]["outcome"] == "passed" + assert observer.pytest_root == "." + assert observer.coverage_threshold == 80 + assert not observer.coverage_origin["relocations"] + assert observer.coverage_origin["diagnostic"] == "coverage_origin_unavailable:AttributeError" + assert target_pytest._native_coverage_relocations(None, []) == ({}, "") + + +def test_native_alias_uses_coverage_topdir_and_restores_customer_cwd(aliased_project, tmp_path, monkeypatch): + project = aliased_project + elsewhere = tmp_path / "customer-cwd" + elsewhere.mkdir() + monkeypatch.chdir(elsewhere) + relocations = _native_relocations(project) + assert Path(relocations[str(project.installed)]).resolve() == project.source.resolve() + assert Path.cwd() == elsewhere + + +def test_native_alias_cannot_credit_renamed_unmatched_source(aliased_project, tmp_path, monkeypatch): + project = aliased_project + destination = project.snapshot / "src/original/math.py" + destination.parent.mkdir() + project.source.rename(destination) + project.source = destination + project.source.write_text("def add(left, right):\n return left - right\n") + (project.snapshot / "pyproject.toml").write_text( + '[tool.coverage.paths]\nsource=["src/original", "*/site-packages/capsule_owned"]\n' + ) + bridge = installed_coverage.plan_installed_coverage( + [project.source, project.good], snapshot=project.snapshot, site_packages=project.site + ) + result, raw = _native_coverage(project, bridge, tmp_path) + assert result.returncode == 0, result.stdout + result.stderr + attributed, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations=_native_relocations(project) + ) + assert "coverage_alias_origin_unverified" in diagnostics.get(str(project.source), "") + monkeypatch.chdir(project.snapshot) + findings = evaluate_portable_pytest_coverage( + [project.source, project.good], {"coverage": attributed, "coverage_attribution_errors": diagnostics} + ) + assert any( + finding.tool == "pytest" and "coverage_alias_origin_unverified" in finding.message for finding in findings + ) + + +def test_missing_measured_origin_receipt_cannot_authenticate_snapshot_row(installed_project): + project = installed_project + bridge = _bridge(project) + raw = {"files": {str(project.source): {"executed_lines": [1, 2], "summary": {"percent_covered": 100}}}} + _, diagnostics = installed_coverage.attribute_installed_coverage( + bridge, raw, snapshot=project.snapshot, relocations={str(project.installed): str(project.installed)} + ) + assert diagnostics[str(project.source)] == "coverage_origin_unavailable" + + +@pytest.fixture(name="installed_single_module") +def installed_single_module_fixture(tmp_path: Path) -> SimpleNamespace: + snapshot, site = tmp_path / "snapshot", tmp_path / "runtime/site-packages" + snapshot.mkdir() + site.mkdir(parents=True) + source, installed = snapshot / "standalone.py", site / "standalone.py" + source.write_text("def answer():\n return 42\n") + installed.write_bytes(source.read_bytes()) + metadata = _distribution(site, "standalone-owned", [installed]) + foreign = site / "unrelated.py" + foreign.write_text("VALUE = 99\n") + _distribution(site, "unrelated-owned", [foreign], local=False) + tests = snapshot / "tests" + tests.mkdir() + (tests / "test_math.py").write_text( + "import standalone, unrelated\ndef test_answer():\n" + " assert standalone.answer() == 42\n assert unrelated.VALUE == 99\n" + ) + return SimpleNamespace(snapshot=snapshot, site=site, source=source, installed=installed, metadata=metadata) + + +@pytest.mark.parametrize("layout", ["root", "src"]) +def test_native_single_module_keeps_narrow_measurement_and_verified_attribution( + installed_single_module, tmp_path, layout +): + project = installed_single_module + if layout == "src": + target = project.snapshot / "src/standalone.py" + target.parent.mkdir() + project.source.rename(target) + project.source = target + bridge = _bridge(project) + assert len(bridge.mappings) == 1, bridge.diagnostics + assert bridge.directories == () + assert bridge.modules == ("standalone",) + assert str(project.installed) in bridge.measured_origins + result, raw = _native_coverage(project, bridge, tmp_path, import_mode="importlib") + assert result.returncode == 0, result.stdout + result.stderr + _assert_single_module_attribution(project, bridge, raw) + + +def _assert_single_module_attribution(project, bridge, raw): + original = json.dumps(raw, sort_keys=True) + assert not any(path.endswith("unrelated.py") for path in raw["files"]) + attributed, diagnostics = _normalize(project, bridge, raw) + assert not diagnostics + assert attributed["files"][str(project.source)]["summary"]["percent_covered"] == 100 + assert json.dumps(raw, sort_keys=True) == original + + +@pytest.mark.parametrize("invalid", ["duplicate-source", "duplicate-owner", "content", "module-name"]) +def test_single_module_identity_rejection_keeps_measurement_closed(installed_single_module, invalid): + project = installed_single_module + if invalid == "duplicate-source": + duplicate = project.snapshot / "src/standalone.py" + duplicate.parent.mkdir() + duplicate.write_bytes(project.source.read_bytes()) + elif invalid == "duplicate-owner": + _distribution(project.site, "second-owner", [project.installed]) + elif invalid == "content": + project.source.write_text("def answer():\n return 0\n") + else: + source = project.source.with_name("not-a-module.py") + installed = project.installed.with_name(source.name) + project.source.rename(source) + project.installed.rename(installed) + project.source = source + (project.metadata / "RECORD").write_text( + f"{installed.name},{_record_hash(installed)},{installed.stat().st_size}\n" + ) + bridge = _bridge(project) + assert not bridge.mappings + assert not getattr(bridge, "modules", ()) + assert not bridge.directories + assert str(project.source) in bridge.diagnostics + + +@pytest.mark.parametrize("changed", ["source", "installed"]) +def test_single_module_changed_after_execution_cannot_receive_credit(installed_single_module, tmp_path, changed): + project = installed_single_module + bridge = _bridge(project) + result, raw = _native_coverage(project, bridge, tmp_path, import_mode="importlib") + assert result.returncode == 0, result.stdout + result.stderr + getattr(project, changed).write_text("def answer():\n return -1\n") + attributed, diagnostics = _normalize(project, bridge, raw) + assert diagnostics[str(project.source)] == "source_identity_changed_after_execution" + assert str(project.source) not in attributed["files"] diff --git a/tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py b/tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py new file mode 100644 index 00000000..7a08602a --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_portable_pytest_discovery.py @@ -0,0 +1,277 @@ +"""Full review delegates discovery to actual target pytest configuration.""" + +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import portable_worker, target_pytest +from specfact_code_review.run.portable_worker import select_test_paths, validate_observation +from specfact_code_review.run.runner import _capsule_member_response +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +OBSERVE_DISCOVERY = """ +import importlib.util, json, sys +from pathlib import Path +import pytest +spec = importlib.util.spec_from_file_location('observer_module', sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +for name in ('pytest_xdist_node_collection_finished', 'pytest_testnodedown'): + pytest.hookimpl(optionalhook=True)(getattr(module.Observer, name)) +observer = module.Observer() +output = Path(sys.argv[2]) +coverage_output = output.with_suffix('.coverage.json') +args = ['-q', '-p', 'pytest_cov', '--cov=.', + '--cov-report=json:' + str(coverage_output), + '-o', 'cache_dir=' + str(output.with_suffix('.cache')), + *json.loads(sys.argv[3])] +code = pytest.main(args, plugins=[observer]) +output.write_text(json.dumps({ + 'exit_code': int(code), 'collected': sorted(observer.collected), + 'deselected': sorted(observer.deselected), 'records': observer.records, + 'collection_errors': observer.collection_errors, + 'internal_errors': observer.internal_errors, + 'pytest_version': pytest.__version__, + 'coverage': json.loads(coverage_output.read_text()) if coverage_output.is_file() else {}, +})) +raise SystemExit(int(code)) +""" + + +def _observe_discovery(root: Path, output: Path, selectors: tuple[str, ...]) -> tuple[dict, str]: + environment = { + **os.environ, + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "PYTEST_ADDOPTS": "", + "COVERAGE_FILE": str(output.with_suffix(".coverage")), + } + completed = subprocess.run( + [ + sys.executable, + "-c", + OBSERVE_DISCOVERY, + str(Path(target_pytest.__file__).resolve()), + str(output), + json.dumps(selectors), + ], + cwd=root, + env=environment, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + observation = json.loads(output.read_text()) + assert observation["exit_code"] == completed.returncode + return observation, completed.stdout + completed.stderr + + +@pytest.mark.parametrize( + ("configuration", "expected", "warns"), + [ + ("testpaths = absent", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = absent tests", ["tests/test_app.py"], False), + ("testpaths = tests/*.py", ["tests/test_app.py"], False), + ("testpaths = absent*", ["tests/test_app.py", "other/test_else.py"], True), + ("testpaths = tests\naddopts = other/test_else.py", ["other/test_else.py"], False), + ("testpaths =", ["tests/test_app.py", "other/test_else.py"], False), + ], +) +def test_full_review_matches_native_discovery( + tmp_path: Path, configuration: str, expected: list[str], warns: bool +) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\n{configuration}\n") + for name in ("tests/test_app.py", "other/test_else.py"): + test = root / name + test.parent.mkdir(exist_ok=True) + test.write_text("def test_valid(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native.json", ()) + selectors = select_test_paths(discover_project(root), [], full=True) + portable, portable_output = _observe_discovery(root, tmp_path / "portable.json", selectors) + assert native["exit_code"] == 0, native_output + assert portable["exit_code"] == 0, portable_output + expected_nodes = sorted(f"{name}::test_valid" for name in expected) + assert native["collected"] == portable["collected"] == expected_nodes + assert portable["records"] == native["records"] + warning = "No files were found in testpaths" + assert (warning in native_output) == (warning in portable_output) == warns + validate_observation(portable, portable["exit_code"]) + + +@pytest.mark.parametrize("selection", ["source", "test", "mixed"]) +def test_targeted_review_does_not_expand_into_full_discovery(tmp_path: Path, selection: str) -> None: + source = tmp_path / "app.py" + source.touch() + selected = tmp_path / "test_app.py" + selected.write_text("def test_selected(): assert True\n") + (tmp_path / "test_other.py").write_text("def test_other(): assert False\n") + files = {"source": [source], "test": [selected], "mixed": [source, selected]}[selection] + selectors = select_test_paths(discover_project(tmp_path), files, full=False) + observation, output = _observe_discovery(tmp_path, tmp_path / "portable.json", selectors) + assert observation["exit_code"] == 0, output + assert observation["collected"] == ["test_app.py::test_selected"] + validate_observation(observation, 0) + + +@pytest.mark.parametrize("has_failure", [True, False]) +def test_native_fallback_preserves_failure_and_empty_execution(tmp_path: Path, has_failure: bool) -> None: + (tmp_path / "pytest.ini").write_text("[pytest]\ntestpaths = absent\n") + if has_failure: + (tmp_path / "test_case.py").write_text("def test_defect(): assert 2 + 2 == 5\n") + selectors = select_test_paths(discover_project(tmp_path), [], full=True) + observation, output = _observe_discovery(tmp_path, tmp_path / "portable.json", selectors) + assert "No files were found in testpaths" in output + if has_failure: + assert observation["exit_code"] == 1, output + assert any(row["phase"] == "call" and row["outcome"] == "failed" for row in observation["records"]) + validate_observation(observation, 1) + else: + assert observation["exit_code"] == 5, output + with pytest.raises(ProjectRuntimeError, match="execution_incomplete"): + validate_observation(observation, 5) + + +def _parse_recorded_observation(tmp_path: Path, observation: dict, monkeypatch) -> list: + monkeypatch.chdir(tmp_path) + output = tmp_path / "adapter-observation.json" + monkeypatch.setattr( + portable_worker, + "Path", + lambda value: output if value == "/opt/specfact/tmp/pytest-observation.json" else Path(value), + ) + monkeypatch.setattr(portable_worker, "target_command", lambda *_args: ["recorded-worker"]) + + def recorded_worker(*_args, **_kwargs): + output.write_text(json.dumps(observation)) + return SimpleNamespace(returncode=observation["exit_code"]) + + monkeypatch.setattr(portable_worker.subprocess, "run", recorded_worker) + return portable_worker.run_portable_pytest([tmp_path / "test_case.py"], ("portable-pytest-v2", "{}")) + + +def _assert_incomplete_setup(response: dict, findings: list) -> None: + assert response["execution_state"] == "error" + assert response["evidence_outcome"] == "UNKNOWN" + diagnostic = next(finding.message for finding in findings if finding.category == "tool_error") + assert "project_pytest_unexecuted:test_case.py::test_affected" in diagnostic + assert "fixture setup" in diagnostic + + +def _assert_phase_failure_evidence(findings: list, phase: str) -> None: + failures = [finding.message for finding in findings if finding.rule == "TEST_OUTCOME_NOT_PASS"] + assert failures == [f"Test test_case.py::test_affected failed during {phase}."] + response = _capsule_member_response("targeted-pytest-coverage", findings) + if phase == "setup": + _assert_incomplete_setup(response, findings) + else: + assert response["execution_state"] == "ran" + assert response["evidence_outcome"] == "FAIL" + assert not any(finding.category == "tool_error" for finding in findings) + + +@pytest.mark.parametrize("phase", ["setup", "call", "teardown"]) +def test_mixed_phase_failures_preserve_actual_body_execution(tmp_path: Path, monkeypatch, phase: str) -> None: + effects = { + "setup": " raise RuntimeError('controlled setup failure')\n yield\n", + "call": " yield\n", + "teardown": " yield\n raise RuntimeError('controlled teardown failure')\n", + } + body = "assert False" if phase == "call" else "assert True" + (tmp_path / "pytest.ini").write_text("[pytest]\n") + (tmp_path / "test_case.py").write_text( + "import pytest\n" + "@pytest.fixture\n" + "def fixture_effect():\n" + f"{effects[phase]}" + f"def test_affected(fixture_effect): {body}\n" + "def test_healthy(): assert True\n" + ) + observation, output = _observe_discovery(tmp_path, tmp_path / "phases.json", ("test_case.py",)) + assert observation["exit_code"] == 1, output + executed = {row["nodeid"] for row in observation["records"] if row["phase"] == "call"} + assert "test_case.py::test_healthy" in executed + assert ("test_case.py::test_affected" in executed) == (phase != "setup") + findings = _parse_recorded_observation(tmp_path, observation, monkeypatch) + _assert_phase_failure_evidence(findings, phase) + + +@pytest.mark.parametrize("outcome", ["skip", "xfail"]) +def test_intentional_setup_outcomes_retain_existing_policy(tmp_path: Path, outcome: str) -> None: + (tmp_path / "pytest.ini").write_text("[pytest]\n") + (tmp_path / "test_case.py").write_text( + "import pytest\n" + "@pytest.fixture\n" + f"def fixture_outcome(): pytest.{outcome}('intentional setup outcome')\n" + "def test_affected(fixture_outcome): assert False\n" + "def test_healthy(): assert True\n" + ) + observation, output = _observe_discovery(tmp_path, tmp_path / "outcomes.json", ("test_case.py",)) + assert observation["exit_code"] == 0, output + assert any( + row["nodeid"] == "test_case.py::test_affected" and row["phase"] == "setup" and row["outcome"] == "skipped" + for row in observation["records"] + ) + validate_observation(observation, 0) + + +@pytest.mark.parametrize( + "testpaths,relative,patterns", + [ + ("tests/test_app.py", "tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/test_app.py", "test_*.py"), + ("tests/*", "tests/unit/test_app.py", "test_*.py"), + ("tests/**/test_app.py", "tests/unit/test_app.py", "test_*.py"), + ("absent tests/*.py", "tests/test_app.py", "test_*.py"), + ("absent*", "tests/test_app.py", "test_*.py"), + ('"integration tests/*.py"', "integration tests/test_app.py", "test_*.py"), + ("tests/*.py", "tests/app_test.py", "*_test.py"), + ], +) +def test_source_review_expands_native_testpaths(tmp_path: Path, testpaths: str, relative: str, patterns: str) -> None: + root = tmp_path / "project" + root.mkdir() + (root / "pytest.ini").write_text(f"[pytest]\ntestpaths = {testpaths}\npython_files = {patterns}\n") + # Native pytest.ini precedence must also remain the discovery authority. + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\ntestpaths = ["wrong"]\n') + source = root / "app.py" + source.write_text("VALUE = 1\n") + test = root / relative + test.parent.mkdir(parents=True) + test.write_text("def test_selected(): assert 2 + 2 == 4\n") + native, native_output = _observe_discovery(root, tmp_path / "native-source.json", ()) + assert native["exit_code"] == 0, native_output + expected = [f"{relative}::test_selected"] + assert native["collected"] == expected + selectors = select_test_paths(discover_project(root), [source], full=False) + assert selectors == (relative,) + portable, output = _observe_discovery(root, tmp_path / "portable-source.json", selectors) + assert portable["exit_code"] == 0, output + assert portable["collected"] == expected + validate_observation(portable, 0) + + +@pytest.mark.parametrize("selection", ["test", "mixed"]) +def test_glob_testpaths_preserve_explicit_selection(tmp_path: Path, selection: str) -> None: + (tmp_path / "pytest.ini").write_text("[pytest]\ntestpaths = tests/*.py\n") + (tmp_path / "tests").mkdir() + (tmp_path / "tests/test_other.py").write_text("def test_other(): assert False\n") + source = tmp_path / "app.py" + source.touch() + test = tmp_path / "test_app.py" + test.write_text("def test_selected(): assert True\n") + files = [test] if selection == "test" else [source, test] + selectors = select_test_paths(discover_project(tmp_path), files, full=False) + assert selectors == ("test_app.py",) + observation, output = _observe_discovery(tmp_path, tmp_path / "explicit-glob.json", selectors) + assert observation["exit_code"] == 0, output + assert observation["collected"] == ["test_app.py::test_selected"] + validate_observation(observation, 0) diff --git a/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py b/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py new file mode 100644 index 00000000..7b972f0a --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_portable_pytest_evidence.py @@ -0,0 +1,294 @@ +"""Portable evidence must enforce the same non-pass and source coverage gates.""" + +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from specfact_code_review.run import portable_worker, runner, target_pytest + + +def _record(nodeid, outcome="passed", phase="call", wasxfail="") -> dict[str, object]: + return {"nodeid": nodeid, "phase": phase, "outcome": outcome, "wasxfail": wasxfail} + + +def _observation(*records, coverage=None, threshold=None): + return { + "exit_code": 0, + "collected": list(dict.fromkeys(record["nodeid"] for record in records)), + "records": list(records), + "coverage": {"files": coverage or {"src/app.py": {"summary": {"percent_covered": 100.0}}}}, + "coverage_threshold": threshold, + "test_roots": ["tests"], + } + + +def _run(tmp_path, monkeypatch, observation, names=("src/app.py", "tests/test_app.py")): + monkeypatch.chdir(tmp_path) + files = [tmp_path / name for name in names] + for path in files: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("VALUE = 1\n") + output = tmp_path / "observation.json" + monkeypatch.setattr( + portable_worker, + "Path", + lambda value: output if value == "/opt/specfact/tmp/pytest-observation.json" else Path(value), + ) + monkeypatch.setattr(portable_worker, "target_command", lambda *args: ["observed-worker"]) + + def execute(*_args, **_kwargs): + output.write_text(json.dumps(observation)) + return SimpleNamespace(returncode=observation["exit_code"]) + + monkeypatch.setattr(portable_worker.subprocess, "run", execute) + return portable_worker.run_portable_pytest(files, ("portable-pytest-v2", "{}")) + + +@pytest.mark.parametrize( + "outcome,phase,xfail", + [ + ("skipped", "setup", ""), + ("skipped", "call", ""), + ("skipped", "call", "known bug"), + ("passed", "call", "known bug"), + ("failed", "teardown", ""), + ], +) +def test_mixed_non_pass_outcomes_cannot_pass(tmp_path, monkeypatch, outcome, phase, xfail): + observation = _observation( + _record("tests/test_app.py::test_pass"), + _record("tests/test_app.py::test_other", outcome, phase, xfail), + ) + if outcome == "failed": + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) + assert any(item.rule == "TEST_OUTCOME_NOT_PASS" and item.severity == "error" for item in findings) + if outcome != "failed": + assert not any(item.rule == "tool_error" for item in findings) + + +@pytest.mark.parametrize( + "coverage,threshold,expected", + [ + ({"src/other.py": {"summary": {"percent_covered": 100}}}, None, "tool_error"), + ({"src/app.py": {"summary": {"percent_covered": 20}}}, None, "TEST_COVERAGE_LOW"), + ( + {"src/app.py": {"summary": {"percent_covered": 85}}, "src/other.py": {"summary": {"percent_covered": 100}}}, + 90, + "TEST_COVERAGE_LOW", + ), + ({"src/app.py": {"summary": {"percent_covered": 79}}}, 20, "TEST_COVERAGE_LOW"), + ], +) +def test_each_reviewed_source_requires_sufficient_coverage(tmp_path, monkeypatch, coverage, threshold, expected): + observation = _observation(_record("tests/test_app.py::test_pass"), coverage=coverage, threshold=threshold) + findings = _run(tmp_path, monkeypatch, observation) + assert any(item.rule == expected and item.severity == "error" for item in findings) + + +def test_healthy_coverage_excludes_test_support_and_type_stubs(tmp_path, monkeypatch): + observation = _observation(_record("tests/test_app.py::test_pass")) + assert not _run( + tmp_path, + monkeypatch, + observation, + ( + "src/app.py", + "tests/test_app.py", + "tests/support.py", + "conftest.py", + "src/app.pyi", + ), + ) + + +def test_actual_custom_test_module_is_not_a_production_coverage_target(tmp_path, monkeypatch): + observation = _observation(_record("checks/check_app.py::test_pass")) + observation["test_roots"] = [] + assert not _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_app.py")) + + +def test_deselected_test_module_does_not_require_production_coverage(tmp_path, monkeypatch): + observation = _observation(_record("checks/check_app.py::test_pass")) + observation["deselected"] = ["checks/check_other.py::test_other"] + observation["test_roots"] = [] + assert not _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_other.py")) + + +def test_project_root_as_test_root_cannot_exempt_production_sources(tmp_path, monkeypatch): + observation = _observation( + _record("tests/test_app.py::test_pass"), + coverage={"src/other.py": {"summary": {"percent_covered": 100}}}, + ) + observation["test_roots"] = ["."] + assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) + + +def test_customer_nonempty_initializer_cannot_use_specfact_omit_exemption(tmp_path, monkeypatch): + observation = _observation(_record("tests/test_app.py::test_pass")) + assert any( + item.rule == "tool_error" + for item in _run( + tmp_path, + monkeypatch, + observation, + ("src/customer/__init__.py", "tests/test_app.py"), + ) + ) + + +def test_real_failure_remains_visible_alongside_missing_coverage(tmp_path, monkeypatch): + observation = _observation( + _record("tests/test_app.py::test_pass", "failed"), + coverage={"src/other.py": {"summary": {"percent_covered": 100}}}, + ) + observation["exit_code"] = 1 + findings = _run(tmp_path, monkeypatch, observation) + assert {"TEST_OUTCOME_NOT_PASS", "tool_error"} <= {item.rule for item in findings} + + +@pytest.mark.parametrize("threshold", ["bad", -1, 101, float("nan"), float("inf"), True]) +def test_invalid_effective_threshold_cannot_pass(tmp_path, monkeypatch, threshold): + observation = _observation(_record("tests/test_app.py::test_pass"), threshold=threshold) + assert any(item.rule == "tool_error" for item in _run(tmp_path, monkeypatch, observation)) + + +def test_empty_reason_xpass_cannot_pass(tmp_path, monkeypatch): + record = _record("tests/test_app.py::test_xpass") + record["has_xfail"] = True + observation = _observation(record) + findings = _run(tmp_path, monkeypatch, observation) + assert any(item.rule == "TEST_OUTCOME_NOT_PASS" for item in findings) + + +def test_nested_pytest_root_resolves_custom_test_modules(tmp_path, monkeypatch): + observation = _observation(_record("check_app.py::test_pass")) + observation["pytest_root"] = "checks" + observation["test_roots"] = [] + assert not _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_app.py")) + + +def test_package_discovery_root_does_not_exempt_production_source(tmp_path, monkeypatch): + observation = _observation( + _record("package/tests/test_app.py::test_pass"), + coverage={"package/app.py": {"summary": {"percent_covered": 0}}}, + ) + observation["test_roots"] = ["package"] + findings = _run(tmp_path, monkeypatch, observation, ("package/app.py", "package/tests/test_app.py")) + assert any(item.rule == "TEST_COVERAGE_LOW" for item in findings) + + +def test_external_pytest_root_is_incomplete(tmp_path, monkeypatch): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = str(tmp_path.parent) + findings = _run(tmp_path, monkeypatch, observation) + assert any(item.rule == "tool_error" and "pytest_root" in item.message for item in findings) + + +@pytest.mark.parametrize("pytest_root", ["checks", "absolute"]) +def test_nested_nonpass_finding_uses_snapshot_relative_path(tmp_path, monkeypatch, pytest_root): + nodeid = "check_app.py::test_failure" + observation = _observation(_record(nodeid, "failed")) + observation["exit_code"] = 1 + observation["pytest_root"] = str(tmp_path / "checks") if pytest_root == "absolute" else pytest_root + findings = _run(tmp_path, monkeypatch, observation, ("src/app.py", "checks/check_app.py")) + failed = [finding for finding in findings if finding.rule == "TEST_OUTCOME_NOT_PASS"] + assert len(failed) == 1 + assert failed[0].file == "checks/check_app.py" + assert nodeid in failed[0].message + assert observation["records"][0]["nodeid"] == nodeid + assert observation["collected"] == [nodeid] + + +@pytest.mark.parametrize( + "pytest_root,nodeid", [("../outside", "check_app.py::test_failure"), (".", "../check_app.py::test_failure")] +) +def test_escaping_outcome_path_is_incomplete_without_false_attribution(tmp_path, monkeypatch, pytest_root, nodeid): + observation = _observation(_record(nodeid, "failed")) + observation["exit_code"] = 1 + observation["pytest_root"] = pytest_root + findings = _run(tmp_path, monkeypatch, observation) + assert any(finding.rule == "tool_error" for finding in findings) + assert not any(finding.rule == "TEST_OUTCOME_NOT_PASS" for finding in findings) + + +@pytest.mark.parametrize("pytest_root", [None, "", 12, [], {}]) +def test_malformed_observed_root_is_an_actionable_diagnostic(tmp_path, monkeypatch, pytest_root): + observation = _observation(_record("tests/test_app.py::test_pass")) + observation["pytest_root"] = pytest_root + findings = _run(tmp_path, monkeypatch, observation) + assert len(findings) == 1 + assert findings[0].rule == "tool_error" + assert "project_pytest_root_missing_or_invalid" in findings[0].message + + +def test_native_usage_error_retains_capsule_member_and_target_execution(tmp_path, monkeypatch): + (tmp_path / "pytest.ini").write_text("[pytest]\n") + native_output = tmp_path / "native-observation.json" + program = """ +import importlib.util, json, sys +from pathlib import Path +import pytest +spec = importlib.util.spec_from_file_location('native_observer', sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +observer = module.Observer() +code = pytest.main(['--specfact-controlled-invalid-option'], plugins=[observer]) +Path(sys.argv[2]).write_text(json.dumps({ + 'exit_code': int(code), 'pytest_root': observer.pytest_root, + 'records': observer.records, 'collected': sorted(observer.collected), + 'coverage': {}, 'argv': ['--specfact-controlled-invalid-option'], +})) +raise SystemExit(int(code)) +""" + native = subprocess.run( + [sys.executable, "-c", program, str(Path(target_pytest.__file__).resolve()), str(native_output)], + cwd=tmp_path, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "PYTEST_ADDOPTS": ""}, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert native.returncode == 4, native.stdout + native.stderr + observation = json.loads(native_output.read_text()) + assert observation["pytest_root"] is None + assert observation["records"] == [] + assert "--specfact-controlled-invalid-option" in native.stderr + result_path = tmp_path / "result.json" + observed_path = tmp_path / "observation.json" + files = [tmp_path / "src/app.py"] + monkeypatch.setattr( + runner, + "_load_capsule_request", + lambda _path: ("targeted-pytest-coverage", files, False, ("portable-pytest-v2", "{}"), False), + ) + monkeypatch.setattr( + runner, + "_member_findings", + lambda *_args, **_kwargs: _run(tmp_path, monkeypatch, observation), + ) + redirects = { + "/opt/specfact/tmp/pytest-observation.json": observed_path, + "/opt/specfact/output/result.json": result_path, + } + monkeypatch.setattr( + runner, "Path", Mock(wraps=Path, side_effect=lambda value: redirects.get(str(value), Path(value))) + ) + runner._capsule_process_request(tmp_path / "request.json") + response = json.loads(result_path.read_text()) + assert response["member"] == "targeted-pytest-coverage" + assert response["execution_state"] == "error" + assert response["evidence_outcome"] == "UNKNOWN" + assert response["target_execution"] == observation + assert any( + "project_pytest_configuration_or_collection_failed:exit=4" in finding["message"] + and "target_execution" in finding["message"] + for finding in response["findings"] + ) diff --git a/tests/unit/specfact_code_review/run/test_portable_pytest_policy.py b/tests/unit/specfact_code_review/run/test_portable_pytest_policy.py new file mode 100644 index 00000000..816cf6fa --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_portable_pytest_policy.py @@ -0,0 +1,164 @@ +"""Native pytest resolves portable coverage thresholds and test support roots.""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from specfact_code_review.run import target_pytest + + +OBSERVE_POLICY = """ +import importlib.util, json, sys +from pathlib import Path +import pytest +spec = importlib.util.spec_from_file_location('target_observer', sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +module.SNAPSHOT_ROOT = Path(sys.argv[2]) +for name in ('pytest_xdist_node_collection_finished', 'pytest_testnodedown'): + pytest.hookimpl(optionalhook=True)(getattr(module.Observer, name)) +observer = module.Observer() +code = pytest.main(json.loads(sys.argv[4]), plugins=[observer]) +Path(sys.argv[3]).write_text(json.dumps({ + 'exit_code': int(code), + 'coverage_threshold': getattr(observer, 'coverage_threshold', None), + 'test_roots': getattr(observer, 'test_roots', None), + 'pytest_root': getattr(observer, 'pytest_root', None), + 'records': observer.records, + 'collected': sorted(observer.collected), + 'deselected': sorted(observer.deselected), +})) +raise SystemExit(int(code)) +""" + + +def _observe_policy(root: Path, output: Path, arguments: list[str]) -> dict: + environment = { + **os.environ, + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "PYTEST_ADDOPTS": "", + "COVERAGE_FILE": str(output.with_suffix(".coverage")), + } + command = [ + sys.executable, + "-c", + OBSERVE_POLICY, + str(Path(target_pytest.__file__).resolve()), + str(root), + str(output), + json.dumps( + [ + "-q", + "-p", + "pytest_cov", + "--cov=.", + "--cov-report=", + "-o", + f"cache_dir={output.with_suffix('.cache')}", + *arguments, + ] + ), + ] + result = subprocess.run(command, cwd=root, env=environment, text=True, capture_output=True, check=False, timeout=30) + assert result.returncode == 0, result.stdout + result.stderr + return json.loads(output.read_text()) + + +@pytest.mark.parametrize( + "arguments,expected", [([], 70.0), (["--cov-fail-under=60"], 60.0), (["--cov-config=alternate.ini"], 90.0)] +) +def test_native_coverage_threshold_preserves_configuration_and_cli_precedence( + tmp_path: Path, arguments: list[str], expected: float +) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "pytest.ini").write_text("[pytest]\n") + (project / ".coveragerc").write_text("[report]\nfail_under = 70\n") + (project / "alternate.ini").write_text("[report]\nfail_under = 90\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "policy.json", arguments) + assert result["coverage_threshold"] == expected + + +@pytest.mark.parametrize( + "configuration,testpaths,expected", + [ + ("pytest.ini", "tests", ["tests"]), + ("pytest.ini", "tests/*", ["tests/unit"]), + ("pytest.ini", ".", []), + ("pytest.ini", "tests/unit/test_policy.py", []), + ("tests/pytest.ini", ".", ["tests"]), + ("tests/pytest.ini", "unit", ["tests/unit"]), + ], +) +def test_native_test_support_roots_expand_from_configuration_root( + tmp_path: Path, configuration: str, testpaths: str, expected: list[str] +) -> None: + project = tmp_path / "project" + (project / "tests/unit").mkdir(parents=True) + (project / configuration).write_text(f"[pytest]\ntestpaths = {testpaths}\n") + (project / "tests/unit/test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "roots.json", ["-c", configuration]) + assert result["test_roots"] == expected + + +def test_native_test_support_roots_exclude_paths_outside_snapshot(tmp_path: Path) -> None: + project = tmp_path / "project" + project.mkdir() + outside = tmp_path / "outside" + outside.mkdir() + (outside / "test_external.py").write_text("def test_external(): assert True\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + (project / "pytest.ini").write_text("[pytest]\ntestpaths = ../outside\n") + result = _observe_policy(project, tmp_path / "outside.json", ["test_policy.py"]) + assert result["test_roots"] == [] + + +@pytest.mark.parametrize("directory", [".", "checks"]) +def test_native_custom_test_nodeids_retain_their_configuration_root(tmp_path: Path, directory: str) -> None: + project = tmp_path / "project" + config_root = project / directory + config_root.mkdir(parents=True) + configuration = config_root / "pytest.ini" + configuration.write_text("[pytest]\ntestpaths = check_app.py\npython_files = check_*.py\n") + (config_root / "check_app.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "root-context.json", ["-c", str(configuration)]) + assert result["collected"] == ["check_app.py::test_valid"] + assert result["pytest_root"] == directory + + +def test_native_outside_configuration_root_is_explicit(tmp_path: Path) -> None: + project = tmp_path / "project" + project.mkdir() + configuration = tmp_path / "pytest.ini" + configuration.write_text("[pytest]\n") + (project / "test_policy.py").write_text("def test_valid(): assert True\n") + result = _observe_policy(project, tmp_path / "outside-root.json", ["-c", str(configuration), "test_policy.py"]) + assert result["collected"] == ["project/test_policy.py::test_valid"] + assert result["pytest_root"] == str(tmp_path.resolve()) + + +@pytest.mark.parametrize("outcome", ["skip", "xfail", "xpass", "xpass-empty"]) +def test_native_nonpassing_outcomes_preserve_phase_facts(tmp_path: Path, outcome: str) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "pytest.ini").write_text("[pytest]\n") + body = "assert True" if outcome.startswith("xpass") else f"pytest.{outcome}('expected')" + reason = "" if outcome == "xpass-empty" else "expected" + marker = f"@pytest.mark.xfail(reason={reason!r}, strict=False)\n" if outcome.startswith("xpass") else "" + (project / "test_policy.py").write_text( + f"import pytest\n{marker}def test_affected(): {body}\ndef test_healthy(): assert True\n" + ) + result = _observe_policy(project, tmp_path / "outcomes.json", []) + affected = next( + row for row in result["records"] if row["nodeid"].endswith("test_affected") and row["phase"] == "call" + ) + assert affected["outcome"] == ("passed" if outcome.startswith("xpass") else "skipped") + assert bool(affected["wasxfail"]) == (outcome not in {"skip", "xpass-empty"}) + if outcome == "xpass-empty": + assert affected.get("has_xfail") is True + assert result["collected"] == ["test_policy.py::test_affected", "test_policy.py::test_healthy"] diff --git a/tests/unit/specfact_code_review/run/test_portable_snapshot.py b/tests/unit/specfact_code_review/run/test_portable_snapshot.py new file mode 100644 index 00000000..f4146036 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_portable_snapshot.py @@ -0,0 +1,408 @@ +"""Automatic scope attachment preserves static evidence on preparation failure.""" + +from pathlib import Path +from types import SimpleNamespace +from typing import Any, cast + +import pytest + +from specfact_code_review.run import portable_snapshot, runner +from specfact_code_review.run.portable_snapshot import ( + ProjectSnapshotRequest, + project_runtime_requested, + run_project_snapshot, +) +from specfact_code_review.run.portable_worker import DEPENDENT_MEMBERS +from specfact_code_review.run.runtime_models import PreparedRuntime, ProjectRuntimeError + + +def test_failed_preparation_never_runs_dependency_members(tmp_path: Path, monkeypatch) -> None: + (tmp_path / "pyproject.toml").write_text('[project]\nname="consumer"\ndependencies=["pandas"]\n') + source = tmp_path / "app.py" + source.write_text("import pandas\n") + calls = [] + + def fail(*_args, **_kwargs): + raise ProjectRuntimeError("project_native_library_missing:libodbc.so.2") + + monkeypatch.setattr("specfact_code_review.run.portable_snapshot.prepare_runtime", fail) + + def dispatch(request, **_kwargs): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + snapshot, evidence = run_project_snapshot( + runtime, + ProjectSnapshotRequest( + snapshot_root=tmp_path, files=[source], options=runner.ReviewOptions(), assurance_kind="explicit_files" + ), + ) + assert not set(calls) & {"basedpyright", "pylint", "contracts", "targeted-pytest-coverage"} + assert "ruff" in calls + assert evidence["diagnostic"] == "project_native_library_missing:libodbc.so.2" + assert snapshot.evidence["basedpyright"]["evidence_outcome"] == "UNKNOWN" + + +def test_runtime_kwargs_are_accepted(tmp_path: Path) -> None: + config = tmp_path / "review.toml" + options = runner._review_options_from_kwargs(None, {"project_config": config}) + assert options.project_config == config + + +def test_immutable_pair_prepares_each_side_and_downgrades_authority(tmp_path: Path, monkeypatch) -> None: + + base_root, head_root = tmp_path / "base", tmp_path / "head" + base_root.mkdir() + head_root.mkdir() + calls = [] + + def run_side(_runtime, request): + calls.append((request.snapshot_root, request.options.project_runtime)) + return runner.CapsuleSnapshotResult({}, {}), {"identity": str(request.snapshot_root)} + + monkeypatch.setattr(portable_snapshot, "run_project_snapshot", run_side) + monkeypatch.setattr(runner, "_snapshot_python_files", lambda *args: []) + monkeypatch.setattr(runner, "_classify_range_findings", lambda *args: ({}, {})) + monkeypatch.setattr(runner, "_capsule_report", lambda *args, **kwargs: kwargs["scope_evidence"]) + resolution = SimpleNamespace( + base_snapshot=SimpleNamespace(root=base_root, contents={"app.py": b""}), + head_snapshot=SimpleNamespace(root=head_root, contents={"app.py": b""}), + selected_paths=("pyproject.toml",), + ) + descriptor = tmp_path / "project-runtime.json" + observed = portable_snapshot.run_project_scope_pair( + resolution, + runtime=object(), + options=runner.ReviewOptions(project_runtime=descriptor), + scope_evidence={"assurance_kind": "range_candidate"}, + ) + assert calls == [(base_root, None), (head_root, descriptor)] + assert observed["assurance_kind"] == "range_preview" + assert observed["project_runtime"]["base"]["identity"] != observed["project_runtime"]["head"]["identity"] + + +def _portable_range_resolution(tmp_path: Path, change: str, source: bytes) -> SimpleNamespace: + base_contents = {"unrelated.py": source, "pyproject.toml": b"[project]\nname='fixture'\n"} + head_contents = dict(base_contents) + selected: tuple[str, ...] = () if change == "empty" else ("pyproject.toml",) + if change in {"deleted", "added"}: + selected = (f"{change}.py",) + (base_contents if change == "deleted" else head_contents)[selected[0]] = source + (head_contents if change == "deleted" else base_contents).pop("pyproject.toml") + snapshots = [] + for side, contents in (("base", base_contents), ("head", head_contents)): + root = tmp_path / side + root.mkdir() + for name, content in contents.items(): + (root / name).write_bytes(content) + snapshots.append(SimpleNamespace(root=root, contents=contents)) + return SimpleNamespace( + base_snapshot=snapshots[0], + head_snapshot=snapshots[1], + selected_paths=selected, + path_statuses={f"{change}.py": "D" if change == "deleted" else "A"} if change in {"deleted", "added"} else {}, + ) + + +@pytest.mark.parametrize("change", ["deleted", "added", "metadata", "empty"]) +@pytest.mark.parametrize("preparation_fails", [False, True]) +def test_portable_range_keeps_absent_selection_out_of_unrelated_findings( + tmp_path: Path, monkeypatch, change: str, preparation_fails: bool +) -> None: + source = b"def check():\n return missing_name\n" + resolution = _portable_range_resolution(tmp_path, change, source) + artifact = tmp_path / "artifact" + artifact.mkdir() + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + + def prepare(plan, **_context): + if preparation_fails and not (plan.root / "pyproject.toml").exists(): + raise ProjectRuntimeError("project_fixture_preparation_unavailable") + return prepared + + executed = [] + + def analyze(request): + findings = [] + if request.member == "ruff": + for path in request.files: + assert path.read_bytes() == source + relative = path.relative_to(request.snapshot_root).as_posix() + executed.append(relative) + findings.append( + { + "category": "style", + "severity": "warning", + "tool": "ruff", + "rule": "F821", + "file": relative, + "line": 2, + "message": "Undefined name missing_name", + "fixable": False, + } + ) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": findings} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *_args, **_context: prepared) + monkeypatch.setattr(runner, "_execute_capsule_member", analyze) + runtime = runner.CapsuleRuntime( + tmp_path, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + report = portable_snapshot.run_project_scope_pair( + resolution, + runtime=runtime, + options=runner.ReviewOptions(no_tests=True), + scope_evidence={"assurance_kind": "range_preview"}, + ) + expected = { + "deleted": (["deleted.py"], [("deleted.py", "fixed")]), + "added": (["added.py"], [("added.py", "introduced")]), + "metadata": (["unrelated.py", "unrelated.py"], [("unrelated.py", "unchanged")]), + "empty": ([], []), + } + expected_files, expected_findings = expected[change] + assert [(finding.file, finding.differential_state) for finding in report.findings] == expected_findings + assert executed == expected_files + if preparation_fails and change in {"deleted", "added"}: + absent_side = "head" if change == "deleted" else "base" + assert report.scope_evidence["project_runtime"][absent_side]["status"] == "UNKNOWN" + + +def test_source_change_during_full_analysis_invalidates_runtime_binding(tmp_path: Path, monkeypatch) -> None: + + root = tmp_path / "project" + root.mkdir() + source = root / "app.py" + source.write_text("VALUE = 1\n") + artifact = tmp_path / "artifact" + prepared = PreparedRuntime(artifact, artifact / "project-runtime.json", "sha256:" + "b" * 64, {"inventory": {}}) + runtime = runner.CapsuleRuntime( + root, + "sha256:" + "a" * 64, + "linux-x86_64-cp312", + "python", + "bootstrap", + runner.BubblewrapIdentity( + path="bwrap", + format="ELF", + architecture="x86_64", + linkage="static", + interpreter=(), + needed=(), + sha256="a" * 64, + descriptor_digest="b" * 64, + ), + ) + monkeypatch.setattr(portable_snapshot, "prepare_runtime", lambda *args, **kwargs: prepared) + monkeypatch.setattr(portable_snapshot, "load_runtime", lambda *args, **kwargs: prepared) + + def analyze(*_args, **_kwargs): + source.write_text("VALUE = 2\n") + return runner.CapsuleSnapshotResult({name: {"evidence_outcome": "PASS"} for name in DEPENDENT_MEMBERS}, {}) + + monkeypatch.setattr(runner, "_run_capsule_snapshot", analyze) + snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(root, [source], runner.ReviewOptions(no_tests=True), "full") + ) + assert evidence["status"] == "UNKNOWN" + assert all(snapshot.evidence[name]["evidence_outcome"] == "UNKNOWN" for name in DEPENDENT_MEMBERS) + + +def test_pip_tools_source_input_triggers_automatic_runtime(tmp_path: Path) -> None: + + (tmp_path / "requirements.in").write_text("requests\n") + assert project_runtime_requested(tmp_path, SimpleNamespace(project_config=None, project_runtime=None)) + + +@pytest.mark.parametrize("python_pin", [None, "3.12", "3.99"]) +def test_python_version_only_project_routes_preparation_and_preserves_static_fallback( + tmp_path: Path, monkeypatch, python_pin: str | None +) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + if python_pin is not None: + (tmp_path / ".python-version").write_text(python_pin + "\n") + monkeypatch.chdir(tmp_path) + prepared_pins = [] + members = [] + local_snapshot = runner.CapsuleSnapshotResult({}, {}) + + def prepare_project(plan, **_kwargs): + prepared_pins.append(plan.python) + raise ProjectRuntimeError("project_native_library_missing:libfixture.so") + + def dispatch(request, **_kwargs): + members.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(portable_snapshot, "prepare_runtime", prepare_project) + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + monkeypatch.setattr(runner, "_run_local_capsule_snapshot", lambda *_args, **_kwargs: local_snapshot) + monkeypatch.setattr(runner, "_finalize_local_capsule_snapshot", lambda snapshot, _context: snapshot) + runtime = cast( + runner.CapsuleRuntime, SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + ) + evidence: dict[str, Any] = {"assurance_kind": "explicit_files"} + snapshot = runner._run_local_capsule_context(runtime, [source], runner.ReviewOptions(), evidence, "explicit_files") + if python_pin is None: + assert snapshot is local_snapshot + assert not prepared_pins and not members + assert "project_runtime" not in evidence + else: + assert prepared_pins == (["3.12"] if python_pin == "3.12" else []) + assert "ruff" in members and not set(members) & DEPENDENT_MEMBERS + assert snapshot.evidence["basedpyright"]["evidence_outcome"] == "UNKNOWN" + expected = ( + "project_native_library_missing:libfixture.so" + if python_pin == "3.12" + else "project_python_incompatible:pin=3.99" + ) + assert evidence["project_runtime"]["diagnostic"].startswith(expected) + assert evidence["project_runtime"]["status"] == "UNKNOWN" + + +@pytest.mark.parametrize("relative", [False, True]) +def test_analysis_source_copy_excludes_local_environment_files(tmp_path: Path, monkeypatch, relative: bool) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / ".env").write_text("SYNTHETIC_SECRET=fixture\n") + (source / ".venv").mkdir() + (source / ".venv/private.txt").write_text("synthetic excluded fixture") + observed = [] + + def analyze(_runtime, *, snapshot_root, files, **_context): + assert snapshot_root != source + assert not (snapshot_root / ".env").exists() + assert not (snapshot_root / ".venv").exists() + assert files == [snapshot_root / "app.py"] + assert files[0].read_text() == "VALUE = 1\n" + observed.append(snapshot_root) + return runner.CapsuleSnapshotResult({}, {}) + + monkeypatch.setattr(runner, "_run_capsule_snapshot", analyze) + selected = Path("app.py") if relative else source / "app.py" + request = ProjectSnapshotRequest(source, [selected], runner.ReviewOptions(), "explicit_files") + portable_snapshot._run_in_private_source(object(), request, runner.CapsuleSnapshotSettings()) + assert observed and not observed[0].exists() + assert (source / ".env").read_text() == "SYNTHETIC_SECRET=fixture\n" + + +@pytest.mark.parametrize( + ("filename", "contents", "diagnostic"), + [ + ("setup.cfg", "missing section header\n", "project_config_invalid:setup.cfg:"), + ("pyproject.toml", "tool=1\n", "project_config_invalid:pyproject.toml:tool"), + ("hatch.toml", "envs=false\n", "project_config_invalid:hatch.toml:envs"), + ("pytest.toml", "pytest=1\n", "project_pytest_config_invalid:pytest.toml:pytest"), + ("pyproject.toml", "[tool]\npytest=false\n", "project_pytest_config_invalid:pyproject.toml:tool.pytest"), + ( + "pyproject.toml", + "[tool.pytest]\nini_options=[]\n", + "project_pytest_config_invalid:pyproject.toml:tool.pytest.ini_options", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\npythonpath=[1]\n", + "project_pytest_config_invalid:pyproject.toml:pythonpath", + ), + ( + "pyproject.toml", + "[tool.pytest.ini_options]\ntestpaths=1\n", + "project_pytest_config_invalid:pyproject.toml:testpaths", + ), + ], +) +def test_malformed_configuration_preserves_independent_analyzer_evidence( + tmp_path: Path, monkeypatch, filename: str, contents: str, diagnostic: str +) -> None: + (tmp_path / filename).write_text(contents) + source = tmp_path / "app.py" + source.write_text("import dependency\n") + calls = [] + + def dispatch(request, **_context): + calls.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + assert "ruff" in calls and not set(calls) & DEPENDENT_MEMBERS + assert evidence["diagnostic"].startswith(diagnostic) + assert snapshot.evidence["basedpyright"]["evidence_outcome"] == "UNKNOWN" + + +@pytest.mark.parametrize("selected", ["../outside.py", "/outside.py"]) +def test_analysis_source_copy_rejects_escaping_selection(tmp_path: Path, monkeypatch, selected: str) -> None: + def unexpected(*_args, **_kwargs): + pytest.fail("escaping selection reached analysis") + + monkeypatch.setattr(runner, "_run_capsule_snapshot", unexpected) + request = ProjectSnapshotRequest(tmp_path, [Path(selected)], runner.ReviewOptions(), "explicit_files") + with pytest.raises(ValueError): + portable_snapshot._run_in_private_source(object(), request, runner.CapsuleSnapshotSettings()) + + +@pytest.mark.parametrize("reason", ["project_config_invalid:setup.cfg", "project_native_library_missing:libodbc.so.2"]) +def test_preparation_fallback_uses_only_sanitized_source(tmp_path: Path, monkeypatch, reason: str) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + (tmp_path / ".env").write_text("PRIVATE=fixture\n") + (tmp_path / ".git/objects").mkdir(parents=True) + observed = [] + + def dispatch(request, **_kwargs): + assert request.snapshot_root != tmp_path + assert not (request.snapshot_root / ".env").exists() + assert not (request.snapshot_root / ".git").exists() + assert request.files[0].read_text() == "VALUE = 1\n" + observed.append(request.member) + return {"execution_state": "ran", "evidence_outcome": "PASS", "findings": []} + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + snapshot, evidence = portable_snapshot._failed_project_snapshot( + runtime, snapshot_root=tmp_path, files=[source], options=runner.ReviewOptions(), reason=reason + ) + assert "ruff" in observed and not set(observed) & DEPENDENT_MEMBERS + assert snapshot.evidence["basedpyright"]["evidence_outcome"] == "UNKNOWN" + assert evidence["diagnostic"] == reason + + +def test_unsafe_fallback_source_never_dispatches_analyzer(tmp_path: Path, monkeypatch) -> None: + source = tmp_path / "app.py" + source.write_text("VALUE = 1\n") + (tmp_path / ".env").write_text("PRIVATE=fixture\n") + (tmp_path / "alias.py").symlink_to(".env") + + def dispatch(*_args, **_kwargs): + pytest.fail("unsafe original source reached an analyzer") + + monkeypatch.setattr(runner, "_dispatch_capsule_member", dispatch) + runtime = SimpleNamespace(identity="sha256:" + "a" * 64, environment_id="linux-x86_64-cp312") + snapshot, evidence = run_project_snapshot( + runtime, ProjectSnapshotRequest(tmp_path, [source], runner.ReviewOptions(), "explicit_files") + ) + assert evidence["status"] == "UNKNOWN" + assert "project_source_copy_failed" in evidence["diagnostic"] + assert all(row["evidence_outcome"] == "UNKNOWN" for row in snapshot.evidence.values()) diff --git a/tests/unit/specfact_code_review/run/test_portable_worker.py b/tests/unit/specfact_code_review/run/test_portable_worker.py new file mode 100644 index 00000000..f1677f59 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_portable_worker.py @@ -0,0 +1,451 @@ +"""Portable workers keep customer imports out of controller startup.""" + +import json +from collections.abc import Callable +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest + +from specfact_code_review.run import portable_worker +from specfact_code_review.run.findings import ReviewFinding +from specfact_code_review.run.installed_coverage import CoverageBridge +from specfact_code_review.run.portable_worker import ( + _portable_pytest_command, + preparation_failure_snapshot, + select_test_paths, + validate_observation, +) +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError + + +def test_explicit_test_selection_preserves_hatch_plugin_config(tmp_path: Path) -> None: + (tmp_path / "src").mkdir() + (tmp_path / "tests").mkdir() + source, test = tmp_path / "src/app.py", tmp_path / "tests/test_app.py" + source.touch() + test.touch() + (tmp_path / "pyproject.toml").write_text( + '[tool.pytest.ini_options]\naddopts="-p pytest_asyncio.plugin --import-mode=importlib"\ntestpaths=["tests"]\n' + ) + plan = discover_project(tmp_path) + assert select_test_paths(plan, [source, test], full=False) == ("tests/test_app.py",) + assert "-p pytest_asyncio.plugin" in plan.pytest_config["addopts"] + + +def test_source_only_review_finds_corresponding_test(tmp_path: Path) -> None: + (tmp_path / "tests").mkdir() + source, test = tmp_path / "calculator.py", tmp_path / "tests/test_calculator.py" + source.touch() + test.touch() + assert select_test_paths(discover_project(tmp_path), [source], full=False) == ("tests/test_calculator.py",) + + +def test_no_corresponding_tests_is_actionable(tmp_path: Path) -> None: + source = tmp_path / "app.py" + source.touch() + with pytest.raises(ProjectRuntimeError, match="test_selection_empty"): + select_test_paths(discover_project(tmp_path), [source], full=False) + + +def test_preparation_failure_marks_only_dependent_members() -> None: + rows = preparation_failure_snapshot("project_manager_ambiguous:poetry,uv") + assert set(rows) == {"basedpyright", "pylint", "contracts", "targeted-pytest-coverage"} + assert all(row["evidence_outcome"] == "UNKNOWN" for row in rows.values()) + assert all(row["diagnostic"] == "project_runtime_required" for row in rows.values()) + + +def test_pytest_collection_only_cannot_pass() -> None: + + with pytest.raises(ProjectRuntimeError, match="execution_incomplete"): + validate_observation({"exit_code": 0, "collected": ["tests/test_a.py::test_a"], "records": []}, 0) + + +def test_pytest_early_stop_is_incomplete_even_with_real_failure() -> None: + + observation = { + "exit_code": 1, + "collected": ["a", "b"], + "records": [{"nodeid": "a", "phase": "call", "outcome": "failed"}], + } + with pytest.raises(ProjectRuntimeError, match=r"unexecuted.*b"): + validate_observation(observation, 1) + + +def test_pytest_success_requires_coverage_evidence() -> None: + + observation = { + "exit_code": 0, + "collected": ["a"], + "records": [{"nodeid": "a", "phase": "call", "outcome": "passed"}], + } + with pytest.raises(ProjectRuntimeError, match="coverage_missing"): + validate_observation(observation, 0) + + +def test_failed_pytest_startup_cannot_reuse_previous_observation(tmp_path, monkeypatch) -> None: + + observation = tmp_path / "pytest-observation.json" + observation.write_text( + json.dumps( + { + "exit_code": 0, + "collected": ["a"], + "records": [{"nodeid": "a", "phase": "call", "outcome": "passed"}], + "coverage": {"files": {"app.py": {}}}, + } + ) + ) + monkeypatch.setattr( + portable_worker, + "Path", + lambda value: observation if value == "/opt/specfact/tmp/pytest-observation.json" else Path(value), + ) + monkeypatch.setattr(portable_worker, "target_command", lambda *args: ["child"]) + monkeypatch.setattr(portable_worker.subprocess, "run", lambda *args, **kwargs: SimpleNamespace(returncode=0)) + assert portable_worker.run_portable_pytest([tmp_path / "app.py"], ("portable-pytest-v2", "{}")) + assert not observation.exists() + + +@pytest.mark.parametrize("field", ["collection_errors", "internal_errors"]) +def test_real_failure_does_not_hide_partial_pytest_execution(field) -> None: + observation = { + "exit_code": 1, + "collected": ["a"], + "records": [{"nodeid": "a", "phase": "call", "outcome": "failed"}], + "coverage": {"files": {"app.py": {}}}, + field: ["controlled startup or collection error"], + } + with pytest.raises(ProjectRuntimeError, match=r"project_pytest_.*error"): + validate_observation(observation, 1) + + +@pytest.mark.parametrize("configuration", [{}, {"testpaths": []}, {"testpaths": ""}]) +def test_pytest_native_default_root_is_preserved(tmp_path: Path, configuration: dict) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config=configuration) + assert not select_test_paths(plan, [source], full=True) + assert select_test_paths(plan, [source], full=False) == ("test_app.py",) + + +@pytest.mark.parametrize("name", [".venv", "venv", "custom-python"]) +def test_default_test_discovery_excludes_environment_tests(tmp_path: Path, name: str) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "test_app.py").touch() + environment = tmp_path / name + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home=/usr/bin\n") + (environment / "test_app.py").touch() + assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("test_app.py",) + + +@pytest.mark.parametrize("directory", ["build", "dist", ".cache", "package.egg", "_darcs", "CVS", "venv", "{arch}"]) +def test_default_recursion_exclusions_prevent_false_test_ambiguity(tmp_path: Path, directory: str) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", directory): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() + assert select_test_paths(ProjectPlan(tmp_path, manager="pip"), [source], full=False) == ("tests/test_app.py",) + + +@pytest.mark.parametrize( + "patterns", [["generated"], "generated", ["fixtures/generated"], "fixtures/generated", ["fixtures/*"]] +) +def test_custom_recursion_exclusions_replace_pytest_defaults(tmp_path: Path, patterns: object) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("build", "fixtures/generated"): + (tmp_path / name).mkdir(parents=True) + (tmp_path / name / "test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) + assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + + +@pytest.mark.parametrize("patterns", [[], ""]) +def test_empty_recursion_exclusions_enable_default_excluded_directory(tmp_path: Path, patterns: object) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "build").mkdir() + (tmp_path / "build/test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) + assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + + +def test_explicit_test_root_is_not_pruned_by_recursion_defaults(tmp_path: Path) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / "build").mkdir() + (tmp_path / "build/test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"testpaths": ["build"]}) + assert select_test_paths(plan, [source], full=False) == ("build/test_app.py",) + + +@pytest.mark.parametrize("absolute", [True, False]) +def test_recursion_exclusions_support_absolute_and_quoted_directory_names(tmp_path: Path, absolute: bool) -> None: + source = tmp_path / "app.py" + source.touch() + for name in ("tests", "generated cache"): + (tmp_path / name).mkdir() + (tmp_path / name / "test_app.py").touch() + patterns = [str(tmp_path / "generated cache")] if absolute else '"generated cache"' + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"norecursedirs": patterns}) + assert select_test_paths(plan, [source], full=False) == ("tests/test_app.py",) + + +@pytest.mark.parametrize("quote", ['"', "'"]) +def test_quoted_pytest_paths_match_native_configuration(tmp_path: Path, monkeypatch, quote: str) -> None: + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") + (tmp_path / "source tree").mkdir() + (tmp_path / "integration tests").mkdir() + source = tmp_path / "source tree/app name.py" + source.touch() + selected = tmp_path / "integration tests/test_app name.py" + selected.write_text("def test_example(): assert True\n") + configuration = tmp_path / "pytest.ini" + configuration.write_text( + "[pytest]\n" + f"testpaths = {quote}integration tests{quote}\n" + f"pythonpath = {quote}source tree{quote}\n" + f"python_files = {quote}test_* name.py{quote}\n" + ) + native = pytest.Config.fromdictargs({}, ["-c", str(configuration)]) + try: + plan = discover_project(tmp_path) + assert plan.source_roots == tuple(path.relative_to(tmp_path).as_posix() for path in native.getini("pythonpath")) + assert not select_test_paths(plan, [source], full=True) + assert select_test_paths(plan, [source], full=False) == (selected.relative_to(tmp_path).as_posix(),) + assert select_test_paths(plan, [selected], full=False) == (selected.relative_to(tmp_path).as_posix(),) + finally: + native._ensure_unconfigure() + + +@pytest.mark.parametrize("option", ["pythonpath", "testpaths", "python_files", "norecursedirs"]) +def test_invalid_pytest_quoting_names_configuration_option(tmp_path: Path, option: str) -> None: + (tmp_path / "pytest.ini").write_text(f'[pytest]\n{option} = "unterminated\n') + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:pytest.ini:{option}"): + discover_project(tmp_path) + + +def test_expanded_testpaths_reject_escaping_symlinks(tmp_path: Path) -> None: + root = tmp_path / "project" + (root / "tests").mkdir(parents=True) + source = root / "app.py" + source.touch() + outside = tmp_path / "external" + outside.mkdir() + (outside / "test_app.py").touch() + (root / "tests/external").symlink_to(outside, target_is_directory=True) + plan = ProjectPlan(root, manager="pip", pytest_config={"testpaths": ["tests/*"]}) + with pytest.raises(ProjectRuntimeError, match="project_test_path_escape"): + select_test_paths(plan, [source], full=False) + + +def test_glob_testpaths_do_not_admit_excluded_environment(tmp_path: Path) -> None: + source = tmp_path / "app.py" + source.touch() + (tmp_path / ".venv").mkdir() + (tmp_path / ".venv/test_app.py").touch() + plan = ProjectPlan(tmp_path, manager="pip", pytest_config={"testpaths": [".venv/*.py"]}) + with pytest.raises(ProjectRuntimeError, match="project_test_selection_empty"): + select_test_paths(plan, [source], full=False) + + +@pytest.fixture(name="observe_coverage_policy") +def fixture_observe_coverage_policy( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Callable[[dict[str, Any], int], list[ReviewFinding]]: + """Exercise the controller against serialized child evidence and a real exit status.""" + monkeypatch.chdir(tmp_path) + observation_file = tmp_path / "pytest-observation.json" + monkeypatch.setattr( + portable_worker, + "Path", + lambda value: observation_file if value == "/opt/specfact/tmp/pytest-observation.json" else Path(value), + ) + monkeypatch.setattr( + portable_worker, "_portable_pytest_command", lambda *_args: (CoverageBridge((), (), {}, {}), ["child"]) + ) + + def observe(observation: dict[str, Any], exit_code: int) -> list[ReviewFinding]: + def complete(*_args: object, **_kwargs: object) -> SimpleNamespace: + observation_file.write_text(json.dumps(observation), encoding="utf-8") + return SimpleNamespace(returncode=exit_code) + + monkeypatch.setattr(portable_worker.subprocess, "run", complete) + return portable_worker.run_portable_pytest([Path("test_app.py")], ("portable-pytest-v2", "{}")) + + return observe + + +def _coverage_observation(exit_code: int = 1) -> dict[str, Any]: + """Give the controller completed tests with independently failing native coverage.""" + return { + "exit_code": exit_code, + "rootpath": str(Path.cwd()), + "collected": ["test_app.py::test_app"], + "records": [{"nodeid": "test_app.py::test_app", "phase": "call", "outcome": "passed"}], + "coverage": {"files": {"test_app.py": {}}}, + "coverage_threshold": 80, + "coverage_policy": { + "mode": "native", + "threshold": 80, + "precision": 2, + "measured_total": 46.88, + "native_threshold_failed": True, + }, + } + + +def test_native_aggregate_coverage_failure_has_explicit_blocking_finding(observe_coverage_policy) -> None: + """A real native aggregate failure remains a failure even when every test passed.""" + findings = observe_coverage_policy(_coverage_observation(), 1) + assert len(findings) == 1 + finding = findings[0] + assert (finding.rule, finding.category, finding.severity) == ("TEST_COVERAGE_POLICY_FAILED", "testing", "error") + assert all(value in finding.message for value in ("46.88", "80", "precision=2")) + + +@pytest.mark.parametrize("empty_records", [False, True]) +def test_native_coverage_failure_retains_incomplete_and_failed_test_evidence( + observe_coverage_policy, empty_records: bool +) -> None: + """The aggregate finding supplements, rather than replaces, partial test evidence.""" + observation = _coverage_observation() + observation["collection_errors"] = ["controlled collection failure"] + observation["records"] = ( + [] if empty_records else [{"nodeid": "test_app.py::test_app", "phase": "call", "outcome": "failed"}] + ) + findings = observe_coverage_policy(observation, 1) + assert "TEST_COVERAGE_POLICY_FAILED" in {row.rule for row in findings} + assert any("project_pytest_collection_error" in row.message for row in findings) + assert ("TEST_OUTCOME_NOT_PASS" in {row.rule for row in findings}) is not empty_records + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("mode", "unknown"), + ("threshold", True), + ("threshold", 101), + ("threshold", float("nan")), + ("precision", True), + ("precision", -1), + ("precision", 10), + ("measured_total", "46.88"), + ("measured_total", float("inf")), + ("measured_total", None), + ("native_threshold_failed", 1), + ("mode", "reviewer"), + ("mode", "disabled"), + ], +) +def test_malformed_or_contradictory_coverage_policy_is_actionable(observe_coverage_policy, field, value) -> None: + """Reject invalid evidence without attributing arbitrary nonzero exits to coverage.""" + observation = _coverage_observation() + observation["coverage_policy"][field] = value + findings = observe_coverage_policy(observation, 1) + assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) + assert not any(row.rule == "TEST_COVERAGE_POLICY_FAILED" for row in findings) + + +@pytest.mark.parametrize("policy", [None, [], {}, {"mode": "native"}]) +def test_incomplete_coverage_policy_shape_is_actionable(observe_coverage_policy, policy) -> None: + """A present but incomplete receipt cannot use the legacy missing-receipt path.""" + observation = _coverage_observation() + observation["coverage_policy"] = policy + findings = observe_coverage_policy(observation, 1) + assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) + + +def test_native_coverage_failure_cannot_contradict_successful_exit(observe_coverage_policy) -> None: + """A failed-policy receipt paired with exit zero must remain incomplete evidence.""" + findings = observe_coverage_policy(_coverage_observation(0), 0) + assert any("project_pytest_coverage_policy_invalid" in row.message for row in findings) + + +@pytest.mark.parametrize("mode", ["native", "reviewer", "disabled", "legacy"]) +@pytest.mark.parametrize("exit_code", [0, 1]) +def test_unfailed_coverage_policy_preserves_existing_exit_behavior(observe_coverage_policy, mode, exit_code) -> None: + """Neither reviewer collection nor an absent receipt explains an arbitrary exit one.""" + observation = _coverage_observation(exit_code) + observation["coverage_policy"].update(mode=mode, measured_total=None, native_threshold_failed=False) + if mode == "legacy": + del observation["coverage_policy"] + findings = observe_coverage_policy(observation, exit_code) + assert not any(row.rule == "TEST_COVERAGE_POLICY_FAILED" for row in findings) + assert bool(findings) is bool(exit_code) + if exit_code: + assert findings[0].message == "project_pytest_failure_without_observed_test" + + +@pytest.mark.parametrize( + "diagnostic", + [ + "project_pytest_coverage_disabled:--no-cov; required reviewer evidence unavailable", + "project_pytest_coverage_xdist_unsupported:missing worker coverage", + "project_pytest_coverage_export_failed:configured report suppression", + ], +) +def test_specific_missing_coverage_diagnostic_preserves_other_failures(observe_coverage_policy, diagnostic) -> None: + """Keep the exact target remedy together with native policy and actual test failures.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + observation["records"][0]["outcome"] = "failed" + findings = observe_coverage_policy(observation, 1) + assert any(row.message == diagnostic for row in findings) + assert {"TEST_COVERAGE_POLICY_FAILED", "TEST_OUTCOME_NOT_PASS"} <= {row.rule for row in findings} + assert not any("project_pytest_coverage_missing" in row.message for row in findings) + + +@pytest.mark.parametrize("diagnostic", [None, False, 7, [], {"reason": "untrusted"}]) +def test_malformed_missing_coverage_diagnostic_is_not_stringified(observe_coverage_policy, diagnostic) -> None: + """Reject a malformed target diagnostic instead of formatting an arbitrary object.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) + assert any("project_pytest_coverage_diagnostic_invalid" in row.message for row in findings) + + +@pytest.mark.parametrize("field", ["collection_errors", "internal_errors"]) +def test_collection_errors_precede_missing_coverage_diagnostic(observe_coverage_policy, field) -> None: + """Coverage reporting cannot hide earlier collection or plugin failures.""" + observation = _coverage_observation() + observation["coverage"] = {} + observation["coverage_diagnostic"] = "controlled coverage diagnostic" + observation[field] = ["controlled collection or plugin failure"] + findings = observe_coverage_policy(observation, 1) + expected = "project_pytest_collection_error" if field == "collection_errors" else "project_pytest_internal_error" + assert any(expected in row.message for row in findings) + assert not any(row.message == observation["coverage_diagnostic"] for row in findings) + + +@pytest.mark.parametrize("diagnostic", ["", "absent"]) +def test_empty_or_absent_missing_coverage_diagnostic_retains_legacy_remedy(observe_coverage_policy, diagnostic) -> None: + """Older observers and empty diagnostic strings retain the established coverage error.""" + observation = _coverage_observation() + observation["coverage"] = {} + if diagnostic != "absent": + observation["coverage_diagnostic"] = diagnostic + findings = observe_coverage_policy(observation, 1) + assert any("project_pytest_coverage_missing" in row.message for row in findings) + + +def test_portable_pytest_command_transmits_verified_module_names(monkeypatch): + bridge = CoverageBridge((), (), {}, {}, modules=("standalone",)) + monkeypatch.setattr(portable_worker, "plan_installed_coverage", lambda *_args, **_kwargs: bridge) + monkeypatch.setattr(portable_worker, "target_command", lambda domain, arguments: [domain, *arguments]) + actual, command = _portable_pytest_command([], json.dumps({"selectors": ["tests"]})) + assert actual is bridge + assert command[0] == "pytest-observe" + request = json.loads(command[1]) + assert request["coverage_modules"] == ["standalone"] + assert request["coverage_directories"] == [] + assert request["selectors"] == ["tests"] diff --git a/tests/unit/specfact_code_review/run/test_runner.py b/tests/unit/specfact_code_review/run/test_runner.py index 72334826..4621144a 100644 --- a/tests/unit/specfact_code_review/run/test_runner.py +++ b/tests/unit/specfact_code_review/run/test_runner.py @@ -13,6 +13,7 @@ import pytest from pytest import MonkeyPatch +from specfact_code_review.run import portable_snapshot from specfact_code_review.run.findings import ReviewFinding, ReviewReport, read_review_report from specfact_code_review.run.runner import ( _changed_lines_from_git, @@ -1725,16 +1726,25 @@ def test_capsule_cached_enforcement_rejects_unbound_selected_path( assert snapshot_calls == [] +def _assert_case_distinct_blobs(destination: Path, paths: tuple[str, ...], collision_kind: str) -> None: + """Require exact Git bytes and separate filesystem identities for case-distinct paths.""" + upper_path, lower_path = (destination / path for path in paths) + assert upper_path.read_bytes() == b"UPPER\n" + assert lower_path.read_bytes() == b"lower\n" + assert not upper_path.samefile(lower_path) + if collision_kind == "directory": + assert not upper_path.parent.samefile(lower_path.parent) + + @pytest.mark.parametrize("collision_kind", ["file", "directory"]) -def test_cached_tree_materialization_rejects_case_collisions( +def test_cached_tree_materialization_preserves_case_identities( tmp_path: Path, collision_kind: str, ) -> None: probe = tmp_path / "probe" probe.mkdir() (probe / "Case").write_text("probe", encoding="utf-8") - if not (probe / "case").exists(): - pytest.skip("filesystem preserves case-distinct identities") + case_insensitive = (probe / "case").exists() runner_api = _c14_runner() git_env = runner_api._candidate_git_environment() repository = tmp_path / "repo" @@ -1762,7 +1772,13 @@ def _blob_oid(content: bytes) -> str: destination = tmp_path / "snapshot" destination.mkdir() - assert runner_api._materialize_cached_tree(repository, destination, entries) is None + identities = runner_api._materialize_cached_tree(repository, destination, entries) + if case_insensitive: + assert identities is None + else: + assert identities is not None + _assert_case_distinct_blobs(destination, tuple(entries), collision_kind) + assert runner_api._materialized_tree_matches_entries(destination, entries, identities) @pytest.mark.parametrize("bypass_kind", ["clean_filter", "assume_unchanged", "runtime_mutation", "replace_ref"]) @@ -2604,7 +2620,7 @@ def execute(request: object) -> dict[str, object]: snapshot_root=Path("/snapshot"), files=[Path("/snapshot/src/app.py")], options=runner_api.ReviewOptions(bug_hunt=False), - member_argv={"semgrep-bugs": ("/opt/specfact/config/0",)}, + settings=runner_api.CapsuleSnapshotSettings(member_argv={"semgrep-bugs": ("/opt/specfact/config/0",)}), ) assert "semgrep-bugs" in observed @@ -2650,8 +2666,10 @@ def execute(request: Any) -> dict[str, object]: snapshot_root=Path("/snapshot"), files=[], options=runner_api.ReviewOptions(), - member_argv={"targeted-pytest-coverage": ("--", "tests/test_app.py::test_app")}, - scope_paths=("src/deleted.py",), + settings=runner_api.CapsuleSnapshotSettings( + member_argv={"targeted-pytest-coverage": ("--", "tests/test_app.py::test_app")}, + scope_paths=("src/deleted.py",), + ), ) assert launched == ["targeted-pytest-coverage"] @@ -2902,7 +2920,7 @@ def test_immutable_review_reuses_authenticated_project_runtime_for_both_snapshot ) def run_snapshot(*_args: object, **kwargs: object) -> Any: - observed_roots.append(cast(Path | None, kwargs.get("project_runtime_root"))) + observed_roots.append(cast(Path | None, kwargs["settings"].project_runtime_root)) return runner_api.CapsuleSnapshotResult(_synthetic_complete_profile_evidence(runner_api), {}) monkeypatch.setattr(runner_api, "_run_capsule_snapshot", run_snapshot) @@ -3035,7 +3053,9 @@ def test_immutable_review_explicitly_loads_only_authenticated_project_pytest_plu ) def run_snapshot(*_args: object, **kwargs: object) -> Any: - observed_argv.append(cast(dict[str, tuple[str, ...]], kwargs["member_argv"])["targeted-pytest-coverage"]) + observed_argv.append( + cast(dict[str, tuple[str, ...]], kwargs["settings"].member_argv)["targeted-pytest-coverage"] + ) return runner_api.CapsuleSnapshotResult(_synthetic_complete_profile_evidence(runner_api), {}) monkeypatch.setattr(runner_api, "_run_capsule_snapshot", run_snapshot) @@ -3174,7 +3194,7 @@ def test_index_review_uses_head_policy_for_both_immutable_snapshots(monkeypatch: ) def run_snapshot(*_args: object, **kwargs: object) -> Any: - config_roots = cast(tuple[Path, ...], kwargs["config_roots"]) + config_roots = cast(tuple[Path, ...], kwargs["settings"].config_roots) pytest_config = next(path for root in config_roots for path in root.iterdir() if path.name == "pytest.ini") observed_configs.append(pytest_config.read_text(encoding="utf-8")) return runner_api.CapsuleSnapshotResult(_synthetic_complete_profile_evidence(runner_api), {}) @@ -3221,7 +3241,7 @@ def test_immutable_review_binds_each_complete_pytest_inventory_to_its_snapshot( monkeypatch.setattr(runner_api, "_prepare_capsule_runtime", lambda **_kwargs: (capsule, "")) def run_snapshot(*_args: object, **kwargs: object) -> Any: - observed_argv.append(cast(dict[str, tuple[str, ...]], kwargs["member_argv"])) + observed_argv.append(cast(dict[str, tuple[str, ...]], kwargs["settings"].member_argv)) return runner_api.CapsuleSnapshotResult(_synthetic_complete_profile_evidence(runner_api), {}) monkeypatch.setattr(runner_api, "_run_capsule_snapshot", run_snapshot) @@ -3410,8 +3430,15 @@ def test_protected_pr_candidate_payload_is_reconstructed_from_verified_git_bytes selected.staged_source.cleanup() -def test_github_candidate_context_failure_never_uses_stale_official_payload(monkeypatch: MonkeyPatch) -> None: +def test_github_candidate_context_failure_never_uses_stale_official_payload( + tmp_path: Path, monkeypatch: MonkeyPatch +) -> None: runner_api = _c14_runner() + (tmp_path / ".git").mkdir() + runner_file = tmp_path / "packages/specfact-code-review/src/specfact_code_review/run/runner.py" + runner_file.parent.mkdir(parents=True) + runner_file.write_text("# isolated publisher checkout fixture\n", encoding="utf-8") + monkeypatch.setattr(runner_api, "__file__", str(runner_file)) monkeypatch.setenv("GITHUB_ACTIONS", "true") monkeypatch.setenv("GITHUB_REPOSITORY", "nold-ai/specfact-cli-modules") monkeypatch.setenv("GITHUB_EVENT_NAME", "push") @@ -7283,6 +7310,47 @@ def test_pytest_outcome_reconciliation_accepts_complete_parametrized_expansion() assert result.status == "PASS" +def test_pytest_parameter_delimiters_preserve_real_junit_identities(tmp_path: Path) -> None: + runner_api = _c14_runner() + tests_root = tmp_path / "tests" + tests_root.mkdir() + test_file = tests_root / "test_param.py" + test_file.write_text( + "import pytest\n" + "parameters = pytest.mark.parametrize('value', ['plain', 'left::right', '[left::right]'])\n" + "@parameters\n" + "def test_value(value):\n" + " assert value\n" + "class TestValues:\n" + " @parameters\n" + " def test_value(self, value):\n" + " assert value\n", + encoding="utf-8", + ) + + process, coverage_path, observer_path, junit_path = runner_api._run_pytest_selection_with_coverage( + (str(test_file),), + coverage_source=tmp_path, + policy_argv=("--rootdir", str(tmp_path)), + ) + try: + observer, junit = runner_api._load_pytest_outcome_evidence(observer_path, junit_path) + result = runner_api.reconcile_pytest_outcomes( + observer=observer, + junit=junit, + process_exit=process.returncode, + planned=("tests/test_param.py::test_value", "tests/test_param.py::TestValues::test_value"), + ) + finally: + coverage_path.unlink(missing_ok=True) + observer_path.unlink(missing_ok=True) + junit_path.unlink(missing_ok=True) + + assert process.returncode == 0, process.stdout + process.stderr + assert len(junit) == 6 + assert result.status == "PASS" + + def test_partial_parametrized_deselection_is_unknown_from_real_pytest(tmp_path: Path) -> None: runner_api = _c14_runner() tests_root = tmp_path / "tests" @@ -7639,7 +7707,7 @@ def execute(request: Any) -> dict[str, object]: snapshot_root=tmp_path, files=[stub], options=runner_api.ReviewOptions(), - member_argv={"targeted-pytest-coverage": ("--",)}, + settings=runner_api.CapsuleSnapshotSettings(member_argv={"targeted-pytest-coverage": ("--",)}), ) assert "targeted-pytest-coverage" not in launched @@ -7699,3 +7767,43 @@ def test_customer_github_actions_uses_verified_installed_payload( ) result = runner_api._selected_module_payload() assert result.payload is installed + + +@pytest.fixture(autouse=True) +def isolate_legacy_runtime_boundary(monkeypatch: MonkeyPatch) -> None: + """These tests exercise the v1/stdlib path; portable preparation has dedicated tests.""" + monkeypatch.setattr("specfact_code_review.run.portable_snapshot.project_runtime_requested", lambda *args: False) + + +def test_pytest_worker_does_not_inherit_controller_diff_selection(monkeypatch: MonkeyPatch) -> None: + runner_api = _c14_runner() + monkeypatch.setenv("SPECFACT_CODE_REVIEW_CHANGED_DIFF", "cached") + assert "SPECFACT_CODE_REVIEW_CHANGED_DIFF" not in runner_api._pytest_env() + assert os.environ["SPECFACT_CODE_REVIEW_CHANGED_DIFF"] == "cached" + + +def test_pytest_worker_does_not_inherit_calling_git_index(monkeypatch: MonkeyPatch) -> None: + runner_api = _c14_runner() + monkeypatch.setenv("GIT_INDEX_FILE", "/private/controller/index") + assert "GIT_INDEX_FILE" not in runner_api._pytest_env() + assert os.environ["GIT_INDEX_FILE"] == "/private/controller/index" + + +def test_cached_portable_request_carries_repository_and_index_context(tmp_path, monkeypatch) -> None: + runner_api = _c14_runner() + source = tmp_path / "index" + source.mkdir() + cached = runner_api.CachedAnalysisSnapshot( + tmp_path, source, [], {}, (), runner_api.CachedDiffIdentity("a" * 40, "b" * 40, "") + ) + requests = [] + monkeypatch.setattr(runner_api, "_cached_review_requested", lambda *_: True) + monkeypatch.setattr(runner_api, "_cached_analysis_snapshot", lambda *_: cached) + monkeypatch.setattr(portable_snapshot, "project_runtime_requested", lambda *_: True) + monkeypatch.setattr( + portable_snapshot, "run_project_snapshot", lambda _runtime, request: (requests.append(request), {}) + ) + monkeypatch.setattr(runner_api, "_finalize_local_capsule_snapshot", lambda *_: object()) + runner_api._run_local_capsule_context(object(), [], runner_api.ReviewOptions(), {}, "worktree") + assert requests[0].source_snapshot is cached + assert cached.commit == "index-" + "b" * 40 diff --git a/tests/unit/specfact_code_review/run/test_runtime_adapters.py b/tests/unit/specfact_code_review/run/test_runtime_adapters.py new file mode 100644 index 00000000..a09c9afb --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_adapters.py @@ -0,0 +1,112 @@ +"""Native adapters preserve lock and explicit dependency-group selection.""" + +import subprocess +import sys +from pathlib import Path + +import pytest + +from specfact_code_review.run.runtime_adapters import install_commands +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError + + +def test_pip_preserves_requirement_constraints_and_groups(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[project]\nname="consumer"\n') + (tmp_path / "constraints.txt").write_text("requests<3\n") + config = tmp_path / "review.toml" + config.write_text('manager="pip"\ngroups=["test"]\nextras=["speed"]\nconstraints=["constraints.txt"]\n') + commands = install_commands(discover_project(tmp_path, config_path=config), python="/runtime/python") + combined = [arg for command in commands for arg in command] + assert "--group" in combined and "test" in combined + assert "-c" in combined and "constraints.txt" in combined + assert ".[speed]" in combined + + +def test_uv_uses_locked_resolution_without_changing_source(tmp_path: Path) -> None: + (tmp_path / "uv.lock").write_text("version=1\n") + commands = install_commands(discover_project(tmp_path), python="/runtime/python") + assert "--locked" in [arg for command in commands for arg in command] + assert not (tmp_path / ".venv").exists() + + +def test_hatch_prepare_uses_selected_native_environment(tmp_path: Path) -> None: + (tmp_path / "hatch.toml").write_text("[envs.review]\ndetached=true\n") + config = tmp_path / "review.toml" + config.write_text('manager="hatch"\nenvironment="review"\n') + commands = install_commands(discover_project(tmp_path, config_path=config), python="/runtime/python") + assert commands == (("/runtime/python", "-m", "hatch", "env", "create", "review"),) + + +def test_explicit_groups_do_not_install_unrelated_defaults(tmp_path: Path) -> None: + + uv = install_commands(ProjectPlan(root=tmp_path, manager="uv", groups=("tests",)), python="python")[0] + poetry = install_commands(ProjectPlan(root=tmp_path, manager="poetry", groups=("test",)), python="python")[0] + assert "--no-default-groups" in uv + assert poetry[poetry.index("--only") + 1] == "main,test" + + +@pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) +@pytest.mark.parametrize("field", ["requirements", "constraints"]) +def test_native_adapter_rejects_unconsumed_dependency_inputs(tmp_path: Path, manager: str, field: str) -> None: + plan = ProjectPlan( + root=tmp_path, + manager=manager, + requirements=("dependencies.txt",) if field == "requirements" else (), + constraints=("dependencies.txt",) if field == "constraints" else (), + ) + with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + install_commands(plan, python="python") + assert field in str(error.value) + + +def test_tool_only_setup_cfg_prepares_requirements_without_installing_root(tmp_path: Path) -> None: + (tmp_path / "setup.cfg").write_text("[tool:pytest]\naddopts = --strict-markers\npythonpath = .\n") + (tmp_path / "requirements.txt").write_text("packaging\n") + (tmp_path / "constraints.txt").write_text("packaging>=1\n") + config = tmp_path / "review.toml" + config.write_text('manager="pip"\nconstraints=["constraints.txt"]\n') + before = {path.name: path.read_bytes() for path in tmp_path.iterdir()} + plan = discover_project(tmp_path, config_path=config) + assert plan.pytest_config["addopts"] == "--strict-markers" + assert plan.source_roots == (".",) + commands = install_commands(plan, python=sys.executable) + # pip's no-index dry run validates the actual adapter command without installs. + completed = subprocess.run( + [*commands[0], "--no-index", "--no-deps", "--dry-run"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert commands[0][-4:] == ("-r", "requirements.txt", "-c", "constraints.txt") + assert {path.name: path.read_bytes() for path in tmp_path.iterdir()} == before + + +@pytest.mark.parametrize("contents", ["[tool:pytest]\naddopts = --strict-markers\n", "[metadata]\nname = example\n"]) +def test_setup_cfg_without_package_entrypoint_requires_no_install(tmp_path: Path, contents: str) -> None: + (tmp_path / "setup.cfg").write_text(contents) + assert install_commands(discover_project(tmp_path), python="python") == () + + +@pytest.mark.parametrize( + "name,contents", + [ + ("setup.py", 'raise AssertionError("discovery must not execute setup.py")\n'), + ("pyproject.toml", '[project]\nname="example"\nversion="1.0"\n'), + ("pyproject.toml", '[tool.pytest.ini_options]\naddopts="--strict-markers"\n'), + ], +) +def test_pip_native_package_entrypoints_retain_root_install_and_extras( + tmp_path: Path, name: str, contents: str +) -> None: + (tmp_path / name).write_text(contents) + plan = ProjectPlan(root=tmp_path, manager="pip", extras=("speed",)) + assert install_commands(plan, python="python")[0][-1] == ".[speed]" + + +@pytest.mark.parametrize("name", ["pyproject.toml", "setup.py"]) +def test_pip_package_entrypoint_must_be_a_regular_file(tmp_path: Path, name: str) -> None: + (tmp_path / name).mkdir() + assert install_commands(ProjectPlan(root=tmp_path, manager="pip"), python="python") == () diff --git a/tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py b/tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py new file mode 100644 index 00000000..995cf6ea --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_artifact_boundary.py @@ -0,0 +1,143 @@ +"""Reject builder-owned filesystem indirection before controller-side processing.""" + +import json +import os +import struct +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import runtime_artifacts, runtime_builder, runtime_native +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +def _prepare_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + source = tmp_path / "source" + source.mkdir() + plan = discover_project(source) + runtime = SimpleNamespace( + environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64, root=tmp_path / "capsule" + ) + runtime.root.mkdir() + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + return plan, runtime + + +def _artifact(staging: Path) -> Path: + artifact = staging / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "inventory.json").write_text("{}") + return artifact + + +def _mutate_artifact(artifact: Path, outside: Path, mutation: str) -> Path: + if mutation == "root": + artifact.rename(outside / "payload") + artifact.symlink_to(outside / "payload", target_is_directory=True) + return artifact + if mutation == "native-directory": + (artifact / "native").symlink_to(outside, target_is_directory=True) + return artifact + if mutation == "inventory": + (artifact / "inventory.json").unlink() + (artifact / "inventory.json").symlink_to(outside / "sentinel.json") + return artifact + if mutation == "nested-file": + (artifact / "site-packages/customer.so").symlink_to(outside / "sentinel.json") + return artifact + os.mkfifo(artifact / "site-packages/customer.fifo") + return artifact + + +@pytest.mark.parametrize("mutation", ["root", "native-directory", "inventory", "nested-file", "fifo"]) +def test_prepare_rejects_artifact_topology_before_inventory_access( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutation: str +) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside = tmp_path / "simulated-host" + outside.mkdir() + sentinel = outside / "sentinel.json" + sentinel.write_text('{"private": "disposable sentinel"}') + original = sentinel.read_bytes() + + def build(_plan, _runtime, staging, **_kwargs): + return _mutate_artifact(_artifact(staging), outside, mutation) + + original_read_text = Path.read_text + + def guarded_read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("controller read inventory before rejecting untrusted artifact topology") + return original_read_text(path, *args, **kwargs) + + def forbidden_native(*_args, **_kwargs): + raise AssertionError("controller entered native enrichment before rejecting untrusted artifact topology") + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", guarded_read) + monkeypatch.setattr(runtime_builder, "inventory_native", forbidden_native) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + assert "project_runtime_build_artifact_invalid" in str(error.value) + assert sentinel.read_bytes() == original + + +def test_prepare_native_directory_cannot_overwrite_external_sentinels( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + outside, system = tmp_path / "simulated-host", tmp_path / "system-fixtures" + outside.mkdir() + system.mkdir() + payload = bytearray(64) + payload[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" None: + plan, runtime = _prepare_fixture(tmp_path, monkeypatch) + monkeypatch.setattr(runtime_builder, "_build", lambda _plan, _runtime, staging, **_kwargs: _artifact(staging)) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + prepared = runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + reused = runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache", offline=True) + assert reused.identity == prepared.identity + assert ( + json.loads(prepared.descriptor_path.read_text(encoding="utf-8"))["payload"]["inventory.json"]["kind"] == "file" + ) + + +def test_build_artifact_validation_never_reads_payloads(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + artifact = _artifact(tmp_path) + + def forbidden_read(*_args, **_kwargs): + raise AssertionError("topology validation must not read payload bytes") + + monkeypatch.setattr(Path, "read_bytes", forbidden_read) + monkeypatch.setattr(Path, "read_text", forbidden_read) + runtime_artifacts.validate_build_artifact(artifact) diff --git a/tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py b/tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py new file mode 100644 index 00000000..03a5be69 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_artifact_hardlinks.py @@ -0,0 +1,134 @@ +"""Runtime payloads cannot retain external inode aliases or private build logs.""" + +import os +import shutil +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import runtime_artifacts, runtime_builder +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +@pytest.fixture(name="runtime_fixture") +def artifact_tree_fixture(tmp_path: Path): + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + (artifact / "site-packages").mkdir(parents=True) + payload = artifact / "site-packages/customer.py" + payload.write_text("VALUE = 1\n", encoding="utf-8") + return discover_project(source), artifact, payload + + +def _seal(plan, artifact): + return runtime_artifacts.seal_runtime(artifact, plan=plan, environment_id="test-abi", worker_identity="worker") + + +def _guard_bytes(monkeypatch: pytest.MonkeyPatch, forbidden: Path): + original = Path.read_bytes + + def read(path): + if path == forbidden: + raise AssertionError("linked payload read before rejection") + return original(path) + + monkeypatch.setattr(Path, "read_bytes", read) + + +@pytest.mark.parametrize("phase", ["build", "seal", "load"]) +def test_linked_payload_is_rejected_before_read(runtime_fixture, tmp_path: Path, monkeypatch, phase: str) -> None: + plan, artifact, payload = runtime_fixture + prepared = _seal(plan, artifact) if phase == "load" else None + os.link(payload, tmp_path / "external-alias") + _guard_bytes(monkeypatch, payload) + with pytest.raises(ProjectRuntimeError, match="hardlink"): + if phase == "build": + runtime_artifacts.validate_build_artifact(artifact) + elif phase == "seal": + _seal(plan, artifact) + else: + assert prepared is not None + runtime_artifacts.load_runtime( + prepared.descriptor_path, plan=plan, environment_id="test-abi", worker_identity="worker" + ) + + +def test_linked_descriptor_is_rejected_before_read(runtime_fixture, tmp_path: Path, monkeypatch) -> None: + plan, artifact, _ = runtime_fixture + prepared = _seal(plan, artifact) + os.link(prepared.descriptor_path, tmp_path / "descriptor-alias") + original = Path.read_text + + def read(path, *args, **kwargs): + if path == prepared.descriptor_path: + raise AssertionError("linked descriptor read before rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError, match="hardlink"): + runtime_artifacts.load_runtime( + prepared.descriptor_path, plan=plan, environment_id="test-abi", worker_identity="worker" + ) + + +@pytest.mark.parametrize("linked_name", ["inventory.json", "site-packages/retained.log"]) +def test_prepare_rejects_log_alias_before_inventory(tmp_path: Path, monkeypatch, linked_name: str) -> None: + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + source = tmp_path / "source" + source.mkdir() + plan = discover_project(source) + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="worker", root=tmp_path / "capsule") + cache = tmp_path / "cache" + + def build(_plan, _runtime, staging, *, build_log): + artifact = staging / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "inventory.json").write_text("{}", encoding="utf-8") + build_log.write(b"DISPOSABLE_PRIVATE_DIAGNOSTIC\n") + build_log.flush() + target = artifact / linked_name + target.unlink(missing_ok=True) + # Reconstruct the topology; this does not simulate Linux bind-mount traversal. + os.link(build_log.name, target) + return artifact + + original = Path.read_text + + def read(path, *args, **kwargs): + if path.name == "inventory.json": + raise AssertionError("inventory read before linked artifact rejection") + return original(path, *args, **kwargs) + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(Path, "read_text", read) + with pytest.raises(ProjectRuntimeError) as error: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + assert "hardlink" in str(error.value) + assert "DISPOSABLE_PRIVATE_DIAGNOSTIC" not in str(error.value) + logs = list(cache.glob("failed-*.log")) + assert len(logs) == 1 + assert logs[0].read_text(encoding="utf-8") == "DISPOSABLE_PRIVATE_DIAGNOSTIC\n" + assert logs[0].stat().st_nlink == 1 + assert not list(cache.glob("*/project-runtime.json")) + assert not list(cache.glob(".preparing-*")) + + +def test_copied_distribution_aliases_produce_independent_payload(runtime_fixture, tmp_path: Path) -> None: + plan, artifact, _ = runtime_fixture + manager_cache = tmp_path / "manager-cache" + environment = tmp_path / "environment" + manager_cache.mkdir() + environment.mkdir() + cached = manager_cache / "distribution.py" + cached.write_text("VALUE = 2\n", encoding="utf-8") + os.link(cached, environment / cached.name) + shutil.copytree(environment, artifact / "site-packages", dirs_exist_ok=True) + runtime_artifacts.validate_build_artifact(artifact) + prepared = _seal(plan, artifact) + loaded = runtime_artifacts.load_runtime( + prepared.descriptor_path, plan=plan, environment_id="test-abi", worker_identity="worker" + ) + assert loaded.identity == prepared.identity + assert (artifact / "site-packages" / cached.name).stat().st_nlink == 1 diff --git a/tests/unit/specfact_code_review/run/test_runtime_artifacts.py b/tests/unit/specfact_code_review/run/test_runtime_artifacts.py new file mode 100644 index 00000000..190c2390 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_artifacts.py @@ -0,0 +1,132 @@ +"""Portable runtime artifacts reject stale, corrupt and escaping content.""" + +import json +from pathlib import Path + +import pytest + +from specfact_code_review.run.runtime_artifacts import load_runtime, seal_runtime +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectRuntimeError, document_digest + + +def test_local_descriptor_roundtrip_and_corruption(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + artifact = tmp_path / "artifact" + (artifact / "site-packages").mkdir(parents=True) + payload = artifact / "site-packages" / "consumer.py" + payload.write_text("VALUE = 1\n", encoding="utf-8") + plan = discover_project(source) + prepared = seal_runtime( + artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="sha256:" + "a" * 64 + ) + observed = load_runtime( + prepared.descriptor_path, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="sha256:" + "a" * 64 + ) + assert observed.identity == prepared.identity + assert observed.descriptor["provenance"]["authority"] == "local_build" + payload.write_text("VALUE = 2\n", encoding="utf-8") + with pytest.raises(ProjectRuntimeError, match="payload"): + load_runtime( + prepared.descriptor_path, + plan=plan, + environment_id="linux-x86_64-cp312", + worker_identity="sha256:" + "a" * 64, + ) + + +def test_runtime_rejects_wrong_abi_and_inputs(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "requirements.txt").write_text("requests<3\n", encoding="utf-8") + artifact = tmp_path / "artifact" + (artifact / "site-packages").mkdir(parents=True) + plan = discover_project(source) + prepared = seal_runtime( + artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="sha256:" + "a" * 64 + ) + with pytest.raises(ProjectRuntimeError, match="environment"): + load_runtime( + prepared.descriptor_path, + plan=plan, + environment_id="linux-x86_64-cp311", + worker_identity="sha256:" + "a" * 64, + ) + (source / "requirements.txt").write_text("requests<4\n", encoding="utf-8") + with pytest.raises(ProjectRuntimeError, match="inputs"): + load_runtime( + prepared.descriptor_path, + plan=discover_project(source), + environment_id="linux-x86_64-cp312", + worker_identity="sha256:" + "a" * 64, + ) + + +def test_runtime_rejects_links_and_descriptor_tampering(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + artifact = tmp_path / "artifact" + (artifact / "site-packages").mkdir(parents=True) + (artifact / "site-packages" / "bad").symlink_to("/etc/passwd") + with pytest.raises(ProjectRuntimeError, match="symlink"): + seal_runtime( + artifact, + plan=discover_project(source), + environment_id="linux-x86_64-cp312", + worker_identity="sha256:" + "a" * 64, + ) + (artifact / "site-packages" / "bad").unlink() + prepared = seal_runtime( + artifact, + plan=discover_project(source), + environment_id="linux-x86_64-cp312", + worker_identity="sha256:" + "a" * 64, + ) + data = json.loads(prepared.descriptor_path.read_text(encoding="utf-8")) + data["provenance"]["authority"] = "protected_pr" + prepared.descriptor_path.write_text(json.dumps(data), encoding="utf-8") + with pytest.raises(ProjectRuntimeError, match="descriptor"): + load_runtime( + prepared.descriptor_path, + plan=discover_project(source), + environment_id="linux-x86_64-cp312", + worker_identity="sha256:" + "a" * 64, + ) + + +def test_descriptor_rejects_a_different_analyzer_worker(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + artifact = tmp_path / "artifact" + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime( + artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="sha256:" + "a" * 64 + ) + with pytest.raises(ProjectRuntimeError, match="worker_identity_mismatch"): + load_runtime( + prepared.descriptor_path, + plan=plan, + environment_id="linux-x86_64-cp312", + worker_identity="sha256:" + "b" * 64, + ) + + +@pytest.mark.parametrize( + "inventory", + [[], {"analyzer_conflicts": []}, {"member_graphs": []}, {"pytest_arguments": 7}, {"pytest_arguments": [False]}], +) +def test_descriptor_rejects_malformed_inventory(tmp_path: Path, inventory) -> None: + + source, artifact = tmp_path / "source", tmp_path / "artifact" + source.mkdir() + artifact.mkdir() + plan = discover_project(source) + prepared = seal_runtime(artifact, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") + data = prepared.descriptor + data["inventory"] = inventory + data["identity"] = document_digest({key: value for key, value in data.items() if key != "identity"}) + prepared.descriptor_path.write_text(json.dumps(data), encoding="utf-8") + with pytest.raises(ProjectRuntimeError, match="inventory_invalid"): + load_runtime(prepared.descriptor_path, plan=plan, environment_id="linux-x86_64-cp312", worker_identity="worker") diff --git a/tests/unit/specfact_code_review/run/test_runtime_build_driver.py b/tests/unit/specfact_code_review/run/test_runtime_build_driver.py new file mode 100644 index 00000000..8e2f5eb7 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_build_driver.py @@ -0,0 +1,124 @@ +"""Builder inventories retain dependency facts without credentials.""" + +import json + +import pytest + +from specfact_code_review.run import runtime_build_driver as driver +from specfact_code_review.run.runtime_build_driver import ( + clean_inventory, + copy_executables, + installer_environment, + select_hatch_environment, +) + + +def test_inventory_never_serializes_index_credentials() -> None: + + inventory = clean_inventory( + { + "installed": [ + { + "metadata": { + "name": "consumer", + "version": "1", + "description": "do not retain arbitrary metadata", + "requires_dist": ["dependency @ https://user:secret@example.test/pkg.whl?token=secret"], + }, + "direct_url": {"url": "https://user:secret@example.test/pkg.whl?token=secret"}, + } + ] + } + ) + + encoded = json.dumps(inventory) + assert "secret" not in encoded + assert "description" not in encoded + assert "example.test/pkg.whl" in encoded + + +def test_hatch_export_selects_one_native_matrix_environment() -> None: + + export = {"hatch-test.py3.11": {"python": "3.11"}, "hatch-test.py3.12": {"python": "3.12"}} + assert select_hatch_environment(export, "hatch-test", "3.12") == "hatch-test.py3.12" + + +def test_hatch_export_rejects_ambiguous_matrix() -> None: + + export = {"test.py3.12-a": {"python": "3.12"}, "test.py3.12-b": {"python": "3.12"}} + with pytest.raises(RuntimeError, match="project_environment_ambiguous"): + select_hatch_environment(export, "test", "3.12") + + +def test_distribution_executables_are_rebased_and_unowned_files_excluded(tmp_path) -> None: + + environment, artifact = tmp_path / "env", tmp_path / "artifact" + (environment / "bin").mkdir(parents=True) + artifact.mkdir() + (environment / "bin/customer").write_text('#!/private/build/env/bin/python\nprint("customer")\n') + (environment / "bin/uv").write_bytes(b"\x7fELFowned-native-executable") + (environment / "bin/unowned").write_text("do not import host tools") + records = [ + {"path": str(environment / "bin/customer"), "distribution": "customer"}, + {"path": str(environment / "bin/uv"), "distribution": "uv"}, + ] + copied = copy_executables(environment, artifact, records) + assert len(copied) == 2 + assert "/private/build" not in (artifact / "bin/customer").read_text() + assert (artifact / "bin/customer").read_text().startswith("#!/opt/specfact/project-runtime/bin/python\n") + assert (artifact / "executables/uv").read_bytes().startswith(b"\x7fELF") + assert not (artifact / "bin/unowned").exists() + + +def test_hatch_export_has_its_owned_uv_executable(monkeypatch) -> None: + + observed = [] + + def run(_command, *, env=None): + assert env is not None + observed.append(env.copy()) + return '{"review": {}}' + + monkeypatch.setattr(driver, "_run", run) + driver._prepare_hatch({"environment": "review"}, "/private/tools/bin/python", {}) + assert observed[0]["HATCH_UV"] == "/private/tools/bin/uv" + + +def test_hatch_dependencies_are_not_redirected_into_the_pip_environment() -> None: + + hatch = installer_environment("hatch", {"PATH": "/usr/bin", "PIP_PYTHON": "/wrong/python", "VIRTUAL_ENV": "/wrong"}) + assert "PIP_PYTHON" not in hatch + assert "VIRTUAL_ENV" not in hatch + assert installer_environment("pip", {})["PIP_PYTHON"] == "/opt/specfact/output/env/bin/python" + assert installer_environment("poetry", {})["VIRTUAL_ENV"] == "/opt/specfact/output/env" + + +def test_hatch_native_extra_arguments_are_retained(monkeypatch) -> None: + + monkeypatch.setattr( + driver, + "_run", + lambda *args, **kwargs: '{"review": {"extra-args": ["--dist", "worksteal", "-p", "no:randomly"]}}', + ) + config = {"environment": "review"} + driver._prepare_hatch(config, "/tools/bin/python", {}) + assert config["pytest_arguments"] == ["--dist", "worksteal", "-p", "no:randomly"] + + +def test_exact_hatch_environment_cannot_select_an_incompatible_abi() -> None: + + with pytest.raises(RuntimeError, match="project_python_incompatible"): + select_hatch_environment({"review": {"python": "3.11"}}, "review", "3.12") + assert select_hatch_environment({"review": {}}, "review", "3.12") == "review" + + +def test_python_prefixed_distribution_command_is_preserved(tmp_path) -> None: + + environment, artifact = tmp_path / "environment", tmp_path / "artifact" + (environment / "bin").mkdir(parents=True) + artifact.mkdir() + command = environment / "bin/python-lsp-server" + command.write_text('#!/build/python\nprint("language server")\n') + copied = copy_executables(environment, artifact, [{"path": str(command), "distribution": "python-lsp-server"}]) + assert copied == [{"name": "python-lsp-server", "distribution": "python-lsp-server"}] + assert (artifact / "bin/python-lsp-server").is_file() diff --git a/tests/unit/specfact_code_review/run/test_runtime_builder.py b/tests/unit/specfact_code_review/run/test_runtime_builder.py new file mode 100644 index 00000000..797f44b6 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_builder.py @@ -0,0 +1,250 @@ +"""Runtime builder separates dependency acquisition from source and host state.""" + +import errno +import json +import os +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import runtime_builder, runtime_git +from specfact_code_review.run.runtime_builder import builder_command, copy_project, prepare_runtime +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError +from specfact_code_review.run.runtime_sources import source_identity + + +def test_plain_pip_build_runs_without_git(tmp_path: Path, monkeypatch) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "requirements.txt").write_text("example-package==1.0\n") + plan = ProjectPlan( + source, manager="pip", requirements=("requirements.txt",), source_identity=source_identity(source) + ) + staging = tmp_path / "staging" + staging.mkdir() + runtime = SimpleNamespace( + root=tmp_path / "capsule", interpreter="/opt/specfact/python/bin/python", bubblewrap="bwrap" + ) + monkeypatch.setattr(runtime_git, "GIT", tmp_path / "unavailable-git") + monkeypatch.setattr( + runtime_builder.sandbox, "_verified_bubblewrap_descriptor", lambda *_args: os.open(os.devnull, os.O_RDONLY) + ) + launched = [] + + def acquire(command, **_kwargs): + launched.append(command) + (staging / "artifact/site-packages").mkdir(parents=True) + return SimpleNamespace(returncode=0) + + monkeypatch.setattr(runtime_builder.subprocess, "run", acquire) + assert runtime_builder._build(plan, runtime, staging) == staging / "artifact" + assert len(launched) == 1 + config = json.loads((staging / "build.json").read_text()) + assert config["commands"][0][-2:] == ["-r", "requirements.txt"] + + +def test_builder_mounts_source_copy_and_no_host_home(tmp_path: Path) -> None: + runtime = SimpleNamespace(root=tmp_path / "capsule", interpreter="/opt/specfact/python/bin/python3") + command = builder_command(runtime, staging=tmp_path / "staging", executable="/proc/self/fd/12") + assert command[0] == "/proc/self/fd/12" + assert "--clearenv" in command + assert "--share-net" in command + assert str(Path.home()) not in command + assert str(tmp_path / "staging") in command + assert "/opt/specfact/output/build_driver.py" in command + + +def test_copy_project_preserves_input_and_excludes_sidecars(tmp_path: Path) -> None: + source, target = tmp_path / "source", tmp_path / "copy" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / ".venv").mkdir() + (source / ".venv" / "secret").write_text("not part of source") + copy_project(source, target) + assert (target / "app.py").read_bytes() == (source / "app.py").read_bytes() + assert not (target / ".venv").exists() + + +def test_copy_project_rejects_external_symlink(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "escape").symlink_to("/etc/passwd") + with pytest.raises(ProjectRuntimeError, match="symlink"): + copy_project(source, tmp_path / "copy") + + +def test_copy_project_supports_repository_local_symlink(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "cert.pem").write_text("test certificate fixture") + (source / "cert-link.pem").symlink_to("cert.pem") + copy_project(source, tmp_path / "copy") + assert (tmp_path / "copy/cert-link.pem").read_text() == "test certificate fixture" + + +def test_builder_provides_vcs_metadata_tool_without_host_configuration(tmp_path: Path) -> None: + runtime = SimpleNamespace(root=tmp_path / "capsule", interpreter="/opt/specfact/python/bin/python") + command = builder_command(runtime, staging=tmp_path / "staging", executable="/proc/self/fd/12") + assert "/opt/specfact/output/builder-tools/bin:/opt/specfact/python/bin:/usr/bin:/bin" in command + assert "GIT_CONFIG_NOSYSTEM" in command + + +def test_python_patch_constraint_uses_signed_interpreter_version(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + plan = ProjectPlan(tmp_path, manager="pip", requires_python=">=3.12.1,<3.13") + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache", offline=True) + + +@pytest.mark.parametrize("number", [errno.EEXIST, errno.ENOTEMPTY]) +def test_concurrent_cache_publication_accepts_existing_winner(tmp_path, monkeypatch, number) -> None: + + def raced(source, destination): + raise OSError(number, "concurrent publication") + + monkeypatch.setattr(runtime_builder.os, "rename", raced) + runtime_builder.publish_artifact(tmp_path / "candidate", tmp_path / "winner") + + +@pytest.mark.parametrize("target", [".env", ".venv/private.txt", ".venv", ".git/config", "nested/.env"]) +def test_project_links_cannot_alias_excluded_inputs(tmp_path: Path, target: str) -> None: + + source = tmp_path / "source" + source.mkdir() + secret = source / target + if target == ".venv": + secret.mkdir() + (secret / "private.txt").write_text("synthetic excluded fixture") + else: + secret.parent.mkdir(parents=True, exist_ok=True) + secret.write_text("synthetic excluded fixture") + (source / "alias").symlink_to(target) + with pytest.raises(ProjectRuntimeError, match=r"symlink.*excluded"): + source_identity(source) + with pytest.raises(ProjectRuntimeError, match=r"symlink.*excluded"): + copy_project(source, tmp_path / "copy") + + +def test_source_copy_preserves_directory_alias_without_copying_excluded_children(tmp_path: Path) -> None: + source = tmp_path / "source" + (source / "package").mkdir(parents=True) + (source / "package/app.py").write_text("VALUE = 1\n") + (source / "package/.env").write_text("synthetic excluded fixture") + (source / "alias").symlink_to("package") + target = tmp_path / "copy" + copy_project(source, target) + assert (target / "alias").is_symlink() + assert (target / "alias/app.py").read_text() == "VALUE = 1\n" + assert not (target / "alias/.env").exists() + + +def test_source_copy_rebases_absolute_internal_alias(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / "alias.py").symlink_to(source / "app.py") + target = tmp_path / "copy" + copy_project(source, target) + assert (target / "alias.py").is_symlink() + assert (target / "alias.py").resolve() == target / "app.py" + + +def test_source_copy_rebases_internal_link_spelled_through_parent(tmp_path: Path) -> None: + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + (source / "alias.py").symlink_to("../source/app.py") + target = tmp_path / "copy" + copy_project(source, target) + assert (target / "alias.py").is_symlink() + assert (target / "alias.py").resolve() == target / "app.py" + + +def test_builder_sets_private_git_transport_path(tmp_path: Path) -> None: + runtime = SimpleNamespace(root=tmp_path / "capsule", interpreter="/opt/specfact/python/bin/python") + command = builder_command(runtime, staging=tmp_path / "staging", executable="/proc/self/fd/12") + index = command.index("GIT_EXEC_PATH") + assert command[index + 1] == "/opt/specfact/output/builder-tools/git-core" + + +def test_source_package_named_venv_is_retained_but_real_environments_are_excluded(tmp_path: Path) -> None: + source = tmp_path / "project" + package = source / "src/customer/venv" + package.mkdir(parents=True) + (package / "core.py").write_text("VALUE = 1\n") + before = source_identity(source) + (package / "core.py").write_text("VALUE = 2\n") + assert source_identity(source) != before + for name in ("venv", "custom-python"): + environment = source / name + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home = /usr/bin\n") + (environment / "private.txt").write_text("excluded fixture") + copy_project(source, tmp_path / "copy", include_vcs=False) + assert (tmp_path / "copy/src/customer/venv/core.py").read_text() == "VALUE = 2\n" + assert not (tmp_path / "copy/venv").exists() + assert not (tmp_path / "copy/custom-python").exists() + + +def test_alias_cannot_import_actual_custom_named_environment(tmp_path: Path) -> None: + source = tmp_path / "project" + environment = source / "custom-python" + environment.mkdir(parents=True) + (environment / "pyvenv.cfg").write_text("home = /usr/bin\n") + (environment / "private.txt").write_text("excluded fixture") + (source / "alias.txt").symlink_to("custom-python/private.txt") + with pytest.raises(ProjectRuntimeError, match="symlink_excluded"): + copy_project(source, tmp_path / "copy", include_vcs=False) + + +def _assert_policy_change_invalidates_reuse(tmp_path: Path, monkeypatch, helper_name: str) -> None: + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + helpers = tmp_path / "helpers" + helpers.mkdir() + for helper in Path(runtime_builder.__file__).parent.glob("*.py"): + (helpers / helper.name).write_bytes(helper.read_bytes()) + lock = Path("resources/contracts/pr-range-v1-toolchain-lock.json") + (tmp_path / lock).parent.mkdir(parents=True) + (tmp_path / lock).write_bytes((Path(runtime_builder.__file__).parents[1] / lock).read_bytes()) + monkeypatch.setattr(runtime_builder, "__file__", str(helpers / "runtime_builder.py")) + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "fixed-git-identity") + plan = ProjectPlan(tmp_path, manager="pip") + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + cache = tmp_path / "cache" + identities = [] + digest = runtime_builder.document_digest + + def record_identity(values): + identities.append(values) + return digest(values) + + monkeypatch.setattr(runtime_builder, "document_digest", record_identity) + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + cached_path = cache / digest(identities[-1])[7:] + cached_path.mkdir() + loaded = [] + + def load_cached(path, **_kwargs): + loaded.append(path) + return SimpleNamespace(descriptor={"project_identity": plan.identity}) + + monkeypatch.setattr(runtime_builder, "load_runtime", load_cached) + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + helper = helpers / helper_name + helper.write_bytes(helper.read_bytes() + b"\n# changed dispatcher fixture\n") + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + prepare_runtime(plan, runtime=runtime, cache_root=cache, offline=True) + assert loaded == [cached_path / "project-runtime.json"] + assert identities[0] == identities[1] + assert identities[2]["builder"][helper_name] == runtime_builder.content_digest(helper.read_bytes()) + assert digest(identities[2]) != digest(identities[0]) + + +@pytest.mark.parametrize("helper_name", ["target_pylint.py", "runtime_compatibility.py"]) +def test_controller_policy_change_invalidates_offline_runtime_reuse( + tmp_path: Path, monkeypatch, helper_name: str +) -> None: + _assert_policy_change_invalidates_reuse(tmp_path, monkeypatch, helper_name) diff --git a/tests/unit/specfact_code_review/run/test_runtime_builder_logging.py b/tests/unit/specfact_code_review/run/test_runtime_builder_logging.py new file mode 100644 index 00000000..27a48a16 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_builder_logging.py @@ -0,0 +1,381 @@ +"""Untrusted build paths never select controller log files or retention targets.""" + +import os +import subprocess +import sys +import time +from contextlib import contextmanager +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import runtime_build_driver, runtime_builder +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError +from specfact_code_review.run.runtime_sources import source_identity + + +KEY = "b" * 64 +SUCCESS_SCRIPT = ( + "(staging/'artifact/site-packages').mkdir(parents=True); " + "(staging/'artifact/inventory.json').write_text('{}'); print('success')" +) + + +def _capture_subprocess(monkeypatch, *, timeout: bool = False): + captures = [] + native_run = subprocess.run + + def execute(command, **kwargs): + captures.append(kwargs.get("stdout")) + if timeout: + kwargs["timeout"] = 0.3 + return native_run(command, check=kwargs.pop("check", False), **kwargs) + + monkeypatch.setattr(subprocess, "run", execute) + return captures + + +def _builder(tmp_path: Path, monkeypatch, script: str, *, timeout: bool = False): + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + source = tmp_path / "source" + source.mkdir() + (source / "app.py").write_text("VALUE = 1\n") + plan = ProjectPlan(source, manager="pip", source_identity=source_identity(source)) + runtime = SimpleNamespace( + root=tmp_path / "capsule", + interpreter="/opt/specfact/python/bin/python", + bubblewrap="bwrap", + environment_id="linux-x86_64-cp312", + identity="sha256:" + "a" * 64, + ) + monkeypatch.setattr(runtime_builder, "stage_git", lambda *_args: None) + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "fixed-git") + monkeypatch.setattr(runtime_builder, "document_digest", lambda _value: "sha256:" + KEY) + monkeypatch.setattr( + runtime_builder.sandbox, "_verified_bubblewrap_descriptor", lambda *_args: os.open(os.devnull, os.O_RDONLY) + ) + + def fixture_builder_command(_runtime, *, staging, executable): + # This subprocess fixture deliberately uses the host Python below. + del executable + return [ + sys.executable, + "-c", + "import os,sys,time; from pathlib import Path; staging=Path(sys.argv[1]); " + script, + str(staging), + ] + + monkeypatch.setattr(runtime_builder, "builder_command", fixture_builder_command) + captures = [] + native_log = runtime_builder.tempfile.NamedTemporaryFile + + @contextmanager + def capture_log(**kwargs): + with native_log(**kwargs) as log: + captures.append(log.file) + yield log + + monkeypatch.setattr(runtime_builder.tempfile, "NamedTemporaryFile", capture_log) + if timeout: + monkeypatch.setattr(runtime_builder, "_BUILD_TIMEOUT_SECONDS", 0.3) + return plan, runtime, tmp_path / "cache", captures + + +def _retained_logs(cache: Path) -> list[Path]: + return [path for path in cache.glob("failed-*.log") if not path.is_symlink() and path.is_file()] + + +def _failed_build(plan, runtime, cache: Path) -> ProjectRuntimeError: + with pytest.raises(ProjectRuntimeError) as failure: + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + assert not list(cache.glob(".preparing-*")) + return failure.value + + +def test_staging_log_symlink_cannot_write_or_read_host_sentinel(tmp_path: Path, monkeypatch) -> None: + sentinel = tmp_path / "sentinel" + sentinel.write_text("PRIVATE_SENTINEL_UNCHANGED\n") + script = f"(staging/'build.log').symlink_to({str(sentinel)!r}); print('private-builder-output'); sys.exit(1)" + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, script) + error = _failed_build(plan, runtime, cache) + assert sentinel.read_text() == "PRIVATE_SENTINEL_UNCHANGED\n" + logs = _retained_logs(cache) + assert len(logs) == 1 + assert logs[0].read_text() == "private-builder-output\n" + assert str(logs[0]) in str(error) + assert "private-builder-output" not in str(error) + assert logs[0].stat().st_mode & 0o777 == 0o600 + assert captures[0] is not None and captures[0].closed + + +@pytest.mark.parametrize("kind", ["fifo", "directory"]) +def test_controller_never_opens_special_staging_log(tmp_path: Path, monkeypatch, kind: str) -> None: + operation = "os.mkfifo(staging/'build.log')" if kind == "fifo" else "(staging/'build.log').mkdir()" + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, operation + "; print('build-failed'); sys.exit(1)") + native_open = Path.open + + def checked_open(path, *args, **kwargs): + if path.name == "build.log" and path.parent.name.startswith(".preparing-"): + raise AssertionError("controller attempted an untrusted special-file open") + return native_open(path, *args, **kwargs) + + monkeypatch.setattr(Path, "open", checked_open) + _failed_build(plan, runtime, cache) + assert _retained_logs(cache)[0].read_text() == "build-failed\n" + + +@pytest.mark.parametrize("kind", ["symlink", "fifo", "directory"]) +def test_failure_retention_never_reopens_predictable_destination(tmp_path: Path, monkeypatch, kind: str) -> None: + sentinel = tmp_path / "sentinel" + sentinel.write_text("UNCHANGED\n") + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, "print('build-failed'); sys.exit(1)") + cache.mkdir() + old_name = cache / f"failed-{KEY}.log" + if kind == "symlink": + old_name.symlink_to(sentinel) + elif kind == "fifo": + os.mkfifo(old_name) + else: + old_name.mkdir() + error = _failed_build(plan, runtime, cache) + assert sentinel.read_text() == "UNCHANGED\n" + logs = _retained_logs(cache) + assert len(logs) == 1 + assert logs[0] != old_name and str(logs[0]) in str(error) + assert logs[0].read_text() == "build-failed\n" + + +def test_timeout_retains_private_partial_output_and_closes_descriptor(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder( + tmp_path, monkeypatch, "print('partial-builder-output', flush=True); time.sleep(10)", timeout=True + ) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) + assert len(logs) == 1 and logs[0].read_text() == "partial-builder-output\n" + assert "partial-builder-output" not in str(error) + assert captures[0] is not None and captures[0].closed + + +def test_launch_failure_closes_descriptor_and_retains_private_log(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, "pass") + monkeypatch.setattr( + runtime_builder, "builder_command", lambda *_args, **_kwargs: [str(tmp_path / "missing-builder")] + ) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) + assert len(logs) == 1 and str(logs[0]) in str(error) + assert logs[0].read_text() == "" + assert captures[0].closed + + +def test_log_creation_failure_never_starts_builder_or_claims_retained_log(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, "pass") + + def unavailable_log(**_kwargs): + raise OSError("private log creation failed") + + monkeypatch.setattr(runtime_builder.tempfile, "NamedTemporaryFile", unavailable_log) + with pytest.raises(OSError, match=r"^private log creation failed$"): + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + assert not captures and not list(cache.iterdir()) + + +def test_success_closes_and_removes_private_build_log(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, SUCCESS_SCRIPT) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + prepared = runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=cache) + assert prepared.descriptor_path.is_file() + assert not list(cache.glob("failed-*.log")) and not list(cache.glob(".preparing-*")) + assert captures[0] is not None and captures[0].closed + + +@pytest.mark.parametrize("stage", ["inventory_native", "seal_runtime", "publish_artifact", "load_runtime"]) +def test_downstream_failure_retains_log_and_original_diagnostic(tmp_path: Path, monkeypatch, stage: str) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, SUCCESS_SCRIPT) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def failed_stage(*_args, **_kwargs): + raise ProjectRuntimeError("project_native_library_missing:libcustomer.so") + + monkeypatch.setattr(runtime_builder, stage, failed_stage) + error = _failed_build(plan, runtime, cache) + logs = _retained_logs(cache) + assert len(logs) == 1 and logs[0].read_text() == "success\n" + assert "project_native_library_missing:libcustomer.so" in str(error) + assert str(logs[0]) in str(error) + assert captures[0].closed + + +def test_driver_forwards_diagnostics_without_opening_builder_log(tmp_path: Path, monkeypatch, capsys) -> None: + (tmp_path / "project").mkdir() + sentinel = tmp_path / "sentinel" + sentinel.write_text("UNCHANGED\n") + (tmp_path / "build.log").symlink_to(sentinel) + monkeypatch.setattr(runtime_build_driver, "ROOT", tmp_path) + output = runtime_build_driver._run( + [sys.executable, "-c", "import sys; print('native-out'); print('native-err', file=sys.stderr)"] + ) + assert output == "native-out" + assert sentinel.read_text() == "UNCHANGED\n" + assert capsys.readouterr().out == "native-out\nnative-err\n" + + +def test_driver_forwards_native_timeout_partial_output(tmp_path: Path, monkeypatch, capsys) -> None: + (tmp_path / "project").mkdir() + monkeypatch.setattr(runtime_build_driver, "ROOT", tmp_path) + _capture_subprocess(monkeypatch, timeout=True) + with pytest.raises(subprocess.TimeoutExpired): + runtime_build_driver._run( + [sys.executable, "-c", "import time; print('native-partial', flush=True); time.sleep(10)"] + ) + assert capsys.readouterr().out == "native-partial\n" + + +def test_builder_stdout_is_pipe_not_private_log_inode(tmp_path: Path, monkeypatch) -> None: + script = ( + "import stat; " + "assert stat.S_ISFIFO(os.fstat(1).st_mode), 'controller log inode exposed'; " + "print('PIPE_BOUNDARY_OK', flush=True); sys.exit(1)" + ) + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, script) + _failed_build(plan, runtime, cache) + assert _retained_logs(cache)[0].read_text() == "PIPE_BOUNDARY_OK\n" + + +@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Linux proc descriptor contract") +def test_builder_proc_reopen_cannot_access_regular_log_inode(tmp_path: Path, monkeypatch) -> None: + script = ( + "import stat; " + "fd=os.open('/proc/self/fd/1', os.O_RDONLY | os.O_NONBLOCK); " + "assert stat.S_ISFIFO(os.fstat(fd).st_mode), 'proc reopened private log inode'; " + "os.close(fd); print('PROC_PIPE_BOUNDARY_OK', flush=True); sys.exit(1)" + ) + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, script) + _failed_build(plan, runtime, cache) + assert _retained_logs(cache)[0].read_text() == "PROC_PIPE_BOUNDARY_OK\n" + + +def test_pipe_capture_preserves_large_binary_output_without_communicate(tmp_path: Path, monkeypatch) -> None: + script = "sys.stdout.buffer.write(bytes(range(256))*8192); sys.stdout.buffer.flush(); sys.exit(1)" + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, script) + + def forbidden_communicate(*_args, **_kwargs): + raise AssertionError("builder capture must not buffer its complete output") + + monkeypatch.setattr(subprocess.Popen, "communicate", forbidden_communicate) + _failed_build(plan, runtime, cache) + assert _retained_logs(cache)[0].read_bytes() == bytes(range(256)) * 8192 + assert captures[0].closed + + +def test_closed_output_pipe_does_not_disable_child_timeout(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder( + tmp_path, monkeypatch, "os.close(1); os.close(2); time.sleep(10)", timeout=True + ) + started = time.monotonic() + _failed_build(plan, runtime, cache) + assert time.monotonic() - started < 5 + assert captures[0].closed + assert _retained_logs(cache)[0].read_bytes() == b"" + + +def test_log_write_failure_reaps_child_and_closes_capture(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, captures = _builder(tmp_path, monkeypatch, "print('output', flush=True); time.sleep(10)") + native_log = runtime_builder.tempfile.NamedTemporaryFile + native_popen = subprocess.Popen + children = [] + + class BrokenLog: + def write(self, _chunk: bytes) -> None: + raise OSError("controller log write failed") + + @contextmanager + def broken_log(**kwargs): + with native_log(**kwargs) as log: + yield SimpleNamespace(name=log.name, file=BrokenLog()) + + @contextmanager + def record_child(*args, **kwargs): + with native_popen(*args, **kwargs) as process: + children.append(process) + yield process + + monkeypatch.setattr(runtime_builder.tempfile, "NamedTemporaryFile", broken_log) + monkeypatch.setattr(subprocess, "Popen", record_child) + started = time.monotonic() + _failed_build(plan, runtime, cache) + assert time.monotonic() - started < 5 + assert len(children) == 1 and children[0].poll() is not None + assert children[0].stdout.closed and captures[0].closed + + +def test_pipe_readiness_race_retries_without_blocking_past_deadline(tmp_path: Path, monkeypatch) -> None: + plan, runtime, cache, _ = _builder( + tmp_path, monkeypatch, "print('before', flush=True); time.sleep(0.05); print('after', flush=True); sys.exit(1)" + ) + native_popen = subprocess.Popen + native_read = os.read + stdout_descriptors = [] + observed_blocking = [] + + @contextmanager + def record_stdout(*args, **kwargs): + with native_popen(*args, **kwargs) as process: + assert process.stdout is not None + stdout_descriptors.append(process.stdout.fileno()) + yield process + + def transiently_drained(fd, size): + if fd in stdout_descriptors and not observed_blocking: + observed_blocking.append(os.get_blocking(fd)) + raise BlockingIOError("another reader drained the pipe after readiness") + return native_read(fd, size) + + monkeypatch.setattr(subprocess, "Popen", record_stdout) + monkeypatch.setattr(os, "read", transiently_drained) + _failed_build(plan, runtime, cache) + assert _retained_logs(cache)[0].read_text() == "before\nafter\n" + assert observed_blocking == [False] + + +@pytest.mark.parametrize(("termination", "exit_status"), [("sys.exit(7)", 7), ("os.kill(os.getpid(), 15)", -15)]) +def test_preparation_reports_numeric_builder_exit(tmp_path: Path, monkeypatch, termination, exit_status) -> None: + plan, runtime, cache, _ = _builder( + tmp_path, monkeypatch, "print('PRIVATE_FIXTURE_OUTPUT', flush=True); " + termination + ) + error = _failed_build(plan, runtime, cache) + assert f"project_runtime_prepare_failed:exit={exit_status}" in str(error) + assert "PRIVATE_FIXTURE_OUTPUT" not in str(error) + log = _retained_logs(cache)[0].read_text(encoding="utf-8") + assert "PRIVATE_FIXTURE_OUTPUT" in log + + +@pytest.mark.parametrize( + ("failure", "expected"), + [ + ( + ProjectRuntimeError("project_runtime_inputs_changed_during_build:PRIVATE_FIXTURE_PATH"), + "project_runtime_inputs_changed_during_build", + ), + (ProjectRuntimeError("PRIVATE_FIXTURE_MESSAGE"), "ProjectRuntimeError"), + (OSError("PRIVATE_FIXTURE_IO"), "OSError"), + (ValueError("PRIVATE_FIXTURE_VALUE"), "ValueError"), + ], +) +def test_preparation_separates_public_codes_from_private_details( + tmp_path: Path, monkeypatch, failure, expected +) -> None: + plan, runtime, cache, _ = _builder(tmp_path, monkeypatch, "pass") + + def fail_build(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_builder, "_build", fail_build) + error = _failed_build(plan, runtime, cache) + assert expected in str(error) + assert "PRIVATE_FIXTURE" not in str(error) diff --git a/tests/unit/specfact_code_review/run/test_runtime_commands.py b/tests/unit/specfact_code_review/run/test_runtime_commands.py new file mode 100644 index 00000000..9b848a34 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_commands.py @@ -0,0 +1,48 @@ +"""Public portable runtime CLI surfaces and review option propagation.""" + +import json +from pathlib import Path + +from typer.testing import CliRunner + +from specfact_code_review.review.commands import app +from specfact_code_review.run import runner +from specfact_code_review.run.commands import _build_review_run_request +from specfact_code_review.run.runner import ReviewOptions + + +def test_runtime_inspect_is_read_only(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text('[project]\nname="consumer"\n') + result = CliRunner().invoke(app, ["review", "runtime", "inspect", "--json"]) + assert result.exit_code == 0, result.output + assert json.loads(result.output)["manager"] == "pip" + assert sorted(path.name for path in tmp_path.iterdir()) == ["pyproject.toml"] + + +def test_review_help_exposes_project_handoff() -> None: + result = CliRunner().invoke(app, ["review", "run", "--help"], env={"TERM": "dumb", "NO_COLOR": "1"}) + assert result.exit_code == 0 + assert "--project-config" in result.output + assert "--project-runtime" in result.output + + +def test_review_request_carries_runtime_options(tmp_path: Path) -> None: + config, descriptor = tmp_path / "review.toml", tmp_path / "project-runtime.json" + request = _build_review_run_request([], {"project_config": config, "project_runtime": descriptor}) + assert request.project_config == config + assert request.project_runtime == descriptor + options = ReviewOptions(project_config=config, project_runtime=descriptor) + assert options.project_runtime == descriptor + + +def test_customer_actions_source_layout_does_not_select_publisher(monkeypatch) -> None: + + marker = object() + monkeypatch.setenv("GITHUB_ACTIONS", "true") + monkeypatch.setenv("GITHUB_REPOSITORY", "customer/project") + monkeypatch.setattr(runner, "_official_installed_payload", lambda: (marker, "")) + monkeypatch.setattr( + runner, "_protected_candidate_payload", lambda: (_ for _ in ()).throw(AssertionError("publisher selected")) + ) + assert runner._selected_module_payload().payload is marker diff --git a/tests/unit/specfact_code_review/run/test_runtime_compatibility.py b/tests/unit/specfact_code_review/run/test_runtime_compatibility.py new file mode 100644 index 00000000..ee02c57c --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_compatibility.py @@ -0,0 +1,203 @@ +"""Per-analyzer dependency conflicts never silently substitute project versions.""" + +import importlib.util +import json +import shutil +import tomllib +from importlib import import_module +from importlib.metadata import distribution +from pathlib import Path + +import pytest +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name +from packaging.version import Version + +from specfact_code_review.run import target_bootstrap, target_launch +from specfact_code_review.run.runtime_compatibility import analyzer_dependency_conflicts +from specfact_code_review.run.runtime_domains import member_dependency_graphs + + +def test_incompatible_dependency_identifies_only_affected_member(tmp_path: Path) -> None: + + metadata = tmp_path / "pylint-4.0.7.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text( + "Metadata-Version: 2.1\nName: pylint\nVersion: 4.0.7\nRequires-Dist: shared>=2\n" + ) + inventory = { + "installed": [{"metadata": {"name": "shared", "version": "1.0"}}], + "environment": {"python_version": "3.12"}, + } + conflicts = analyzer_dependency_conflicts(inventory, tmp_path) + assert "shared" in conflicts["pylint"] + assert ">=2" in conflicts["pylint"] + assert "basedpyright" not in conflicts + + +@pytest.mark.parametrize("version", ["2.1", "2.1rc1"]) +def test_compatible_common_project_package_is_not_rejected(tmp_path: Path, version: str) -> None: + + metadata = tmp_path / "pylint-4.0.7.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text( + "Metadata-Version: 2.1\nName: pylint\nVersion: 4.0.7\nRequires-Dist: shared>=2\n" + ) + inventory = { + "installed": [ + {"metadata": {"name": "shared", "version": version}}, + {"metadata": {"name": "requests", "version": "2.32.4"}}, + ] + } + assert not analyzer_dependency_conflicts(inventory, tmp_path) + + +def _declared_development_dependencies(root: Path) -> dict[str, Requirement]: + return { + str(canonicalize_name(requirement.name)): requirement + for value in tomllib.loads((root / "pyproject.toml").read_text())["tool"]["hatch"]["envs"]["default"][ + "dependencies" + ] + for requirement in (Requirement(value),) + } + + +def _newest_declared_version(requirement: Requirement, pinned: Version, observed: str) -> Version: + candidates = (pinned, Version(observed)) + return max(version for version in candidates if requirement.specifier.contains(version)) + + +@pytest.mark.parametrize("environment_index", [0, 1, 2]) +def test_declared_development_tools_admit_compatible_signed_entries(tmp_path: Path, environment_index: int) -> None: + """Declared dev ranges must exclude the real incompatible native resolution.""" + root = Path(__file__).resolve().parents[4] + contract = root / "packages/specfact-code-review/src/specfact_code_review/resources/contracts" + environment = json.loads((contract / "pr-range-v1-toolchain-lock.json").read_text())["environments"][ + environment_index + ] + declared = _declared_development_dependencies(root) + # Recorded native run 35068293754; raw inventory remains in the change evidence. + observed = {"pylint": "4.0.8", "basedpyright": "1.40.1", "nodejs-wheel-binaries": "24.19.0"} + inventory = {"installed": []} + incompatible_inventory = {"installed": []} + for component in environment["components"]: + name = component["normalized_name"] + if name not in observed: + continue + pinned = Version(component["version"]) + assert declared[name].specifier.contains(pinned), f"Declared {name} excludes signed entry {pinned}" + selected = _newest_declared_version(declared[name], pinned, observed[name]) + metadata = tmp_path / f"{name}-{pinned}.dist-info" + metadata.mkdir() + dependency = "Requires-Dist: nodejs-wheel-binaries>=20.13.1\n" if name == "basedpyright" else "" + (metadata / "METADATA").write_text(f"Metadata-Version: 2.1\nName: {name}\nVersion: {pinned}\n{dependency}") + inventory["installed"].append({"metadata": {"name": name, "version": str(selected)}}) + incompatible_inventory["installed"].append({"metadata": {"name": name, "version": observed[name]}}) + assert {row["metadata"]["name"] for row in inventory["installed"]} == set(observed) + assert not analyzer_dependency_conflicts(inventory, tmp_path) + conflicts = analyzer_dependency_conflicts(incompatible_inventory, tmp_path) + assert set(conflicts) == {"pylint", "basedpyright"} + assert "nodejs-wheel-binaries" in conflicts["basedpyright"] + + +def _node_distribution(root: Path, name: str, version: str, module: str, requirement: str = "") -> None: + """Write a bounded package inventory using the real BasedPyright dependency name.""" + metadata = root / f"{name.replace('-', '_')}-{version}.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text( + f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n" + + (f"Requires-Dist: {requirement}\n" if requirement else "") + ) + payload = root / module / "__init__.py" + payload.parent.mkdir() + payload.write_text("ORIGIN = 'sealed-node'\n") + (metadata / "RECORD").write_text(f"{module}/__init__.py,,\n") + + +@pytest.fixture(name="node_analyzers") +def sealed_node_analyzer_fixture(tmp_path: Path) -> Path: + """Model the signed entries and the wheel's actual Requires-Dist metadata.""" + sealed = tmp_path / "sealed" + sealed.mkdir() + _node_distribution(sealed, "basedpyright", "1.39.10", "basedpyright", "nodejs-wheel-binaries>=20.13.1") + _node_distribution(sealed, "nodejs-wheel-binaries", "24.16.0", "nodejs_wheel") + for name in ("pylint", "crosshair-tool", "pytest", "pytest-cov"): + _node_distribution(sealed, name, "1.0", name.replace("-", "_")) + return sealed + + +def test_basedpyright_node_version_conflict_is_explicit(node_analyzers: Path) -> None: + """A compatible upstream range cannot replace a sealed member entry.""" + inventory = {"installed": [{"metadata": {"name": "nodejs-wheel-binaries", "version": "24.19.0"}}]} + assert analyzer_dependency_conflicts(inventory, node_analyzers) == { + "basedpyright": "project_analyzer_dependency_incompatible:" + "nodejs-wheel-binaries: target=24.19.0, sealed entry=24.16.0" + } + + +def _mount_node_member(tmp_path: Path, sealed: Path, graph: dict, monkeypatch: pytest.MonkeyPatch) -> Path: + """Materialize only the real launcher's admitted paths, without invoking bwrap.""" + member = tmp_path / "member" + member.mkdir() + (tmp_path / "project-runtime.json").write_text( + json.dumps({"inventory": {"member_graphs": {"basedpyright": graph}}}) + ) + monkeypatch.setattr(target_launch, "PROJECT", tmp_path) + monkeypatch.setattr(target_launch, "SEALED", sealed) + monkeypatch.setattr(target_launch, "MEMBER", member) + mounts = target_launch.member_mounts("basedpyright") + for index, argument in enumerate(mounts): + if argument == "--ro-bind": + shutil.copytree(mounts[index + 1], mounts[index + 2]) + return member + + +@pytest.mark.parametrize("project_version", [None, "24.16.0"], ids=["absent", "matching"]) +def test_basedpyright_node_closure_keeps_verified_import( + tmp_path: Path, node_analyzers: Path, monkeypatch: pytest.MonkeyPatch, project_version: str | None +) -> None: + """Matching or absent target Node must remain available through the sealed finder.""" + installed = ( + [] if project_version is None else [{"metadata": {"name": "nodejs-wheel-binaries", "version": project_version}}] + ) + inventory = {"installed": installed} + assert not analyzer_dependency_conflicts(inventory, node_analyzers) + graph = member_dependency_graphs(inventory, node_analyzers)["basedpyright"] + member = _mount_node_member(tmp_path, node_analyzers, graph, monkeypatch) + monkeypatch.setattr(target_bootstrap, "ANALYZERS", member) + finder = target_bootstrap.DomainFinder(graph) + finder.names.update(target_bootstrap._TOOL_IMPORTS["basedpyright"]) + spec = finder.find_spec("nodejs_wheel") + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + assert module.ORIGIN == "sealed-node" + assert module.__file__ is not None + assert Path(module.__file__).is_relative_to(member) + assert (member / "nodejs_wheel_binaries-24.16.0.dist-info/METADATA").is_file() + assert next(row for row in graph["installed"] if row["name"] == "nodejs-wheel-binaries")["origin"] == "analyzer" + + +def test_unrelated_legacy_node_distribution_is_not_restricted(node_analyzers: Path) -> None: + """A project package with the obsolete name is outside this member closure.""" + inventory = {"installed": [{"metadata": {"name": "nodejs-wheel", "version": "99.0"}}]} + assert not analyzer_dependency_conflicts(inventory, node_analyzers) + graph = member_dependency_graphs(inventory, node_analyzers)["basedpyright"] + assert "nodejs-wheel" not in {row["name"] for row in graph["installed"]} + assert "nodejs_wheel" in graph["sealed_imports"] + + +def test_declared_core_dependency_supplies_registry_handoff() -> None: + """Hermetic Hatch inputs must include the real registry API used by review.""" + root = Path(__file__).resolve().parents[4] + declared = _declared_development_dependencies(root) + requirement = declared.get("specfact-cli") + assert requirement is not None, "Core installed by developer bootstrap is not a declared target dependency" + installed = distribution("specfact-cli") + assert str(requirement.specifier) == f"=={installed.version}", "Declare the verified immutable core release" + for module_name, entry in ( + ("module_discovery", "discover_all_modules_for_project_with_shadowed"), + ("module_installer", "verify_module_artifact"), + ): + module = import_module(f"specfact_cli.registry.{module_name}") + assert callable(getattr(module, entry)), f"Declared core does not supply registry API {entry}" diff --git a/tests/unit/specfact_code_review/run/test_runtime_discovery.py b/tests/unit/specfact_code_review/run/test_runtime_discovery.py new file mode 100644 index 00000000..ab3564ab --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_discovery.py @@ -0,0 +1,439 @@ +"""Portable runtime discovery scenarios: never execute project code.""" + +import re +from pathlib import Path + +import pytest +from packaging.specifiers import SpecifierSet + +from specfact_code_review.run import runtime_discovery +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +def test_hatchling_does_not_override_uv_lock(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[build-system]\nbuild-backend="hatchling.build"\n[project]\nname="consumer"\nrequires-python=">=3.11"\n' + ) + (tmp_path / "uv.lock").write_text("version = 1\n") + plan = discover_project(tmp_path) + assert plan.manager == "uv" + assert not (tmp_path / ".venv").exists() + + +def test_ambiguous_managers_need_explicit_selection(tmp_path: Path) -> None: + (tmp_path / "uv.lock").touch() + (tmp_path / "poetry.lock").touch() + with pytest.raises(ProjectRuntimeError, match=r"ambiguous.*poetry.*uv"): + discover_project(tmp_path) + (tmp_path / "review.toml").write_text('manager="uv"\ngroups=["test"]\n') + plan = discover_project(tmp_path, config_path=tmp_path / "review.toml") + assert plan.manager == "uv" + assert plan.groups == ("test",) + + +def test_detached_hatch_env_does_not_inherit_default(tmp_path: Path) -> None: + (tmp_path / "hatch.toml").write_text( + '[envs.default]\ndependencies=["unrelated"]\n' + '[envs.review]\ndetached=true\ndependencies=["pandas", "pytest-asyncio"]\n' + ) + (tmp_path / "review.toml").write_text('manager="hatch"\nenvironment="review"\n') + plan = discover_project(tmp_path, config_path=tmp_path / "review.toml") + assert plan.environment == "review" + assert plan.manager == "hatch" + assert "hatch.toml" in plan.inputs + assert plan.inputs["hatch.toml"].startswith("sha256:") + + +def test_requirements_include_constraint_affects_identity(tmp_path: Path) -> None: + (tmp_path / "requirements.txt").write_text("-r requirements-dev.txt\n-c constraints.txt\n") + (tmp_path / "requirements-dev.txt").write_text("requests\n") + constraints = tmp_path / "constraints.txt" + constraints.write_text("requests==2.32.3\n") + before = discover_project(tmp_path) + constraints.write_text("requests==2.32.4\n") + after = discover_project(tmp_path) + assert before.identity != after.identity + assert set(before.inputs) >= {"requirements.txt", "requirements-dev.txt", "constraints.txt"} + + +@pytest.mark.parametrize("include", ["-r ../secret.txt", "-c /etc/passwd", "-r requirements.txt"]) +def test_unsafe_or_cyclic_requirements_are_rejected(tmp_path: Path, include: str) -> None: + (tmp_path / "requirements.txt").write_text(include + "\n") + with pytest.raises(ProjectRuntimeError, match=r"(escape|cycle|absolute)"): + discover_project(tmp_path) + + +def test_symlinked_input_is_rejected(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").symlink_to("/etc/passwd") + with pytest.raises(ProjectRuntimeError, match="symlink"): + discover_project(tmp_path) + + +def test_input_change_invalidates_even_without_git(tmp_path: Path) -> None: + project = tmp_path / "pyproject.toml" + project.write_text('[project]\nname="consumer"\ndependencies=["requests<3"]\n') + first = discover_project(tmp_path) + project.write_text('[project]\nname="consumer"\ndependencies=["requests<4"]\n') + assert first.identity != discover_project(tmp_path).identity + + +def test_source_roots_and_pytest_config_are_recorded(tmp_path: Path) -> None: + (tmp_path / "src").mkdir() + (tmp_path / "pyproject.toml").write_text( + '[project]\nname="consumer"\n[tool.pytest.ini_options]\npythonpath=["src", "tools", "."]\n' + 'addopts="-ra -v --import-mode=importlib -p pytest_asyncio.plugin"\n' + ) + (tmp_path / "tools").mkdir() + plan = discover_project(tmp_path) + assert plan.source_roots == ("src", "tools", ".") + assert "-p pytest_asyncio.plugin" in plan.pytest_config["addopts"] + + +def test_unknown_config_fields_are_actionable(tmp_path: Path) -> None: + config = tmp_path / "review.toml" + config.write_text('manger="uv"\n') + with pytest.raises(ProjectRuntimeError, match="manger"): + discover_project(tmp_path, config_path=config) + + +def test_inspect_does_not_run_setup_py(tmp_path: Path) -> None: + (tmp_path / "setup.py").write_text('raise RuntimeError("must not execute")\n') + plan = discover_project(tmp_path) + assert plan.manager == "pip" + assert "setup.py" in plan.inputs + + +def test_pyproject_pytest_config_precedes_tox(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[tool.pytest.ini_options]\naddopts="--import-mode=importlib"\n') + (tmp_path / "tox.ini").write_text("[pytest]\naddopts=--collect-only\n") + assert discover_project(tmp_path).pytest_config["addopts"] == "--import-mode=importlib" + + +def test_changed_local_workspace_source_invalidates_runtime(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[project]\nname="consumer"\n') + workspace = tmp_path / "components" / "local" + workspace.mkdir(parents=True) + source = workspace / "library.py" + source.write_text("VALUE = 1\n") + before = discover_project(tmp_path) + source.write_text("VALUE = 2\n") + assert before.identity != discover_project(tmp_path).identity + + +def test_pytest_paths_cannot_escape_snapshot(tmp_path: Path) -> None: + (tmp_path / "pytest.ini").write_text("[pytest]\ntestpaths=../outside\n") + with pytest.raises(ProjectRuntimeError, match="escape"): + discover_project(tmp_path) + + +@pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) +@pytest.mark.parametrize("quote", ['"', "'"]) +@pytest.mark.parametrize("path_kind", ["parent", "absolute", "symlink"]) +def test_quoted_pytest_paths_are_checked_after_parsing(tmp_path: Path, option: str, quote: str, path_kind: str) -> None: + config = tmp_path / "review.toml" + config.write_text('source_roots=["."]\n') + if path_kind == "symlink": + relative = "external tree" + (tmp_path / relative).symlink_to(tmp_path.parent / "outside tree", target_is_directory=True) + else: + relative = "../outside tree" if path_kind == "parent" else str(tmp_path.parent / "outside tree") + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={quote}{relative}{quote}\n") + with pytest.raises(ProjectRuntimeError, match=r"project_input_(escape|symlink)"): + discover_project(tmp_path, config_path=config) + + +@pytest.mark.parametrize("option", ["testpaths", "pythonpath"]) +@pytest.mark.parametrize("config_format", ["ini", "toml-list"]) +def test_contained_pytest_paths_with_spaces_retain_native_values( + tmp_path: Path, option: str, config_format: str +) -> None: + relative = "source tree" + (tmp_path / relative).mkdir() + if config_format == "ini": + recorded = f'"{relative}"' + (tmp_path / "pytest.ini").write_text(f"[pytest]\n{option}={recorded}\n") + else: + recorded = [relative] + (tmp_path / "pytest.toml").write_text(f'[pytest]\n{option}=["{relative}"]\n') + plan = discover_project(tmp_path) + assert plan.pytest_config[option] == recorded + if option == "pythonpath": + assert plan.source_roots == (relative,) + + +def test_single_declared_test_group_is_prepared_automatically(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[dependency-groups]\ntest=["pytest"]\ndocs=["sphinx"]\n') + assert discover_project(tmp_path).groups == ("test",) + + +def test_competing_hatch_environments_require_selection(tmp_path: Path) -> None: + (tmp_path / "hatch.toml").write_text( + '[envs.default]\ndependencies=["pytest<8"]\n[envs.test]\ndependencies=["pytest>=9"]\n' + ) + with pytest.raises(ProjectRuntimeError, match=r"environment_ambiguous.*default.*test"): + discover_project(tmp_path) + + +def test_requirements_parent_include_inside_repository(tmp_path: Path) -> None: + (tmp_path / "requirements").mkdir() + (tmp_path / "requirements.txt").write_text("-r requirements/dev.txt\n") + (tmp_path / "requirements/dev.txt").write_text("-c ../constraints.txt\nrequests\n") + (tmp_path / "constraints.txt").write_text("requests<3\n") + assert "constraints.txt" in discover_project(tmp_path).inputs + + +def test_verified_active_hatch_context_resolves_competing_signals(tmp_path: Path, monkeypatch) -> None: + environment = tmp_path / ".venv" + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home = /usr/bin\n") + (tmp_path / "uv.lock").touch() + (tmp_path / "hatch.toml").write_text("[envs.default]\n[envs.review]\ndetached=true\n") + monkeypatch.setenv("VIRTUAL_ENV", str(environment)) + monkeypatch.setenv("HATCH_ENV_ACTIVE", "review") + monkeypatch.setattr(runtime_discovery.sys, "prefix", str(environment)) + plan = discover_project(tmp_path) + assert (plan.manager, plan.environment) == ("hatch", "review") + + +def test_unverified_active_context_cannot_override_repository(tmp_path: Path, monkeypatch) -> None: + (tmp_path / "uv.lock").touch() + monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / "missing-env")) + monkeypatch.setenv("HATCH_ENV_ACTIVE", "review") + assert discover_project(tmp_path).manager == "uv" + + +def test_unlocked_pip_tools_input_is_discovered(tmp_path: Path) -> None: + (tmp_path / "requirements.in").write_text("requests<3\n") + plan = discover_project(tmp_path) + assert plan.requirements == ("requirements.in",) + assert "requirements.in" in plan.inputs + (tmp_path / "requirements.txt").write_text("requests==2.32.5\n") + assert discover_project(tmp_path).requirements == ("requirements.txt",) + + +def test_legacy_static_python_constraint_is_imported(tmp_path: Path) -> None: + (tmp_path / "setup.cfg").write_text("[options]\npython_requires = >=3.11,<3.12\n") + assert discover_project(tmp_path).requires_python == ">=3.11,<3.12" + + +def test_poetry_python_constraint_intersects_package_metadata(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project]\nrequires-python=">=3.12"\n[tool.poetry.dependencies]\npython=">=3.11,<3.13"\n' + ) + constraint = SpecifierSet(discover_project(tmp_path).requires_python) + assert "3.12.1" in constraint + assert "3.11.1" not in constraint and "3.13.1" not in constraint + + +@pytest.mark.parametrize("declared", ["^3.12", "~3.12", ">=3.11,<3.12 || >=3.13", {"version": ">=3.12"}]) +def test_unsupported_poetry_python_syntax_has_exact_remedy(tmp_path: Path, declared) -> None: + text = '{version=">=3.12"}' if isinstance(declared, dict) else f'"{declared}"' + (tmp_path / "pyproject.toml").write_text(f"[tool.poetry.dependencies]\npython={text}\n") + with pytest.raises(ProjectRuntimeError, match="project_python_constraint_unsupported") as error: + discover_project(tmp_path) + assert "tool.poetry.dependencies.python" in str(error.value) + assert "PEP440" in str(error.value) + + +def test_explicit_pip_does_not_import_poetry_resolver_constraint(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project]\nrequires-python=">=3.11"\n[tool.poetry.dependencies]\npython="^3.12"\n' + ) + config = tmp_path / "review.toml" + config.write_text('manager="pip"\n') + assert discover_project(tmp_path, config_path=config).requires_python == ">=3.11" + + +@pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) +@pytest.mark.parametrize("fields", [("requirements",), ("constraints",), ("requirements", "constraints")]) +def test_non_pip_rejects_explicit_dependency_inputs_before_reading(tmp_path: Path, manager: str, fields) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\n' + "".join(f'{field}=["absent.txt"]\n' for field in fields)) + with pytest.raises(ProjectRuntimeError, match=f"project_manager_inputs_unsupported:{manager}") as error: + discover_project(tmp_path, config_path=config) + assert all(field in str(error.value) for field in fields) + assert "pip" in str(error.value) and "--project-config" in str(error.value) + + +@pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) +def test_native_manager_does_not_select_unrelated_pip_exports(tmp_path: Path, manager: str) -> None: + (tmp_path / "pyproject.toml").write_text( + f"[tool.{manager}]\n" if manager != "hatch" else "[tool.hatch.envs.test]\n" + ) + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\n') + (tmp_path / "requirements.txt").write_text("-r absent.txt\n") + plan = discover_project(tmp_path) + assert plan.manager == manager + assert not plan.requirements and not plan.constraints + assert "requirements.txt" not in plan.inputs + + +@pytest.mark.parametrize("manager", ["uv", "hatch", "poetry"]) +def test_non_pip_accepts_empty_dependency_overrides(tmp_path: Path, manager: str) -> None: + config = tmp_path / "review.toml" + config.write_text(f'manager="{manager}"\nrequirements=[]\nconstraints=[]\n') + plan = discover_project(tmp_path, config_path=config) + assert not plan.requirements and not plan.constraints + + +def test_one_test_extra_is_prepared_automatically(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[project.optional-dependencies]\ntest=["pytest"]\n') + assert discover_project(tmp_path).extras == ("test",) + + +def test_group_and_extra_require_explicit_selection(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project.optional-dependencies]\ntest=["pytest"]\n[dependency-groups]\ntest=["pytest<9"]\n' + ) + with pytest.raises(ProjectRuntimeError, match="test_dependencies_ambiguous"): + discover_project(tmp_path) + configuration = tmp_path / "runtime.toml" + configuration.write_text('extras=["test"]\n') + plan = discover_project(tmp_path, config_path=configuration) + assert plan.extras == ("test",) and not plan.groups + + +def test_executable_source_mode_is_part_of_runtime_identity(tmp_path: Path) -> None: + script = tmp_path / "manage" + script.write_text("#!/usr/bin/env python\nprint('ready')\n") + script.chmod(0o644) + before = discover_project(tmp_path).identity + script.chmod(0o755) + assert discover_project(tmp_path).identity != before + + +def test_pylock_uses_native_pip_input_without_requirement_line_parsing(tmp_path: Path) -> None: + (tmp_path / "pylock.toml").write_text( + 'lock-version="1.0"\ncreated-by="pip"\n[[packages]]\nname="requests"\nversion="2.32.5"\n' + ) + assert discover_project(tmp_path).requirements == ("pylock.toml",) + (tmp_path / "requirements.txt").write_text("requests<2\n") + with pytest.raises(ProjectRuntimeError, match="requirements_ambiguous"): + discover_project(tmp_path) + + +def test_remote_requirement_include_diagnostic_omits_credentials(tmp_path: Path) -> None: + (tmp_path / "requirements.txt").write_text("-r https://username:secret-token@example.invalid/requirements.txt\n") + with pytest.raises(ProjectRuntimeError) as error: + discover_project(tmp_path) + assert "remote_include" in str(error.value) + assert "secret-token" not in str(error.value) + + +def test_source_identity_distinguishes_executable_permission_classes(tmp_path: Path) -> None: + script = tmp_path / "manage" + script.write_text("#!/usr/bin/env python\n") + script.chmod(0o754) + before = discover_project(tmp_path).identity + script.chmod(0o745) + assert discover_project(tmp_path).identity != before + + +def test_pylock_and_uncompiled_requirements_need_explicit_selection(tmp_path: Path) -> None: + (tmp_path / "pylock.toml").write_text('lock-version="1.0"\n') + (tmp_path / "requirements.in").write_text("requests\n") + with pytest.raises(ProjectRuntimeError, match=r"ambiguous.*pylock.toml.*requirements.in"): + discover_project(tmp_path) + config = tmp_path / "selection.toml" + config.write_text('requirements=["requirements.in"]\n') + assert discover_project(tmp_path, config_path=config).requirements == ("requirements.in",) + + +@pytest.mark.parametrize("filename", ["setup.cfg", "pytest.ini", "tox.ini"]) +def test_malformed_ini_is_a_project_diagnostic(tmp_path: Path, filename: str) -> None: + (tmp_path / filename).write_text("not an INI section\n") + with pytest.raises(ProjectRuntimeError, match=r"project_config_invalid:" + filename): + discover_project(tmp_path) + + +@pytest.mark.parametrize( + "filename,section", + [ + ("pytest.toml", "pytest"), + (".pytest.toml", "pytest"), + ("pyproject.toml", "tool.pytest.ini_options"), + ("pyproject.toml", "tool.pytest"), + ], +) +@pytest.mark.parametrize("option", ["pythonpath", "testpaths"]) +@pytest.mark.parametrize("value", ["1", "true", "[1]", '["tests", 1]', "{path = 'tests'}"]) +def test_invalid_pytest_path_shape_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, option: str, value: str +) -> None: + (tmp_path / filename).write_text(f"[{section}]\n{option} = {value}\n") + with pytest.raises(ProjectRuntimeError, match=f"project_pytest_config_invalid:{filename}:{option}"): + discover_project(tmp_path) + + +@pytest.mark.parametrize( + "filename,section", + [("pytest.ini", "pytest"), (".pytest.ini", "pytest"), ("tox.ini", "pytest"), ("setup.cfg", "tool:pytest")], +) +def test_ini_pytest_path_strings_are_preserved(tmp_path: Path, filename: str, section: str) -> None: + (tmp_path / filename).write_text(f"[{section}]\npythonpath = src tests\ntestpaths = tests\n") + plan = discover_project(tmp_path) + assert plan.pytest_config["pythonpath"] == "src tests" + assert plan.pytest_config["testpaths"] == "tests" + assert plan.source_roots == ("src", "tests") + + +@pytest.mark.parametrize( + "filename,section,prefix", + [ + ("pytest.toml", "pytest", ""), + (".pytest.toml", "pytest", ""), + ("pyproject.toml", "tool.pytest", "[tool]\n"), + ("pyproject.toml", "tool.pytest.ini_options", "[tool.pytest]\n"), + ], +) +@pytest.mark.parametrize("value", ["1", "0", "true", "false", "[]", '["tests"]', '"invalid"']) +def test_malformed_pytest_table_has_project_diagnostic( + tmp_path: Path, filename: str, section: str, prefix: str, value: str +) -> None: + (tmp_path / filename).write_text(prefix + section.rsplit(".", 1)[-1] + "=" + value + "\n") + with pytest.raises( + ProjectRuntimeError, match=re.escape(f"project_pytest_config_invalid:{filename}:{section}; expected a table") + ): + discover_project(tmp_path) + + +@pytest.mark.parametrize( + "filename,section", + [ + ("pyproject.toml", "tool"), + ("pyproject.toml", "project"), + ("pyproject.toml", "dependency-groups"), + ("pyproject.toml", "project.optional-dependencies"), + ("pyproject.toml", "tool.uv"), + ("pyproject.toml", "tool.hatch"), + ("pyproject.toml", "tool.hatch.envs"), + ("pyproject.toml", "tool.poetry"), + ("pyproject.toml", "tool.poetry.dependencies"), + ("pyproject.toml", "tool.poetry.group"), + ("pyproject.toml", "tool.poetry.extras"), + ("hatch.toml", "envs"), + ], +) +@pytest.mark.parametrize("value", ["1", "false", "[]", '"invalid"']) +@pytest.mark.parametrize("explicit", [False, True]) +def test_consumed_metadata_tables_have_actionable_shape_diagnostics( + tmp_path: Path, filename: str, section: str, value: str, explicit: bool +) -> None: + parent, _, option = section.rpartition(".") + declaration = (f"[{parent}]\n" if parent else "") + f"{option}={value}\n" + (tmp_path / filename).write_text(declaration) + config = tmp_path / "review-runtime.toml" + config.write_text('manager="pip"\n') + with pytest.raises( + ProjectRuntimeError, match=re.escape(f"project_config_invalid:{filename}:{section}; expected a table") + ): + discover_project(tmp_path, config_path=config if explicit else None) + + +@pytest.mark.parametrize("layout", ["flat", "src"]) +def test_undeclared_pythonpath_does_not_invent_import_roots(tmp_path: Path, layout: str) -> None: + if layout == "src": + (tmp_path / "src").mkdir() + (tmp_path / "requirements.txt").touch() + assert not discover_project(tmp_path).source_roots diff --git a/tests/unit/specfact_code_review/run/test_runtime_domains.py b/tests/unit/specfact_code_review/run/test_runtime_domains.py new file mode 100644 index 00000000..0e24381c --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_domains.py @@ -0,0 +1,121 @@ +"""Worker fallbacks include only the selected member's dependency closure.""" + +from importlib.metadata import Distribution +from pathlib import Path + +import pytest + +from specfact_code_review.run.runtime_domains import _top_level_imports, member_dependency_graphs +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +def _distribution( + root: Path, name: str, *, dependencies: tuple[str, ...] = (), files: tuple[str, ...] | None = None +) -> Path: + metadata = root / f"{name}-1.0.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text( + f"Metadata-Version: 2.1\nName: {name}\nVersion: 1.0\n" + + "".join(f"Requires-Dist: {dependency}\n" for dependency in dependencies) + ) + (metadata / "top_level.txt").write_text(name + "\n") + files = files if files is not None else (name.replace("-", "_") + ".py",) + for relative in files: + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("") + (metadata / "RECORD").write_text("".join(f"{relative},,\n" for relative in files)) + return metadata + + +@pytest.fixture(autouse=True) +def other_member_entries(tmp_path: Path) -> None: + for name in ("basedpyright", "crosshair-tool", "pytest", "pytest-cov"): + _distribution(tmp_path, name) + + +def test_member_graph_excludes_unrelated_analyzer_packages(tmp_path: Path) -> None: + _distribution(tmp_path, "pylint", dependencies=("shared>=1",)) + _distribution(tmp_path, "shared") + _distribution(tmp_path, "unrelated") + graph = member_dependency_graphs({"installed": []}, tmp_path)["pylint"] + assert graph["sealed_imports"] == ["pylint", "shared"] + assert {row["name"] for row in graph["installed"]} == {"pylint", "shared"} + + +def test_project_distribution_replaces_compatible_tool_dependency(tmp_path: Path) -> None: + _distribution(tmp_path, "pylint", dependencies=("shared>=1",)) + _distribution(tmp_path, "shared", dependencies=("unrelated>=1",)) + _distribution(tmp_path, "unrelated") + target = {"installed": [{"metadata": {"name": "shared", "version": "2.0", "requires_dist": []}}]} + graph = member_dependency_graphs(target, tmp_path)["pylint"] + assert graph["sealed_imports"] == ["pylint"] + assert next(row for row in graph["installed"] if row["name"] == "shared")["origin"] == "project" + assert "unrelated" not in {row["name"] for row in graph["installed"]} + + +def test_missing_member_dependency_cannot_be_silently_omitted(tmp_path: Path) -> None: + _distribution(tmp_path, "pylint", dependencies=("absent>=1",)) + with pytest.raises(ProjectRuntimeError, match="project_analyzer_dependency_missing:pylint:absent"): + member_dependency_graphs({"installed": []}, tmp_path) + + +@pytest.mark.parametrize("version,conflict", [("7.0", True), ("8.4", False)]) +def test_pytest_worker_validates_selected_dependency_versions(tmp_path: Path, version: str, conflict: bool) -> None: + _distribution(tmp_path, "pylint") + metadata = tmp_path / "pytest-cov-1.0.dist-info/METADATA" + metadata.write_text(metadata.read_text() + "Requires-Dist: pytest>=8.2\n") + inventory = {"installed": [{"metadata": {"name": "pytest", "version": version}}]} + if conflict: + with pytest.raises(ProjectRuntimeError, match="project_analyzer_dependency_incompatible:pytest-observe:pytest"): + member_dependency_graphs(inventory, tmp_path) + else: + graph = member_dependency_graphs(inventory, tmp_path)["pytest-observe"] + assert next(row for row in graph["installed"] if row["name"] == "pytest")["version"] == version + + +@pytest.mark.parametrize("declaration", [None, "owned\nstale\n", "stale\n"]) +@pytest.mark.parametrize( + "payload", + [ + "owned.py", + "owned/__init__.py", + "owned/component.py", + "owned.cpython-311-x86_64-linux-gnu.so", + "owned.cp312-win_amd64.pyd", + "owned.abi3.so", + "owned.so", + ], +) +def test_import_ownership_comes_from_recorded_runtime_payload( + tmp_path: Path, declaration: str | None, payload: str +) -> None: + metadata = _distribution(tmp_path, "fixture", files=(payload, "docs/readme.md", "stubs.pyi", "invalid.name.py")) + if declaration is None: + (metadata / "top_level.txt").unlink() + else: + (metadata / "top_level.txt").write_text(declaration) + assert _top_level_imports(Distribution.at(metadata)) == {"owned"} + + +def test_stale_top_level_cannot_claim_another_distributions_build_package(tmp_path: Path) -> None: + metadata = _distribution(tmp_path, "pylint") + (metadata / "top_level.txt").write_text("pylint\nbuild\n") + _distribution(tmp_path, "build", files=("build/__init__.py",)) + graph = member_dependency_graphs({"installed": []}, tmp_path)["pylint"] + assert graph["sealed_imports"] == ["pylint"] + assert {row["name"] for row in graph["installed"]} == {"pylint"} + + +def test_missing_recorded_module_is_not_admitted(tmp_path: Path) -> None: + metadata = _distribution(tmp_path, "fixture", files=("owned.py", "missing.py")) + (tmp_path / "missing.py").unlink() + (metadata / "top_level.txt").write_text("owned\nmissing\n") + assert _top_level_imports(Distribution.at(metadata)) == {"owned"} + + +def test_missing_file_inventory_cannot_guess_analyzer_ownership(tmp_path: Path) -> None: + metadata = _distribution(tmp_path, "fixture") + (metadata / "RECORD").unlink() + with pytest.raises(ProjectRuntimeError, match="project_analyzer_import_inventory_missing:fixture"): + _top_level_imports(Distribution.at(metadata)) diff --git a/tests/unit/specfact_code_review/run/test_runtime_git.py b/tests/unit/specfact_code_review/run/test_runtime_git.py new file mode 100644 index 00000000..f23bb13d --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_git.py @@ -0,0 +1,75 @@ +"""Git transport helpers and their identity travel with the disposable builder.""" + +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import runtime_git +from specfact_code_review.run.runtime_models import ProjectRuntimeError + + +def test_missing_git_https_helper_only_fails_when_used(tmp_path: Path, monkeypatch) -> None: + git = tmp_path / "git" + git.write_bytes(b"\x7fELFgit") + (tmp_path / "git-remote-http").write_bytes(b"\x7fELFhttp") + monkeypatch.setattr(runtime_git, "GIT", git) + run = subprocess.run + monkeypatch.setattr(runtime_git.subprocess, "run", lambda *_args, **_kwargs: SimpleNamespace(stdout=str(tmp_path))) + assert set(runtime_git.git_identity()) == {"bin/git", "git-core/git-remote-http"} + monkeypatch.setattr(runtime_git, "elf_dependencies", lambda _path: ()) + monkeypatch.setattr(runtime_git, "inventory_native", lambda *_args, **_kwargs: None) + runtime_git.stage_git(tmp_path) + helper = tmp_path / "builder-tools/git-core/git-remote-https" + completed = run([sys.executable, str(helper)], capture_output=True, text=True, check=False) + assert completed.returncode == 127 + assert "project_builder_tool_missing:git-remote-https" in completed.stderr + assert "install Git HTTP transport helpers" in completed.stderr + assert not helper.with_name(helper.name + ".real").exists() + + +def test_missing_git_is_optional_for_identity_and_staging(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(runtime_git, "GIT", tmp_path / "unavailable-git") + assert runtime_git.git_identity() == {} + runtime_git.stage_git(tmp_path) + assert not (tmp_path / "builder-tools").exists() + + +def test_transport_change_invalidates_builder_identity_and_is_staged(tmp_path: Path, monkeypatch) -> None: + program = tmp_path / "git" + helper = tmp_path / "http" + program.write_bytes(b"\x7fELFgit") + helper.write_bytes(b"\x7fELFhttp") + monkeypatch.setattr(runtime_git, "GIT", program) + monkeypatch.setattr(runtime_git, "_programs", lambda: {"bin/git": program, "git-core/git-remote-https": helper}) + before = runtime_git.git_identity() + helper.write_bytes(b"\x7fELFchanged") + assert runtime_git.git_identity() != before + monkeypatch.setattr(runtime_git, "elf_dependencies", lambda path: ("libcurl.so.4",) if path == helper else ()) + observed = [] + monkeypatch.setattr(runtime_git, "inventory_native", lambda *_args, **kwargs: observed.append(kwargs["declared"])) + runtime_git.stage_git(tmp_path) + copied = tmp_path / "builder-tools/git-core/git-remote-https.real" + assert copied.read_bytes() == helper.read_bytes() + launcher = copied.with_suffix("").read_text() + assert "GIT_EXEC_PATH" in launcher and "--library-path" in launcher + assert "libcurl.so.4" in observed[0] + + +@pytest.mark.parametrize( + "failure", [subprocess.CalledProcessError(1, "git"), subprocess.TimeoutExpired("git", 10), OSError("unavailable")] +) +def test_git_identity_failures_use_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + git = tmp_path / "git" + git.write_bytes(b"\x7fELFgit") + monkeypatch.setattr(runtime_git, "GIT", git) + + def fail(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_git.subprocess, "run", fail) + with pytest.raises(ProjectRuntimeError, match="project_builder_git_identity_failed") as caught: + runtime_git.git_identity() + assert caught.value.__cause__ is failure diff --git a/tests/unit/specfact_code_review/run/test_runtime_interpreter.py b/tests/unit/specfact_code_review/run/test_runtime_interpreter.py new file mode 100644 index 00000000..be65567c --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_interpreter.py @@ -0,0 +1,52 @@ +"""Project Python selection is independent of the controller ABI.""" + +from pathlib import Path + +import pytest + +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_interpreter import select_environment + + +def test_python_version_file_is_selected_and_explicit_config_takes_precedence(tmp_path: Path) -> None: + (tmp_path / ".python-version").write_text("3.11\n") + assert discover_project(tmp_path).python == "3.11" + config = tmp_path / "review.toml" + config.write_text('python="3.13"\n') + assert discover_project(tmp_path, config_path=config).python == "3.13" + + +def test_python_selection_uses_matching_signed_worker(tmp_path: Path) -> None: + + (tmp_path / ".python-version").write_text("3.11\n") + assert select_environment(discover_project(tmp_path), current="linux-x86_64-cp312") == "linux-x86_64-cp311" + + +def test_python_selection_rejects_conflicting_pin(tmp_path: Path) -> None: + + (tmp_path / ".python-version").write_text("3.11\n") + (tmp_path / "pyproject.toml").write_text('[project]\nrequires-python=">=3.12"\n') + with pytest.raises(ValueError, match="project_python_incompatible"): + select_environment(discover_project(tmp_path), current="linux-x86_64-cp312") + + +@pytest.mark.parametrize( + ("declaration", "current", "expected"), + [ + ('[project]\nrequires-python=">=3.13,<3.14"\n', "cp312", "cp313"), + ('[tool.poetry.dependencies]\npython=">=3.12,<3.13"\n', "cp313", "cp312"), + ], +) +def test_python_constraints_can_select_compatible_worker_without_a_pin( + tmp_path: Path, declaration: str, current: str, expected: str +) -> None: + (tmp_path / "pyproject.toml").write_text(declaration) + assert ( + select_environment(discover_project(tmp_path), current=f"linux-x86_64-{current}") == f"linux-x86_64-{expected}" + ) + + +def test_poetry_constraint_preserves_ambiguous_worker_selection(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[tool.poetry.dependencies]\npython=">=3.11,<3.13"\n') + with pytest.raises(ValueError, match="project_python_ambiguous"): + select_environment(discover_project(tmp_path), current="linux-x86_64-cp313") diff --git a/tests/unit/specfact_code_review/run/test_runtime_models.py b/tests/unit/specfact_code_review/run/test_runtime_models.py new file mode 100644 index 00000000..45ae4d0c --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_models.py @@ -0,0 +1,12 @@ +"""Runtime plans can relocate while retaining their dependency identity.""" + +from pathlib import Path + +from specfact_code_review.run.runtime_models import ProjectPlan + + +def test_plan_identity_excludes_checkout_location(tmp_path: Path) -> None: + first = ProjectPlan(root=tmp_path / "first", manager="pip", inputs={"requirements.txt": "sha256:" + "a" * 64}) + second = ProjectPlan(root=tmp_path / "second", manager="pip", inputs=dict(first.inputs)) + assert first.identity == second.identity + assert "root" not in first.document() diff --git a/tests/unit/specfact_code_review/run/test_runtime_native.py b/tests/unit/specfact_code_review/run/test_runtime_native.py new file mode 100644 index 00000000..00ac3ffe --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_native.py @@ -0,0 +1,177 @@ +"""Native dependency inventories fail precisely without invoking untrusted binaries.""" + +import struct +from pathlib import Path + +import pytest + +from specfact_code_review.run.runtime_models import ProjectRuntimeError +from specfact_code_review.run.runtime_native import elf_dependencies, inventory_native + + +def test_declared_missing_native_library_names_exact_requirement(tmp_path: Path) -> None: + + root = tmp_path / "runtime" + root.mkdir() + with pytest.raises(ValueError, match=r"project_native_library_missing:libspecfact_missing_473\.so\.1"): + inventory_native( + root, capsule_root=tmp_path / "capsule", declared=("libspecfact_missing_473.so.1",), system_roots=() + ) + + +def test_native_inventory_does_not_copy_unrequested_host_files(tmp_path: Path) -> None: + + root, system = tmp_path / "artifact", tmp_path / "system" + root.mkdir() + system.mkdir() + (system / "unrelated-secret").write_text("must not copy") + assert inventory_native(root, capsule_root=tmp_path / "capsule", declared=(), system_roots=(system,)) == [] + assert not list(root.rglob("unrelated-secret")) + + +def _elf(path: Path, needed: tuple[str, ...]) -> None: + + strings = b"\0" + b"".join(name.encode() + b"\0" for name in needed) + offsets = [] + cursor = 1 + for name in needed: + offsets.append(cursor) + cursor += len(name) + 1 + dynamic = b"".join(struct.pack(" None: + + artifact, capsule, system = (tmp_path / name for name in ("artifact", "capsule", "system")) + for root in (artifact, capsule, system): + root.mkdir() + _elf(system / "libodbc.so.2", ("libc.so.6",)) + _elf(system / "libc.so.6", ("ld-linux-x86-64.so.2",)) + _elf(system / "ld-linux-x86-64.so.2", ()) + _elf(capsule / "libc.so.6", ()) + original = (capsule / "libc.so.6").read_bytes() + records = inventory_native( + artifact, capsule_root=capsule, declared=("libodbc.so.2",), system_roots=(system,), target_loader=True + ) + assert {Path(row["path"]).name for row in records} == {"libodbc.so.2", "libc.so.6", "ld-linux-x86-64.so.2"} + assert (artifact / "native/ld-linux-x86-64.so.2").stat().st_mode & 0o111 + assert (capsule / "libc.so.6").read_bytes() == original + + +def test_owned_native_executables_include_their_shared_library_closure(tmp_path: Path) -> None: + + artifact, system = tmp_path / "artifact", tmp_path / "system" + (artifact / "executables").mkdir(parents=True) + system.mkdir() + _elf(artifact / "executables/customer-tool", ("libcustomer.so.1",)) + _elf(system / "libcustomer.so.1", ()) + records = inventory_native(artifact, capsule_root=tmp_path / "capsule", declared=(), system_roots=(system,)) + assert (artifact / "native/libcustomer.so.1").is_file() + assert any(row["path"] == "executables/customer-tool" for row in records) + + +def test_wrong_machine_elf_is_rejected_before_inventory(tmp_path: Path) -> None: + payload = bytearray(64) + payload[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" bytearray: + """Build an ELF64 shared object with both section and program metadata.""" + strings = b"\0libcustomer.so.1\0" + payload = bytearray(704) + payload[:7] = b"\x7fELF\x02\x01\x01" + struct.pack_into(" bytearray: + struct.pack_into(" None: + artifact, system = tmp_path / "artifact", tmp_path / "system" + artifact.mkdir() + system.mkdir() + binary = artifact / "extension.so" + payload = _segmented_elf() + binary.write_bytes(payload) + assert elf_dependencies(binary) == ("libcustomer.so.1",) + binary.write_bytes(_remove_section_table(payload, entry_size)) + assert elf_dependencies(binary) == ("libcustomer.so.1",) + _elf(system / "libcustomer.so.1", ()) + records = inventory_native(artifact, capsule_root=tmp_path / "capsule", declared=(), system_roots=(system,)) + assert (artifact / "native/libcustomer.so.1").read_bytes() == (system / "libcustomer.so.1").read_bytes() + assert next(row for row in records if row["path"] == "extension.so")["needed"] == ("libcustomer.so.1",) + + +@pytest.mark.parametrize( + ("offset", "encoding", "value"), + [ + (32, "Q", 500), # Program-header table extends beyond file. + (54, "H", 55), # Wrong program-header entry size. + (152, "Q", 1024), # Dynamic segment exceeds file bounds. + (160, "Q", 32), # Dynamic segment file size exceeds memory size. + (96, "Q", 176), # String table is only in the load segment's memory tail. + (80, "Q", 2**64 - 1), # Virtual address range overflows ELF64 addresses. + (280, "Q", 0x400200), # String table starts beyond file-backed load bytes. + (296, "Q", 1024), # String table extends beyond load segment. + (264, "Q", 100), # Dependency offset exceeds string table. + (304, "Q", 1), # No DT_NULL termination. + (272, "Q", 10), # Missing DT_STRTAB, duplicate DT_STRSZ. + (193, "B", 65), # Dependency lacks a bounded NUL terminator. + (177, "B", 255), # Dependency is not ASCII. + ], +) +def test_sectionless_elf_rejects_invalid_dynamic_entries( + tmp_path: Path, offset: int, encoding: str, value: int +) -> None: + payload = _remove_section_table(_segmented_elf()) + struct.pack_into("<" + encoding, payload, offset, value) + binary = tmp_path / "invalid.so" + binary.write_bytes(payload) + with pytest.raises(ProjectRuntimeError, match="project_native_elf_invalid"): + elf_dependencies(binary) + + +def test_sectionless_static_elf_has_no_dependencies(tmp_path: Path) -> None: + payload = _remove_section_table(_segmented_elf()) + struct.pack_into(" None: + payload = _remove_section_table(_segmented_elf()) + struct.pack_into(" None: + (tmp_path / "app.py").write_text("VALUE = 1\n") + before = source_identity(tmp_path) + (tmp_path / ".venv").mkdir() + (tmp_path / ".venv/private.txt").write_text("runtime only") + assert source_identity(tmp_path) == before + (tmp_path / "app.py").write_text("VALUE = 2\n") + assert source_identity(tmp_path) != before + + +def test_directory_presence_and_modes_bind_source_identity(tmp_path: Path) -> None: + before = source_identity(tmp_path) + empty = tmp_path / "optional-source" + empty.mkdir() + added = source_identity(tmp_path) + assert added != before + empty.chmod(0o700) + private = source_identity(tmp_path) + empty.chmod(0o755) + assert source_identity(tmp_path) != private + empty.rmdir() + assert source_identity(tmp_path) == before + tmp_path.chmod(0o755) + public_root = source_identity(tmp_path) + tmp_path.chmod(0o700) + assert source_identity(tmp_path) != public_root diff --git a/tests/unit/specfact_code_review/run/test_runtime_tools.py b/tests/unit/specfact_code_review/run/test_runtime_tools.py new file mode 100644 index 00000000..485ac607 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_tools.py @@ -0,0 +1,219 @@ +"""Declared native capabilities remain controller-owned and identity-bound.""" + +import json +import struct +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import runtime_builder, runtime_tools +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError + + +def test_repository_declares_native_tools_without_executing_commands(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text( + '[project]\nname="sample"\nversion="1"\n[tool.specfact.code-review]\nnative_tools=["git", "uname", "sed"]\n' + ) + plan = discover_project(tmp_path) + assert plan.native_tools == ("git", "sed", "uname") + assert plan.document()["native_tools"] == ("git", "sed", "uname") + + +def test_explicit_native_tools_override_repository_declaration(tmp_path: Path) -> None: + (tmp_path / "pyproject.toml").write_text('[tool.specfact.code-review]\nnative_tools=["git"]\n') + config = tmp_path / "review.toml" + config.write_text('native_tools=["uname"]\n') + assert discover_project(tmp_path, config_path=config).native_tools == ("uname",) + + +@pytest.mark.parametrize("declaration", ['["curl"]', '["/usr/bin/git"]', '"git"']) +def test_unknown_native_capabilities_are_not_path_guesses(tmp_path: Path, declaration: str) -> None: + (tmp_path / "pyproject.toml").write_text(f"[tool.specfact.code-review]\nnative_tools={declaration}\n") + with pytest.raises(ProjectRuntimeError, match=r"native_tools|native_tool_unsupported"): + discover_project(tmp_path) + + +def _native_fixture(tmp_path: Path, monkeypatch): + """Use ELF fixtures without executing native code on the host.""" + system = tmp_path / "system" + system.mkdir() + header = bytearray(64) + header[:6] = b"\x7fELF\x02\x01" + struct.pack_into(" None: + tools, system = _native_fixture(tmp_path, monkeypatch) + capture = tools.capture_native_tools(("uname",)) + assert set(capture.files) == { + "bin/uname", + "native-tools/executables/uname", + "native-tools/native/ld-linux-x86-64.so.2", + } + assert all(row["sha256"].startswith("sha256:") for row in capture.inventory.values()) + original = capture.identity + (system / "ld-linux-x86-64.so.2").write_bytes((system / "ld-linux-x86-64.so.2").read_bytes() + b"changed") + assert tools.capture_native_tools(("uname",)).identity != original + artifact = tmp_path / "artifact" + artifact.mkdir() + tools.install_native_tools(capture, artifact) + assert (artifact / "native-tools/native/ld-linux-x86-64.so.2").read_bytes() == capture.files[ + "native-tools/native/ld-linux-x86-64.so.2" + ] + assert b"/usr/bin" not in (artifact / "bin/uname").read_bytes() + + +@pytest.mark.parametrize("collision", ["bin/uname", "bin", "native-tools/forged"]) +def test_builder_tool_collisions_fail_before_any_install(tmp_path: Path, monkeypatch, collision: str) -> None: + tools, _ = _native_fixture(tmp_path, monkeypatch) + capture = tools.capture_native_tools(("uname",)) + artifact = tmp_path / "artifact" + target = artifact / collision + target.parent.mkdir(parents=True) + target.write_text("builder controlled") + with pytest.raises(ProjectRuntimeError, match="native_tool_collision"): + tools.install_native_tools(capture, artifact) + assert target.read_text() == "builder controlled" + assert not (artifact / "native-tools/executables/uname").exists() + + +def test_missing_git_helper_is_actionable_not_an_ambient_fallback(tmp_path: Path, monkeypatch) -> None: + tools, system = _native_fixture(tmp_path, monkeypatch) + (system / "git-remote-http").unlink() + with pytest.raises(ProjectRuntimeError, match="native_tool_missing:git-remote-http"): + tools.capture_native_tools(("git",)) + + +def test_empty_capture_reads_no_native_programs(tmp_path: Path, monkeypatch) -> None: + tools, _ = _native_fixture(tmp_path, monkeypatch) + monkeypatch.setattr(tools, "PROGRAM_ROOT", tmp_path / "absent") + capture = tools.capture_native_tools(()) + assert not capture.files + assert not capture.inventory + + +def _elf_needing(path: Path, needed: str) -> None: + """Give one synthetic ELF a real DT_NEEDED string-table entry.""" + strings = b"\0" + needed.encode() + b"\0" + dynamic = struct.pack(" Path: + capsule = tmp_path / "capsule" + (capsule / "bin").mkdir(parents=True) + (capsule / "bin/sh").write_bytes(b"sealed shell fixture") + (capsule / "bin/sh").chmod(0o755) + return capsule + + +def test_git_helper_transitive_libraries_are_captured(tmp_path: Path, monkeypatch) -> None: + tools, system = _native_fixture(tmp_path, monkeypatch) + (system / "libhelper.so.1").write_bytes((system / "git").read_bytes()) + (system / "libtransitive.so.1").write_bytes((system / "git").read_bytes()) + _elf_needing(system / "git-remote-http", "libhelper.so.1") + _elf_needing(system / "libhelper.so.1", "libtransitive.so.1") + capture = tools.capture_native_tools(("git",), capsule_root=_sealed_shell(tmp_path)) + assert "native-tools/native/libhelper.so.1" in capture.files + assert "native-tools/native/libtransitive.so.1" in capture.files + assert "@capsule/bin/sh" in capture.inventory + original = capture.identity + (system / "libtransitive.so.1").write_bytes((system / "git").read_bytes() + b"changed") + assert tools.capture_native_tools(("git",), capsule_root=tmp_path / "capsule").identity != original + (system / "libtransitive.so.1").unlink() + with pytest.raises(ProjectRuntimeError, match=r"native_library_missing:libtransitive\.so\.1"): + tools.capture_native_tools(("git",), capsule_root=tmp_path / "capsule") + + +def test_native_capture_identity_participates_in_offline_cache_lookup(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + _, system = _native_fixture(tmp_path, monkeypatch) + plan = ProjectPlan(tmp_path, manager="pip", native_tools=("uname",)) + runtime = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + monkeypatch.setattr(runtime_builder, "git_identity", dict) + identities = [] + original_digest = runtime_builder.document_digest + + def record(values): + identities.append(values) + return original_digest(values) + + monkeypatch.setattr(runtime_builder, "document_digest", record) + for _ in range(2): + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache", offline=True) + (system / "ld-linux-x86-64.so.2").write_bytes((system / "uname").read_bytes() + b"changed") + assert identities[0]["native_tools"] != identities[1]["native_tools"] + assert "runtime_tools.py" in identities[0]["builder"] + + +def test_builder_cannot_replace_captured_tool_bytes(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"synthetic public trust") + _, system = _native_fixture(tmp_path, monkeypatch) + plan = ProjectPlan(tmp_path, manager="pip", native_tools=("uname",)) + runtime = SimpleNamespace( + root=tmp_path / "capsule", environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64 + ) + original = (system / "uname").read_bytes() + monkeypatch.setattr(runtime_builder, "git_identity", dict) + monkeypatch.setattr(runtime_builder, "verify_inputs", lambda _plan: None) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def build(_plan, _runtime, staging, **_kwargs): + artifact = staging / "artifact" + artifact.mkdir() + (artifact / "inventory.json").write_text(json.dumps({})) + (system / "uname").write_bytes(original + b"changed after capture") + return artifact + + def project_libraries(artifact, **_kwargs): + assert not (artifact / "native-tools").exists() + return [] + + monkeypatch.setattr(runtime_builder, "_build", build) + monkeypatch.setattr(runtime_builder, "inventory_native", project_libraries) + prepared = runtime_builder.prepare_runtime(plan, runtime=runtime, cache_root=tmp_path / "cache") + assert (prepared.root / "native-tools/executables/uname").read_bytes() == original + assert prepared.descriptor["inventory"]["native_tools"]["native-tools/executables/uname"][ + "sha256" + ] == runtime_builder.content_digest(original) + + +@pytest.mark.parametrize("tool", ["uname", "sed"]) +def test_non_elf_tools_fail_with_exact_capability(tmp_path: Path, monkeypatch, tool: str) -> None: + tools, system = _native_fixture(tmp_path, monkeypatch) + (system / tool).write_text("not an ELF binary") + with pytest.raises(ProjectRuntimeError, match=f"native_tool_not_elf:{tool}"): + tools.capture_native_tools((tool,)) + + +def test_git_requires_sealed_shell_not_host_shell(tmp_path: Path, monkeypatch) -> None: + tools, _ = _native_fixture(tmp_path, monkeypatch) + capsule = tmp_path / "capsule" + (capsule / "bin").mkdir(parents=True) + (capsule / "bin/sh").symlink_to("/bin/sh") + with pytest.raises(ProjectRuntimeError, match="native_tool_shell_missing"): + tools.capture_native_tools(("git",), capsule_root=capsule) + + +def test_declared_git_captures_local_transport_services(tmp_path: Path, monkeypatch) -> None: + tools, _ = _native_fixture(tmp_path, monkeypatch) + capture = tools.capture_native_tools(("git",), capsule_root=_sealed_shell(tmp_path)) + for name in ("git-upload-pack", "git-receive-pack", "git-upload-archive"): + assert f"native-tools/git-core/{name}" in capture.files + assert f"native-tools/executables/git-core/{name}" in capture.files diff --git a/tests/unit/specfact_code_review/run/test_runtime_trust.py b/tests/unit/specfact_code_review/run/test_runtime_trust.py new file mode 100644 index 00000000..464c54ae --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_trust.py @@ -0,0 +1,124 @@ +"""Offline workers receive only identity-bound public trust from the signed worker.""" + +import ssl +from pathlib import Path +from types import SimpleNamespace + +import certifi +import pytest + +from specfact_code_review.run import runtime_builder +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError + + +SOURCE = "opt/specfact/analyzers/certifi/cacert.pem" +DESTINATION = "worker-config/ssl/certs/ca-certificates.crt" + + +@pytest.fixture(name="trust_case") +def trust_case_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> SimpleNamespace: + """Keep the real capture/seal/cache path; replace only dependency acquisition.""" + capsule = tmp_path / "capsule" + source = capsule / SOURCE + source.parent.mkdir(parents=True) + source.write_bytes(Path(certifi.where()).read_bytes()) + case = SimpleNamespace( + source=source, + payload=source.read_bytes(), + plan=ProjectPlan(tmp_path, manager="pip"), + runtime=SimpleNamespace(root=capsule, environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64), + cache=tmp_path / "cache", + mutation=lambda _artifact: None, + ) + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "isolated-test-git") + monkeypatch.setattr(runtime_builder, "verify_inputs", lambda _plan: None) + monkeypatch.setattr(runtime_builder, "inventory_native", lambda *_args, **_kwargs: []) + monkeypatch.setattr(runtime_builder, "analyzer_dependency_conflicts", lambda *_args: {}) + monkeypatch.setattr(runtime_builder, "member_dependency_graphs", lambda *_args: {}) + + def build(_plan, _runtime, staging, **_kwargs): + artifact = staging / "artifact" + (artifact / "worker-config").mkdir(parents=True) + (artifact / "worker-config/hosts").write_text("127.0.0.1 localhost\n") + (artifact / "inventory.json").write_text("{}") + case.mutation(artifact) + return artifact + + monkeypatch.setattr(runtime_builder, "_build", build) + return case + + +def _prepare(case: SimpleNamespace, *, offline: bool = False): + return runtime_builder.prepare_runtime(case.plan, runtime=case.runtime, cache_root=case.cache, offline=offline) + + +def test_signed_public_trust_survives_offline_attachment(trust_case: SimpleNamespace) -> None: + prepared = _prepare(trust_case) + target = prepared.root / DESTINATION + assert target.is_file(), "attached worker /etc lacks the signed public certificate bundle" + assert target.read_bytes() == trust_case.payload + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.load_verify_locations(cafile=str(target)) + assert context.cert_store_stats()["x509_ca"] > 0 + assert context.verify_mode == ssl.CERT_REQUIRED and context.check_hostname + assert prepared.descriptor["inventory"]["public_trust"] == { + "source": SOURCE, + "path": DESTINATION, + "sha256": runtime_builder.content_digest(trust_case.payload), + } + assert _prepare(trust_case, offline=True).identity == prepared.identity + assert (prepared.root / "worker-config/hosts").read_text() == "127.0.0.1 localhost\n" + + +def test_public_trust_change_invalidates_warm_cache(trust_case: SimpleNamespace) -> None: + _prepare(trust_case) + trust_case.source.write_bytes(trust_case.payload + b"\n# changed signed fixture bundle\n") + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + _prepare(trust_case, offline=True) + + +def test_builder_cannot_replace_captured_public_trust(trust_case: SimpleNamespace) -> None: + trust_case.mutation = lambda _artifact: trust_case.source.write_bytes(b"builder modified fixture source") + prepared = _prepare(trust_case) + assert (prepared.root / DESTINATION).is_file(), "public trust must be captured before building" + assert (prepared.root / DESTINATION).read_bytes() == trust_case.payload + + +@pytest.mark.parametrize("violation", ["missing", "symlink", "escaped-parent", "private-key", "malformed", "oversized"]) +def test_unverified_public_trust_is_rejected(trust_case: SimpleNamespace, violation: str) -> None: + source = trust_case.source + if violation == "missing": + source.unlink() + elif violation == "symlink": + destination = source.parent / "elsewhere.pem" + source.rename(destination) + source.symlink_to(destination) + elif violation == "escaped-parent": + outside = trust_case.cache.parent / "outside-trust" + source.parent.rename(outside) + source.parent.symlink_to(outside) + else: + content = { + "private-key": trust_case.payload + b"\n-----BEGIN PRIVATE KEY-----\nsecret\n", + "malformed": b"not a certificate", + "oversized": b"x" * 2_000_001, + }[violation] + source.write_bytes(content) + with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust"): + _prepare(trust_case) + assert not list(trust_case.cache.glob("*/project-runtime.json")) + + +@pytest.mark.parametrize("collision_kind", ["leaf", "parent"]) +def test_builder_certificate_destination_collision_fails_closed( + trust_case: SimpleNamespace, collision_kind: str +) -> None: + def collision(artifact): + destination = artifact / (DESTINATION if collision_kind == "leaf" else "worker-config/ssl") + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(b"builder-selected trust") + + trust_case.mutation = collision + with pytest.raises(ProjectRuntimeError, match="project_runtime_public_trust_collision"): + _prepare(trust_case) + assert not list(trust_case.cache.glob("*/project-runtime.json")) diff --git a/tests/unit/specfact_code_review/run/test_runtime_vcs.py b/tests/unit/specfact_code_review/run/test_runtime_vcs.py new file mode 100644 index 00000000..5ceb4d64 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_runtime_vcs.py @@ -0,0 +1,753 @@ +"""Dynamic package versions bind to sanitized, exact source VCS context.""" + +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from specfact_code_review.run import ( + portable_snapshot, + runner, + runtime_builder, + runtime_commands, + runtime_vcs, + scope, + target_launch, +) +from specfact_code_review.run.portable_snapshot import discover_snapshot +from specfact_code_review.run.runtime_builder import copy_project +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest +from specfact_code_review.run.runtime_sources import verify_inputs +from specfact_code_review.run.runtime_vcs import copy_vcs_context, vcs_context + + +def _git(root: Path, *args: str) -> str: + return subprocess.check_output( + ["git", "-C", str(root), *args], + text=True, + env={key: value for key, value in os.environ.items() if not key.startswith("GIT_")}, + ).strip() + + +def _repository(root: Path, *, empty_template: bool = False) -> None: + root.mkdir() + template_args = ("--template=",) if empty_template else () + _git(root, "init", "-q", *template_args) + (root / "app.py").write_text("VALUE = 1\n") + _git(root, "add", "app.py") + _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "fixture", + ) + + +def test_private_copy_has_correct_index_and_retains_source_modifications(tmp_path: Path) -> None: + root = tmp_path / "project" + _repository(root) + copy_project(root, tmp_path / "clean") + assert _git(tmp_path / "clean", "status", "--porcelain") == "" + (root / "app.py").write_text("VALUE = 2\n") + copy_project(root, tmp_path / "dirty") + assert _git(tmp_path / "dirty", "diff", "--name-only") == "app.py" + + +def test_tag_change_invalidates_runtime_without_source_byte_changes(tmp_path: Path) -> None: + root = tmp_path / "project" + _repository(root) + before = discover_project(root) + _git(root, "tag", "v1.0.0") + after = discover_project(root) + assert before.source_identity == after.source_identity + assert before.identity != after.identity + + +@pytest.mark.parametrize("state", ["fresh", "staged", "orphan"]) +def test_unborn_head_allows_discovery_inspection_and_private_copy(tmp_path: Path, monkeypatch, state: str) -> None: + root = tmp_path / "project" + if state == "orphan": + _repository(root) + _git(root, "tag", "unrelated-v1") + _git(root, "checkout", "--orphan", "new-branch") + else: + root.mkdir() + _git(root, "init", "-q", "--initial-branch=main") + (root / "pyproject.toml").write_text('[project]\nname="fixture"\n') + if state == "staged": + _git(root, "add", "pyproject.toml") + plan = discover_project(root) + assert not plan.vcs + verify_inputs(plan) + monkeypatch.chdir(root) + result = CliRunner().invoke(runtime_commands.app, ["inspect", "--json"]) + assert result.exit_code == 0, result.stdout + assert json.loads(result.stdout)["vcs"] == {} + copied = tmp_path / "copied" + copy_project(root, copied) + assert (copied / "pyproject.toml").read_bytes() == (root / "pyproject.toml").read_bytes() + assert not (copied / ".git").exists() + _commit_fixture(root, "first commit on selected branch") + with pytest.raises(ProjectRuntimeError, match="project_runtime_vcs_changed_during_build"): + verify_inputs(plan) + + +@pytest.mark.parametrize("selection", [{"commit": "missing"}, {"commit": "HEAD~1"}, {"tree": "1" * 40}]) +def test_unborn_head_does_not_hide_missing_explicit_revision(tmp_path: Path, selection) -> None: + _git(tmp_path, "init", "-q", "--initial-branch=main") + with pytest.raises(ProjectRuntimeError, match="project_git_snapshot_failed"): + vcs_context(tmp_path, **selection) + + +@pytest.mark.parametrize( + ("metadata_path", "content"), + [ + ("HEAD", "1" * 40 + "\n"), + ("HEAD", "malformed head\n"), + ("HEAD", "ref: refs/tags/missing\n"), + ("refs/heads/main", "1" * 40 + "\n"), + ("refs/heads/main", "malformed ref\n"), + ("config", "[malformed configuration\n"), + ], + ids=["detached-missing", "malformed-head", "non-branch-head", "missing-object", "malformed-branch", "bad-config"], +) +def test_invalid_git_state_is_not_treated_as_unborn(tmp_path: Path, metadata_path: str, content: str) -> None: + _git(tmp_path, "init", "-q", "--initial-branch=main") + (tmp_path / ".git" / metadata_path).write_text(content) + with pytest.raises(ProjectRuntimeError, match="project_git_snapshot_failed"): + vcs_context(tmp_path) + + +@pytest.mark.parametrize("failure", [OSError("synthetic detail"), subprocess.TimeoutExpired("git", 120)]) +def test_git_execution_failure_cannot_be_downgraded_to_unborn(tmp_path: Path, monkeypatch, failure: Exception) -> None: + _git(tmp_path, "init", "-q", "--initial-branch=main") + commands = [] + + def fail(*args, **_kwargs): + commands.append(args) + raise failure + + monkeypatch.setattr(runtime_vcs.subprocess, "run", fail) + with pytest.raises(ProjectRuntimeError) as caught: + vcs_context(tmp_path) + assert caught.value.__cause__ is failure + assert len(commands) == 1 + assert "synthetic detail" not in str(caught.value) + + +def test_immutable_snapshot_retains_selected_commit_context(tmp_path: Path) -> None: + + root = tmp_path / "project" + _repository(root) + commit = _git(root, "rev-parse", "HEAD") + snapshot = scope._materialize_commit(root, commit) + try: + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + assert plan.vcs_repository == root + assert plan.vcs["commit"] == commit + assert "vcs_repository" not in plan.document() + assert not (snapshot.root / ".git").exists() + finally: + shutil.rmtree(snapshot.root) + + +def test_vcs_fixtures_never_change_the_calling_hooks_index(tmp_path: Path, monkeypatch) -> None: + parent = tmp_path / "parent" + _repository(parent) + index = parent / ".git/index" + before = index.read_bytes() + monkeypatch.setenv("GIT_INDEX_FILE", str(index)) + _repository(tmp_path / "child") + assert index.read_bytes() == before + + +def test_analyzer_copy_excludes_reachable_git_history(tmp_path: Path, monkeypatch) -> None: + root = tmp_path / "project" + _repository(root) + observed = [] + + def analyze(_runtime, *, snapshot_root, **_kwargs): + assert not (snapshot_root / ".git").exists() + assert (snapshot_root / "app.py").read_text() == "VALUE = 1\n" + observed.append(snapshot_root) + + monkeypatch.setattr(runner, "_run_capsule_snapshot", analyze) + request = portable_snapshot.ProjectSnapshotRequest( + root, [root / "app.py"], runner.ReviewOptions(), "explicit_files" + ) + portable_snapshot._run_in_private_source(object(), request, runner.CapsuleSnapshotSettings()) + assert observed and (root / ".git/objects").is_dir() + + +@pytest.fixture(name="staged_project") +def staged_project_fixture(tmp_path: Path): + root = tmp_path / "project" + _repository(root) + _git(root, "mv", "app.py", "renamed.py") + resolution = scope.resolve_scope(scope.ScopeRequest(repository=root, scope="index", portable_project_runtime=True)) + assert resolution.status == "PASS" + try: + yield root, resolution + finally: + scope.cleanup_scope_resolution(resolution) + + +def test_staged_snapshot_binds_captured_tree_independently_of_live_index(staged_project) -> None: + root, resolution = staged_project + snapshot = resolution.head_snapshot + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + assert plan.vcs["tree"] == snapshot.tree + assert plan.vcs["tree"] != resolution.base_snapshot.tree + _git(root, "read-tree", "HEAD") + verify_inputs(plan) + assert vcs_context(root, plan.vcs["commit"], tree=snapshot.tree) == plan.vcs + + +def test_private_build_index_retains_staged_rename_and_head(staged_project, tmp_path: Path) -> None: + root, resolution = staged_project + snapshot = resolution.head_snapshot + copied = tmp_path / "build-copy" + copy_project(snapshot.root, copied, include_vcs=False) + copy_vcs_context(root, copied, resolution.base_snapshot.commit, tree=snapshot.tree) + assert _git(copied, "rev-parse", "HEAD") == resolution.base_snapshot.commit + assert _git(copied, "write-tree") == snapshot.tree + assert _git(copied, "ls-files") == "renamed.py" + assert _git(copied, "diff", "--name-only") == "" + assert "renamed.py" in _git(copied, "diff", "--cached", "--name-only") + + +def test_index_snapshot_keeps_captured_commit_when_repository_head_moves(staged_project) -> None: + root, resolution = staged_project + _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "advance HEAD", + ) + assert _git(root, "rev-parse", "HEAD") != resolution.base_snapshot.commit + snapshot = resolution.head_snapshot + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + assert plan.vcs["commit"] == resolution.base_snapshot.commit + assert plan.vcs["tree"] == snapshot.tree + + +def test_builder_attaches_captured_tree_before_dependency_installation( + staged_project, tmp_path: Path, monkeypatch +) -> None: + _root, resolution = staged_project + snapshot = resolution.head_snapshot + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + staging = tmp_path / "staging" + staging.mkdir() + + def stop_after_vcs(*_args, **_kwargs): + raise RuntimeError("stop before dependency acquisition") + + monkeypatch.setattr(runtime_builder, "stage_git", stop_after_vcs) + with pytest.raises(RuntimeError, match="stop before dependency acquisition"): + runtime_builder._build(plan, object(), staging) + assert _git(staging / "project", "write-tree") == snapshot.tree + assert _git(staging / "project", "ls-files") == "renamed.py" + + +@pytest.mark.parametrize("changed_state", ["head", "index"]) +def test_real_cached_analysis_snapshot_preserves_portable_vcs_provenance( + staged_project, tmp_path: Path, monkeypatch, changed_state: str +) -> None: + root, resolution = staged_project + monkeypatch.chdir(root) + destination = tmp_path / "cached" + destination.mkdir() + snapshot = runner._cached_analysis_snapshot([Path("renamed.py")], destination) + assert snapshot is not None + if changed_state == "head": + _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "advance HEAD", + ) + else: + _git(root, "read-tree", "HEAD") + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + assert plan.vcs["commit"] == resolution.base_snapshot.commit + assert plan.vcs["tree"] == resolution.head_snapshot.tree + assert plan.vcs["tree"] == snapshot.diff.index_tree + copied = tmp_path / "cached-build" + copy_project(snapshot.root, copied, include_vcs=False) + copy_vcs_context(root, copied, plan.vcs["commit"], tree=plan.vcs["tree"]) + assert _git(copied, "write-tree") == snapshot.diff.index_tree + assert _git(copied, "ls-files") == "renamed.py" + + +def test_cached_capture_rejects_head_commit_change_with_identical_tree( + staged_project, tmp_path: Path, monkeypatch +) -> None: + root, _resolution = staged_project + monkeypatch.chdir(root) + destination = tmp_path / "cached-race" + destination.mkdir() + original = runner._git_tree_identity + + def move_head_after_index(repository: Path, revision: str): + result = original(repository, revision) + if revision == "index": + previous = _git(root, "rev-parse", "HEAD") + tree = _git(root, "rev-parse", "HEAD^{tree}") + commit = _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "commit-tree", + tree, + "-p", + previous, + "-m", + "same tree new commit", + ) + _git(root, "update-ref", "HEAD", commit) + return result + + monkeypatch.setattr(runner, "_git_tree_identity", move_head_after_index) + assert runner._cached_analysis_snapshot([Path("renamed.py")], destination) is None + + +def _commit_fixture(root: Path, message: str) -> str: + _git(root, "add", "-A") + _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + message, + ) + return _git(root, "rev-parse", "HEAD") + + +def _annotated_tag(root: Path, name: str, message: str) -> None: + _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "tag", + "-a", + name, + "-m", + message, + ) + + +@pytest.mark.parametrize("packed", [False, True]) +def test_private_git_contains_only_bound_objects_and_tag_refs(tmp_path: Path, packed: bool) -> None: + root = tmp_path / "source" + _repository(root) + _annotated_tag(root, "v1", "bound annotated tag") + _git(root, "tag", "lightweight") + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected ancestry") + _git(root, "checkout", "--orphan", "unrelated") + _git(root, "rm", "-rf", ".") + (root / "private.txt").write_text("unbound branch data") + unrelated = _commit_fixture(root, "unrelated branch") + _git(root, "update-ref", "refs/remotes/origin/private", unrelated) + _git(root, "checkout", "--detach", selected) + if packed: + _git(root, "repack", "-ad") + secret = tmp_path / "unreachable.txt" + secret.write_text("unreachable object data") + unreachable = _git(root, "hash-object", "-w", str(secret)) + before = vcs_context(root, selected) + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied, selected) + expected = set( + _git( + root, "rev-list", "--objects", "--no-object-names", selected, "refs/tags/v1", "refs/tags/lightweight" + ).splitlines() + ) + actual = set(_git(copied, "cat-file", "--batch-all-objects", "--batch-check=%(objectname)").splitlines()) + assert actual == expected + assert unrelated not in actual and unreachable not in actual + assert _git(copied, "for-each-ref", "--format=%(refname)") == "refs/tags/lightweight\nrefs/tags/v1" + assert _git(copied, "describe", "--tags", "HEAD") == _git(root, "describe", "--tags", selected) + assert _git(copied, "rev-list", "--count", "HEAD") == "2" + assert vcs_context(root, selected) == before + + +def test_private_git_preserves_bound_shallow_history_and_annotated_tag(tmp_path: Path) -> None: + original = tmp_path / "original" + _repository(original) + ancestor = _git(original, "rev-parse", "HEAD") + (original / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(original, "shallow tip") + _annotated_tag(original, "v2", "tip tag") + source = tmp_path / "shallow" + _git(original, "clone", "--quiet", "--depth=1", original.as_uri(), str(source)) + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(source, copied) + assert _git(copied, "rev-parse", "HEAD") == selected + assert _git(copied, "describe", "--tags", "HEAD") == "v2" + assert _git(copied, "rev-parse", "--is-shallow-repository") == "true" + assert _git(copied, "rev-list", "--count", "HEAD") == "1" + assert vcs_context(copied) == vcs_context(source) + objects = _git(copied, "cat-file", "--batch-all-objects", "--batch-check=%(objectname)").splitlines() + assert ancestor not in objects + + +def test_unbound_replacement_ref_cannot_change_selected_vcs_context(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + ancestor = _git(root, "rev-parse", "HEAD") + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + before = vcs_context(root) + _git(root, "replace", selected, ancestor) + assert vcs_context(root) == before + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied) + assert _git(copied, "rev-parse", "HEAD^{tree}") == before["tree"] + assert not _git(copied, "for-each-ref", "--format=%(refname)", "refs/replace") + + +def test_missing_promisor_object_does_not_run_host_transport(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + blob = _git(root, "rev-parse", "HEAD:app.py") + marker = tmp_path / "transport-executed" + transport = tmp_path / "transport.sh" + transport.write_text(f"#!/bin/sh\ntouch '{marker}'\nexit 1\n") + transport.chmod(0o700) + _git(root, "config", "core.repositoryformatversion", "1") + _git(root, "config", "extensions.partialClone", "origin") + _git(root, "config", "remote.origin.promisor", "true") + _git(root, "config", "remote.origin.url", "ssh://example.invalid/project") + _git(root, "config", "core.sshCommand", str(transport)) + (root / ".git/objects" / blob[:2] / blob[2:]).unlink() + copied = tmp_path / "copy" + copied.mkdir() + with pytest.raises(ValueError, match="project_git_snapshot_failed"): + copy_vcs_context(root, copied) + assert not marker.exists() + assert not (copied / ".git").exists() + + +@pytest.mark.parametrize("empty_template", [False, True]) +def test_unbound_grafts_cannot_remove_selected_ancestry(tmp_path: Path, empty_template: bool) -> None: + root = tmp_path / "source" + _repository(root, empty_template=empty_template) + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + before = vcs_context(root) + (root / ".git/info").mkdir(exist_ok=True) + (root / ".git/info/grafts").write_text(selected + "\n") + assert vcs_context(root) == before + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied) + assert _git(copied, "rev-list", "--count", "HEAD") == "2" + assert not (copied / ".git/info/grafts").exists() + + +@pytest.mark.parametrize( + "failure", + [subprocess.TimeoutExpired("git", 120, stderr=b"private transport detail"), OSError("private filesystem detail")], +) +def test_git_export_failure_uses_runtime_diagnostic(tmp_path: Path, monkeypatch, failure: Exception) -> None: + def raise_export_failure(*_args, **_kwargs): + raise failure + + monkeypatch.setattr(runtime_vcs.subprocess, "run", raise_export_failure) + with pytest.raises(ProjectRuntimeError, match="project_git_snapshot_failed:rev-list") as caught: + runtime_vcs._git_bytes(tmp_path, "rev-list", "--objects", "HEAD") + assert caught.value.__cause__ is failure + assert "private" not in str(caught.value) + + +def test_frozen_vcs_tags_exclude_newly_added_tag_history(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + bound = vcs_context(root) + unrelated = _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "commit-tree", + bound["tree"], + "-m", + "unbound commit", + ) + _git(root, "tag", "late", unrelated) + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied, bound_vcs=bound) + assert not _git(copied, "for-each-ref", "--format=%(refname)") + assert unrelated not in _git(copied, "cat-file", "--batch-all-objects", "--batch-check=%(objectname)").splitlines() + + +def test_frozen_shallow_boundary_controls_object_traversal(tmp_path: Path) -> None: + root = tmp_path / "source" + _repository(root) + (root / "app.py").write_text("VALUE = 2\n") + selected = _commit_fixture(root, "selected") + bound = vcs_context(root) + (root / ".git/shallow").write_text(selected + "\n") + copied = tmp_path / "copy" + copied.mkdir() + copy_vcs_context(root, copied, bound_vcs=bound) + assert _git(copied, "rev-parse", "--is-shallow-repository") == "false" + assert _git(copied, "rev-list", "--count", "HEAD") == "2" + + +def test_builder_copies_frozen_plan_refs_after_input_verification(tmp_path: Path, monkeypatch) -> None: + root = tmp_path / "source" + _repository(root) + plan = discover_project(root) + unrelated = _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "commit-tree", + plan.vcs["tree"], + "-m", + "unbound commit", + ) + staging = tmp_path / "staging" + staging.mkdir() + + def change_tags_after_verification(selected_plan): + verify_inputs(selected_plan) + _git(root, "tag", "late", unrelated) + + def stop_before_acquisition(*_args, **_kwargs): + raise RuntimeError("stop before acquisition") + + monkeypatch.setattr(runtime_builder, "verify_inputs", change_tags_after_verification) + monkeypatch.setattr(runtime_builder, "stage_git", stop_before_acquisition) + with pytest.raises(RuntimeError, match="stop before acquisition"): + runtime_builder._build(plan, object(), staging) + assert not _git(staging / "project", "for-each-ref", "--format=%(refname)") + + +@pytest.mark.parametrize("immutable", [False, True]) +@pytest.mark.parametrize("remove_origin", [False, True]) +def test_builder_requires_captured_git_origin_after_verification( + tmp_path: Path, monkeypatch, immutable: bool, remove_origin: bool +) -> None: + root = tmp_path / "source" + _repository(root) + snapshot = scope._materialize_commit(root, _git(root, "rev-parse", "HEAD")) if immutable else None + try: + plan = ( + discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + if snapshot is not None + else discover_project(root) + ) + staging = tmp_path / "staging" + staging.mkdir() + acquisition = [] + + def remove_git_after_verification(selected_plan): + verify_inputs(selected_plan) + if remove_origin: + (root / ".git").rename(tmp_path / "removed-git") + + def stop_before_acquisition(*_args, **_kwargs): + acquisition.append(True) + assert _git(staging / "project", "rev-parse", "HEAD") == plan.vcs["commit"] + raise RuntimeError("stop before acquisition") + + monkeypatch.setattr(runtime_builder, "verify_inputs", remove_git_after_verification) + monkeypatch.setattr(runtime_builder, "stage_git", stop_before_acquisition) + expected = ProjectRuntimeError if remove_origin else RuntimeError + diagnostic = "project_git_snapshot_origin_missing" if remove_origin else "stop before acquisition" + with pytest.raises(expected, match=diagnostic): + runtime_builder._build(plan, object(), staging) + assert bool(acquisition) is not remove_origin + if remove_origin: + assert not (staging / "project/.git").exists() + finally: + if snapshot is not None: + shutil.rmtree(snapshot.root) + + +@pytest.fixture(name="attached_vcs_git") +def attached_vcs_git_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Path]: + """Use real Git with a synthetic attested mount layout and empty system PATH.""" + actual_git = shutil.which("git") + assert actual_git is not None + repository = tmp_path / "repository" + _repository(repository) + project = tmp_path / "attached" + launcher = project / "bin/git" + launcher.parent.mkdir(parents=True) + observed = tmp_path / "git-observation.json" + launcher.write_text( + f"#!{sys.executable}\nimport json,os,sys\n" + f"with open({str(observed)!r}, 'w') as stream: json.dump(dict(os.environ), stream)\n" + f"os.execv({actual_git!r}, [{actual_git!r}, *sys.argv[1:]])\n" + ) + launcher.chmod(0o755) + descriptor = project / "project-runtime.json" + descriptor.write_text( + json.dumps( + { + "project": {"native_tools": ["git"]}, + "inventory": { + "native_tools": { + "bin/git": { + "source": "generated-controller-launcher", + "sha256": content_digest(launcher.read_bytes()), + } + } + }, + } + ) + ) + builtin = tmp_path / "builtin" + token = builtin / target_launch.DOMAIN_FILES["pytest-observe"] + token.parent.mkdir(parents=True) + token.write_text("trusted startup fixture\n") + context = tmp_path / "python-context" + os.link(token, context) + monkeypatch.setattr(target_launch, "PROJECT", project) + monkeypatch.setattr(target_launch, "BUILTIN", builtin) + monkeypatch.setattr(target_launch, "CONTEXT", context) + empty = tmp_path / "empty-system-bin" + empty.mkdir() + monkeypatch.setattr(runtime_vcs.os, "defpath", str(empty)) + return { + "repository": repository, + "project": project, + "launcher": launcher, + "descriptor": descriptor, + "observed": observed, + "context": context, + } + + +@pytest.mark.parametrize( + ("option", "expected"), + [ + ("GIT_CONFIG_NOSYSTEM", "1"), + ("GIT_CONFIG_GLOBAL", "/dev/null"), + ("GIT_NO_REPLACE_OBJECTS", "1"), + ("GIT_GRAFT_FILE", os.devnull), + ("GIT_NO_LAZY_FETCH", "1"), + ("GIT_ALLOW_PROTOCOL", ""), + ("GIT_TERMINAL_PROMPT", "0"), + ], +) +def test_attached_vcs_git_survives_sanitized_path( + attached_vcs_git: dict[str, Path], option: str, expected: str +) -> None: + """Metadata export uses declared Git while retaining each isolated configuration control.""" + selected = vcs_context(attached_vcs_git["repository"]) + assert len(selected["commit"]) == 40 + environment = json.loads(attached_vcs_git["observed"].read_text()) + assert environment["PATH"] == os.defpath + assert environment[option] == expected + + +def _invalidate_attached_git(paths: dict[str, Path], descriptor: dict, invalid: str) -> None: + if invalid == "undeclared": + descriptor["project"]["native_tools"] = [] + return + if invalid == "wrong-origin": + descriptor["inventory"]["native_tools"]["bin/git"]["source"] = "project" + return + if invalid == "wrong-digest": + descriptor["inventory"]["native_tools"]["bin/git"]["sha256"] = "sha256:" + "0" * 64 + return + original = paths["launcher"].read_bytes() + paths["launcher"].unlink() + if invalid == "symlink": + escaped = paths["project"].parent / "escaped-git" + escaped.write_bytes(original) + paths["launcher"].symlink_to(escaped) + + +@pytest.mark.parametrize("invalid", ["undeclared", "wrong-origin", "wrong-digest", "missing", "symlink"]) +def test_attached_vcs_rejects_unverified_git_launcher(attached_vcs_git: dict[str, Path], invalid: str) -> None: + """A verified worker cannot substitute an unrecorded or altered executable.""" + paths = attached_vcs_git + descriptor = json.loads(paths["descriptor"].read_text()) + _invalidate_attached_git(paths, descriptor, invalid) + paths["descriptor"].write_text(json.dumps(descriptor)) + with pytest.raises(ProjectRuntimeError, match="project_git_attached_tool_invalid"): + vcs_context(paths["repository"]) + assert not paths["observed"].exists() + + +def test_host_vcs_ignores_ambient_path_and_forged_worker_markers( + attached_vcs_git: dict[str, Path], monkeypatch: pytest.MonkeyPatch +) -> None: + """An environment marker cannot select target Git outside the trusted context.""" + paths = attached_vcs_git + paths["context"].unlink() + trusted = paths["project"].parent / "system-bin" + trusted.mkdir() + (trusted / "git").write_bytes(paths["launcher"].read_bytes()) + (trusted / "git").chmod(0o755) + paths["launcher"].write_text("untrusted attached launcher") + hostile = paths["project"].parent / "hostile-bin" + hostile.mkdir() + (hostile / "git").write_text("#!/bin/sh\nexit 99\n") + (hostile / "git").chmod(0o755) + monkeypatch.setattr(runtime_vcs.os, "defpath", str(trusted)) + monkeypatch.setenv("PATH", str(hostile)) + monkeypatch.setenv("SPECFACT_TARGET_PYTEST", "1") + monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(hostile / "config")) + assert len(vcs_context(paths["repository"])["commit"]) == 40 + environment = json.loads(paths["observed"].read_text()) + assert "SPECFACT_TARGET_PYTEST" not in environment + assert environment["GIT_CONFIG_GLOBAL"] == "/dev/null" + + +def test_forged_worker_context_cannot_select_attached_git(attached_vcs_git: dict[str, Path]) -> None: + """A copied marker file is not the inherited read-only builtin inode.""" + context = attached_vcs_git["context"] + text = context.read_text() + context.unlink() + context.write_text(text) + with pytest.raises(ProjectRuntimeError, match="project_git_attached_context_invalid"): + vcs_context(attached_vcs_git["repository"]) + assert not attached_vcs_git["observed"].exists() diff --git a/tests/unit/specfact_code_review/run/test_scope.py b/tests/unit/specfact_code_review/run/test_scope.py index 0308906d..1bf696cd 100644 --- a/tests/unit/specfact_code_review/run/test_scope.py +++ b/tests/unit/specfact_code_review/run/test_scope.py @@ -6,11 +6,15 @@ import os import subprocess import sys +from dataclasses import dataclass, replace from pathlib import Path from typing import Any import pytest +from specfact_code_review.run import scope +from specfact_code_review.run.runtime_builder import copy_project + _GIT_LOCAL_ENV_VARS = frozenset( { @@ -49,8 +53,8 @@ def _commit(repo: Path, message: str) -> str: return _git(repo, "rev-parse", "HEAD") -@pytest.fixture -def git_repo(tmp_path: Path) -> Path: +@pytest.fixture(name="git_repo") +def git_repo_fixture(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git(repo, "init", "-b", "main") @@ -67,9 +71,8 @@ def git_repo(tmp_path: Path) -> Path: return repo -@pytest.fixture -def scope_api() -> Any: - from specfact_code_review.run import scope +@pytest.fixture(name="scope_api") +def scope_api_fixture() -> Any: return scope @@ -289,7 +292,7 @@ def test_materialized_governed_input_uses_nofollow_regular_blob_identity(scope_a @pytest.mark.parametrize("scope_name", ["index", "range"]) @pytest.mark.parametrize("option", ["fix", "preview_fixes", "with_mutation"]) def test_index_and_range_reject_fix_preview_and_mutation_options( - scope_api: Any, git_repo: Path, scope_name: str, option: str + scope_api: Any, git_repo: Path, scope_name: scope.ScopeKind, option: str ) -> None: base, head = _make_range(git_repo) kwargs: dict[str, Any] = {option: True} @@ -775,6 +778,130 @@ def test_index_change_to_transitive_policy_file_is_governed( assert result.selected_paths == (referenced_path,) +@dataclass(frozen=True) +class _TransitivePolicyCase: + primary_path: str + primary_content: str + reference_path: str + reference_content: str + leaf_path: str + leaf_content: str + + +@dataclass(frozen=True) +class _PolicyReferenceChangeCase: + primary_path: str + before: str + after: str + old_path: str + new_path: str + content: str + + +@pytest.mark.parametrize("portable", [False, True], ids=["existing", "portable"]) +@pytest.mark.parametrize("changed", [False, True], ids=["clean-index", "changed-source"]) +@pytest.mark.parametrize( + "case", + [ + _TransitivePolicyCase( + "ruff.toml", + "extend='config/base.toml'\n", + "config/base.toml", + "extend='leaf.toml'\n", + "config/leaf.toml", + "line-length=80\n", + ), + _TransitivePolicyCase( + "pyrightconfig.json", + '{"extends":"config/base.json"}\n', + "config/base.json", + '{"extends":"leaf.json"}\n', + "config/leaf.json", + '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], +) +def test_index_unchanged_policy_manifest_preserves_selection( + git_repo: Path, + portable: bool, + changed: bool, + case: _TransitivePolicyCase, +) -> None: + """Harden pre-existing evidence omission without widening index review selection.""" + (git_repo / "config").mkdir() + for path, content in ( + (case.primary_path, case.primary_content), + (case.reference_path, case.reference_content), + (case.leaf_path, case.leaf_content), + ): + (git_repo / path).write_text(content, encoding="utf-8") + _commit(git_repo, "add transitive policy closure") + if changed: + (git_repo / "src/app.py").write_text("VALUE = 2\n", encoding="utf-8") + _git(git_repo, "add", "src/app.py") + + result = scope.resolve_scope( + scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=portable) + ) + try: + assert result.selected_paths == (("src/app.py",) if changed else ()) + assert result.status == ("PASS" if changed else "NOT_APPLICABLE") + assert result.reason == ("resolved" if changed else "no_governed_impact") + assert result.assurance_kind == result.effective_assurance_kind == "index" + for path in (case.reference_path, case.leaf_path): + assert path in result.policy_paths + assert result.input_manifest[path] == result.base_input_manifest[path] + assert result.input_manifest[path].blob_sha == _git(git_repo, "rev-parse", f"HEAD:{path}") + assert result.input_manifest[path].open_policy == "descriptor-relative-nofollow" + finally: + scope.cleanup_scope_resolution(result) + + +@pytest.mark.parametrize( + "case", + [ + _PolicyReferenceChangeCase( + "ruff.toml", + "extend='config/old.toml'\n", + "extend='config/new.toml'\n", + "config/old.toml", + "config/new.toml", + "line-length=80\n", + ), + _PolicyReferenceChangeCase( + "pyrightconfig.json", + '{"extends":"config/old.json"}\n', + '{"extends":"config/new.json"}\n', + "config/old.json", + "config/new.json", + '{"typeCheckingMode":"basic"}\n', + ), + ], + ids=["ruff", "basedpyright"], +) +def test_index_policy_manifest_retains_base_and_head_reference_closures( + git_repo: Path, case: _PolicyReferenceChangeCase +) -> None: + (git_repo / "config").mkdir() + (git_repo / case.primary_path).write_text(case.before, encoding="utf-8") + for path in (case.old_path, case.new_path): + (git_repo / path).write_text(case.content, encoding="utf-8") + _commit(git_repo, "add alternative policy references") + (git_repo / case.primary_path).write_text(case.after, encoding="utf-8") + _git(git_repo, "add", case.primary_path) + + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index")) + try: + assert result.status == "PASS" + assert result.selected_paths == (case.primary_path,) + for path in (case.old_path, case.new_path): + assert result.base_input_manifest[path].blob_sha == _git(git_repo, "rev-parse", f"HEAD:{path}") + assert result.input_manifest[path] == result.base_input_manifest[path] + finally: + scope.cleanup_scope_resolution(result) + + @pytest.mark.parametrize( ("primary_path", "primary_content", "reference_path", "unsafe_path", "stage_graph_together"), [ @@ -1347,3 +1474,157 @@ def test_semgrep_ai_bloat_rule_pack_is_governed_and_sealed(scope_api: Any, tmp_p assert bundle.clean.identity_kind == "signed_module_payload" assert bundle.ai_bloat.identity_kind == "signed_module_payload" assert bundle.bundle_digest.startswith("sha256:") + + +def test_portable_lock_only_range_remains_analyzable_preview(scope_api: Any, git_repo: Path) -> None: + + base, head = _make_range(git_repo, path="uv.lock", content="version = 1\n") + request = replace(_range_request(scope_api, git_repo, base, head), portable_project_runtime=True) + result = scope_api.resolve_scope(request) + try: + assert result.status == "PASS", (result.reason, result.diagnostics) + assert result.effective_assurance_kind == "range_preview" + assert "uv.lock" in result.selected_paths + finally: + scope_api.cleanup_scope_resolution(result) + + +def test_portable_range_records_candidate_referenced_policy(scope_api: Any, git_repo: Path) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo / "config").mkdir() + (git_repo / "ruff.toml").write_text('extend="config/candidate.toml"\n') + (git_repo / "config/candidate.toml").write_text("line-length=99\n") + head = _commit(git_repo, "candidate policy closure") + result = scope_api.resolve_scope(_range_request(scope_api, git_repo, base, head, portable_project_runtime=True)) + try: + assert result.status == "PASS", result.diagnostics + assert "config/candidate.toml" in result.selected_paths + assert "config/candidate.toml" in result.policy_paths + finally: + scope_api.cleanup_scope_resolution(result) + + +@pytest.mark.parametrize("scope_name", ["range", "index"]) +def test_portable_immutable_snapshots_retain_package_and_data_links( + git_repo: Path, tmp_path: Path, scope_name: scope.ScopeKind +) -> None: + package = git_repo / "src/customer" + package.mkdir() + (package / "__init__.py").write_text("VALUE = 1\n") + (package / "data.txt").write_text("original") + (git_repo / "customer").symlink_to("src/customer", target_is_directory=True) + (git_repo / "data-link").symlink_to("src/customer/data.txt") + base = _commit(git_repo, "project links") + (git_repo / "src/app.py").write_text("VALUE = 2\n") + (package / "data.txt").write_text("updated") + _git(git_repo, "add", "-A") + head = _commit(git_repo, "update project") if scope_name == "range" else None + request = scope.ScopeRequest( + repository=git_repo, + scope=scope_name, + base_ref=base if head else None, + head_ref=head, + portable_project_runtime=True, + ) + result = scope.resolve_scope(request) + try: + assert result.status == "PASS" + for side, snapshot in (("base", result.base_snapshot), ("head", result.head_snapshot)): + assert snapshot is not None + assert (snapshot.root / "customer").is_symlink() + assert os.readlink(snapshot.root / "data-link") == "src/customer/data.txt" + copied = tmp_path / side + copy_project(snapshot.root, copied, include_vcs=False) + assert (copied / "customer/__init__.py").read_text() == "VALUE = 1\n" + assert (copied / "data-link").read_text() == ("original" if side == "base" else "updated") + finally: + scope.cleanup_scope_resolution(result) + + +@pytest.mark.parametrize("scope_name", ["range", "index"]) +@pytest.mark.parametrize( + "link_name,target", [("data-link", "../outside.toml"), ("pyproject.toml", "../outside.toml"), ("setup.cfg", ".env")] +) +def test_portable_immutable_snapshots_reject_unsafe_links_before_policy_reads( + git_repo: Path, scope_name: scope.ScopeKind, link_name: str, target: str, monkeypatch +) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo.parent / "outside.toml").write_text('[project]\nname="private"\n') + (git_repo / ".env").write_text("private fixture") + (git_repo / link_name).symlink_to(target) + _git(git_repo, "add", "-A") + head = _commit(git_repo, "unsafe project link") if scope_name == "range" else None + + def forbid_policy_read(*_args, **_kwargs): + pytest.fail("unsafe source links reached policy discovery") + + monkeypatch.setattr(scope, "_range_policy_closure", forbid_policy_read) + monkeypatch.setattr(scope, "_resolved_index_policy_paths", forbid_policy_read) + result = scope.resolve_scope( + scope.ScopeRequest( + repository=git_repo, + scope=scope_name, + base_ref=base if head else None, + head_ref=head, + portable_project_runtime=True, + ) + ) + try: + assert result.status == "UNKNOWN" + assert "project_source_symlink_" in result.diagnostics + finally: + scope.cleanup_scope_resolution(result) + + +@pytest.mark.parametrize("scope_name", ["range", "index"]) +def test_portable_governed_python_links_remain_rejected(git_repo: Path, scope_name: scope.ScopeKind) -> None: + base = _git(git_repo, "rev-parse", "HEAD") + (git_repo / "README.md").write_text("VALUE = 1\n") + (git_repo / "src/app.py").unlink() + (git_repo / "src/app.py").symlink_to("../README.md") + _git(git_repo, "add", "-A") + head = _commit(git_repo, "governed link") if scope_name == "range" else None + result = scope.resolve_scope( + scope.ScopeRequest( + repository=git_repo, + scope=scope_name, + base_ref=base if head else None, + head_ref=head, + portable_project_runtime=True, + ) + ) + try: + assert result.status == "UNKNOWN" + assert result.reason == "unsafe_governed_input" + finally: + scope.cleanup_scope_resolution(result) + + +@pytest.mark.parametrize( + "failure_stage", ["_range_paths", "_discovered_index_policy_paths", "_snapshot_input_manifest"] +) +def test_index_discovery_failure_removes_both_materialized_roots( + git_repo: Path, tmp_path: Path, monkeypatch, failure_stage: str +) -> None: + (git_repo / "src/app.py").write_text("VALUE = 2\n") + _git(git_repo, "add", "-A") + created = [] + + def tracked_mkdtemp(*, prefix: str) -> str: + root = tmp_path / f"{prefix}{len(created)}" + root.mkdir() + created.append(root) + return str(root) + + def fail_discovery(*_args, **_kwargs): + raise scope.GitResolutionError("simulated immutable input read failure") + + monkeypatch.setattr(scope.tempfile, "mkdtemp", tracked_mkdtemp) + monkeypatch.setattr(scope, failure_stage, fail_discovery) + result = scope.resolve_scope(scope.ScopeRequest(repository=git_repo, scope="index", portable_project_runtime=True)) + assert result.status == "UNKNOWN" + assert result.reason == "git_resolution_failed" + assert "simulated immutable input read failure" in result.diagnostics + snapshots = [root for root in created if root.name.startswith("specfact-review-")] + assert len(snapshots) == 2 + assert all(not root.exists() for root in created) diff --git a/tests/unit/specfact_code_review/run/test_sitecustomize.py b/tests/unit/specfact_code_review/run/test_sitecustomize.py new file mode 100644 index 00000000..1be14283 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_sitecustomize.py @@ -0,0 +1,17 @@ +"""Native Python CLI startup must fail before user code when runtime context is absent.""" + +import os +import subprocess +import sys +from pathlib import Path + +from specfact_code_review.run import target_bootstrap + + +def test_project_python_fails_closed_before_executing_code_without_descriptor() -> None: + env = {**os.environ, "PYTHONPATH": str(Path(target_bootstrap.__file__).parent), "SPECFACT_PROJECT_PYTHON": "1"} + result = subprocess.run( + [sys.executable, "-s", "-c", 'print("MUST_NOT_EXECUTE")'], env=env, capture_output=True, text=True, check=False + ) + assert result.returncode == 78 + assert not result.stdout diff --git a/tests/unit/specfact_code_review/run/test_snapshot_activation.py b/tests/unit/specfact_code_review/run/test_snapshot_activation.py new file mode 100644 index 00000000..832286b3 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_snapshot_activation.py @@ -0,0 +1,167 @@ +"""Immutable snapshot discovery retains verified activation, never live dependencies.""" + +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +from specfact_code_review.run import runner, runtime_discovery, scope +from specfact_code_review.run.portable_snapshot import discover_snapshot +from specfact_code_review.run.runtime_models import ProjectRuntimeError, content_digest + + +def _git(root: Path, *arguments: str) -> None: + subprocess.run( + ["git", "-C", str(root), *arguments], + check=True, + capture_output=True, + env={key: value for key, value in os.environ.items() if not key.startswith("GIT_")}, + timeout=30, + ) + + +@pytest.fixture(name="activated_repository") +def activated_repository_fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """Capture a real index whose dependency bytes differ from the live checkout.""" + root = tmp_path / "project" + root.mkdir() + _git(root, "init", "-q") + (root / "app.py").write_text("VALUE = 1\n") + (root / ".gitignore").write_text(".venv/\n") + metadata = ( + '[project]\nname="customer"\n[tool.hatch.envs.review]\n' + 'dependencies=["requests==2.32.3"]\n[tool.hatch.envs.other]\n' + ) + (root / "pyproject.toml").write_text(metadata) + _git(root, "add", ".") + _git( + root, + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + "commit", + "-qm", + "fixture", + ) + (root / "pyproject.toml").write_text(metadata.replace("2.32.3", "2.32.4")) + _git(root, "add", "pyproject.toml") + (root / "pyproject.toml").write_text(metadata.replace("2.32.3", "2.32.5")) + environment = root / ".venv" + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home=/fixture/python\n") + monkeypatch.setenv("VIRTUAL_ENV", str(environment)) + monkeypatch.setenv("HATCH_ENV_ACTIVE", "review") + monkeypatch.setattr(runtime_discovery.sys, "prefix", str(environment)) + monkeypatch.chdir(root) + return root + + +@pytest.mark.parametrize("active", ["review", "default"]) +def test_index_activation_keeps_snapshot_dependency_identity( + activated_repository: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, active: str +) -> None: + """The actual cached review path selects activation without importing live inputs.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", active) + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([Path("app.py")], tmp_path / "snapshot") + assert snapshot is not None + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + assert plan.environment == active + assert plan.root == snapshot.root.resolve() + content = (snapshot.root / "pyproject.toml").read_bytes() + assert b"2.32.4" in content and b"2.32.5" not in content + assert plan.inputs["pyproject.toml"] == content_digest(content) + assert plan.inputs["pyproject.toml"] != content_digest((activated_repository / "pyproject.toml").read_bytes()) + + +@pytest.mark.parametrize("context", ["unrelated", "spoofed"]) +@pytest.mark.parametrize("active", ["review", "default"]) +def test_snapshot_rejects_unverified_activation( + activated_repository: Path, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + context: str, + active: str, +) -> None: + """Only the actual repository-local running interpreter can select an environment.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", active) + environment = tmp_path / "unrelated" + environment.mkdir() + (environment / "pyvenv.cfg").write_text("home=/fixture/python\n") + monkeypatch.setenv("VIRTUAL_ENV", str(environment)) + if context == "unrelated": + monkeypatch.setattr(runtime_discovery.sys, "prefix", str(environment)) + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([activated_repository / "app.py"], tmp_path / "snapshot") + assert snapshot is not None + with pytest.raises(ProjectRuntimeError, match="project_environment_ambiguous"): + discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + + +@pytest.mark.parametrize("manager", ['manager="hatch"\n', ""]) +@pytest.mark.parametrize("active", ["review", "default"]) +def test_snapshot_explicit_selection_overrides_activation( + activated_repository: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, manager: str, active: str +) -> None: + """An explicit environment continues to outrank valid ambient activation.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", active) + metadata = activated_repository / "pyproject.toml" + original = metadata.read_text() + metadata.write_text('[project]\nname="customer"\n[tool.hatch.envs.other]\n') + _git(activated_repository, "add", "pyproject.toml") + metadata.write_text(original) + config = tmp_path / "review.toml" + config.write_text(manager + 'environment="other"\n') + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([activated_repository / "app.py"], tmp_path / "snapshot") + assert snapshot is not None + assert discover_snapshot(snapshot.root, config_path=config, source_snapshot=snapshot).environment == "other" + + +def test_removed_active_environment_is_not_substituted(activated_repository: Path, tmp_path: Path) -> None: + """Each selected revision must contain the verified active environment itself.""" + metadata = activated_repository / "pyproject.toml" + metadata.write_text('[project]\nname="customer"\n[tool.hatch.envs.other]\n') + _git(activated_repository, "add", "pyproject.toml") + metadata.write_text('[project]\nname="customer"\n[tool.hatch.envs.review]\n[tool.hatch.envs.other]\n') + (tmp_path / "snapshot").mkdir() + snapshot = runner._cached_analysis_snapshot([activated_repository / "app.py"], tmp_path / "snapshot") + assert snapshot is not None + with pytest.raises(ProjectRuntimeError, match="project_active_environment_missing:review"): + discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + + +@pytest.mark.parametrize("active", ["review", "default"]) +def test_revision_activation_binds_its_own_dependency_inputs( + activated_repository: Path, monkeypatch: pytest.MonkeyPatch, active: str +) -> None: + """A revision side reuses only verified selection, never another side's dependency bytes.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", active) + commit = subprocess.check_output(["git", "-C", str(activated_repository), "rev-parse", "HEAD"], text=True).strip() + snapshot = scope._materialize_commit(activated_repository, commit) + try: + plan = discover_snapshot(snapshot.root, config_path=None, source_snapshot=snapshot) + assert plan.environment == active + content = (snapshot.root / "pyproject.toml").read_bytes() + assert b"2.32.3" in content and b"2.32.5" not in content + assert plan.inputs["pyproject.toml"] == content_digest(content) + assert plan.vcs["commit"] == commit + finally: + shutil.rmtree(snapshot.root) + + +def test_live_activation_recognizes_implicit_hatch_default( + activated_repository: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Hatch defines default even when only custom environment tables exist.""" + monkeypatch.setenv("HATCH_ENV_ACTIVE", "default") + plan = runtime_discovery.discover_project(activated_repository) + assert (plan.manager, plan.environment) == ("hatch", "default") + content = (activated_repository / "pyproject.toml").read_bytes() + assert b"[tool.hatch.envs.default]" not in content + assert plan.inputs["pyproject.toml"] == content_digest(content) diff --git a/tests/unit/specfact_code_review/run/test_target_bootstrap.py b/tests/unit/specfact_code_review/run/test_target_bootstrap.py new file mode 100644 index 00000000..5726d4f7 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_bootstrap.py @@ -0,0 +1,582 @@ +"""Analyzer entry-point lookup rejects project-owned replacements.""" + +import json +import os +import shutil +import subprocess +import sys +import zipfile +from importlib.metadata import DistributionFinder +from pathlib import Path + +import pytest + +from specfact_code_review.run import target_bootstrap +from specfact_code_review.run.runtime_discovery import discover_project +from specfact_code_review.run.target_bootstrap import _project_python_command, python_execution_domain + + +def test_analyzer_entry_point_is_confined_to_verified_root(tmp_path: Path, monkeypatch) -> None: + analyzers = tmp_path / "analyzers" + analyzers.mkdir() + (analyzers / "owned.py").write_text('raise AssertionError("lookup must not execute")\n') + monkeypatch.setattr(target_bootstrap, "ANALYZERS", analyzers) + finder = target_bootstrap.AnalyzerFinder({"owned"}) + specification = finder.find_spec("owned") + assert specification is not None + assert specification.origin == str(analyzers / "owned.py") + assert finder.find_spec("project_dependency") is None + hostile = tmp_path / "hostile" + hostile.mkdir() + (hostile / "owned.py").write_text('raise AssertionError("must not execute")\n') + with pytest.raises(ImportError, match="origin_mismatch"): + finder.find_spec("owned", [str(hostile)]) + + +def test_python_cli_keeps_code_separate_from_bootstrap() -> None: + + code = 'from __future__ import annotations\nprint("project code")' + command, environment = _project_python_command(["-u", "-c", code]) + assert command[-3:] == ["-u", "-c", code] + assert environment["SPECFACT_PROJECT_PYTHON"] == "1" + assert environment["PYTHONPATH"] == "/opt/specfact/builtin/specfact_code_review/run" + + +def test_member_fallback_rejects_unrelated_analyzer_imports(tmp_path: Path, monkeypatch) -> None: + (tmp_path / "needed.py").write_text("VALUE = 1\n") + (tmp_path / "unrelated.py").write_text("VALUE = 2\n") + monkeypatch.setattr(target_bootstrap, "ANALYZERS", tmp_path) + finder = target_bootstrap.DomainFinder({"sealed_imports": ["needed"], "installed": []}) + assert finder.find_spec("needed") is not None + assert finder.find_spec("unrelated") is None + + +@pytest.fixture(name="collision_runtime") +def collision_runtime_fixture(tmp_path: Path) -> dict[str, Path]: + project = tmp_path / "project" + installed = project / "site-packages" + snapshot = tmp_path / "snapshot" + analyzers = tmp_path / "analyzers" + for directory in (installed, snapshot, analyzers): + directory.mkdir(parents=True) + (snapshot / "packaging.py").write_text('OWNER = "snapshot"\n') + (analyzers / "packaging.py").write_text('OWNER = "analyzer"\n') + (installed / "project_owned.py").write_text('OWNER = "project"\n') + (analyzers / "project_owned.py").write_text('OWNER = "wrong-analyzer"\n') + descriptor = { + "project": {"source_roots": ["."]}, + "inventory": { + "member_graphs": { + "pylint": { + "sealed_imports": ["packaging"], + "installed": [ + {"name": "packaging", "origin": "analyzer"}, + {"name": "project-owned", "origin": "project"}, + ], + } + } + }, + } + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + return {"PROJECT": project, "SNAPSHOT": snapshot, "ANALYZERS": analyzers} + + +def _probe_dependency_origins(paths: dict[str, Path], module: str, preload: str) -> subprocess.CompletedProcess[str]: + if preload == "pth": + (paths["PROJECT"] / "site-packages/preload.pth").write_text("import packaging; import project_owned\n") + script = f""" +import json, runpy, sys +from pathlib import Path +bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) +configure = bootstrap['_configure_runtime'] +configure.__globals__.update( + PROJECT=Path({str(paths["PROJECT"])!r}), + SNAPSHOT=Path({str(paths["SNAPSHOT"])!r}), + ANALYZERS=Path({str(paths["ANALYZERS"])!r}), +) +if {preload!r} == 'before': + sys.path.insert(0, {str(paths["SNAPSHOT"])!r}) + import packaging + sys.path.pop(0) +configure({module!r}) +preloaded = 'packaging' in sys.modules +import packaging, project_owned +print(json.dumps([packaging.OWNER, packaging.__file__, project_owned.OWNER, preloaded])) +""" + return subprocess.run( + [sys.executable, "-I", "-S", "-c", script], cwd=paths["SNAPSHOT"], capture_output=True, text=True, check=False + ) + + +@pytest.mark.parametrize( + "module,preload,expected_owner,origin_root", + [ + ("pylint", "none", "analyzer", "ANALYZERS"), + ("pylint", "pth", "analyzer", "ANALYZERS"), + ("project-python", "none", "snapshot", "SNAPSHOT"), + ("project-python", "pth", "snapshot", "SNAPSHOT"), + ("project-python", "before", "snapshot", "SNAPSHOT"), + ], +) +def test_analyzer_dependency_origin_survives_snapshot_collision_and_pth_preload( + collision_runtime: dict[str, Path], module: str, preload: str, expected_owner: str, origin_root: str +) -> None: + completed = _probe_dependency_origins(collision_runtime, module, preload) + assert completed.returncode == 0, completed.stderr + owner, origin, project_owner, preloaded = json.loads(completed.stdout) + assert owner == expected_owner + assert origin == str(collision_runtime[origin_root] / "packaging.py") + assert project_owner == "project" + assert preloaded is (preload != "none") + + +def test_analyzer_rejects_dependency_loaded_before_bootstrap(collision_runtime: dict[str, Path]) -> None: + completed = _probe_dependency_origins(collision_runtime, "pylint", "before") + assert completed.returncode != 0 + assert "project_worker_analyzer_origin_mismatch:packaging" in completed.stderr + + +@pytest.mark.parametrize( + "operation", + [ + "sys.meta_path[:] = [entry for entry in sys.meta_path if not hasattr(entry, 'names')]", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).names.clear()", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names')).find_spec = lambda *args: None", + "importlib.machinery.PathFinder.find_spec = lambda *args, **kwargs: None", + "builtins.__import__ = lambda *args, **kwargs: None", + ], +) +def test_executable_pth_cannot_change_protected_import_state( + collision_runtime: dict[str, Path], operation: str +) -> None: + pth = collision_runtime["PROJECT"] / "site-packages/changed-imports.pth" + pth.write_text(f"import sys, importlib.machinery, builtins; {operation}\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode != 0 + assert "project_worker_startup_import_state_changed" in completed.stderr + + +def _workspace_dependency(paths: dict[str, Path]) -> Path: + (paths["PROJECT"] / "site-packages/project_owned.py").unlink() + (paths["ANALYZERS"] / "project_owned.py").unlink() + workspace = paths["SNAPSHOT"] / "workspace/member" + workspace.mkdir(parents=True) + (workspace / "project_owned.py").write_text('OWNER = "project"\n') + return workspace + + +@pytest.mark.parametrize("placement", ["append", "prepend"]) +def test_executable_editable_finder_remains_available_behind_sealed_imports( + collision_runtime: dict[str, Path], placement: str +) -> None: + workspace = _workspace_dependency(collision_runtime) + installed = collision_runtime["PROJECT"] / "site-packages" + hook = f""" +import importlib.machinery, sys +class EditableFinder: + def find_spec(self, fullname, path=None, target=None): + roots = {{'project_owned': {str(workspace)!r}, 'packaging': {str(collision_runtime["SNAPSHOT"])!r}}} + if fullname in roots: + return importlib.machinery.PathFinder.find_spec(fullname, [roots[fullname]]) +def install(): + if {placement!r} == 'prepend': + sys.meta_path.insert(0, EditableFinder()) + else: + sys.meta_path.append(EditableFinder()) +""" + (installed / "editable_fixture.py").write_text(hook) + (installed / "editable.pth").write_text("import editable_fixture; editable_fixture.install()\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode == 0, completed.stderr + owner, origin, project_owner, _ = json.loads(completed.stdout) + assert owner == "analyzer" + assert origin == str(collision_runtime["ANALYZERS"] / "packaging.py") + assert project_owner == "project" + + +def test_path_only_pth_retains_rebased_workspace_import(collision_runtime: dict[str, Path]) -> None: + workspace = _workspace_dependency(collision_runtime) + pth = collision_runtime["PROJECT"] / "site-packages/workspace.pth" + pth.write_text(str(workspace) + "\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode == 0, completed.stderr + assert json.loads(completed.stdout)[2] == "project" + + +def test_executable_editable_namespace_path_hook_is_retained(collision_runtime: dict[str, Path]) -> None: + workspace = _workspace_dependency(collision_runtime) + installed = collision_runtime["PROJECT"] / "site-packages" + hook = f""" +import importlib.machinery, sys +PLACEHOLDER = '__editable__.fixture.__path_hook__' +class EditablePathFinder: + def find_spec(self, fullname, target=None): + if fullname == 'project_owned': + return importlib.machinery.PathFinder.find_spec(fullname, [{str(workspace)!r}]) +def path_hook(path): + if path == PLACEHOLDER: + return EditablePathFinder() + raise ImportError +def install(): + sys.path_hooks.append(path_hook) + sys.path.append(PLACEHOLDER) +""" + (installed / "editable_namespace.py").write_text(hook) + (installed / "namespace.pth").write_text("import editable_namespace; editable_namespace.install()\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode == 0, completed.stderr + assert json.loads(completed.stdout)[2] == "project" + + +def test_pth_paths_outside_runtime_roots_have_actionable_diagnostic( + collision_runtime: dict[str, Path], tmp_path: Path +) -> None: + outside = tmp_path / "outside-runtime" + outside.mkdir() + pth = collision_runtime["PROJECT"] / "site-packages/external.pth" + pth.write_text(str(outside) + "\n") + completed = _probe_dependency_origins(collision_runtime, "pylint", "none") + assert completed.returncode != 0 + assert "project_worker_startup_path_escape" in completed.stderr + assert "rebuild editable installs against the copied project" in completed.stderr + + +@pytest.mark.parametrize("form", ["module", "namespace", "symlink", "missing"]) +def test_sealed_dependency_rejects_unverified_origins(tmp_path: Path, monkeypatch, form: str) -> None: + analyzers = tmp_path / "analyzers" + hostile = tmp_path / "hostile" + analyzers.mkdir() + hostile.mkdir() + monkeypatch.setattr(target_bootstrap, "ANALYZERS", analyzers) + if form == "namespace": + (hostile / "needed").mkdir() + elif form != "missing": + (hostile / "needed.py").write_text("VALUE = 1\n") + if form == "symlink": + (analyzers / "needed.py").symlink_to(hostile / "needed.py") + finder = target_bootstrap.DomainFinder({"sealed_imports": ["needed"], "installed": []}) + search = [str(analyzers)] if form in {"symlink", "missing"} else [str(hostile)] + with pytest.raises(ImportError, match="project_worker_analyzer_origin_mismatch:needed"): + finder.find_spec("needed", search) + + +def test_member_metadata_lookup_cannot_expand_explicit_target_inventory(tmp_path: Path, monkeypatch) -> None: + + metadata = tmp_path / "needed-1.0.dist-info" + metadata.mkdir() + (metadata / "METADATA").write_text("Metadata-Version: 2.1\nName: needed\nVersion: 1.0\n") + monkeypatch.setattr(target_bootstrap, "ANALYZERS", tmp_path) + finder = target_bootstrap.DomainFinder( + { + "sealed_imports": ["needed"], + "installed": [{"name": "needed", "version": "1.0", "origin": "analyzer"}], + } + ) + context = DistributionFinder.Context(path=["/project-only"]) + assert not list(finder.find_distributions(context)) + default_context = DistributionFinder.Context(name="needed") + assert [dist.version for dist in finder.find_distributions(default_context)] == ["1.0"] + + +def test_nested_python_domain_uses_namespace_identity(tmp_path: Path, monkeypatch) -> None: + context = tmp_path / "context" + monkeypatch.setattr(target_bootstrap, "CONTEXT", context) + monkeypatch.setattr(target_bootstrap, "BUILTIN", Path(target_bootstrap.__file__).parents[2]) + context.symlink_to(Path(target_bootstrap.__file__)) + monkeypatch.setenv("SPECFACT_TARGET_PYTEST", "1") + assert python_execution_domain() == "project-python" + context.unlink() + context.symlink_to(Path(target_bootstrap.__file__).with_name("target_pytest.py")) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST") + assert python_execution_domain() == "pytest-observe" + + +@pytest.mark.parametrize( + "arguments, option", [(["-I", "-c", "pass"], "-I"), (["-uS", "script"], "-S"), (["-E", "-m", "pytest"], "-E")] +) +def test_python_startup_cannot_bypass_runtime(arguments, option) -> None: + with pytest.raises(RuntimeError, match=f"project_python_option_unsupported:{option}"): + target_bootstrap._project_python_command(arguments) + + +def _write_pytest_runtime(project: Path, snapshot: Path, configuration: Path | None = None) -> dict[str, Path]: + descriptor = { + "project": discover_project(snapshot, config_path=configuration).document(), + "inventory": {"member_graphs": {"pytest-observe": {"sealed_imports": [], "installed": []}}}, + } + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + return {"PROJECT": project, "SNAPSHOT": snapshot, "ANALYZERS": Path(pytest.__file__).parent.parent} + + +def _install_generated_package(project: Path) -> None: + """Install a local wheel whose generated output differs from the raw sources.""" + wheel = project / "capsule_origin-1.0-py3-none-any.whl" + files = { + "capsule_origin/__init__.py": "from .generated import ORIGIN\n", + "capsule_origin/generated.py": 'ORIGIN = "built"\n', + "capsule_origin-1.0.dist-info/METADATA": "Metadata-Version: 2.1\nName: capsule-origin\nVersion: 1.0\n", + "capsule_origin-1.0.dist-info/WHEEL": ( + "Wheel-Version: 1.0\nGenerator: test\nRoot-Is-Purelib: true\nTag: py3-none-any\n" + ), + } + record = "capsule_origin-1.0.dist-info/RECORD" + files[record] = "".join(f"{name},,\n" for name in [*files, record]) + with zipfile.ZipFile(wheel, "w") as archive: + for name, content in files.items(): + archive.writestr(name, content) + subprocess.run( + [ + sys.executable, + "-m", + "pip", + "install", + "--no-index", + "--no-deps", + "--disable-pip-version-check", + "--target", + str(project / "site-packages"), + str(wheel), + ], + check=True, + capture_output=True, + text=True, + ) + + +def _run_origin_pytest( + paths: dict[str, Path], *, capsule: bool, import_mode: str, selection: str = "tests/test_origin.py" +) -> subprocess.CompletedProcess[str]: + native_setup = f""" +import site +site.addsitedir({str(paths["PROJECT"] / "site-packages")!r}) +site.addsitedir({str(paths["ANALYZERS"])!r}) +""" + capsule_setup = f""" +bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) +configure = bootstrap['_configure_runtime'] +configure.__globals__.update({", ".join(f"{key}=Path({str(value)!r})" for key, value in paths.items())}) +configure('pytest-observe') +""" + script = f""" +import runpy, sys +from pathlib import Path +{capsule_setup if capsule else native_setup} +import pytest +raise SystemExit(pytest.main(['-q', '--import-mode={import_mode}', {selection!r}])) +""" + return subprocess.run( + [sys.executable, "-I", "-S", "-c", script], + cwd=paths["SNAPSHOT"], + capture_output=True, + text=True, + check=False, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1"}, + ) + + +@pytest.mark.parametrize("import_mode", ["prepend", "importlib"]) +@pytest.mark.parametrize("paths_mode", ["undeclared", "empty", "pytest", "explicit", "editable"]) +def test_target_pytest_preserves_built_or_explicit_source_imports( + tmp_path: Path, paths_mode: str, import_mode: str +) -> None: + project, snapshot = tmp_path / "runtime", tmp_path / "snapshot" + project.mkdir() + source = snapshot / "src with spaces" if paths_mode == "pytest" else snapshot / "src" + package = source / "capsule_origin" + package.mkdir(parents=True) + (package / "__init__.py").write_text('ORIGIN = "raw"\n') + (snapshot / "tests").mkdir() + _install_generated_package(project) + expected = "raw" if paths_mode in {"pytest", "explicit", "editable"} else "built" + (snapshot / "tests/test_origin.py").write_text( + f"import capsule_origin\ndef test_origin():\n assert capsule_origin.ORIGIN == {expected!r}\n" + ) + configuration = None + if paths_mode == "pytest": + (snapshot / "pytest.ini").write_text('[pytest]\npythonpath = "src with spaces"\n') + if paths_mode == "empty": + (snapshot / "pytest.ini").write_text("[pytest]\npythonpath =\n") + if paths_mode == "explicit": + configuration = snapshot / "review.toml" + configuration.write_text('source_roots = ["src"]\n') + if paths_mode == "editable": + # The selected environment's ordinary .pth editable mapping owns this path. + (project / "site-packages/capsule-editable.pth").write_text(str(source) + "\n") + # A real editable installation supplies source instead of the built package. + shutil.rmtree(project / "site-packages/capsule_origin") + paths = _write_pytest_runtime(project, snapshot, configuration) + if paths_mode != "explicit": + native = _run_origin_pytest(paths, capsule=False, import_mode=import_mode) + assert native.returncode == 0, native.stdout + native.stderr + observed = _run_origin_pytest(paths, capsule=True, import_mode=import_mode) + assert observed.returncode == 0, observed.stdout + observed.stderr + + +@pytest.mark.parametrize("pytest_child", [False, True]) +@pytest.mark.parametrize("invocation", ["code", "module", "script"]) +def test_native_python_path_zero_survives_runtime_startup(tmp_path: Path, pytest_child: bool, invocation: str) -> None: + project, snapshot = tmp_path / "runtime", tmp_path / "snapshot" + (project / "site-packages").mkdir(parents=True) + snapshot.mkdir() + source = snapshot / "tools" if invocation == "script" else snapshot + source.mkdir(exist_ok=True) + (source / "flat_module.py").write_text('VALUE = "native-path-zero"\n') + probe = "import flat_module\nprint(flat_module.VALUE)\n" + (source / "probe.py").write_text(probe) + _write_pytest_runtime(project, snapshot) + startup = tmp_path / "startup" + startup.mkdir() + context = tmp_path / "python-context" + token = "target_pytest.py" if pytest_child else "target_bootstrap.py" + context.symlink_to(Path(target_bootstrap.__file__).with_name(token)) + (startup / "sitecustomize.py").write_text(f""" +import runpy +from pathlib import Path +bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) +configure = bootstrap['_configure_runtime'] +configure.__globals__.update(PROJECT=Path({str(project)!r}), SNAPSHOT=Path({str(snapshot)!r}), + CONTEXT=Path({str(context)!r}), BUILTIN=Path({str(Path(target_bootstrap.__file__).parents[2])!r}), + ANALYZERS=Path({str(Path(pytest.__file__).parent.parent)!r})) +configure(bootstrap['python_execution_domain']()) +""") + arguments = {"code": ["-c", probe], "module": ["-m", "probe"], "script": [str(source / "probe.py")]}[invocation] + environment = {**os.environ, "SPECFACT_TARGET_PYTEST": "1" if pytest_child else "0"} + environment.pop("PYTHONPATH", None) + command = [sys.executable, "-s", *arguments] + native = subprocess.run(command, cwd=snapshot, env=environment, capture_output=True, text=True, check=False) + environment["PYTHONPATH"] = str(startup) + attached = subprocess.run(command, cwd=snapshot, env=environment, capture_output=True, text=True, check=False) + assert native.returncode == attached.returncode == 0, attached.stderr + assert native.stdout == attached.stdout == "native-path-zero\n" + assert not attached.stderr + + +def test_native_pytest_adds_flat_test_module_root_without_runtime_override(tmp_path: Path) -> None: + project, snapshot = tmp_path / "runtime", tmp_path / "snapshot" + (project / "site-packages").mkdir(parents=True) + snapshot.mkdir() + (snapshot / "flat_module.py").write_text("VALUE = 42\n") + (snapshot / "test_flat.py").write_text("import flat_module\ndef test_flat():\n assert flat_module.VALUE == 42\n") + paths = _write_pytest_runtime(project, snapshot) + for capsule in (False, True): + result = _run_origin_pytest(paths, capsule=capsule, import_mode="prepend", selection="test_flat.py") + assert result.returncode == 0, result.stdout + result.stderr + + +def _probe_member_dispatch(paths: dict[str, Path], module: str, operation: str) -> subprocess.CompletedProcess[str]: + builtin = paths["SNAPSHOT"] / "builtin" + observer = builtin / "specfact_code_review/run/target_pytest.py" + observer.parent.mkdir(parents=True) + observer.write_text('print("VERIFIED_DISPATCH")\n') + observer.with_name("target_pylint.py").write_text("import pylint\n") + (paths["ANALYZERS"] / "pylint.py").write_text('print("VERIFIED_DISPATCH")\n') + (paths["SNAPSHOT"] / "counterfeit.py").write_text('print("COUNTERFEIT_DISPATCH")\n') + descriptor_path = paths["PROJECT"] / "project-runtime.json" + descriptor = json.loads(descriptor_path.read_text()) + descriptor["inventory"]["member_graphs"]["pytest-observe"] = {"sealed_imports": [], "installed": []} + descriptor_path.write_text(json.dumps(descriptor)) + (paths["PROJECT"] / "site-packages/dispatch.pth").write_text( + f"import sys, runpy, pkgutil, io, builtins, importlib.util; {operation}\n" + ) + script = f""" +import runpy, sys +from pathlib import Path +bootstrap = runpy.run_path({str(Path(target_bootstrap.__file__))!r}) +bootstrap['main'].__globals__.update({", ".join(f"{key}=Path({str(value)!r})" for key, value in paths.items())}, + BUILTIN=Path({str(builtin)!r})) +sys.argv[:] = ['target_bootstrap.py', {module!r}] +bootstrap['main']() +""" + return subprocess.run( + [sys.executable, "-I", "-S", "-c", script], cwd=paths["SNAPSHOT"], capture_output=True, text=True, check=False + ) + + +@pytest.mark.parametrize( + "module,operation", + [ + ("pylint", "runpy.run_module = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pytest-observe", "runpy.run_path = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._run_code.__globals__['exec'] = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ("pylint", "runpy._TempModule.__enter__ = lambda *a: None"), + ("pylint", "runpy.run_module.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ("pylint", "runpy._run_code.__code__ = (lambda *a, **k: print('COUNTERFEIT_DISPATCH')).__code__"), + ( + "pytest-observe", + "runpy._get_code_from_file = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')", + ), + ( + "pytest-observe", + "pkgutil.read_code = lambda *a: compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')", + ), + ("pytest-observe", "io.open_code = lambda *a: io.BytesIO(b'print(\"COUNTERFEIT_DISPATCH\")')"), + ( + "pylint", + "importlib.util.find_spec = lambda name: importlib.util.spec_from_file_location(name, 'counterfeit.py')", + ), + ( + "pylint", + "importlib.machinery.SourceFileLoader.get_code = lambda *a: " + "compile('print(\"COUNTERFEIT_DISPATCH\")', '', 'exec')", + ), + ("pylint", "builtins.exec = lambda *a, **k: print('COUNTERFEIT_DISPATCH')"), + ( + "pytest-observe", + "next(entry for entry in sys.meta_path if hasattr(entry, 'names'))" + ".find_spec.__func__.__globals__['BUILTIN'] = __import__('pathlib').Path('unverified')", + ), + ], +) +def test_executable_pth_cannot_substitute_analyzer_dispatch( + collision_runtime: dict[str, Path], module: str, operation: str +) -> None: + completed = _probe_member_dispatch(collision_runtime, module, operation) + assert completed.returncode != 0, completed.stdout + assert "project_worker_startup_import_state_changed" in completed.stderr + assert "VERIFIED_DISPATCH" not in completed.stdout + + +@pytest.mark.parametrize("module", ["pylint", "pytest-observe"]) +def test_additive_startup_hooks_preserve_verified_member_dispatch( + collision_runtime: dict[str, Path], module: str +) -> None: + editable = collision_runtime["SNAPSHOT"] / "editable" + editable.mkdir() + completed = _probe_member_dispatch(collision_runtime, module, f"sys.path.append({str(editable)!r})") + assert completed.returncode == 0, completed.stderr + assert completed.stdout == "VERIFIED_DISPATCH\n" + + +@pytest.mark.parametrize("module", ["pylint", "pytest-observe"]) +def test_startup_rejection_has_distinct_exit_despite_analyzer_shaped_stdout( + collision_runtime: dict[str, Path], module: str +) -> None: + completed = _probe_member_dispatch(collision_runtime, module, "print('[]'); runpy._run_code = lambda *a, **k: None") + assert completed.returncode == 78 + assert completed.stdout == "[]\n" + assert "project_worker_startup_import_state_changed" in completed.stderr + + +def test_preloaded_analyzer_origin_failure_uses_startup_exit(collision_runtime: dict[str, Path]) -> None: + operation = ( + "spec = importlib.util.spec_from_file_location('packaging', 'packaging.py'); " + "module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module); " + "sys.modules['packaging'] = module; print('[]')" + ) + completed = _probe_member_dispatch(collision_runtime, "pylint", operation) + assert completed.returncode == 78 + assert completed.stdout == "[]\n" + assert "project_worker_analyzer_origin_mismatch:packaging" in completed.stderr + + +@pytest.mark.parametrize("owner", ["SourceFileLoader", "SourceFileLoader.__mro__[1]"]) +def test_loader_filename_substitution_cannot_redirect_verified_dispatch( + collision_runtime: dict[str, Path], owner: str +) -> None: + operation = f"importlib.machinery.{owner}.get_filename = lambda self, fullname=None: 'counterfeit.py'" + completed = _probe_member_dispatch(collision_runtime, "pylint", operation) + assert completed.returncode == 78, completed.stdout + completed.stderr + assert "project_worker_startup_import_state_changed" in completed.stderr + assert "VERIFIED_DISPATCH" not in completed.stdout + assert "COUNTERFEIT_DISPATCH" not in completed.stdout diff --git a/tests/unit/specfact_code_review/run/test_target_coverage.py b/tests/unit/specfact_code_review/run/test_target_coverage.py new file mode 100644 index 00000000..1fe23564 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_coverage.py @@ -0,0 +1,377 @@ +"""Native policy and reviewer coverage retain separate evidence and outcomes.""" + +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import target_coverage, target_pytest +from specfact_code_review.run.target_coverage import _request + + +_HELPER = Path(target_pytest.__file__).with_name("target_coverage.py") +_SCRIPT = r""" +import importlib.util, inspect, json, os, sys +from pathlib import Path +import pytest +helper, snapshot, output, extra = map(Path, sys.argv[1:]) +args = ["-q", "-p", "pytest_cov", *json.loads(extra.read_text()), "tests"] +if helper.is_file(): + spec = importlib.util.spec_from_file_location("_specfact_target_coverage", helper) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + options = {"modules": json.loads(os.environ.get("TEST_COVERAGE_MODULES", "[]"))} if "modules" in inspect.signature(module.configure).parameters else {} + module.configure(snapshot=snapshot, output=output, directories=[], **options) + args[0:0] = ["-p", spec.name] + code = pytest.main(args) + evidence = module.evidence() +else: + # Exact pre-change injection: a valued snapshot scope and replacing JSON report. + args[-1:-1] = [f"--cov={snapshot}", f"--cov-report=json:{output}"] + code = pytest.main(args) + evidence = {"coverage": json.loads(output.read_text()) if output.exists() else {}, + "coverage_policy": {"mode": "legacy-forced", "native_threshold_failed": False}} +output.with_suffix(".receipt").write_text(json.dumps({"exit_code": int(code), **evidence})) +raise SystemExit(int(code)) +""" + + +def _project(tmp_path, *, native="", extra_config="", failure=False): + source = tmp_path / "project" + source.mkdir() + (source / "tests").mkdir() + (source / "app.py").write_text("def covered():\n return 1\ndef untouched():\n return 2\n") + (source / "unrelated.py").write_text("VALUE = 99\n") + expected = 0 if failure else 1 + (source / "tests/test_app.py").write_text( + f"from app import covered\ndef test_app():\n assert covered() == {expected}\n" + ) + (source / "pyproject.toml").write_text( + '[tool.pytest.ini_options]\npythonpath=["."]\n' + f"addopts={json.dumps(native)}\n" + '[tool.coverage.run]\nsource=["app"]\n' + "[tool.coverage.report]\nfail_under=95\nprecision=2\n" + extra_config + ) + return source + + +def _run(source, tmp_path, extra=(), *, startup=None, modules=()): + output = tmp_path / "private.json" + arguments = tmp_path / "arguments.json" + arguments.write_text(json.dumps(extra)) + completed = subprocess.run( + [sys.executable, "-c", _SCRIPT, str(_HELPER), str(source), str(output), str(arguments)], + cwd=source, + env={ + **os.environ, + "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", + "COVERAGE_FILE": str(tmp_path / ".coverage"), + "TEST_COVERAGE_MODULES": json.dumps(modules), + **({"PYTHONPATH": str(startup)} if startup else {}), + }, + text=True, + capture_output=True, + check=False, + timeout=60, + ) + receipt = output.with_suffix(".receipt") + assert receipt.is_file(), completed.stdout + completed.stderr + return completed, json.loads(receipt.read_text()) + + +def test_reviewer_does_not_activate_dormant_aggregate_policy(tmp_path): + completed, receipt = _run(_project(tmp_path), tmp_path) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage_policy"]["mode"] == "reviewer" + assert receipt["coverage_policy"]["threshold"] == 95 + assert receipt["coverage"]["totals"]["percent_covered"] < 95 + assert all("unrelated.py" not in path for path in receipt["coverage"]["files"]) + + +def test_native_threshold_failure_and_json_destination_remain_native(tmp_path): + source = _project(tmp_path, native="--cov --cov-report=json:customer.json") + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 1 + assert (source / "customer.json").is_file() + assert receipt["coverage_policy"]["mode"] == "native" + assert receipt["coverage_policy"]["native_threshold_failed"] is True + assert all("unrelated.py" not in path for path in receipt["coverage"]["files"]) + + +@pytest.mark.parametrize("options", ["--no-cov", "--cov --no-cov"]) +def test_explicit_coverage_disable_leaves_evidence_incomplete(tmp_path, options): + completed, receipt = _run(_project(tmp_path, native=options), tmp_path) + assert completed.returncode == 0 + assert receipt["coverage"] == {} + assert receipt["coverage_policy"]["mode"] == "disabled" + assert "--no-cov" in receipt["coverage_diagnostic"] + + +def test_native_no_report_control_does_not_disable_private_measurement(tmp_path): + source = _project(tmp_path, native="--cov --cov-report= --cov-fail-under=0") + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 + assert receipt["coverage"]["files"] + assert receipt["coverage_policy"]["mode"] == "native" + assert not (source / "htmlcov").exists() + + +def test_native_failure_report_suppression_remains_incomplete(tmp_path): + source = _project(tmp_path, native="--cov --no-cov-on-fail", failure=True) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 1 + assert receipt["coverage"] == {} + assert "--no-cov-on-fail" in receipt["coverage_diagnostic"] + + +def test_target_observer_does_not_override_native_coverage_arguments(tmp_path, monkeypatch): + runtime = tmp_path / "runtime" + runtime.mkdir() + (runtime / "project-runtime.json").write_text( + json.dumps( + { + "project": {"pytest_config": {"addopts": "--cov=app --cov-report=json:customer.json"}}, + "inventory": {"pytest_arguments": []}, + } + ) + ) + measurement = {"coverage": {}, "coverage_policy": {"mode": "reviewer"}, "coverage_threshold": 95} + configured = {} + collector = SimpleNamespace(configure=lambda **kwargs: configured.update(kwargs), evidence=lambda: measurement) + monkeypatch.setitem(sys.modules, "_specfact_target_coverage", collector) + monkeypatch.setattr(target_pytest, "ROOT", runtime) + monkeypatch.setattr(target_pytest, "SNAPSHOT_ROOT", tmp_path) + monkeypatch.setattr( + target_pytest, "Path", lambda value: Path(str(value).replace("/opt/specfact/tmp", str(tmp_path))) + ) + (runtime / "site-packages").mkdir() + (runtime / "site-packages/standalone.py").write_text("VALUE = 42\n") + monkeypatch.setattr( + sys, "argv", ["observer", json.dumps({"selectors": ["tests"], "coverage_modules": ["standalone"]})] + ) + captured = [] + + def run(arguments, *, plugins): + assert len(plugins) == 1 + captured.extend(arguments) + return 0 + + monkeypatch.setattr(pytest, "main", run) + with pytest.raises(SystemExit) as result: + target_pytest.main() + assert result.value.code == 0 + assert not any(token.startswith(("--cov=", "--cov-report=")) for token in captured) + assert "_specfact_target_coverage" in captured + assert configured["modules"] == ["standalone"] + observation = json.loads((tmp_path / "pytest-observation.json").read_text()) + assert observation["coverage_policy"] == measurement["coverage_policy"] + + +def test_excluded_measurement_preserves_success_and_incomplete_reason(tmp_path): + source = _project(tmp_path) + config = source / "pyproject.toml" + config.write_text(config.read_text().replace('source=["app"]', 'source=["not_installed_anywhere"]')) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 + assert receipt["coverage"] == {} + assert "No data to report" in receipt["coverage_diagnostic"] + + +def test_native_collect_only_does_not_claim_aggregate_failure(tmp_path): + source = _project(tmp_path, native="--cov --collect-only") + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 + assert receipt["coverage_policy"]["native_threshold_failed"] is False + + +def test_private_export_uses_native_topdir_after_customer_chdir(tmp_path): + source = _project(tmp_path) + (source / "tests/test_app.py").write_text( + 'import os\nfrom app import covered\ndef test_app():\n assert covered() == 1\n os.chdir("tests")\n' + ) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0, completed.stderr + assert receipt["coverage"]["files"]["app.py"]["executed_lines"] == [1, 2, 3] + + +@pytest.mark.parametrize("native", ["", "--cov --cov-fail-under=0"]) +def test_parallel_measurement_combines_real_workers(tmp_path, native): + source = _project(tmp_path, native=native) + (source / "tests/test_second.py").write_text( + "from app import untouched\ndef test_second():\n assert untouched() == 2\n" + ) + startup = _worker_startup(tmp_path) + completed, receipt = _run(source, tmp_path, ["-p", "xdist.plugin", "-n", "2"], startup=startup) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert "2 passed" in completed.stdout + assert receipt["coverage"]["files"]["app.py"]["executed_lines"] == [1, 2, 3, 4] + assert receipt["coverage"]["totals"]["percent_covered"] == 100 + assert receipt["coverage_policy"]["mode"] == ("native" if native else "reviewer") + + +def _worker_startup(tmp_path): + startup = tmp_path / "startup" + startup.mkdir() + (startup / "sitecustomize.py").write_text( + "import importlib.util,sys\n" + f'spec=importlib.util.spec_from_file_location("_specfact_target_coverage",{str(_HELPER)!r})\n' + "module=importlib.util.module_from_spec(spec)\nsys.modules[spec.name]=module\nspec.loader.exec_module(module)\n" + ) + return startup + + +def test_missing_parallel_coverage_receipt_stays_incomplete(tmp_path): + source = _project(tmp_path) + (source / "conftest.py").write_text( + "import pytest\n@pytest.hookimpl(tryfirst=True)\n" + "def pytest_testnodedown(node,error):\n" + ' if node.gateway.id == "gw0":\n node.workeroutput.pop("cov_worker_node_id",None)\n' + ) + completed, receipt = _run(source, tmp_path, ["-p", "xdist.plugin", "-n", "2"], startup=_worker_startup(tmp_path)) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage"] == {} + assert "coverage_worker_missing" in receipt["coverage_diagnostic"] + + +def test_configured_child_interpreter_retains_measured_lines(tmp_path): + source = _project(tmp_path) + config = source / "pyproject.toml" + config.write_text(config.read_text().replace('source=["app"]', 'source=["app"]\npatch=["subprocess"]')) + (source / "tests/test_app.py").write_text( + "import subprocess,sys\ndef test_app():\n" + ' child=subprocess.run([sys.executable,"-c","from app import covered; assert covered()==1"],check=False)\n' + " assert child.returncode == 0\n" + ) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage"]["files"]["app.py"]["executed_lines"] == [1, 2, 3] + + +def test_native_report_failure_is_precise_and_keeps_test_exit(tmp_path): + source = _project(tmp_path, native="--cov --cov-fail-under=0 --cov-report=json:occupied") + (source / "occupied").mkdir() + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 3, completed.stdout + completed.stderr + assert "IsADirectoryError" in completed.stdout + assert receipt["coverage_policy"]["native_threshold_failed"] is False + + +def test_native_report_warning_does_not_hide_unavailable_reporting(tmp_path): + source = _project(tmp_path, native="--cov", extra_config='omit=["*"]\n') + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 1 + assert receipt["coverage"] == {} + assert "NoDataError:No data to report" in receipt["coverage_diagnostic"] + assert "Failed to generate report: No data to report" in completed.stdout + + +def test_explicit_blocked_cov_plugin_stays_incomplete(tmp_path): + completed, receipt = _run(_project(tmp_path), tmp_path, ["-p", "no:pytest_cov"]) + assert completed.returncode == 0 + assert receipt["coverage"] == {} + assert "no:pytest_cov" in receipt["coverage_diagnostic"] + + +def test_unsupported_plugin_contract_is_actionable(tmp_path, monkeypatch): + target_coverage.configure(snapshot=tmp_path, output=tmp_path / "out.json", directories=[]) + monkeypatch.setattr(target_coverage.importlib.metadata, "version", lambda _name: "8.0.0") + target_coverage.pytest_load_initial_conftests(None, None, []) + receipt = target_coverage.evidence() + assert receipt["coverage"] == {} + assert "unsupported_pytest_cov_contract:8.0.0" in receipt["coverage_diagnostic"] + assert receipt["coverage_policy"]["native_threshold_failed"] is False + + +def test_verified_preload_does_not_break_strict_rewrite_warning_policy(tmp_path): + completed, receipt = _run(_project(tmp_path), tmp_path, ["-W", "error::pytest.PytestAssertRewriteWarning"]) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage"]["files"] + + +def test_effective_scope_receipt_reflects_native_cli_overrides(tmp_path): + source = _project(tmp_path, native="--cov=unrelated --cov-branch --cov-fail-under=0 --cov-precision=4") + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0 + configuration = receipt["coverage_configuration"] + assert configuration["run"]["source"] == ["app"] + assert configuration["effective"]["run"]["source"] == ["unrelated"] + assert configuration["effective"]["run"]["branch"] is True + assert configuration["effective"]["threshold"] == 0 + assert configuration["effective"]["precision"] == 4 + assert receipt["coverage_policy"]["mode"] == "native" + + +def _installed_module_project(tmp_path, scope, collision): + """Build a real installed module and retain the requested native source scope.""" + source = _project(tmp_path) + if collision: + (source / "standalone").mkdir() + site = tmp_path / "site-packages" + site.mkdir() + (site / "standalone.py").write_text("import foreign\ndef answer():\n return foreign.VALUE\n") + (site / "foreign.py").write_text("VALUE = 42\n") + (source / "tests/test_installed.py").write_text( + "from standalone import answer\ndef test_answer():\n assert answer() == 42\n" + ) + configuration = source / "pyproject.toml" + configuration.write_text(configuration.read_text().replace('source=["app"]\n', "")) + if scope == "configured": + configuration.write_text( + configuration.read_text().replace("[tool.coverage.run]", '[tool.coverage.run]\nsource=["app"]') + ) + return source, site + + +def _assert_module_selector_ambiguity(receipt): + """Ambiguous default selectors cannot claim coverage from the wrong directory.""" + assert receipt["coverage"] == {} + assert "project_pytest_coverage_module_selector_ambiguous:standalone" in receipt["coverage_diagnostic"] + assert "explicit coverage source" in receipt["coverage_diagnostic"] + + +def _assert_module_measurement_scope(receipt, *, included): + """Verify exact installed execution while excluding unrelated imported site code.""" + assert not receipt["coverage_diagnostic"] + measured = receipt["coverage"]["files"] + assert any(path.endswith("standalone.py") for path in measured) is included + assert not any(path.endswith("foreign.py") for path in measured) + if included: + row = next(row for path, row in measured.items() if path.endswith("standalone.py")) + assert row["executed_lines"] == [1, 2, 3] + + +@pytest.mark.parametrize("scope", ["default", "configured", "native"]) +@pytest.mark.parametrize("collision", [False, True], ids=["unambiguous", "directory-collision"]) +def test_top_level_installed_module_respects_reviewer_and_native_scope(tmp_path, scope, collision): + source, site = _installed_module_project(tmp_path, scope, collision) + extra = ("--cov=app", "--cov-fail-under=0") if scope == "native" else () + completed, receipt = _run(source, tmp_path, extra, startup=site, modules=["standalone"]) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage_policy"]["mode"] == ("native" if scope == "native" else "reviewer") + assert "2 passed" in completed.stdout + if scope == "default" and collision: + _assert_module_selector_ambiguity(receipt) + return + _assert_module_measurement_scope(receipt, included=scope == "default") + + +@pytest.mark.parametrize("modules", [None, "standalone", [1], ["pkg.module"], ["class"], ["../escape"]]) +def test_coverage_child_request_rejects_invalid_module_selectors(tmp_path, monkeypatch, modules): + monkeypatch.setenv( + "SPECFACT_REVIEW_COVERAGE_REQUEST", + json.dumps( + { + "snapshot": str(tmp_path), + "output": str(tmp_path / "coverage.json"), + "directories": [], + "modules": modules, + } + ), + ) + with pytest.raises(ValueError, match="project_pytest_coverage_request_invalid"): + _request() diff --git a/tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py b/tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py new file mode 100644 index 00000000..cb910f21 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_coverage_bootstrap.py @@ -0,0 +1,231 @@ +"""Reviewer coverage helpers stay confined to the authenticated pytest domain.""" + +import importlib.util +import json +import os +import subprocess +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace +from typing import cast + +import pytest + +from specfact_code_review.run import runtime_builder, target_bootstrap +from specfact_code_review.run.runtime_models import ProjectPlan, ProjectRuntimeError +from specfact_code_review.run.target_bootstrap import _load_pytest_coverage + + +@pytest.fixture(name="coverage_runtime") +def coverage_runtime_fixture(tmp_path: Path) -> dict[str, Path]: + roots = { + "PROJECT": tmp_path / "project", + "SNAPSHOT": tmp_path / "snapshot", + "ANALYZERS": tmp_path / "analyzers", + "BUILTIN": tmp_path / "builtin", + "CONTEXT": tmp_path / "context", + } + for name, path in roots.items(): + if name != "CONTEXT": + path.mkdir() + (roots["PROJECT"] / "site-packages").mkdir() + descriptor = { + "project": {"source_roots": ["."]}, + "inventory": {"member_graphs": {"pytest-observe": {"sealed_imports": [], "installed": []}}}, + } + (roots["PROJECT"] / "project-runtime.json").write_text(json.dumps(descriptor)) + helpers = roots["BUILTIN"] / "specfact_code_review/run" + helpers.mkdir(parents=True) + source = Path(target_bootstrap.__file__).parent + for name in ("target_bootstrap.py", "target_launch.py", "sitecustomize.py"): + text = (source / name).read_text() + for variable, path in roots.items(): + if name == "target_bootstrap.py": + original = getattr(target_bootstrap, variable) + text = text.replace(f'{variable} = Path("{original}")', f"{variable} = Path({str(path)!r})") + (helpers / name).write_text(text) + (helpers / "target_coverage.py").write_text('OWNER = "verified-builtin"\n') + (helpers / "target_pytest.py").write_text("import _specfact_target_coverage as collector\nprint(collector.OWNER)\n") + os.link(helpers / "target_pytest.py", roots["CONTEXT"]) + (roots["SNAPSHOT"] / "_specfact_target_coverage.py").write_text('OWNER = "customer-collision"\n') + roots["HELPERS"] = helpers + return roots + + +def _probe(roots: dict[str, Path], script: str) -> subprocess.CompletedProcess[str]: + bootstrap = roots["HELPERS"] / "target_bootstrap.py" + prefix = f"import runpy,sys; from pathlib import Path; b=runpy.run_path({str(bootstrap)!r}); " + return subprocess.run( + [sys.executable, "-I", "-S", "-c", prefix + script], + cwd=roots["SNAPSHOT"], + capture_output=True, + text=True, + check=False, + timeout=15, + ) + + +@pytest.mark.parametrize("route", ["first", "nested"]) +def test_pytest_domain_loads_fixed_helper_in_actual_startup_routes(coverage_runtime, route): + if route == "first": + script = "sys.argv=['bootstrap','pytest-observe']; b['main']()" + else: + script = ( + "import os;os.environ['SPECFACT_PROJECT_PYTHON']='1';" + f"runpy.run_path({str(coverage_runtime['HELPERS'] / 'sitecustomize.py')!r});" + "import _specfact_target_coverage as collector; print(collector.OWNER)" + ) + result = _probe(coverage_runtime, script) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "verified-builtin" + + +@pytest.mark.parametrize( + "script, expected", + [ + pytest.param( + "b['_configure_runtime']('project-python'); print('_specfact_target_coverage' in sys.modules)", + "False", + id="project-python-does-not-load-collector", + ), + pytest.param( + "load=b['_load_pytest_coverage']; print(load() is load())", + "True", + id="owned-collector-object-is-reused", + ), + ], +) +def test_coverage_helper_respects_domain_and_owned_object_lifecycle(coverage_runtime, script, expected): + result = _probe(coverage_runtime, script) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == expected + + +@pytest.mark.parametrize("forged_origin", ["different", "same"]) +def test_preloaded_coverage_alias_cannot_claim_builtin_ownership(coverage_runtime, forged_origin): + path = ( + coverage_runtime["HELPERS"] / "target_coverage.py" + if forged_origin == "same" + else Path("/customer/collector.py") + ) + script = ( + "import types,importlib.util; alias='_specfact_target_coverage'; fake=types.ModuleType(alias);" + f"fake.__file__={str(path)!r};fake.__spec__=importlib.util.spec_from_file_location(alias,{str(path)!r});" + "sys.modules[alias]=fake; b['_load_pytest_coverage']()" + ) + result = _probe(coverage_runtime, script) + assert result.returncode != 0 + assert "project_pytest_coverage_helper_origin_mismatch" in result.stderr + + +def test_failed_helper_load_does_not_leave_authenticated_alias(coverage_runtime): + (coverage_runtime["HELPERS"] / "target_coverage.py").write_text("raise RuntimeError('fixture failure')\n") + script = ( + "\ntry:\n b['_load_pytest_coverage']()\nexcept RuntimeError:\n" + " print('_specfact_target_coverage' in sys.modules)\n" + ) + result = _probe(coverage_runtime, script) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "False" + + +def test_changed_collector_bytes_invalidate_runtime_cache(tmp_path, monkeypatch): + helpers = tmp_path / "helpers" + helpers.mkdir() + for path in Path(runtime_builder.__file__).parent.glob("*.py"): + (helpers / path.name).write_bytes(path.read_bytes()) + helper = helpers / "target_coverage.py" + helper.write_text("POLICY = 1\n") + lock = Path("resources/contracts/pr-range-v1-toolchain-lock.json") + (tmp_path / lock).parent.mkdir(parents=True) + (tmp_path / lock).write_bytes((Path(runtime_builder.__file__).parents[1] / lock).read_bytes()) + monkeypatch.setattr(runtime_builder, "__file__", str(helpers / "runtime_builder.py")) + monkeypatch.setattr(runtime_builder, "capture_public_trust", lambda _root: b"fixture trust") + monkeypatch.setattr(runtime_builder, "git_identity", lambda: "fixed-git") + plan = ProjectPlan(tmp_path, manager="pip") + worker = SimpleNamespace(environment_id="linux-x86_64-cp312", identity="sha256:" + "a" * 64) + identities = [] + digest = runtime_builder.document_digest + + def record_identity(value): + identities.append(value) + return digest(value) + + monkeypatch.setattr(runtime_builder, "document_digest", record_identity) + for content in ("POLICY = 1\n", "POLICY = 2\n"): + helper.write_text(content) + with pytest.raises(ProjectRuntimeError, match="offline_cache_miss"): + runtime_builder.prepare_runtime(plan, runtime=worker, cache_root=tmp_path / "cache", offline=True) + assert digest(identities[0]) != digest(identities[1]) + assert identities[1]["builder"]["target_coverage.py"] == runtime_builder.content_digest(helper.read_bytes()) + + +@pytest.fixture(name="owned_helper") +def owned_helper_fixture(tmp_path, monkeypatch): + """Run the production loader with a contained fixture, restoring process state.""" + helper = tmp_path / "target_coverage.py" + helper.write_text('ORIGIN = "owned-fixture"\n') + alias = "_specfact_target_coverage" + missing = object() + previous = sys.modules.pop(alias, missing) + monkeypatch.setattr(target_bootstrap, "__file__", str(tmp_path / "target_bootstrap.py")) + monkeypatch.setattr(target_bootstrap, "_OWNED_COVERAGE_MODULES", {}) + try: + yield helper + finally: + sys.modules.pop(alias, None) + if previous is not missing: + sys.modules[alias] = cast(ModuleType, previous) + + +def test_production_loader_retains_actual_owned_module(owned_helper): + loaded = _load_pytest_coverage() + assert loaded.ORIGIN == "owned-fixture" + assert loaded.__file__ == str(owned_helper) + assert loaded.__spec__ is not None + assert loaded.__spec__.origin == str(owned_helper) + assert _load_pytest_coverage() is loaded + assert sys.modules["_specfact_target_coverage"] is loaded + + +@pytest.mark.parametrize("same_origin", [False, True]) +def test_production_loader_rejects_unowned_cached_module(owned_helper, same_origin): + alias = "_specfact_target_coverage" + origin = owned_helper if same_origin else owned_helper.with_name("customer.py") + forged = ModuleType(alias) + forged.__file__ = str(origin) + forged.__spec__ = importlib.util.spec_from_file_location(alias, origin) + sys.modules[alias] = forged + with pytest.raises(ImportError, match="coverage_helper_origin_mismatch"): + _load_pytest_coverage() + assert sys.modules[alias] is forged + + +@pytest.mark.parametrize("kind", ["missing", "symlink"]) +def test_production_loader_requires_regular_contained_helper(owned_helper, kind): + owned_helper.unlink() + if kind == "symlink": + alternate = owned_helper.with_name("alternate.py") + alternate.write_text("raise AssertionError('must not execute')\n") + owned_helper.symlink_to(alternate) + with pytest.raises(ImportError, match="coverage_helper_unavailable"): + _load_pytest_coverage() + assert "_specfact_target_coverage" not in sys.modules + + +def test_production_loader_removes_its_failed_module(owned_helper): + owned_helper.write_text("raise RuntimeError('fixture execution failed')\n") + with pytest.raises(RuntimeError, match="fixture execution failed"): + _load_pytest_coverage() + assert "_specfact_target_coverage" not in sys.modules + + +@pytest.mark.parametrize("field", ["__file__", "__spec__"]) +def test_production_loader_rejects_owned_origin_drift(owned_helper, field): + loaded = _load_pytest_coverage() + if field == "__file__": + loaded.__file__ = str(owned_helper.with_name("other.py")) + else: + loaded.__spec__ = None + with pytest.raises(ImportError, match="coverage_helper_origin_mismatch"): + _load_pytest_coverage() diff --git a/tests/unit/specfact_code_review/run/test_target_coverage_boundaries.py b/tests/unit/specfact_code_review/run/test_target_coverage_boundaries.py new file mode 100644 index 00000000..fff43235 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_coverage_boundaries.py @@ -0,0 +1,247 @@ +"""Exercise target coverage hooks in process using actual Coverage measurement and export.""" + +import importlib.util +import io +import json +import runpy +from pathlib import Path +from types import ModuleType, SimpleNamespace + +import coverage +import pytest + +from specfact_code_review.run import target_coverage + + +@pytest.fixture(name="coverage_boundary") +def fixture_coverage_boundary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """Provide a fresh public-hook instance and real measured source for each boundary.""" + spec = importlib.util.spec_from_file_location("boundary_target_coverage", target_coverage.__file__) + assert spec is not None and spec.loader is not None + helper = importlib.util.module_from_spec(spec) + spec.loader.exec_module(helper) + source = tmp_path / "source" + source.mkdir() + app = source / "app.py" + app.write_text("def covered():\n return 1\ndef untouched():\n return 2\n") + configuration = source / ".coveragerc" + configuration.write_text("[run]\nsource = .\n[report]\nfail_under = 80\nprecision = 2\n") + output = tmp_path / "private.json" + monkeypatch.chdir(source) + monkeypatch.setenv("SPECFACT_REVIEW_COVERAGE_REQUEST", "") + monkeypatch.setenv("COVERAGE_FILE", str(tmp_path / ".coverage")) + helper.configure(snapshot=source, output=output, directories=[]) + options = SimpleNamespace( + cov_config=str(configuration), + cov_source=[True], + cov_report={}, + cov_fail_under=None, + cov_precision=None, + no_cov=False, + no_cov_on_fail=False, + collectonly=False, + cov_branch=False, + cov_append=False, + cov_context=None, + ) + return SimpleNamespace( + helper=helper, + source=source, + app=app, + configuration=configuration, + output=output, + options=options, + manager=pytest.PytestPluginManager(), + ) + + +def _measure(boundary: SimpleNamespace, *, execute: bool = True) -> coverage.Coverage: + """Record real executed Python statements under the disposable project's native config.""" + measured = coverage.Coverage(config_file=str(boundary.configuration), data_file=None) + measured.start() + try: + if execute: + namespace = runpy.run_path(str(boundary.app)) + assert namespace["covered"]() == 1 + finally: + measured.stop() + return measured + + +def _native_plugin(boundary: SimpleNamespace, *, execute: bool = True) -> SimpleNamespace: + """Model pytest-cov's completed lifecycle with real Coverage data at its public boundary.""" + measured = _measure(boundary, execute=execute) + total = measured.report(file=io.StringIO()) if execute else None + options = SimpleNamespace(collectonly=False, no_cov_on_fail=False, cov_fail_under=80, cov_precision=2) + plugin = SimpleNamespace( + cov_controller=SimpleNamespace(cov=measured, topdir=str(boundary.source), failed_workers=[]), + cov_total=total, + failed=False, + options=options, + _should_report=lambda: True, + ) + boundary.manager.register(plugin, "_cov") + return plugin + + +def _start(boundary: SimpleNamespace) -> None: + """Invoke the ordinary early pytest hook with the actual native plugin manager.""" + config = SimpleNamespace(known_args_namespace=boundary.options, pluginmanager=boundary.manager) + boundary.helper.pytest_load_initial_conftests(config, None, []) + + +def _finish(boundary: SimpleNamespace, *, exit_code: int = 0, worker: bool = False) -> None: + """Publish the upstream session outcome through the exported observation hook.""" + config = SimpleNamespace(workerinput={}) if worker else SimpleNamespace() + boundary.helper.pytest_sessionfinish(SimpleNamespace(config=config, exitstatus=exit_code), exit_code) + + +def test_native_hooks_export_real_private_coverage_and_restore_cwd(coverage_boundary, tmp_path, monkeypatch) -> None: + """Native policy and actual JSON lines survive export after customer cwd changes.""" + boundary = coverage_boundary + plugin = _native_plugin(boundary) + _start(boundary) + assert boundary.helper.active_plugin() is plugin + _finish(boundary, exit_code=1) + elsewhere = tmp_path / "elsewhere" + elsewhere.mkdir() + monkeypatch.chdir(elsewhere) + receipt = boundary.helper.evidence() + assert Path.cwd() == elsewhere + assert receipt["coverage"] == json.loads(boundary.output.read_text()) + assert receipt["coverage"]["files"]["app.py"]["executed_lines"] == [1, 2, 3] + assert receipt["coverage_policy"]["native_threshold_failed"] is True + assert receipt["coverage_policy"]["measured_total"] == 75 + assert receipt["coverage_configuration"]["effective"]["threshold"] == 80 + assert receipt["coverage_diagnostic"] == "" + + +@pytest.mark.parametrize("configured", [True, False]) +def test_reviewer_hooks_measure_real_code_without_native_aggregate_failure(coverage_boundary, configured) -> None: + """The real owned pytest-cov collector exports evidence without activating native fail-under.""" + boundary = coverage_boundary + if not configured: + boundary.configuration.write_text("[report]\nfail_under = 80\nprecision = 2\n") + _start(boundary) + plugin = boundary.helper.active_plugin() + assert plugin is not None + try: + namespace = runpy.run_path(str(boundary.app)) + assert namespace["covered"]() == 1 + finally: + plugin.cov_controller.finish() + _finish(boundary) + receipt = boundary.helper.evidence() + assert receipt["coverage_policy"] == { + "mode": "reviewer", + "threshold": 80, + "precision": 2, + "measured_total": 75.0, + "native_threshold_failed": False, + } + assert receipt["coverage"]["files"]["app.py"]["missing_lines"] == [4] + + +@pytest.mark.filterwarnings("ignore:No data was collected:coverage.exceptions.CoverageWarning") +def test_empty_collection_retains_precise_coverage_diagnostic(coverage_boundary) -> None: + """A finished but empty native collector cannot fabricate coverage evidence.""" + boundary = coverage_boundary + boundary.configuration.write_text("[run]\ninclude = absent-file.py\n") + _native_plugin(boundary, execute=False) + _start(boundary) + _finish(boundary) + receipt = boundary.helper.evidence() + assert receipt["coverage"] == {} + assert "No data to report" in receipt["coverage_diagnostic"] + + +@pytest.mark.parametrize( + ("failure", "expected"), + [ + ("worker", "project_pytest_coverage_controller_observation_missing"), + ("no-session", "project_pytest_coverage_controller_observation_missing"), + ("failed-worker", "project_pytest_coverage_worker_missing"), + ("suppressed", "project_pytest_coverage_suppressed:--no-cov-on-fail"), + ], +) +def test_upstream_incomplete_lifecycle_cannot_export_success(coverage_boundary, failure, expected) -> None: + """Actual measured data cannot conceal missing workers or native report suppression.""" + boundary = coverage_boundary + plugin = _native_plugin(boundary) + plugin.cov_controller.failed_workers = ["gw0"] if failure == "failed-worker" else [] + plugin.failed = failure == "suppressed" + plugin.options.no_cov_on_fail = failure == "suppressed" + _start(boundary) + if failure != "no-session": + _finish(boundary, worker=failure == "worker") + receipt = boundary.helper.evidence() + assert receipt["coverage"] == {} + assert expected in receipt["coverage_diagnostic"] + assert not boundary.output.exists() + + +@pytest.mark.parametrize( + ("field", "value"), [("snapshot", "relative"), ("output", None), ("directories", ["relative"]), ("directories", {})] +) +def test_invalid_public_request_is_an_actionable_early_diagnostic(coverage_boundary, monkeypatch, field, value) -> None: + """Malformed request inputs fail before a collector can start or write an artifact.""" + boundary = coverage_boundary + request = {"snapshot": str(boundary.source), "output": str(boundary.output), "directories": []} + request[field] = value + monkeypatch.setenv("SPECFACT_REVIEW_COVERAGE_REQUEST", json.dumps(request)) + _start(boundary) + receipt = boundary.helper.evidence() + assert "project_pytest_coverage_request_invalid" in receipt["coverage_diagnostic"] + assert boundary.helper.active_plugin() is None + assert not boundary.output.exists() + + +def test_invalid_native_coverage_config_is_named_before_measurement(coverage_boundary) -> None: + """Coverage's real parser error survives the early-hook adapter.""" + boundary = coverage_boundary + boundary.configuration.write_text("[report]\nprecision = invalid-number\n") + _start(boundary) + receipt = boundary.helper.evidence() + assert "ConfigError" in receipt["coverage_diagnostic"] + assert "invalid-number" in receipt["coverage_diagnostic"] + assert str(boundary.configuration) in receipt["coverage_diagnostic"] + assert boundary.helper.active_plugin() is None + + +def test_unsupported_pytest_cov_contract_is_named_before_measurement(coverage_boundary, monkeypatch) -> None: + """An unsupported upstream lifecycle version cannot silently use a guessed collector.""" + boundary = coverage_boundary + monkeypatch.setattr(boundary.helper.importlib.metadata, "version", lambda _name: "99.0.0") + _start(boundary) + receipt = boundary.helper.evidence() + assert "unsupported_pytest_cov_contract:99.0.0" in receipt["coverage_diagnostic"] + assert boundary.helper.active_plugin() is None + assert not boundary.output.exists() + + +@pytest.mark.parametrize("disabled", ["option", "blocked", "missing-options"]) +def test_explicit_disabled_collector_names_the_native_control(coverage_boundary, disabled) -> None: + """Native plugin disablement remains an explicit incomplete-evidence result.""" + boundary = coverage_boundary + boundary.options.no_cov = disabled == "option" + if disabled == "blocked": + boundary.manager.set_blocked("pytest_cov") + if disabled == "missing-options": + del boundary.options.cov_source + _start(boundary) + receipt = boundary.helper.evidence() + expected = "--no-cov" if disabled == "option" else "-p no:pytest_cov" + assert expected in receipt["coverage_diagnostic"] + assert receipt["coverage_policy"]["mode"] == "disabled" + assert boundary.helper.active_plugin() is None + + +def test_unconfigured_public_hooks_do_not_invent_a_collector(coverage_boundary, monkeypatch) -> None: + """An absent request never starts measurement or manufactures an observation.""" + boundary = coverage_boundary + monkeypatch.delenv("SPECFACT_REVIEW_COVERAGE_REQUEST") + _start(boundary) + _finish(boundary) + helper: ModuleType = boundary.helper + assert helper.active_plugin() is None + assert helper.evidence()["coverage_diagnostic"] == "project_pytest_coverage_plugin_not_activated" diff --git a/tests/unit/specfact_code_review/run/test_target_coverage_options.py b/tests/unit/specfact_code_review/run/test_target_coverage_options.py new file mode 100644 index 00000000..04ffc3af --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_coverage_options.py @@ -0,0 +1,130 @@ +"""Real native coverage controls retain source scope, reports and per-file policy.""" + +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from specfact_code_review.run.runner import evaluate_portable_pytest_coverage +from tests.unit.specfact_code_review.run.test_target_coverage import _project, _run + + +def test_native_cov_reset_restores_configured_source(tmp_path: Path) -> None: + """Reset a preceding CLI source before bare --cov resolves the native config.""" + source = _project(tmp_path) + completed, receipt = _run(source, tmp_path, ["--cov=unrelated", "--cov-reset", "--cov", "--cov-fail-under=0"]) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert set(receipt["coverage"]["files"]) == {"app.py"} + assert receipt["coverage_configuration"]["cov_source"] is None + assert receipt["coverage_configuration"]["run"]["source"] == ["app"] + + +def test_native_custom_cov_config_preserves_filters_threshold_and_precision(tmp_path: Path) -> None: + """An explicit Coverage config replaces project defaults without scope expansion.""" + source = _project(tmp_path) + (source / "custom.coveragerc").write_text( + "[run]\ninclude = app.py, unrelated.py\nomit = unrelated.py\n[report]\nfail_under = 76.5\nprecision = 3\n" + ) + completed, receipt = _run(source, tmp_path, ["--cov", "--cov-config=custom.coveragerc", "--cov-report="]) + assert completed.returncode == 1 + assert set(receipt["coverage"]["files"]) == {"app.py"} + assert receipt["coverage_policy"] == { + "mode": "native", + "threshold": 76.5, + "precision": 3, + "measured_total": 75.0, + "native_threshold_failed": True, + } + configured = receipt["coverage_configuration"] + assert configured["cov_config"] == "custom.coveragerc" + assert configured["run"]["include"] == ["app.py", "unrelated.py"] + assert configured["run"]["omit"] == ["unrelated.py"] + + +@pytest.mark.parametrize("native", ["", "--cov --cov-fail-under=0"]) +def test_excluded_reviewed_source_remains_missing(tmp_path: Path, monkeypatch, native: str) -> None: + """Neither native nor reviewer measurement may override a configured source omission.""" + source = _project(tmp_path, native=native) + config = source / "pyproject.toml" + config.write_text(config.read_text().replace('source=["app"]', 'source=["."]\nomit=["app.py"]')) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert "app.py" not in receipt["coverage"]["files"] + monkeypatch.chdir(source) + findings = evaluate_portable_pytest_coverage([Path("app.py")], {**receipt, "rootpath": str(source)}) + assert len(findings) == 1 + assert "Coverage data missing for app.py" in findings[0].message + + +def test_customer_lcov_report_matches_direct_native_pytest_bytes(tmp_path: Path) -> None: + """The private export must leave the requested native report byte-identical.""" + source = _project(tmp_path, native="--cov --cov-fail-under=0 --cov-report=lcov:customer.lcov") + direct = subprocess.run( + [sys.executable, "-m", "pytest", "-q", "-p", "pytest_cov", "tests"], + cwd=source, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / "direct.coverage")}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + assert direct.returncode == 0, direct.stdout + direct.stderr + report = source / "customer.lcov" + expected = report.read_bytes() + report.unlink() + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert report.read_bytes() == expected + assert receipt["coverage"]["files"]["app.py"]["executed_lines"] == [1, 2, 3] + + +def _imbalanced_project(tmp_path: Path, selected: str, threshold: int) -> Path: + """Separate selected-file coverage from a larger independent module's aggregate.""" + source = _project(tmp_path, native=f"--cov --cov-fail-under={threshold} --cov-report=") + config = source / "pyproject.toml" + config.write_text(config.read_text().replace('source=["app"]', 'source=["app", "ballast"]')) + ballast = "\n".join(f"VALUE_{index} = {index}" for index in range(200)) + "\n" + if selected == "complete": + (source / "app.py").write_text("VALUE = 1\n") + ballast = "def untouched():\n" + "\n".join(f" value_{index} = {index}" for index in range(200)) + "\n" + elif selected == "ninety": + (source / "app.py").write_text( + "\n".join(f"VALUE_{index} = {index}" for index in range(17)) + + "\ndef untouched():\n value = 1\n return value\n" + ) + (source / "ballast.py").write_text(ballast) + call = "app.covered() == 1" if selected == "seventy_five" else "app is not None" + (source / "tests/test_app.py").write_text( + f"import app\nimport ballast\ndef test_app():\n assert {call}\n assert ballast is not None\n" + ) + return source + + +@pytest.mark.parametrize(("selected", "threshold", "required"), [("seventy_five", 0, 80), ("ninety", 95, 95)]) +def test_high_aggregate_does_not_hide_low_selected_file( + tmp_path: Path, monkeypatch, selected, threshold, required +) -> None: + """Real aggregate success still enforces max(80, configured threshold) per reviewed file.""" + source = _imbalanced_project(tmp_path, selected, threshold) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 0, completed.stdout + completed.stderr + assert receipt["coverage"]["totals"]["percent_covered"] > 95 + monkeypatch.chdir(source) + findings = evaluate_portable_pytest_coverage([Path("app.py")], {**receipt, "rootpath": str(source)}) + assert len(findings) == 1 + assert (findings[0].rule, findings[0].severity) == ("TEST_COVERAGE_LOW", "error") + assert f"required {required:.1f}%" in findings[0].message + + +def test_low_native_aggregate_remains_failure_with_fully_covered_selected_file(tmp_path: Path, monkeypatch) -> None: + """A passing selected-file gate cannot erase the separately observed native aggregate failure.""" + source = _imbalanced_project(tmp_path, "complete", 80) + completed, receipt = _run(source, tmp_path) + assert completed.returncode == 1 + assert receipt["coverage_policy"]["native_threshold_failed"] is True + assert receipt["coverage"]["files"]["app.py"]["summary"]["percent_covered"] == 100 + assert receipt["coverage"]["totals"]["percent_covered"] < 80 + monkeypatch.chdir(source) + assert not evaluate_portable_pytest_coverage([Path("app.py")], {**receipt, "rootpath": str(source)}) diff --git a/tests/unit/specfact_code_review/run/test_target_launch.py b/tests/unit/specfact_code_review/run/test_target_launch.py new file mode 100644 index 00000000..ae37dad7 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_launch.py @@ -0,0 +1,357 @@ +"""Target namespaces cannot write their supervisor results.""" + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from specfact_code_review.run import target_bootstrap, target_launch +from specfact_code_review.run.target_launch import target_command + + +@pytest.fixture(autouse=True) +def empty_member_domains(tmp_path, monkeypatch): + + (tmp_path / "project-runtime.json").write_text( + json.dumps( + { + "inventory": { + "member_graphs": { + name: {"sealed_imports": [], "installed": []} for name in ("pylint", "pytest-observe") + } + } + } + ) + ) + sealed = tmp_path / "sealed-empty" + sealed.mkdir() + monkeypatch.setattr(target_launch, "PROJECT", tmp_path) + monkeypatch.setattr(target_launch, "SEALED", sealed) + + +def test_target_worker_cannot_write_controller_output_or_share_pid_namespace() -> None: + + argv = target_command("pylint", ["app.py"]) + assert "--unshare-all" in argv + assert "--share-net" not in argv + assert argv[argv.index("--ro-bind") + 1 : argv.index("--ro-bind") + 3] == ["/", "/"] + assert argv[argv.index("/opt/specfact/output") - 1 : argv.index("/opt/specfact/output") + 1] == [ + "--tmpfs", + "/opt/specfact/output", + ] + assert "/opt/specfact/builtin/specfact_code_review/run/target_bootstrap.py" in argv + + +def test_project_python_preserves_extensionless_script_arguments(monkeypatch) -> None: + + captured = [] + monkeypatch.setattr(target_launch.sys, "argv", ["python", "manage", "--version"]) + monkeypatch.setattr(target_launch.os, "execv", lambda executable, argv: captured.append(argv)) + target_launch.main() + assert captured[0][-3:] == ["python-argv", "manage", "--version"] + + +def test_native_worker_uses_matching_loader_without_mutating_supervisor(tmp_path, monkeypatch) -> None: + + loader = tmp_path / "native/ld-linux-x86-64.so.2" + loader.parent.mkdir() + loader.touch() + monkeypatch.setattr(target_launch, "PROJECT", tmp_path) + command = target_launch.interpreter_command(["-I", "-S", "worker.py"]) + assert command == [ + str(loader), + "--library-path", + str(loader.parent), + "/opt/specfact/python/bin/python", + "-I", + "-S", + "worker.py", + ] + target = target_launch.target_command("pytest-observe", ["{}"]) + assert "--setenv" not in target[target.index("--library-path") :] + assert "LD_LIBRARY_PATH" not in target + + +def test_offline_target_has_private_localhost_resolution() -> None: + + command = target_command("pytest-observe", []) + assert "/opt/specfact/project-runtime/worker-config" in command + assert "/etc" in command + assert "--unshare-all" in command + assert "--share-net" not in command + + +def test_pytest_children_keep_private_coverage_and_their_member_domain(monkeypatch) -> None: + + def environment(command): + return {command[index + 1]: command[index + 2] for index, arg in enumerate(command) if arg == "--setenv"} + + parent = environment(target_command("pytest-observe", [])) + assert parent["COVERAGE_FILE"] == "/opt/specfact/tmp/.coverage" + assert parent["PYTEST_DISABLE_PLUGIN_AUTOLOAD"] == "1" + assert parent["SPECFACT_TARGET_PYTEST"] == "1" + monkeypatch.setenv("SPECFACT_TARGET_PYTEST", "1") + assert environment(target_command("python-argv", []))["SPECFACT_TARGET_PYTEST"] == "0" + assert environment(target_command("pylint", []))["SPECFACT_TARGET_PYTEST"] == "0" + + +def test_member_mounts_expose_only_recorded_distribution_files(tmp_path, monkeypatch) -> None: + + sealed = tmp_path / "sealed" + sealed.mkdir() + for name in ("pylint", "pylint-4.0.7.dist-info", "unrelated", "unrelated-1.0.dist-info"): + (sealed / name).mkdir() + (tmp_path / "project-runtime.json").write_text( + json.dumps( + { + "inventory": { + "member_graphs": { + "pylint": { + "sealed_imports": ["pylint"], + "installed": [{"name": "pylint", "origin": "analyzer"}], + } + } + } + } + ) + ) + monkeypatch.setattr(target_launch, "PROJECT", tmp_path) + monkeypatch.setattr(target_launch, "SEALED", sealed) + mounts = target_launch.member_mounts("pylint") + assert str(sealed / "pylint") in mounts + assert str(sealed / "pylint-4.0.7.dist-info") in mounts + assert str(sealed / "unrelated") not in mounts + assert str(sealed / "unrelated-1.0.dist-info") not in mounts + + +def _nested_child_policy(tmp_path, monkeypatch, location, remove_home): + """Execute a real child using the emitted launch policy; no Linux bwrap emulation claim.""" + + snapshot = tmp_path / "snapshot" + snapshot.mkdir() + cwd = tmp_path / location + cwd.mkdir(parents=True, exist_ok=True) + (cwd / "data.txt").write_text("parent-data") + monkeypatch.chdir(cwd) + monkeypatch.setenv("CUSTOMER_CONFIG", "caller-overlay") + monkeypatch.setenv("HOME", "caller-home") + if remove_home: + monkeypatch.delenv("HOME") + probe = ( + "import os,json; from pathlib import Path; " + "print(json.dumps([os.getenv('CUSTOMER_CONFIG'),os.getenv('HOME'),os.getcwd()," + "Path('data.txt').read_text() if Path('data.txt').exists() else None]))" + ) + observed = [] + + def execute(_executable, command): + environment = dict(os.environ) + directory = cwd + if "--clearenv" in command: + environment = {command[i + 1]: command[i + 2] for i, arg in enumerate(command) if arg == "--setenv"} + directory = snapshot + child = subprocess.run( + [sys.executable, "-c", probe], cwd=directory, env=environment, capture_output=True, text=True, check=True + ) + observed.append(json.loads(child.stdout)) + + monkeypatch.setattr(target_launch.sys, "argv", ["python", "-c", probe]) + monkeypatch.setattr(target_launch.os, "execv", execute) + target_launch.main() + return observed[0], str(cwd) + + +@pytest.mark.parametrize("location", ["snapshot/subdirectory", "private-tmp/case"]) +@pytest.mark.parametrize("remove_home", [False, True]) +def test_nested_python_preserves_caller_environment_cwd_and_files(tmp_path, monkeypatch, location, remove_home): + observed, cwd = _nested_child_policy(tmp_path, monkeypatch, location, remove_home) + assert observed == ["caller-overlay", None if remove_home else "caller-home", cwd, "parent-data"] + + +def test_first_entry_never_inherits_context_from_environment(monkeypatch): + monkeypatch.setenv("SPECFACT_PROJECT_PYTHON", "1") + monkeypatch.setenv("SPECFACT_TARGET_PYTEST", "1") + monkeypatch.setenv("CUSTOMER_CONFIG", "must-not-leak") + command = target_command("pylint", []) + assert "--clearenv" in command and "--unshare-all" in command + assert "CUSTOMER_CONFIG" not in command + assert command[command.index("--chdir") + 1] == "/opt/specfact/snapshot" + + +@pytest.mark.parametrize( + "domain,relative", + [ + ("project-python", "run/target_bootstrap.py"), + ("pytest-observe", "run/target_pytest.py"), + ("pylint", "run/target_pylint.py"), + ("basedpyright", "tools/basedpyright_runner.py"), + ("crosshair", "tools/contract_runner.py"), + ], +) +def test_nested_member_domain_survives_environment_replacement(tmp_path, monkeypatch, domain, relative): + + builtin = Path(target_launch.__file__).parents[2] + context = tmp_path / "context" + context.symlink_to(builtin / "specfact_code_review" / relative) + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + monkeypatch.setattr(target_launch, "BUILTIN", builtin, raising=False) + monkeypatch.delenv("SPECFACT_TARGET_PYTEST", raising=False) + assert target_launch.execution_domain() == domain + monkeypatch.setenv("SPECFACT_TARGET_PYTEST", "forged-role") + assert target_launch.execution_domain() == domain + + +def test_unrecognized_namespace_context_is_rejected(tmp_path, monkeypatch): + context = tmp_path / "context" + context.write_text("pytest-observe") + monkeypatch.setattr(target_launch, "CONTEXT", context, raising=False) + with pytest.raises(RuntimeError, match="project_python_context"): + target_launch.execution_domain() + + +def _native_nested_wrapper(tmp_path, domain): + """Relocate absolute capsule paths only; execute the actual launcher and bootstrap.""" + + builtin, project = tmp_path / "builtin", tmp_path / "runtime" + context, member = tmp_path / "context", tmp_path / "member" + member.mkdir() + (member / "analyzer_only.py").write_text('VALUE = "member-owned"\n') + installed = project / "site-packages" + installed.mkdir(parents=True) + (installed / "customer_dependency.py").write_text('VALUE = "project-owned"\n') + descriptor = { + "project": {"source_roots": []}, + "inventory": {"member_graphs": {domain: {"sealed_imports": ["analyzer_only"], "installed": []}}}, + } + (project / "project-runtime.json").write_text(json.dumps(descriptor)) + replacements = { + "/opt/specfact/project-runtime": str(project), + "/opt/specfact/builtin": str(builtin), + "/opt/specfact/config/python-context": str(context), + "/opt/specfact/config/member-analyzers": str(member), + "/opt/specfact/python/bin/python": sys.executable, + } + source_root = Path(target_bootstrap.__file__).parents[2] + files = { + *target_launch.DOMAIN_FILES.values(), + "specfact_code_review/run/target_launch.py", + "specfact_code_review/run/sitecustomize.py", + "specfact_code_review/run/target_coverage.py", + } + for relative in files: + content = (source_root / relative).read_text() + for original, replacement in replacements.items(): + content = content.replace(original, replacement) + destination = builtin / relative + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(content) + context.symlink_to(builtin / target_launch.DOMAIN_FILES[domain]) + wrapper = project / "bin/python" + wrapper.parent.mkdir() + wrapper.write_text( + f"#!{sys.executable}\nimport runpy\n" + f"runpy.run_path({str(builtin / 'specfact_code_review/run/target_launch.py')!r}, run_name='__main__')\n" + ) + wrapper.chmod(0o700) + return wrapper + + +@pytest.mark.parametrize("domain", ["project-python", "pytest-observe", "pylint", "basedpyright", "crosshair"]) +@pytest.mark.parametrize("environment", [{}, {"CUSTOMER_CONFIG": "caller-overlay", "HOME": "caller-home"}]) +def test_actual_nested_exec_preserves_environment_and_member_imports(tmp_path, domain, environment): + + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd = tmp_path / "private-tmp/case" + cwd.mkdir(parents=True) + (cwd / "data.txt").write_text("parent-data") + probe = ( + "import os,json,customer_dependency; from pathlib import Path; " + "print(json.dumps([os.getenv('HOME'),os.getcwd(),Path('data.txt').read_text()," + "customer_dependency.VALUE,os.getenv('CUSTOMER_CONFIG')]))" + ) + if domain != "project-python": + probe += "; import analyzer_only; assert analyzer_only.VALUE == 'member-owned'" + completed = subprocess.run( + [str(wrapper), "-c", probe], env=environment, cwd=cwd, capture_output=True, text=True, timeout=30, check=False + ) + assert completed.returncode == 0, completed.stderr + assert json.loads(completed.stdout) == [ + environment.get("HOME"), + str(cwd), + "parent-data", + "project-owned", + environment.get("CUSTOMER_CONFIG"), + ] + + +def _pythonpath_probe(tmp_path, path_case): + """Use a script away from cwd so PYTHONPATH empty components are observable.""" + cwd = tmp_path / "caller" + first, second = cwd / "first", cwd / "second" + first.mkdir(parents=True) + second.mkdir() + for directory, value in ((first, "first"), (second, "second"), (cwd, "cwd")): + (directory / "caller_module.py").write_text(f"VALUE = {value!r}\n") + (directory / "analyzer_only.py").write_text("VALUE = 'counterfeit'\n") + values = { + "absolute": os.pathsep.join((str(first), str(second))), + "relative": os.pathsep.join(("second", "first")), + "empty-component": os.pathsep.join(("missing", "", "first")), + "empty": "", + "omitted": None, + } + probe = tmp_path / "probe.py" + body = "import json,os\nvalues = [os.environ.get('PYTHONPATH')]\n" + if path_case not in {"empty", "omitted"}: + body += "import caller_module\nvalues.append(caller_module.VALUE)\n" + probe.write_text(body + "print(json.dumps(values))\n") + return cwd, probe, values[path_case] + + +@pytest.mark.parametrize("domain", ["project-python", "pylint"]) +@pytest.mark.parametrize("path_case", ["absolute", "relative", "empty-component", "empty", "omitted"]) +@pytest.mark.parametrize("inherited", [False, True]) +def test_nested_pythonpath_matches_native_imports(tmp_path, monkeypatch, domain, path_case, inherited): + wrapper = _native_nested_wrapper(tmp_path, domain) + cwd, probe, pythonpath = _pythonpath_probe(tmp_path, path_case) + monkeypatch.delenv("PYTHONHOME", raising=False) + monkeypatch.delenv("PYTHONPATH", raising=False) + if pythonpath is not None: + monkeypatch.setenv("PYTHONPATH", pythonpath) + environment = None if inherited else ({} if pythonpath is None else {"PYTHONPATH": pythonpath}) + options = {"cwd": cwd, "env": environment, "capture_output": True, "text": True, "timeout": 30} + native = subprocess.run([sys.executable, "-s", str(probe)], check=False, **options) + assert native.returncode == 0, native.stderr + if domain == "pylint": + with probe.open("a") as stream: + stream.write("import analyzer_only\nassert analyzer_only.VALUE == 'member-owned'\n") + attached = subprocess.run([str(wrapper), str(probe)], check=False, **options) + assert attached.returncode == 0, attached.stderr + assert attached.stdout == native.stdout + + +def test_caller_path_prepend_does_not_invoke_project_container_hooks(tmp_path): + wrapper = _native_nested_wrapper(tmp_path, "pylint") + installed = wrapper.parents[1] / "site-packages" + (installed / "analyzer_only.py").write_text("VALUE = 'counterfeit'\n") + hook = ( + "class HookedPaths(list):\n" + " def __setitem__(self, key, value):\n" + " sys.meta_path.pop(0)\n" + " return super().__setitem__(key, value)\n" + "sys.path = HookedPaths(sys.path)\n" + ) + (installed / "container_hook.pth").write_text(f"import sys; exec({hook!r})\n") + result = subprocess.run( + [str(wrapper), "-c", "import analyzer_only; print(analyzer_only.VALUE)"], + env={}, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "member-owned" diff --git a/tests/unit/specfact_code_review/run/test_target_pylint.py b/tests/unit/specfact_code_review/run/test_target_pylint.py new file mode 100644 index 00000000..1314e145 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_pylint.py @@ -0,0 +1,121 @@ +"""Real Pylint uses attached namespace roots without replacing native configuration.""" + +import json +import subprocess +import sys +import tomllib +from pathlib import Path + +import pylint +import pytest + +from specfact_code_review.run import target_bootstrap + + +def _runtime(tmp_path: Path, roots: list[str], editable: bool) -> dict[str, Path]: + paths = {"PROJECT": tmp_path / "runtime", "SNAPSHOT": tmp_path / "snapshot"} + installed = paths["PROJECT"] / "site-packages" + installed.mkdir(parents=True) + files = { + "src/example/example.py": "LEAF = True\n", + "src/example/utils/__init__.py": "", + "src/example/utils/values.py": "VALUE = 1\n", + "tests/__init__.py": "", + "tests/test_values.py": "from example.utils.values import VALUE\n", + } + for name, content in files.items(): + path = paths["SNAPSHOT"] / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + if editable: + (installed / "example.pth").write_text(str(paths["SNAPSHOT"] / "src") + "\n") + descriptor = { + "project": {"source_roots": roots}, + "inventory": {"member_graphs": {"pylint": {"sealed_imports": ["pylint", "astroid"], "installed": []}}}, + } + (paths["PROJECT"] / "project-runtime.json").write_text(json.dumps(descriptor)) + paths["ANALYZERS"] = Path(pylint.__file__).parent.parent + paths["BUILTIN"] = Path(target_bootstrap.__file__).parents[2] + return paths + + +def _run(paths: dict[str, Path], arguments: list[str]) -> subprocess.CompletedProcess[str]: + path_values = {name: str(path) for name, path in paths.items()} + script = f""" +import multiprocessing, runpy, sys +from pathlib import Path +# This fixture has no built interpreter launcher; keep native subprocess startup. +multiprocessing.set_executable(sys.executable) +bootstrap = runpy.run_path({target_bootstrap.__file__!r}) +main = bootstrap['main'] +main.__globals__.update({{name: Path(value) for name, value in {path_values!r}.items()}}) +sys.argv = ['target_bootstrap', 'pylint', *{arguments!r}] +main() +""" + return subprocess.run( + [sys.executable, "-I", "-S", "-c", script], + cwd=paths["SNAPSHOT"], + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + +def _import_findings(paths: dict[str, Path], arguments: list[str]) -> list[dict]: + result = _run( + paths, + [ + "--output-format=json", + "--persistent=no", + "--disable=all", + "--enable=E0401,E0611", + *arguments, + "src/example/utils/values.py", + "tests/test_values.py", + ], + ) + assert result.returncode in (0, 2), result.stderr + return json.loads(result.stdout) + + +@pytest.mark.parametrize("roots,editable", [([], True), (["src"], False), (["."], True)]) +def test_attached_namespace_root_prevents_leaf_shadowing(tmp_path: Path, roots: list[str], editable: bool) -> None: + paths = _runtime(tmp_path, roots, editable) + assert _import_findings(paths, []) == [] + + +@pytest.mark.parametrize("configured", ['["src/example"]', "[]"]) +def test_native_config_overrides_runtime_fallback(tmp_path: Path, configured: str) -> None: + paths = _runtime(tmp_path, [], True) + (paths["SNAPSHOT"] / "pyproject.toml").write_text(f"[tool.pylint.main]\nsource-roots = {configured}\n") + assert {item["message-id"] for item in _import_findings(paths, [])} == {"E0401", "E0611"} + + +@pytest.mark.parametrize("arguments", [["--source-roots=src"], ["--source-roots", "src"]]) +def test_native_cli_overrides_repository_roots(tmp_path: Path, arguments: list[str]) -> None: + paths = _runtime(tmp_path, [], True) + (paths["SNAPSHOT"] / "pyproject.toml").write_text('[tool.pylint.main]\nsource-roots = ["src/example"]\n') + assert _import_findings(paths, arguments) == [] + + +def test_missing_import_is_preserved_with_attached_roots(tmp_path: Path) -> None: + paths = _runtime(tmp_path, [], True) + test = paths["SNAPSHOT"] / "tests/test_values.py" + test.write_text(test.read_text() + "import genuinely_missing_customer_dependency\n") + findings = _import_findings(paths, []) + assert len(findings) == 1 + assert findings[0]["message-id"] == "E0401" + assert "genuinely_missing_customer_dependency" in findings[0]["message"] + + +def test_built_runtime_does_not_infer_raw_source_root(tmp_path: Path) -> None: + paths = _runtime(tmp_path, [], False) + result = _run(paths, ["--generate-toml-config"]) + assert result.returncode == 0, result.stderr + assert tomllib.loads(result.stdout)["tool"]["pylint"]["main"].get("source-roots", []) == [] + + +def test_parallel_pylint_retains_runtime_roots(tmp_path: Path) -> None: + paths = _runtime(tmp_path, [], True) + assert _import_findings(paths, ["--jobs=2"]) == [] diff --git a/tests/unit/specfact_code_review/run/test_target_pytest.py b/tests/unit/specfact_code_review/run/test_target_pytest.py new file mode 100644 index 00000000..cf5bf225 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_pytest.py @@ -0,0 +1,99 @@ +"""Explicit pytest plugin modules must not be registered twice.""" + +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import target_pytest +from specfact_code_review.run.target_pytest import _installed_coverage_modules + + +def test_explicit_plugin_module_does_not_also_load_entry_point(monkeypatch) -> None: + distribution = SimpleNamespace( + metadata={"Name": "pytest-asyncio"}, + entry_points=[SimpleNamespace(group="pytest11", name="asyncio", value="pytest_asyncio.plugin")], + ) + monkeypatch.setattr(target_pytest.importlib.metadata, "distributions", lambda **kwargs: [distribution]) + assert "asyncio" not in target_pytest._plugins({"addopts": "-p pytest_asyncio.plugin"}) + assert "asyncio" in target_pytest._plugins({"addopts": "-ra"}) + assert "asyncio" not in target_pytest._plugins({"addopts": "-p no:asyncio"}) + + +def test_observer_retains_setup_failure_details() -> None: + observer = target_pytest.Observer() + observer.pytest_runtest_logreport( + SimpleNamespace( + nodeid="test_app.py::test_app", + when="setup", + outcome="failed", + longreprtext="TypeError: str expected, not NoneType", + failed=True, + ) + ) + assert observer.records[0]["detail"] == "TypeError: str expected, not NoneType" + observer.pytest_collectreport( + SimpleNamespace(nodeid="test_app.py", failed=True, longreprtext="ImportError: missing_plugin") + ) + assert observer.collection_errors == [{"nodeid": "test_app.py", "detail": "ImportError: missing_plugin"}] + + +def test_observer_retains_internal_plugin_errors() -> None: + observer = target_pytest.Observer() + observer.pytest_internalerror("socket.gaierror: localhost unavailable", None) + assert observer.internal_errors == ["socket.gaierror: localhost unavailable"] + + +def test_native_hatch_plugin_options_participate_in_explicit_selection() -> None: + descriptor = { + "project": {"pytest_config": {"addopts": "-ra"}}, + "inventory": {"pytest_arguments": ["-p", "no:randomly"]}, + } + assert target_pytest._effective_pytest_config(descriptor)["addopts"] == ["-ra", "-p", "no:randomly"] + + +def test_observer_retains_xdist_worker_startup_failure() -> None: + observer = target_pytest.Observer() + observer.pytest_testnodedown(None, "coverage storage is read-only") + assert observer.internal_errors == ["coverage storage is read-only"] + + +def test_disabled_autoload_preserves_only_explicit_plugins_and_coverage(monkeypatch) -> None: + packages = [ + SimpleNamespace( + metadata={"Name": name}, entry_points=[SimpleNamespace(group="pytest11", name=entry, value=value)] + ) + for name, entry, value in [ + ("pytest-asyncio", "asyncio", "pytest_asyncio.plugin"), + ("pytest-cov", "pytest_cov", "pytest_cov.plugin"), + ] + ] + monkeypatch.setattr(target_pytest.importlib.metadata, "distributions", lambda **_kwargs: packages) + for options in ["--disable-plugin-autoload", ["--disable-plugin-autoload"]]: + assert target_pytest._plugins({"addopts": options}) == ["-p", "pytest_cov"] + assert not target_pytest._plugins( + {"addopts": "--disable-plugin-autoload -p pytest_cov.plugin -p pytest_asyncio.plugin"} + ) + assert not target_pytest._plugins({"addopts": "--disable-plugin-autoload -p no:pytest_cov"}) + + +@pytest.mark.parametrize( + "modules", [None, "standalone", [1], ["../escape"], ["pkg.module"], ["class"], ["missing"], ["linked"]] +) +def test_installed_module_selector_rejects_unattached_or_invalid_names(tmp_path, monkeypatch, modules): + site = tmp_path / "site-packages" + site.mkdir() + external = tmp_path / "external.py" + external.write_text("VALUE = 1\n") + (site / "linked.py").symlink_to(external) + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + with pytest.raises(ValueError, match="project_pytest_installed_coverage_module_invalid"): + _installed_coverage_modules({"coverage_modules": modules}) + + +def test_installed_module_selector_accepts_only_regular_attached_module(tmp_path, monkeypatch): + site = tmp_path / "site-packages" + site.mkdir() + (site / "standalone.py").write_text("VALUE = 1\n") + monkeypatch.setattr(target_pytest, "ROOT", tmp_path) + assert _installed_coverage_modules({"coverage_modules": ["standalone"]}) == ["standalone"] + assert _installed_coverage_modules({}) == [] diff --git a/tests/unit/specfact_code_review/run/test_target_pytest_selection.py b/tests/unit/specfact_code_review/run/test_target_pytest_selection.py new file mode 100644 index 00000000..14818d21 --- /dev/null +++ b/tests/unit/specfact_code_review/run/test_target_pytest_selection.py @@ -0,0 +1,97 @@ +"""Native pytest selection determines the required execution inventory.""" + +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from specfact_code_review.run import target_pytest + + +OBSERVE_SCRIPT = """ +import importlib.util,json,sys +from pathlib import Path +import pytest +spec=importlib.util.spec_from_file_location('observer_module',sys.argv[1]) +module=importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +for name in ('pytest_xdist_node_collection_finished','pytest_testnodedown'): + pytest.hookimpl(optionalhook=True)(getattr(module.Observer,name)) +observer=module.Observer() +code=pytest.main(['test_case.py','-q'],plugins=[observer]) +Path('observation.json').write_text(json.dumps({ + 'exit_code':int(code),'collected':sorted(observer.collected), + 'deselected':sorted(getattr(observer,'deselected',())), + 'records':observer.records, +})) +""" + + +def _observe(root: Path, addopts: str, source: str) -> dict: + (root / "pytest.ini").write_text(f"[pytest]\naddopts = {addopts}\n") + (root / "test_case.py").write_text(source) + environment = {**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "PYTEST_ADDOPTS": ""} + completed = subprocess.run( + [sys.executable, "-c", OBSERVE_SCRIPT, str(Path(target_pytest.__file__).resolve())], + cwd=root, + env=environment, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + assert completed.returncode == 0, completed.stdout + completed.stderr + return json.loads((root / "observation.json").read_text()) + + +@pytest.mark.parametrize("addopts", ["-k keep", "--deselect=test_case.py::test_drop"]) +def test_native_addopts_deselection_does_not_require_removed_tests(tmp_path: Path, addopts: str) -> None: + observation = _observe(tmp_path, addopts, "def test_keep(): pass\ndef test_drop(): pass\n") + assert observation["exit_code"] == 0 + assert observation["collected"] == ["test_case.py::test_keep"] + assert observation["deselected"] == ["test_case.py::test_drop"] + assert {row["nodeid"] for row in observation["records"] if row["phase"] == "call"} == set(observation["collected"]) + + +def test_final_native_inventory_honors_plugin_collection_changes(tmp_path: Path) -> None: + (tmp_path / "conftest.py").write_text( + "def pytest_collection_modifyitems(items):\n" + " items[:] = [item for item in items if item.name == 'test_keep']\n" + ) + observation = _observe(tmp_path, "", "def test_keep(): pass\ndef test_drop(): pass\n") + assert observation["exit_code"] == 0 + assert observation["collected"] == ["test_case.py::test_keep"] + assert observation["deselected"] == ["test_case.py::test_drop"] + + +def test_interrupted_native_run_retains_unexecuted_selected_items(tmp_path: Path) -> None: + observation = _observe(tmp_path, "-x", "def test_first(): assert False\ndef test_later(): pass\n") + assert observation["exit_code"] == 1 + assert observation["collected"] == ["test_case.py::test_first", "test_case.py::test_later"] + assert observation["deselected"] == [] + assert {row["nodeid"] for row in observation["records"] if row["phase"] == "call"} == {"test_case.py::test_first"} + + +@pytest.mark.parametrize("local_finish_first", [True, False]) +def test_final_inventory_preserves_distributed_selected_items(local_finish_first: bool) -> None: + observer = target_pytest.Observer() + session = SimpleNamespace(items=[]) + if local_finish_first: + observer.pytest_collection_finish(session) + observer.pytest_xdist_node_collection_finished(None, ["test_case.py::test_keep"]) + observer.pytest_xdist_node_collection_finished(None, ["test_case.py::test_keep"]) + if not local_finish_first: + observer.pytest_collection_finish(session) + assert observer.collected == {"test_case.py::test_keep"} + assert not observer.deselected + + +def test_empty_distributed_selection_has_controller_only_deselection_inventory() -> None: + observer = target_pytest.Observer() + observer.pytest_xdist_node_collection_finished(None, []) + assert not observer.collected + assert observer.deselection_inventory == "controller_observed" diff --git a/tests/unit/specfact_code_review/test_capsule_tool_commands.py b/tests/unit/specfact_code_review/test_capsule_tool_commands.py index ef22a51e..ae4df22d 100644 --- a/tests/unit/specfact_code_review/test_capsule_tool_commands.py +++ b/tests/unit/specfact_code_review/test_capsule_tool_commands.py @@ -1,8 +1,14 @@ """Child analyzer commands retain sealed startup and private writable state.""" +import json +import subprocess +import sys +from pathlib import Path + import pytest from specfact_code_review import _review_utils +from specfact_code_review._review_utils import analyzer_command, development_runtime @pytest.mark.parametrize( @@ -42,3 +48,27 @@ def test_host_tool_commands_keep_existing_behavior() -> None: """Ordinary compatibility execution keeps the caller's executable and flags.""" for command in (["pylint", "example.py"], ["ruff", "check", "example.py"]): assert _review_utils.analyzer_command(command) == command + + +def test_development_snapshot_uses_its_verified_runtime_without_leaking_context() -> None: + + original = ["basedpyright", "--outputjson", "src/app.py"] + with development_runtime(Path("/private/development/.venv")): + command = analyzer_command(original) + assert "--venvpath" not in command + assert command[command.index("--pythonpath") + 1] == "/private/development/.venv/bin/python" + assert analyzer_command(original) == original + + +def test_development_context_runs_basedpyright_with_import_resolution(tmp_path) -> None: + + source = tmp_path / "app.py" + source.write_text( + "from icontract import require\n@require(lambda x: x > 0)\ndef positive(x: int) -> int:\n return x\n" + ) + with _review_utils.development_runtime(Path(sys.prefix)): + command = _review_utils.analyzer_command(["basedpyright", "--outputjson", str(source)]) + result = subprocess.run(command, cwd=tmp_path, text=True, capture_output=True, check=False) + assert result.returncode in (0, 1), result.stderr + report = json.loads(result.stdout) + assert not [item for item in report["generalDiagnostics"] if item["severity"] == "error"] diff --git a/tests/unit/specfact_code_review/tools/test_pylint_runner.py b/tests/unit/specfact_code_review/tools/test_pylint_runner.py index 58bcf83e..ee747216 100644 --- a/tests/unit/specfact_code_review/tools/test_pylint_runner.py +++ b/tests/unit/specfact_code_review/tools/test_pylint_runner.py @@ -5,14 +5,14 @@ from pathlib import Path from unittest.mock import Mock -from pytest import MonkeyPatch +from pytest import MonkeyPatch, mark from specfact_code_review.tools.pylint_runner import run_pylint from tests.unit.specfact_code_review.tools.helpers import assert_tool_run, completed_process def test_run_pylint_returns_empty_for_no_files() -> None: - assert run_pylint([]) == [] + assert not run_pylint([]) def test_run_pylint_maps_bare_except_to_architecture(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: @@ -167,76 +167,27 @@ def test_run_pylint_empty_stdout_truncates_long_stderr(tmp_path: Path, monkeypat assert f"... ({len(long_stderr)} chars total)" in findings[0].message -def test_run_pylint_coerces_line_zero_to_one(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: +@mark.parametrize( + ("fields", "expected"), + [ + ((0, "line too long"), (1, "line too long")), + ((-5, "line too long"), (1, "line too long")), + ((3, ""), (3, "(pylint provided no message text)")), + ((3, " \t\n "), (3, "(pylint provided no message text)")), + ], +) +def test_run_pylint_normalizes_report_fields( + tmp_path: Path, monkeypatch: MonkeyPatch, fields: tuple[int, str], expected: tuple[int, str] +) -> None: file_path = tmp_path / "target.py" - payload = [ - { - "message-id": "C0301", - "path": str(file_path), - "line": 0, - "message": "line too long", - } - ] - monkeypatch.setattr(subprocess, "run", Mock(return_value=completed_process("pylint", stdout=json.dumps(payload)))) - - findings = run_pylint([file_path]) - - assert len(findings) == 1 - assert findings[0].line == 1 - - -def test_run_pylint_coerces_empty_message_text(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - file_path = tmp_path / "target.py" - payload = [ - { - "message-id": "C0301", - "path": str(file_path), - "line": 3, - "message": "", - } - ] - monkeypatch.setattr(subprocess, "run", Mock(return_value=completed_process("pylint", stdout=json.dumps(payload)))) - - findings = run_pylint([file_path]) - - assert len(findings) == 1 - assert findings[0].message == "(pylint provided no message text)" - - -def test_run_pylint_coerces_negative_line_to_one(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - file_path = tmp_path / "target.py" - payload = [ - { - "message-id": "C0301", - "path": str(file_path), - "line": -5, - "message": "line too long", - } - ] + line, message = fields + payload = [{"message-id": "C0301", "path": str(file_path), "line": line, "message": message}] monkeypatch.setattr(subprocess, "run", Mock(return_value=completed_process("pylint", stdout=json.dumps(payload)))) findings = run_pylint([file_path]) assert len(findings) == 1 - assert findings[0].line == 1 - - -def test_run_pylint_coerces_whitespace_only_message(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: - file_path = tmp_path / "target.py" - payload = [ - { - "message-id": "C0301", - "path": str(file_path), - "line": 3, - "message": " \t\n ", - } - ] - monkeypatch.setattr(subprocess, "run", Mock(return_value=completed_process("pylint", stdout=json.dumps(payload)))) - - findings = run_pylint([file_path]) - - assert len(findings) == 1 - assert findings[0].message == "(pylint provided no message text)" + assert (findings[0].line, findings[0].message) == expected def test_run_pylint_parses_json_with_surrounding_whitespace(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: @@ -271,3 +222,19 @@ def test_run_pylint_returns_tool_error_for_invalid_payload_item(tmp_path: Path, assert len(findings) == 1 assert findings[0].category == "tool_error" assert findings[0].tool == "pylint" + + +def test_run_pylint_rejects_startup_failure_with_clean_json(tmp_path: Path, monkeypatch: MonkeyPatch) -> None: + diagnostic = "project_worker_startup_import_state_changed" + monkeypatch.setattr( + subprocess, + "run", + Mock(return_value=completed_process("pylint", stdout="[]", stderr=diagnostic, returncode=78)), + ) + + findings = run_pylint([tmp_path / "target.py"]) + + assert len(findings) == 1 + assert findings[0].category == "tool_error" + assert diagnostic in findings[0].message + assert "78" in findings[0].message diff --git a/tests/unit/specfact_code_review/tools/test_semgrep_runner.py b/tests/unit/specfact_code_review/tools/test_semgrep_runner.py index d3c0c3f1..4c35ef78 100644 --- a/tests/unit/specfact_code_review/tools/test_semgrep_runner.py +++ b/tests/unit/specfact_code_review/tools/test_semgrep_runner.py @@ -15,6 +15,7 @@ _parse_semgrep_results, _run_semgrep_command, _snip_stderr_tail, + _validate_semgrep_completion, find_semgrep_ai_bloat_config, find_semgrep_bugs_config, find_semgrep_config, @@ -259,6 +260,71 @@ def test_sealed_semgrep_rejects_structured_execution_errors(tmp_path: Path, monk assert "structured errors" in findings[0].message +@pytest.mark.parametrize("bug_pass", [False, True], ids=["clean", "bugs"]) +@pytest.mark.parametrize("returncode", [0, 1]) +def test_structured_semgrep_errors_retain_diagnostic_context( + tmp_path: Path, monkeypatch: MonkeyPatch, bug_pass: bool, returncode: int +) -> None: + """Both passes retain the native error without accepting partial execution.""" + bundle = tmp_path / "bundle" + (bundle / ".semgrep").mkdir(parents=True) + for name in ("clean_code", "bugs"): + (bundle / f".semgrep/{name}.yaml").write_text("rules: []\n", encoding="utf-8") + target = tmp_path / "target.py" + target.write_text("VALUE = 1\n", encoding="utf-8") + payload = { + "results": [], + "errors": [{"type": "ParseError", "code": 3, "message": "target parse failed", "source": "PRIVATE_SOURCE"}], + } + process = Mock(return_value=completed_process("semgrep", stdout=json.dumps(payload), returncode=returncode)) + monkeypatch.setattr(subprocess, "run", process) + + findings = (run_semgrep_bugs if bug_pass else run_semgrep)([target], bundle_root=bundle) + + assert len(findings) == 1 + assert (findings[0].category, findings[0].severity) == ("tool_error", "error") + assert "count=1" in findings[0].message + assert json.loads(findings[0].message.split("; details=", 1)[1]) == [ + {"type": "ParseError", "code": 3, "message": "target parse failed"} + ] + assert process.call_count == 2 + + +def test_structured_semgrep_error_diagnostics_are_bounded_and_escaped() -> None: + """Untrusted nested metadata and long control-filled messages stay bounded.""" + payload: dict[str, object] = { + "errors": [ + {"type": "ParseError", "code": 3, "message": "parse\n\t\x1b" + "x" * 1000}, + {"type": {"source": "PRIVATE_SOURCE"}, "code": ["PRIVATE_SOURCE"], "message": {"source": "PRIVATE_SOURCE"}}, + {"type": "z" * 1000, "code": "z" * 1000, "message": "\x00" * 1000}, + {"type": "FOURTH_ERROR", "message": "OMITTED_MESSAGE"}, + ] + } + + with pytest.raises(ValueError) as error: + _validate_semgrep_completion(completed_process("semgrep", stdout="", returncode=0), payload) + + diagnostic = str(error.value) + assert "count=4" in diagnostic + assert "omitted=1" in diagnostic + assert "PRIVATE_SOURCE" not in diagnostic + assert "FOURTH_ERROR" not in diagnostic + assert "OMITTED_MESSAGE" not in diagnostic + assert "\\n\\t\\u001b" in diagnostic + assert not any(ord(char) < 32 for char in diagnostic) + _assert_bounded_semgrep_details(diagnostic) + + +def _assert_bounded_semgrep_details(diagnostic: str) -> None: + """Validate the aggregate bound and the surviving allowlisted JSON fields.""" + assert len(diagnostic) <= 3900 + details = json.loads(diagnostic.split("; details=", 1)[1]) + assert len(details) == 3 + assert details[1] == {} + assert len(details[0]["message"]) <= 512 + assert len(details[2]["message"]) <= 512 + + @pytest.fixture(autouse=True) def _stub_semgrep_on_path(monkeypatch: MonkeyPatch) -> None: # pyright: ignore[reportUnusedFunction] real_which = shutil.which diff --git a/tests/unit/test_external_capsule_corpus.py b/tests/unit/test_external_capsule_corpus.py new file mode 100644 index 00000000..0c4d668b --- /dev/null +++ b/tests/unit/test_external_capsule_corpus.py @@ -0,0 +1,418 @@ +"""The external corpus accepts findings, but cannot accept incomplete execution.""" + +import hashlib +import importlib.util +import inspect +import json +import os +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + + +def _load(): + source = Path(__file__).parents[2] / "scripts/external_capsule_corpus.py" + spec = importlib.util.spec_from_file_location("external_capsule_corpus", source) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_external_gate_rejects_unknown_despite_fail_status() -> None: + module = _load() + with pytest.raises(ValueError, match="incomplete"): + module.assert_completed_report({"assurance_status": "FAIL", "has_unknown_required_evidence": True}) + + +def test_external_gate_requires_actual_test_execution() -> None: + module = _load() + with pytest.raises(ValueError, match="test execution"): + module.assert_completed_report({"assurance_status": "PASS", "analyzer_evidence": []}) + + +def test_external_gate_rejects_missing_analyzer_even_with_real_tests() -> None: + report = { + "assurance_status": "PASS", + "analyzer_evidence": [ + { + "id": "targeted-pytest-coverage", + "evidence_outcome": "PASS", + "target_execution": { + "collected": ["test_app.py::test_app"], + "records": [{"nodeid": "test_app.py::test_app", "phase": "call", "outcome": "passed"}], + "coverage": {"files": {"app.py": {}}}, + }, + } + ], + } + with pytest.raises(ValueError, match="roster"): + _load().assert_completed_report(report) + + +def test_reconstruction_materializes_real_source_without_mutating_template(tmp_path: Path) -> None: + module = _load() + fixture = tmp_path / "template" + fixture.mkdir() + template = fixture / "test_native.py.in" + template.write_text("def test_native():\n assert True\n") + destination = tmp_path / "checkout" + module.materialize_reconstruction(fixture, destination) + assert (destination / "test_native.py").read_text() == template.read_text() + assert not (destination / "test_native.py.in").exists() + assert template.is_file() + + +def test_corpus_attempts_remaining_repositories_after_a_failure(tmp_path: Path, monkeypatch) -> None: + + module = _load() + manifest = tmp_path / "corpus.json" + manifest.write_text(json.dumps({"python": ["3.12"], "repositories": [{"name": "first"}, {"name": "second"}]})) + monkeypatch.setattr(module, "MANIFEST", manifest) + monkeypatch.setattr(module.sys, "argv", ["corpus", "--workspace", str(tmp_path / "work")]) + monkeypatch.setattr(module.sys, "version_info", type("Version", (), {"major": 3, "minor": 12})()) + monkeypatch.setattr(module.platform, "system", lambda: "Linux") + monkeypatch.setattr(module.platform, "machine", lambda: "x86_64") + monkeypatch.setattr(module.os, "getuid", lambda: 1000) + attempted = [] + + def run_entry(entry, _workspace): + attempted.append(entry["name"]) + if entry["name"] == "first": + raise ValueError("controlled incomplete analysis") + + monkeypatch.setattr(module, "run_entry", run_entry) + assert module.main() == 1 + assert attempted == ["first", "second"] + + +def test_host_baseline_rejects_startup_failure_without_test_execution(tmp_path: Path) -> None: + with pytest.raises(ValueError, match=r"host.*execution"): + _load().assert_host_execution(tmp_path / "missing.xml") + + +def test_host_baseline_rejects_only_setup_errors(tmp_path: Path) -> None: + result = tmp_path / "tests.xml" + result.write_text( + '' + "" + ) + with pytest.raises(ValueError, match=r"host.*execution"): + _load().assert_host_execution(result) + + +def test_source_identity_detects_new_untracked_and_ignored_files(tmp_path: Path) -> None: + module = _load() + (tmp_path / ".git").mkdir() + (tmp_path / "source.py").write_text("VALUE = 1\n") + before = module.tracked_identity(tmp_path) + (tmp_path / "generated.py").write_text("VALUE = 2\n") + assert module.tracked_identity(tmp_path) != before + + +def test_network_evidence_excludes_loopback_and_reports_counter_deltas(tmp_path: Path) -> None: + module = _load() + counters = tmp_path / "net-dev" + counters.write_text( + "Inter-| Receive | Transmit\n" + " face |bytes packets errs drop fifo frame compressed multicast|" + "bytes packets errs drop fifo colls carrier compressed\n" + " lo: 900 0 0 0 0 0 0 0 800 0 0 0 0 0 0 0\n eth0: 120 0 0 0 0 0 0 0 70 0 0 0 0 0 0 0\n" + ) + before = module.network_counters(counters) + assert before == {"received_bytes": 120, "sent_bytes": 70} + counters.write_text(counters.read_text().replace("eth0: 120", "eth0: 220").replace("0 70 0", "0 90 0")) + assert module.network_delta(before, module.network_counters(counters)) == { + "received_bytes": 100, + "sent_bytes": 20, + "scope": "host_non_loopback_interface_counters", + } + assert module.network_counters(tmp_path / "unavailable") is None + + +def test_controlled_defect_preserves_separate_report(tmp_path: Path, monkeypatch) -> None: + module = _load() + root = tmp_path / "upstream" + (root / "tests").mkdir(parents=True) + (root / "src/customer").mkdir(parents=True) + evidence = tmp_path / "evidence" + evidence.mkdir() + (evidence / "cold-auto-report.json").write_text('{"untouched": true}') + + def execute(argv, **_context): + output = Path(argv[argv.index("--out") + 1]) + output.write_text( + json.dumps( + { + "findings": [ + {"rule": "TEST_OUTCOME_NOT_PASS"}, + {"tool": "basedpyright", "file": "specfact_controlled.py"}, + ] + } + ), + encoding="utf-8", + ) + return "" + + monkeypatch.setattr(module, "execute", execute) + monkeypatch.setattr(module, "assert_completed_report", lambda _report: None) + _invoke_controlled(module, root, evidence, tmp_path, _controlled_entry()) + assert (evidence / "controlled-defect-report.json").is_file() + assert json.loads((evidence / "cold-auto-report.json").read_text()) == {"untouched": True} + + +def _controlled_entry() -> dict: + """Identify the exact package context of the disposable regression project.""" + return { + "controlled_defect": {"source": "src/customer/specfact_controlled.py", "import": "customer.specfact_controlled"} + } + + +def _invoke_controlled(module, root, evidence, workspace, entry) -> None: + """Keep the new behavior regression callable against the original four-argument harness.""" + options = {"entry": entry} if "entry" in inspect.signature(module.controlled_defect).parameters else {} + module.controlled_defect(root, evidence, workspace / "config.toml", workspace, **options) + + +def _detected_controlled_findings() -> dict: + """Stub only the unrelated signed analyzer boundary during fixture-generation tests.""" + return {"findings": [{"rule": "TEST_OUTCOME_NOT_PASS"}, {"tool": "basedpyright", "file": "specfact_controlled.py"}]} + + +@pytest.mark.parametrize("scope", ["source_pkgs", "omit_only"]) +def test_controlled_fixture_executes_inside_unchanged_native_measurement(tmp_path, monkeypatch, scope) -> None: + """The generated test must really call the bad function under native coverage filters.""" + module = _load() + root = tmp_path / "upstream" + (root / "tests").mkdir(parents=True) + (root / "src/customer").mkdir(parents=True) + (root / "src/customer/__init__.py").write_text("") + (root / "pyproject.toml").write_text('[tool.pytest.ini_options]\npythonpath=["src"]\n') + configured = "source_pkgs = customer, tests" if scope == "source_pkgs" else "omit = customer/unused/*" + (root / ".coveragerc").write_text(f"[run]\n{configured}\n") + original = module.tracked_identity(root) + + def review(copy, _evidence, _config, paths, **_kwargs): + report = tmp_path / "native-coverage.json" + completed = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "pytest_cov", + "--cov", + f"--cov-report=json:{report}", + paths[1], + ], + cwd=copy, + env={**os.environ, "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1", "COVERAGE_FILE": str(tmp_path / ".coverage")}, + capture_output=True, + text=True, + check=False, + timeout=60, + ) + assert completed.returncode == 1, completed.stdout + completed.stderr + assert "SPECFACT_CONTROLLED_473" in completed.stdout + assert "injected wrong type" in completed.stdout + payload = json.loads(report.read_text()) + selected = "src/customer/specfact_controlled.py" + assert paths == [selected, "tests/test_specfact_controlled.py"] + assert payload["files"][selected]["executed_lines"] == [1, 2] + assert module.tracked_identity(root) == original + assert (copy / ".coveragerc").read_bytes() == (root / ".coveragerc").read_bytes() + return _detected_controlled_findings() + + monkeypatch.setattr(module, "review", review) + _invoke_controlled(module, root, tmp_path, tmp_path, _controlled_entry()) + + +@pytest.mark.parametrize( + "unsafe", ["parent-link", "test-parent-link", "source-exists", "test-exists", "destination-link"] +) +def test_controlled_fixture_rejects_unsafe_write_targets(tmp_path, monkeypatch, unsafe) -> None: + """Copy injection cannot follow customer links or overwrite existing fixture paths.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + outside = tmp_path / "outside" + outside.mkdir() + targets = {"parent-link": root / "src/customer", "test-parent-link": root / "tests"} + if unsafe in targets: + targets[unsafe].rmdir() + targets[unsafe].symlink_to(outside, target_is_directory=True) + elif unsafe == "destination-link": + (tmp_path / "controlled").symlink_to(outside, target_is_directory=True) + else: + name = ( + "src/customer/specfact_controlled.py" if unsafe == "source-exists" else "tests/test_specfact_controlled.py" + ) + (root / name).write_text("ORIGINAL = True\n") + original = module.tracked_identity(root) + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) + with pytest.raises(ValueError, match=r"controlled.*(path|destination)"): + _invoke_controlled(module, root, tmp_path, tmp_path, _controlled_entry()) + assert module.tracked_identity(root) == original + assert not list(outside.iterdir()) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("source", "../outside.py"), + ("source", "/outside.py"), + ("import", "customer;raise RuntimeError"), + ("import", "other.specfact_controlled"), + ("import", "customer.class"), + ], +) +def test_controlled_fixture_rejects_invalid_explicit_package_declaration(tmp_path, monkeypatch, field, value) -> None: + """An explicit corpus path and import must describe the same contained Python module.""" + module = _load() + root = tmp_path / "upstream" + (root / "src/customer").mkdir(parents=True) + (root / "tests").mkdir() + entry = _controlled_entry() + entry["controlled_defect"][field] = value + monkeypatch.setattr(module, "review", lambda *_args, **_kwargs: _detected_controlled_findings()) + with pytest.raises(ValueError, match=r"controlled.*(path|import)"): + _invoke_controlled(module, root, tmp_path, tmp_path, entry) + + +def test_pinned_corpus_records_explicit_controlled_package_context() -> None: + """Each frozen upstream and reconstruction has its own reviewed package destination.""" + module = _load() + manifest = json.loads(module.MANIFEST.read_text()) + entries = [*manifest["repositories"], *manifest["reconstructions"]] + expected = { + "requests": "requests", + "hatch": "hatch", + "flask": "flask", + "poetry": "poetry", + "hatch-detached": "portable_customer", + } + for entry in entries: + package = expected[entry["name"]] + assert entry.get("controlled_defect") == { + "source": f"src/{package}/specfact_controlled.py", + "import": f"{package}.specfact_controlled", + } + + +def test_offline_corpus_requires_provisioned_launcher(tmp_path: Path, monkeypatch) -> None: + module = _load() + monkeypatch.delenv("SPECFACT_CORPUS_OFFLINE_LAUNCHER", raising=False) + launcher = tmp_path / "root/opt/specfact/bin/bwrap-static" + launcher.parent.mkdir(parents=True) + launcher.write_bytes(b"not the signed launcher") + with pytest.raises(ValueError, match=r"administrator-provisioned.*launcher"): + module.offline_command(["specfact", "code", "review", "runtime", "prepare", "--offline"]) + + +def test_offline_corpus_rejects_customer_writable_launcher(tmp_path: Path, monkeypatch) -> None: + module = _load() + launcher = tmp_path / "bwrap" + launcher.write_text('#!/bin/sh\necho SPECFACT_OFFLINE_NETWORK_VERIFIED >&2\nexec "$@"\n') + launcher.chmod(0o755) + monkeypatch.setenv("SPECFACT_CORPUS_OFFLINE_LAUNCHER", str(launcher)) + monkeypatch.setattr(module.os, "readlink", lambda _path: "net:[fixture]") + with pytest.raises(ValueError, match="offline_launcher"): + module.offline_command(["specfact", "code", "review", "run"]) + + +def test_corpus_identity_detects_empty_directories(tmp_path: Path) -> None: + module = _load() + before = module.tracked_identity(tmp_path) + (tmp_path / "new-empty-directory").mkdir() + assert module.tracked_identity(tmp_path) != before + + +@pytest.mark.parametrize("owner,mode", [(1001, 0o100555), (0, 0o100775), (0, 0o100557), (0, 0o120777)]) +def test_offline_launcher_rejects_untrusted_ownership_or_mode(owner: int, mode: int) -> None: + module = _load() + path = SimpleNamespace(name="launcher", lstat=lambda: SimpleNamespace(st_uid=owner, st_mode=mode)) + with pytest.raises(ValueError, match="offline_launcher_untrusted"): + module._check_offline_path(path) + + +def test_offline_launcher_rejects_changed_provisioned_bytes(tmp_path: Path, monkeypatch) -> None: + module = _load() + launcher = tmp_path / "bwrap" + launcher.write_bytes(b"original provisioned executable") + launcher.chmod(0o555) + (tmp_path / "bwrap.sha256").write_text(hashlib.sha256(launcher.read_bytes()).hexdigest()) + monkeypatch.setattr(module, "OFFLINE_ROOT", tmp_path) + # Isolate the byte identity check; ownership/mode rejection is tested separately. + monkeypatch.setattr(module, "_check_offline_path", lambda *_args, **_kwargs: None) + assert module._verified_offline_launcher(str(launcher)) == launcher + launcher.chmod(0o755) + launcher.write_bytes(b"replacement") + with pytest.raises(ValueError, match="offline_launcher_untrusted:identity"): + module._verified_offline_launcher(str(launcher)) + + +def test_offline_provisioning_avoids_writable_hosted_runner_ancestor(monkeypatch) -> None: + module = _load() + payload = b"administrator-provisioned launcher fixture" + + def permissions(path): + mode = 0o100555 if path.name in {"bwrap", "bwrap.sha256"} else 0o40755 + if path == Path("/opt"): + mode = 0o40775 + return SimpleNamespace(st_uid=0, st_mode=mode) + + monkeypatch.setattr(Path, "lstat", permissions) + monkeypatch.setattr(Path, "read_bytes", lambda _path: payload) + monkeypatch.setattr(Path, "read_text", lambda _path, **_kwargs: hashlib.sha256(payload).hexdigest()) + monkeypatch.setattr(module.os, "access", lambda *_args: True) + launcher = module.OFFLINE_ROOT / "bwrap" + assert module._verified_offline_launcher(str(launcher)) == launcher + + +def _run_poetry_import_guard(tmp_path: Path, monkeypatch, phase: str, finding: dict) -> None: + module = _load() + entry = next(row for row in json.loads(module.MANIFEST.read_text())["repositories"] if row["name"] == "poetry") + evidence = tmp_path / "evidence/poetry" + evidence.mkdir(parents=True) + descriptor = tmp_path / "runtime/project-runtime.json" + descriptor.parent.mkdir() + descriptor.write_text("{}") + monkeypatch.setattr(module, "checkout", lambda *_args: None) + monkeypatch.setattr(module, "tracked_identity", lambda *_args: "unchanged") + monkeypatch.setattr(module, "host_test", lambda *_args: None) + monkeypatch.setattr(module, "controlled_defect", lambda *_args, **_kwargs: None) + monkeypatch.setattr(module, "execute", lambda *_args, **_kwargs: json.dumps({"descriptor": str(descriptor)})) + + def review(*_args, **kwargs): + actual_phase = "warm" if "descriptor" in kwargs else "cold" + return {"findings": [finding] if actual_phase == phase else []} + + monkeypatch.setattr(module, "review", review) + module.run_entry(entry, tmp_path) + + +@pytest.mark.parametrize("phase", ["cold", "warm"]) +@pytest.mark.parametrize("line", [7, 9, 10]) +@pytest.mark.parametrize("rule", ["E0401", "E0611"]) +def test_poetry_corpus_rejects_known_import_misresolution(tmp_path: Path, monkeypatch, phase, line, rule) -> None: + finding = {"tool": "pylint", "rule": rule, "file": "tests/utils/test_extras.py", "line": line} + with pytest.raises(ValueError, match="verified import"): + _run_poetry_import_guard(tmp_path, monkeypatch, phase, finding) + + +@pytest.mark.parametrize( + "finding", + [ + {"tool": "pylint", "rule": "E0401", "file": "tests/other.py", "line": 7}, + {"tool": "pylint", "rule": "E0401", "file": "tests/utils/test_extras.py", "line": 20}, + {"tool": "pylint", "rule": "W0611", "file": "tests/utils/test_extras.py", "line": 7}, + ], +) +def test_poetry_corpus_preserves_unrelated_findings(tmp_path: Path, monkeypatch, finding) -> None: + _run_poetry_import_guard(tmp_path, monkeypatch, "cold", finding) + assert json.loads((tmp_path / "evidence/poetry/acceptance.json").read_text())["status"] == "PASS"