diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index d86497b3f4..a9b57d284d 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -1430,6 +1430,38 @@ jobs: writable_npm_cache_dir="" writable_pnpm_store_dir="" + npm_lock_project_dir() { + local candidate_dir="$PWD" + + case "$candidate_dir" in + "$COVERAGE_SOURCE_WORKDIR"|"$COVERAGE_SOURCE_WORKDIR"/*) ;; + *) + echo "::error::npm package directory escaped the validated coverage worktree." >&2 + return 1 + ;; + esac + + while true; do + if [ -f "$candidate_dir/npm-shrinkwrap.json" ] \ + && [ ! -L "$candidate_dir/npm-shrinkwrap.json" ]; then + printf '%s\n' "$candidate_dir" + return 0 + fi + if [ -f "$candidate_dir/package-lock.json" ] \ + && [ ! -L "$candidate_dir/package-lock.json" ]; then + printf '%s\n' "$candidate_dir" + return 0 + fi + if [ "$candidate_dir" = "$COVERAGE_SOURCE_WORKDIR" ]; then + break + fi + candidate_dir="$(dirname "$candidate_dir")" + done + + echo "::error::No regular non-symlink npm lock was found at the package or validated workspace root." >&2 + return 1 + } + trusted_npm_lock_is_materialized() { local relative_dir local lock_name @@ -1639,9 +1671,11 @@ jobs: install_package_dependencies() { local package_runner="$1" + local npm_project_dir case "$package_runner" in npm) - if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then + if npm_project_dir="$(npm_lock_project_dir)"; then + pushd "$npm_project_dir" >/dev/null if ! trusted_npm_lock_is_materialized || ! prepare_writable_npm_cache; then append "### JavaScript/TypeScript dependencies (npm)" append "" @@ -1649,6 +1683,7 @@ jobs: append "- Reason: the current npm lock is not hash-bounded to the validated base or HEAD, or the trusted npm cache is unavailable." append "" failures=$((failures + 1)) + popd >/dev/null return 0 fi run_and_capture "JavaScript/TypeScript dependencies (npm offline ci, lifecycle hooks disabled)" \ @@ -1658,6 +1693,7 @@ jobs: --cache "$writable_npm_cache_dir" \ --no-audit \ --no-fund + popd >/dev/null else append "### JavaScript/TypeScript dependencies (npm)" append "" diff --git a/CHANGELOG.md b/CHANGELOG.md index bf192f6a9e..1e9b34026f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,9 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- Run npm workspace coverage installs from the nearest validated ancestor lock + while keeping tests scoped to the changed package; regular non-symlink lock + files remain hash-bounded by the existing materialization manifest. - **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.** The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`, `opencode-review.yml`, and `noema-review.yml` -- the three required-check diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b9b1c43de3..66c5d61bcd 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -100,7 +100,7 @@ required_workflow_bootstrap_has_if() { local bootstrap_file="$1" awk '/^ required-workflow-bootstrap:$/{p=1; print; next} p && /^ [A-Za-z0-9_-]+:/{exit} p' "$bootstrap_file" | - grep '^[[:space:]]*if:' >/dev/null + grep '^ if:' >/dev/null } seal_opencode_test_artifacts() { diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 321d25bd57..033f91a17d 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -627,6 +627,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "corepack pnpm fetch" in measure_step assert "--store-dir /opt/pnpm-store" in measure_step assert "chmod -R a+rX /opt/corepack /opt/npm-cache /opt/pnpm-store" in measure_step + assert "npm_lock_project_dir() {" in measure_step + assert 'pushd "$npm_project_dir" >/dev/null' in measure_step + assert "No regular non-symlink npm lock was found at the package or validated workspace root." in measure_step assert "trusted_npm_lock_is_materialized()" in measure_step assert ( 'head_blob="$(trusted_git rev-parse "${PR_HEAD_SHA}:${relative_lock}"' @@ -1581,6 +1584,64 @@ def test_opencode_coverage_discovers_changed_nested_javascript_package(tmp_path) assert result.stdout.splitlines() == ["ADFS 연동 라이브러리/Node.JS/Node App"] +def test_opencode_coverage_resolves_ancestor_npm_lock_for_workspace_package(tmp_path): + """Workspace coverage installs from the root lock while testing the nested package.""" + bash = shutil.which("bash") + if bash is None: + pytest.skip("bash is required for the extracted workflow function regression test") + + 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(" npm_lock_project_dir() {\n") + helper_end = measure_step.index( + "\n\n trusted_npm_lock_is_materialized()", helper_start + ) + shell = "\n".join( + ( + "set -euo pipefail", + textwrap.dedent(measure_step[helper_start:helper_end]), + "npm_lock_project_dir", + ) + ) + + repo = tmp_path / "repo" + package = repo / "apps" / "desktop" + package.mkdir(parents=True) + (repo / "package-lock.json").write_text("{}\n", encoding="utf-8") + env = os.environ.copy() + env["COVERAGE_SOURCE_WORKDIR"] = str(repo) + result = subprocess.run( + [bash, "-c", shell], + cwd=package, + env=env, + capture_output=True, + text=True, + timeout=10, + ) + + assert result.returncode == 0, result.stderr + assert result.stdout.splitlines() == [str(repo)] + + outside = tmp_path / "outside" + outside.mkdir() + escaped = subprocess.run( + [bash, "-c", shell], + cwd=outside, + env=env, + capture_output=True, + text=True, + timeout=10, + ) + assert escaped.returncode != 0 + assert "escaped the validated coverage worktree" in escaped.stderr + + def test_opencode_runtime_pin_supports_reasoning_options(): """Keep OpenCode runtime new enough to apply model-level reasoning settings.""" review_workflow = Path(".github/workflows/opencode-review-dispatch.yml").read_text( diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 2e733ac9e9..e88e9ed9db 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,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 = "d86497b3f43bebbabbb4f504eb5132cdf3b7b293" +REVIEW_DISPATCH_BLOB_SHA = "a9b57d284def1ff8f4f82880612583039ea8d52e" def _workflow_text(path: Path) -> str: