diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index ce7939845..dd65d90e1 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -1232,10 +1232,25 @@ jobs: } javascript_test_script_collects_coverage() { - jq -e '(.scripts.test // "") | test("(^|[[:space:]])--coverage([.=[:space:]]|$)|c8([[:space:]]|$)|nyc([[:space:]]|$)")' \ + jq -e '(.scripts.test // "") | test("(^|[[:space:]])--coverage([.=[:space:]]|$)|c8([[:space:]]|$)|nyc([[:space:]]|$)|istanbul([[:space:]]|$)")' \ package.json >/dev/null 2>&1 } + javascript_test_runner_accepts_coverage_flag() { + jq -e ' + (.scripts.test // "") as $test + | ((.devDependencies // {}) + (.dependencies // {})) as $dependencies + | ($test | test("(^|[[:space:]])jest([[:space:]]|$)")) + or ( + ($test | test("(^|[[:space:]])vitest([[:space:]]|$)")) + and ( + ($dependencies | has("@vitest/coverage-v8")) + or ($dependencies | has("@vitest/coverage-istanbul")) + ) + ) + ' package.json >/dev/null 2>&1 + } + declared_package_manager() { if [ -f package.json ]; then jq -r '.packageManager // "" | split("@")[0]' package.json 2>/dev/null || true @@ -1480,6 +1495,36 @@ jobs: writable_pnpm_store_dir="$destination" } + pnpm_supports_trust_lockfile() { + local pnpm_version + local pnpm_major + local pnpm_minor + pnpm_version="$(corepack pnpm --version 2>/dev/null || printf '0')" + pnpm_major="${pnpm_version%%.*}" + case "$pnpm_version" in + *.*) + pnpm_minor="${pnpm_version#*.}" + pnpm_minor="${pnpm_minor%%.*}" + ;; + *) + pnpm_minor=0 + ;; + esac + case "$pnpm_major" in + ''|*[!0-9]*) pnpm_major=0 ;; + esac + case "$pnpm_minor" in + ''|*[!0-9]*) pnpm_minor=0 ;; + esac + if [ "$pnpm_major" -gt 11 ]; then + return 0 + fi + if [ "$pnpm_major" -eq 11 ] && [ "$pnpm_minor" -ge 3 ]; then + return 0 + fi + return 1 + } + install_package_dependencies() { local package_runner="$1" case "$package_runner" in @@ -1514,13 +1559,22 @@ jobs: pnpm) trusted_pnpm_lock_matches_base prepare_writable_pnpm_store - run_and_capture "JavaScript/TypeScript dependencies (pnpm offline install, lifecycle hooks disabled)" \ - corepack pnpm install \ - --offline \ - --frozen-lockfile \ - --trust-lockfile \ - --ignore-scripts \ - --store-dir "$writable_pnpm_store_dir" + if pnpm_supports_trust_lockfile; then + run_and_capture "JavaScript/TypeScript dependencies (pnpm offline install, lifecycle hooks disabled)" \ + corepack pnpm install \ + --offline \ + --frozen-lockfile \ + --trust-lockfile \ + --ignore-scripts \ + --store-dir "$writable_pnpm_store_dir" + else + run_and_capture "JavaScript/TypeScript dependencies (pnpm offline install, lifecycle hooks disabled)" \ + corepack pnpm install \ + --offline \ + --frozen-lockfile \ + --ignore-scripts \ + --store-dir "$writable_pnpm_store_dir" + fi ;; yarn) run_and_capture "JavaScript/TypeScript dependencies (yarn install, lifecycle hooks disabled)" yarn install --immutable --mode=skip-builds @@ -2023,20 +2077,34 @@ jobs: run_package_script_and_capture "JavaScript/TypeScript coverage script" "$package_runner" coverage javascript_coverage_ran=1 elif jq -e '.scripts.test // empty' package.json >/dev/null; then - if javascript_test_script_collects_coverage; then - case "$package_runner" in - npm) run_and_capture "JavaScript/TypeScript test coverage" npm test ;; - pnpm) run_and_capture "JavaScript/TypeScript test coverage" corepack pnpm test ;; - yarn) run_and_capture "JavaScript/TypeScript test coverage" yarn test ;; - esac + if javascript_test_script_collects_coverage || javascript_test_runner_accepts_coverage_flag; then + if javascript_test_script_collects_coverage; then + case "$package_runner" in + npm) run_and_capture "JavaScript/TypeScript test coverage" npm test ;; + pnpm) run_and_capture "JavaScript/TypeScript test coverage" corepack pnpm test ;; + yarn) run_and_capture "JavaScript/TypeScript test coverage" yarn test ;; + esac + else + case "$package_runner" in + npm) run_and_capture "JavaScript/TypeScript test coverage" npm test -- --coverage ;; + pnpm) run_and_capture "JavaScript/TypeScript test coverage" corepack pnpm run test --coverage ;; + yarn) run_and_capture "JavaScript/TypeScript test coverage" yarn test --coverage ;; + esac + fi + javascript_coverage_ran=1 else case "$package_runner" in - npm) run_and_capture "JavaScript/TypeScript test coverage" npm test -- --coverage ;; - pnpm) run_and_capture "JavaScript/TypeScript test coverage" corepack pnpm run test --coverage ;; - yarn) run_and_capture "JavaScript/TypeScript test coverage" yarn test --coverage ;; + npm) run_and_capture "JavaScript/TypeScript tests (coverage provider not declared)" npm test ;; + pnpm) run_and_capture "JavaScript/TypeScript tests (coverage provider not declared)" corepack pnpm test ;; + yarn) run_and_capture "JavaScript/TypeScript tests (coverage provider not declared)" yarn test ;; esac + append "### JavaScript/TypeScript coverage threshold" + append "" + append "- Result: FAIL" + append "- Reason: package.json defines a test script but no compatible coverage provider or coverage-collecting test script. The networkless sandbox ran the tests without inventing an instrumenter, but plain tests cannot satisfy the required frontend coverage gate. Add a lock-pinned provider and a repository-owned coverage command." + append "" + failures=$((failures + 1)) fi - javascript_coverage_ran=1 else append "### JavaScript/TypeScript test coverage" append "" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b0ef8d44..29ddb10bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ Semantic Versioning where the repository publishes a release. during materialization and then rejecting every version except pnpm 11.5.3; route generic coverage and docstring package scripts through the same Corepack boundary instead of invoking a removed bare `pnpm` binary. +- Keep `--trust-lockfile` only for pnpm 11.3 and newer + (`trustLockfile` landed in pnpm 11.3). pnpm 9, 10, and 11.0–11.2 reject + that flag and previously failed LineageWeave JavaScript coverage before + tests could run. Jest test scripts still receive `--coverage` because Jest + documents a native coverage flag. +- Run declared JavaScript test scripts without synthesizing `--coverage` when + the package does not declare a compatible coverage command, but keep the + coverage result failed until the repository adds a lock-pinned provider and + owned coverage command. A generic `c8`, `nyc`, or Istanbul dependency no + longer makes an unrelated test runner receive an unsupported flag. - Fix OpenCode coverage evidence for exact-base, organization-owned Python VCS dependencies without weakening registry hashes or the networkless PR sandbox, reject namespace, ambiguous, linked, native-extension, and installed-metadata diff --git a/docs/doctoring/opencode-pnpm-lock-compat.md b/docs/doctoring/opencode-pnpm-lock-compat.md new file mode 100644 index 000000000..d4bf282e7 --- /dev/null +++ b/docs/doctoring/opencode-pnpm-lock-compat.md @@ -0,0 +1,88 @@ +# OpenCode coverage pnpm lock compatibility + +검토 기준일: **2026-08-23** + +## Decision + +OpenCode coverage-evidence honors the repository-owned `packageManager` +pin through Corepack. `--trust-lockfile` is valid only on pnpm 11.3 and +newer (`trustLockfile` landed in pnpm 11.3; Kochan, 2026). pnpm 11.0, +11.1, and 11.2 still reject the flag. Exact trusted-base lock matching +remains mandatory before any offline install. The sandbox never invents a +JavaScript coverage instrumenter when the package did not declare one, +except that a bare `jest` test script still receives Jest's documented +`--coverage` flag and a Vitest script receives it only when a compatible +Vitest coverage provider is lock-pinned. + +This keeps LineageWeave and other pnpm 9.x products measurable after +Corepack started activating the repository pin instead of a central +pnpm 11.5.3 binary. A paying reviewer of lineage reconstruction would +otherwise see every frontend head blocked on `Unknown option: +'trust-lockfile'` and, after that, on `vitest --coverage` without +`@vitest/coverage-v8`. + +## Root-cause analysis + +1. Coverage images now activate the exact `packageManager` from the + validated base (for LineageWeave, `pnpm@9.15.9`). +2. The install command still passed `--trust-lockfile`, a pnpm 11.3 flag. + pnpm 9, pnpm 10, and pnpm 11.0–11.2 exit before reading the store. +3. After a successful install, coverage appended `--coverage` to `vitest run` + even when no coverage provider was declared, so tests never became + evidence. + +The lock-matching gate is unchanged: a PR-added or PR-mutated +`pnpm-lock.yaml` is still refused. `--offline`, `--frozen-lockfile`, +`--ignore-scripts`, and the writable clone of `/opt/pnpm-store` remain +required. Python still never runs `uv sync --project`. + +## Remediation + +- When `corepack pnpm --version` reports 11.3 or newer (major greater + than 11, or major 11 with minor 3 or greater), keep `--trust-lockfile` + so registry attestation lookups stay suppressed for an exact + trusted-base lock (Kochan, 2026; pnpm, n.d.). +- When the version is below 11.3, omit that flag. pnpm 9, 10, and + 11.0–11.2 already treat `--frozen-lockfile` plus `--offline` as the + integrity boundary. +- When `package.json` has a test script but no coverage script or compatible + coverage-collecting runner, run the tests without synthesizing an undeclared + instrumenter and fail the coverage result with the next action: add a + lock-pinned provider and repository-owned coverage command. Plain passing + tests are not 100% frontend coverage evidence. +- A bare Jest script receives `--coverage` because Jest documents that native + flag (Jest, n.d.). Vitest receives `--coverage` only with + `@vitest/coverage-v8` or `@vitest/coverage-istanbul`. A generic `c8`, `nyc`, + or Istanbul dependency is not proof that an arbitrary runner accepts the + flag; those collectors count only when the test script actually invokes + them. + +Independent OpenCode, Strix, and Noema review remain authorization +gates. This change does not approve, merge, or weaken hash-pinned +Python or npm installs. + +## APA 7th references + +GitHub, Inc. (n.d.). *Using a package.json file to specify the package +manager*. GitHub Docs. Retrieved August 23, 2026, from +https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry + +MITRE. (2026). *CWE-494: Download of code without integrity check*. +https://cwe.mitre.org/data/definitions/494.html + +National Institute of Standards and Technology. (2022). *Secure software +development framework (SSDF) version 1.1: Recommendations for mitigating +the risk of software vulnerabilities* (NIST Special Publication 800-218). +https://doi.org/10.6028/NIST.SP.800-218 + +Jest. (n.d.). *Jest CLI options: --coverage*. Jest Docs. Retrieved +August 23, 2026, from https://jestjs.io/docs/cli#--coverageboolean + +Kochan, Z. (2026, May 24). *pnpm 11.3*. pnpm Blog. +https://pnpm.io/blog/releases/11.3 + +pnpm. (n.d.). *pnpm install*. pnpm Docs. Retrieved August 23, 2026, from +https://pnpm.io/cli/install + +Vitest. (n.d.). *Coverage*. Vitest Docs. Retrieved August 23, 2026, from +https://vitest.dev/guide/coverage diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index bf0a8693e..945eb3fb3 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -981,6 +981,11 @@ assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" ' assert_file_contains "$workflow_file" '"$COVERAGE_SOURCE_WORKDIR/$relative_lock"' "coverage hashes nested pnpm locks from the validated worktree root" assert_file_not_contains "$workflow_file" 'hash-object --no-filters -- "$relative_lock"' "coverage does not double-prefix nested package lock paths from the package working directory" assert_file_contains "$workflow_file" "--trust-lockfile" "coverage suppresses registry attestation lookups only for an exact trusted-base lock" + assert_file_contains "$workflow_file" "pnpm_supports_trust_lockfile()" "coverage gates --trust-lockfile on a helper that parses major and minor" + assert_file_contains "$workflow_file" '[ "$pnpm_major" -eq 11 ] && [ "$pnpm_minor" -ge 3 ]' "coverage omits --trust-lockfile on pnpm versions before 11.3" + assert_file_contains "$workflow_file" "javascript_test_runner_accepts_coverage_flag()" "coverage adds a native flag only for a compatible Jest or provider-backed Vitest runner" + assert_file_not_contains "$workflow_file" "javascript_coverage_provider_declared()" "coverage does not infer runner compatibility from an unused generic provider dependency" + assert_file_contains "$workflow_file" "plain tests cannot satisfy the required frontend coverage gate" "coverage fails closed when a package has no compatible coverage command" assert_file_contains "$workflow_file" "prepare_writable_pnpm_store()" "coverage prepares a sandbox-writable clone of the trusted pnpm store" assert_file_contains "$workflow_file" 'destination="$(mktemp -d /tmp/opencode-pnpm-store.XXXXXX)"' "coverage creates the writable pnpm store at an unpredictable root-owned path" assert_file_contains "$workflow_file" 'cp -R /opt/pnpm-store/. "$destination/"' "coverage clones packages from the trusted image seed" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index e00cc5214..8ee6e86fc 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1003,7 +1003,13 @@ def test_opencode_coverage_does_not_duplicate_existing_javascript_coverage(): measure_step = workflow[measure_start:measure_end] assert "javascript_test_script_collects_coverage()" in measure_step + assert "javascript_test_runner_accepts_coverage_flag()" in measure_step assert "if javascript_test_script_collects_coverage; then" in measure_step + assert ( + "if javascript_test_script_collects_coverage || javascript_test_runner_accepts_coverage_flag; then" + in measure_step + ) + assert "javascript_coverage_provider_declared" not in measure_step assert ( 'npm) run_and_capture "JavaScript/TypeScript test coverage" npm test ;;' in measure_step @@ -1016,14 +1022,164 @@ def test_opencode_coverage_does_not_duplicate_existing_javascript_coverage(): 'pnpm) run_and_capture "JavaScript/TypeScript test coverage" corepack pnpm run test --coverage ;;' in measure_step ) + assert ( + 'pnpm) run_and_capture "JavaScript/TypeScript tests (coverage provider not declared)" corepack pnpm test ;;' + in measure_step + ) assert "pnpm test --coverage" not in measure_step assert "pnpm test -- --coverage" not in measure_step assert 'test("(^|[[:space:]])--coverage([.=[:space:]]|$)' in measure_step - assert '|c8([[:space:]]|$)|nyc([[:space:]]|$)")' in measure_step + assert '|c8([[:space:]]|$)|nyc([[:space:]]|$)|istanbul([[:space:]]|$)")' in measure_step + assert 'test("(^|[[:space:]])jest([[:space:]]|$)"' in measure_step + assert 'test("(^|[[:space:]])vitest([[:space:]]|$)"' in measure_step assert "corepack pnpm install" in measure_step assert 'corepack pnpm --filter "$package_name" run build' in measure_step assert "corepack pnpm test" in measure_step assert "corepack pnpm run test --coverage" in measure_step + assert "pnpm_supports_trust_lockfile()" in measure_step + assert "if pnpm_supports_trust_lockfile; then" in measure_step + assert "[ \"$pnpm_major\" -gt 11 ]" in measure_step + assert "[ \"$pnpm_major\" -eq 11 ] && [ \"$pnpm_minor\" -ge 3 ]" in measure_step + assert "[ \"$pnpm_major\" -ge 11 ]" not in measure_step + + +@pytest.mark.parametrize( + ("test_script", "dev_dependencies", "expected_status"), + ( + ("jest", {}, 0), + ("vitest run", {"@vitest/coverage-v8": "4.1.10"}, 0), + ("vitest run", {"@vitest/coverage-istanbul": "4.1.10"}, 0), + ("vitest run", {}, 1), + ("node --test", {"c8": "10.1.3"}, 1), + ("mocha", {"nyc": "17.1.0"}, 1), + ("node --test", {"@vitest/coverage-v8": "4.1.10"}, 1), + ), +) +def test_opencode_coverage_only_adds_flag_for_compatible_runner( + tmp_path: Path, + test_script: str, + dev_dependencies: dict[str, str], + expected_status: int, +) -> None: + """A merely installed coverage package must not authorize runner flags.""" + bash = shutil.which("bash") + jq = shutil.which("jq") + if bash is None or jq is None: + pytest.skip("bash and jq are required for the extracted workflow regression") + + workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text( + encoding="utf-8" + ) + measure_start = workflow.index( + " - name: Measure test and docstring evidence\n" + ) + measure_end = workflow.index("\n - name:", measure_start + 1) + measure_step = workflow[measure_start:measure_end] + helper_start = measure_step.index( + " javascript_test_runner_accepts_coverage_flag() {\n" + ) + helper_end = measure_step.index("\n\n declared_package_manager()", helper_start) + helper = textwrap.dedent(measure_step[helper_start:helper_end]) + (tmp_path / "package.json").write_text( + json.dumps( + { + "scripts": {"test": test_script}, + "devDependencies": dev_dependencies, + } + ), + encoding="utf-8", + ) + + result = subprocess.run( + [bash, "-c", f"set -euo pipefail\n{helper}\njavascript_test_runner_accepts_coverage_flag"], + cwd=tmp_path, + capture_output=True, + text=True, + timeout=10, + ) + + assert result.returncode == expected_status, result.stderr + + +def test_opencode_missing_javascript_coverage_fails_closed() -> None: + """Plain tests cannot satisfy the organization's frontend coverage gate.""" + workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text( + encoding="utf-8" + ) + missing_provider_start = workflow.index( + 'run_and_capture "JavaScript/TypeScript tests (coverage provider not declared)"' + ) + missing_provider_end = workflow.index( + " else\n" + ' append "### JavaScript/TypeScript test coverage"', + missing_provider_start, + ) + missing_provider_block = workflow[missing_provider_start:missing_provider_end] + + assert 'append "- Result: FAIL"' in missing_provider_block + assert "failures=$((failures + 1))" in missing_provider_block + + +def test_opencode_coverage_gates_trust_lockfile_on_pnpm_11_3(tmp_path): + """Pass --trust-lockfile only when corepack pnpm --version is 11.3 or newer.""" + bash = shutil.which("bash") + if bash is None: + pytest.skip( + "bash is required for the extracted workflow function regression test" + ) + try: + subprocess.run( + [bash, "--version"], capture_output=True, text=True, timeout=5, check=True + ) + except (OSError, subprocess.SubprocessError) as exc: + pytest.skip(f"bash is not usable for this regression test: {exc}") + + workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text( + encoding="utf-8" + ) + measure_start = workflow.index( + " - name: Measure test and docstring evidence\n" + ) + measure_end = workflow.index("\n - name:", measure_start + 1) + measure_step = workflow[measure_start:measure_end] + helper_start = measure_step.index(" pnpm_supports_trust_lockfile() {\n") + helper_end = measure_step.index( + "\n\n install_package_dependencies()", helper_start + ) + helper = textwrap.dedent(measure_step[helper_start:helper_end]) + fake_corepack = tmp_path / "bin" + fake_corepack.mkdir() + (fake_corepack / "corepack").write_text( + "#!/bin/sh\nprintf '%s\\n' \"${FAKE_PNPM_VERSION:-0}\"\n", + encoding="utf-8", + ) + (fake_corepack / "corepack").chmod(0o755) + env = os.environ.copy() + env["PATH"] = f"{fake_corepack}{os.pathsep}{env.get('PATH', '')}" + + cases = ( + ("9.15.9", 1), + ("10.28.1", 1), + ("11.0.0", 1), + ("11.2.3", 1), + ("11.3.0", 0), + ("11.5.3", 0), + ("12.0.0", 0), + ("0", 1), + ) + for version, expected_status in cases: + env["FAKE_PNPM_VERSION"] = version + result = subprocess.run( + [bash, "-c", f"set -euo pipefail\n{helper}\npnpm_supports_trust_lockfile"], + env=env, + capture_output=True, + text=True, + timeout=10, + ) + assert result.returncode == expected_status, ( + f"pnpm {version} expected status {expected_status}, " + f"got {result.returncode}: {result.stderr}" + ) def test_opencode_coverage_discovers_changed_nested_javascript_package(tmp_path): @@ -2195,6 +2351,15 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert "--offline" in coverage_job assert "--frozen-lockfile" in coverage_job assert "--trust-lockfile" in coverage_job + assert "pnpm_supports_trust_lockfile()" in coverage_job + assert 'if pnpm_supports_trust_lockfile; then' in coverage_job + assert "[ \"$pnpm_major\" -gt 11 ]" in coverage_job + assert "[ \"$pnpm_minor\" -ge 3 ]" in coverage_job + assert "[ \"$pnpm_major\" -ge 11 ]" not in coverage_job + assert "javascript_test_runner_accepts_coverage_flag()" in coverage_job + assert "javascript_coverage_provider_declared" not in coverage_job + assert "coverage provider not declared" in coverage_job + assert "plain tests cannot satisfy the required frontend coverage gate" in coverage_job assert "--ignore-scripts" in coverage_job assert "prepare_writable_pnpm_store" in coverage_job assert '--store-dir "$writable_pnpm_store_dir"' in coverage_job diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index d0210b1ab..799b9e9fb 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -19,7 +19,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "ce7939845286be9668a01d5c640e867a8490ee5c" +REVIEW_DISPATCH_BLOB_SHA = "dd65d90e10e5040562b501ade1a40f89572f0984" def _workflow_text(path: Path) -> str: