diff --git a/.github/actions/ci-cli-coverage-shard/action.yaml b/.github/actions/ci-cli-coverage-shard/action.yaml index 19732972755..68738ae1c73 100644 --- a/.github/actions/ci-cli-coverage-shard/action.yaml +++ b/.github/actions/ci-cli-coverage-shard/action.yaml @@ -60,26 +60,7 @@ runs: RIPGREP_VERSION: "14.1.0-1" run: | set -euo pipefail - apt_network_options=( - -o Acquire::Retries=2 - -o Acquire::http::Timeout=20 - -o Acquire::https::Timeout=20 - ) - for attempt in 1 2 3; do - if sudo timeout --signal=TERM --kill-after=10s 120s apt-get \ - "${apt_network_options[@]}" update \ - -o Dir::Etc::sourcelist="sources.list.d/ubuntu.sources" \ - -o Dir::Etc::sourceparts="-"; then - break - fi - if [ "$attempt" -eq 3 ]; then - echo "::error::Could not update pinned Pi search-tool package indexes after three bounded attempts" - exit 1 - fi - echo "::warning::Retrying pinned Pi search-tool package index update (attempt $((attempt + 1))/3)" - done - sudo timeout --signal=TERM --kill-after=10s 180s apt-get \ - "${apt_network_options[@]}" install -y --no-install-recommends \ + bash "$GITHUB_ACTION_PATH/../ci-install-pinned-ubuntu-packages.sh" --retry-transient-update \ "fd-find=${FD_FIND_VERSION}" \ "ripgrep=${RIPGREP_VERSION}" diff --git a/.github/actions/ci-install-pinned-ubuntu-packages.sh b/.github/actions/ci-install-pinned-ubuntu-packages.sh new file mode 100755 index 00000000000..ced9cd5a273 --- /dev/null +++ b/.github/actions/ci-install-pinned-ubuntu-packages.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +update_attempts=1 +update_timeout=300s +install_timeout=300s +acquire_timeout=30 +if [ "${1:-}" = --retry-transient-update ]; then + update_attempts=3 + update_timeout=120s + install_timeout=180s + acquire_timeout=20 + shift +fi + +if [ "$#" -eq 0 ]; then + echo "::error title=Missing APT packages::Provide pinned package=version specifications." >&2 + exit 2 +fi +for package_spec in "$@"; do + if [[ ! "$package_spec" =~ ^[A-Za-z0-9][A-Za-z0-9.+-]*(:[A-Za-z0-9][A-Za-z0-9.+-]*)?=[^[:space:]=]+$ ]]; then + echo "::error title=Unpinned APT package::Expected package=version." >&2 + exit 2 + fi +done + +ubuntu_sources=/etc/apt/sources.list.d/ubuntu.sources +if ! sudo test -f "$ubuntu_sources" || ! sudo test -r "$ubuntu_sources"; then + echo "::error title=Missing Ubuntu APT source::Expected readable $ubuntu_sources." >&2 + exit 1 +fi +if [ -z "${RUNNER_TEMP:-}" ] || [ ! -d "$RUNNER_TEMP" ]; then + echo "::error title=Missing runner temporary directory::RUNNER_TEMP must be a directory." >&2 + exit 1 +fi + +runner_temp_mode="$(stat -c '%a' "$RUNNER_TEMP")" +apt_lists="$(mktemp -d "$RUNNER_TEMP/nemoclaw-apt-lists.XXXXXXXX")" +cleanup() { + local status=$? + trap - EXIT + sudo rm -rf -- "$apt_lists" || status=1 + sudo chmod "$runner_temp_mode" "$RUNNER_TEMP" || status=1 + exit "$status" +} +trap cleanup EXIT + +# APT's _apt user needs traversal into the isolated package-list directory. +sudo chmod o+x "$RUNNER_TEMP" +sudo chmod 0755 "$apt_lists" +sudo install -d -o _apt -g root -m 0700 "$apt_lists/partial" + +apt_options=( + -o "Dir::Etc::sourcelist=$ubuntu_sources" + -o "Dir::Etc::sourceparts=-" + -o "Dir::State::lists=$apt_lists" + -o "Acquire::http::Timeout=$acquire_timeout" + -o "Acquire::https::Timeout=$acquire_timeout" +) +if [ "$update_attempts" -gt 1 ]; then + apt_options+=(-o "Acquire::Retries=2") +fi + +run_apt() { + local operation="$1" + local duration="$2" + local attempt="$3" + local total="$4" + shift 4 + local log status + log="$(mktemp "$RUNNER_TEMP/nemoclaw-apt-${operation}.XXXXXXXX")" + echo "Installing pinned Pi tools: APT $operation started (attempt $attempt/$total)." + if { sudo timeout -k 10s "$duration" apt-get "${apt_options[@]}" "$@"; } >"$log" 2>&1; then + rm -f -- "$log" + echo "Installing pinned Pi tools: APT $operation completed." + return 0 + else + status=$? + fi + if [ "$status" -eq 124 ]; then + if [ "$operation" = update ] && [ "$attempt" -lt "$total" ]; then + echo "::warning title=APT update timed out::Ubuntu package update exceeded $duration on attempt $attempt/$total; retrying the idempotent metadata update." >&2 + else + echo "::error title=APT $operation timed out::Ubuntu package $operation exceeded $duration on attempt $attempt/$total." >&2 + fi + elif [ "$status" -eq 137 ]; then + echo "::error title=APT $operation was force-killed::Ubuntu package $operation exited with status 137 on attempt $attempt/$total; it may have exceeded $duration and been killed by timeout." >&2 + else + echo "::error title=APT $operation failed::Ubuntu package $operation exited with status $status on attempt $attempt/$total." >&2 + fi + tail -c 8192 "$log" | tail -n 60 >&2 + rm -f -- "$log" + return "$status" +} + +for ((attempt = 1; attempt <= update_attempts; attempt++)); do + if run_apt update "$update_timeout" "$attempt" "$update_attempts" update; then + break + else + status=$? + fi + if [ "$status" -ne 124 ] || [ "$attempt" -eq "$update_attempts" ]; then + exit "$status" + fi +done +run_apt install "$install_timeout" 1 1 install -y --no-install-recommends "$@" diff --git a/.github/workflows/pr-review-advisor.yaml b/.github/workflows/pr-review-advisor.yaml index 0be87f8b2c8..d42a2d84a77 100644 --- a/.github/workflows/pr-review-advisor.yaml +++ b/.github/workflows/pr-review-advisor.yaml @@ -217,10 +217,8 @@ jobs: - name: Install locked runtime run: | set -euo pipefail - sudo apt-get update -qq \ - -o Dir::Etc::sourcelist="sources.list.d/ubuntu.sources" \ - -o Dir::Etc::sourceparts="-" - sudo apt-get install -y --no-install-recommends "fd-find=${FD_FIND_VERSION}" "ripgrep=${RIPGREP_VERSION}" + bash "$ADVISOR_DIR/.github/actions/ci-install-pinned-ubuntu-packages.sh" \ + "fd-find=${FD_FIND_VERSION}" "ripgrep=${RIPGREP_VERSION}" test "$(dpkg-query -W -f='${Version}' fd-find)" = "$FD_FIND_VERSION" test "$(dpkg-query -W -f='${Version}' ripgrep)" = "$RIPGREP_VERSION" (cd "$ADVISOR_DIR" && npm ci --ignore-scripts --no-audit --no-fund) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index c6cf4549de6..6d8e05082f7 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -582,6 +582,8 @@ jobs: - name: Checkout trusted CI actions uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # PR shards run the base action. The revised action and sibling installer + # first run together after both land on main. ref: ${{ github.event.pull_request.base.sha }} path: .trusted-ci-actions persist-credentials: false @@ -590,6 +592,7 @@ jobs: .github/actions/ci-build-typecheck .github/actions/ci-compile-artifacts .github/actions/ci-cli-coverage-shard + .github/actions/ci-install-pinned-ubuntu-packages.sh .github/actions/ci-cli-coverage-merge .github/actions/ci-plugin-coverage .github/actions/ci-installer-integration diff --git a/test/automation/pull-requests/pr-workflow-contract.test.ts b/test/automation/pull-requests/pr-workflow-contract.test.ts index 237c56caa32..cd71c6e1ca8 100644 --- a/test/automation/pull-requests/pr-workflow-contract.test.ts +++ b/test/automation/pull-requests/pr-workflow-contract.test.ts @@ -3,7 +3,15 @@ import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; @@ -85,6 +93,109 @@ function runWorkflowShellStep( }; } +function runPinnedAptFixture( + mode: string, + step?: WorkflowStep, + packages = ["fd-find=9.0.0-1", "ripgrep=14.1.0-1"], +) { + const temp = mkdtempSync(join(tmpdir(), "nemoclaw-pinned-apt-")); + const fakeBin = join(temp, "bin"); + const runnerTemp = join(temp, "runner-temp"); + const aptCalls = join(temp, "apt-calls"); + const timeoutCalls = join(temp, "timeout-calls"); + mkdirSync(fakeBin); + mkdirSync(runnerTemp, { mode: 0o700 }); + writeFileSync(aptCalls, ""); + writeFileSync(timeoutCalls, ""); + writeFileSync( + join(fakeBin, "sudo"), + `#!/usr/bin/env bash +printf '%s\\n' "$*" >> "$APT_CALLS" +if [[ "$1" == chmod && "$3" == "$RUNNER_TEMP" ]]; then + command chmod "$2" "$3" || exit $? +fi +if [[ "$1" == test && "$FAKE_APT_MODE" == missing-source ]]; then exit 1; fi +if [[ "$1" == timeout ]]; then shift; timeout "$@"; exit $?; fi +if [[ "$1" == apt-get ]]; then echo 'apt-get must run under sudo timeout' >&2; exit 99; fi +exit 0 +`, + { mode: 0o755 }, + ); + writeFileSync( + join(fakeBin, "apt-get"), + `#!/usr/bin/env bash +printf 'apt-get %s\\n' "$*" >> "$APT_CALLS" +if [[ "$FAKE_APT_MODE" == update-error ]]; then + for ((line=1; line<=100; line++)); do printf 'apt diagnostic %s\\n' "$line" >&2; done + exit 86 +fi +if [[ "$FAKE_APT_MODE" == install-error && "$*" == *' install '* ]]; then exit 100; fi +exit 0 +`, + { mode: 0o755 }, + ); + writeFileSync( + join(fakeBin, "timeout"), + `#!/usr/bin/env bash +printf '%s\\n' "$*" >> "$TIMEOUT_CALLS" +shift 3 +if [[ "$FAKE_APT_MODE" == timeout || ( "$FAKE_APT_MODE" == install-timeout && "$*" == *' install '* ) ]]; then exit 124; fi +if [[ "$FAKE_APT_MODE" == force-killed ]]; then exit 137; fi +if [[ "$FAKE_APT_MODE" == recover-update && "$*" == *' update' && $(wc -l < "$TIMEOUT_CALLS") -lt 3 ]]; then exit 124; fi +"$@" +`, + { mode: 0o755 }, + ); + writeFileSync(join(fakeBin, "stat"), "#!/usr/bin/env bash\nprintf '700\\n'\n", { + mode: 0o755, + }); + writeFileSync( + join(fakeBin, "dpkg-query"), + "#!/usr/bin/env bash\ncase \"${*: -1}\" in fd-find) printf '9.0.0-1';; ripgrep) printf '14.1.0-1';; *) exit 1;; esac\n", + { mode: 0o755 }, + ); + writeFileSync(join(fakeBin, "fdfind"), "#!/usr/bin/env bash\nprintf 'fdfind 9.0.0\\n'\n", { + mode: 0o755, + }); + writeFileSync(join(fakeBin, "rg"), "#!/usr/bin/env bash\nprintf 'ripgrep 14.1.0\\n'\n", { + mode: 0o755, + }); + writeFileSync(join(fakeBin, "npm"), "#!/usr/bin/env bash\nexit 0\n", { mode: 0o755 }); + const env = { + ...process.env, + ADVISOR_DIR: process.cwd(), + APT_CALLS: aptCalls, + FAKE_APT_MODE: mode, + FD_FIND_VERSION: "9.0.0-1", + GITHUB_ACTION_PATH: join(process.cwd(), ".github/actions/ci-cli-coverage-shard"), + PATH: `${fakeBin}:${process.env.PATH ?? ""}`, + RIPGREP_VERSION: "14.1.0-1", + RUNNER_TEMP: runnerTemp, + TIMEOUT_CALLS: timeoutCalls, + }; + try { + const result = step + ? runWorkflowShellStep(step, env) + : spawnSync( + "bash", + [ + join(process.cwd(), ".github/actions/ci-install-pinned-ubuntu-packages.sh"), + ...packages, + ], + { encoding: "utf8", env, timeout: 5_000 }, + ); + return { + status: result.status, + stderr: String(result.stderr), + runnerTempMode: statSync(runnerTemp).mode & 0o777, + calls: readFileSync(aptCalls, "utf8").trim().split("\n"), + timeoutCalls: readFileSync(timeoutCalls, "utf8").trim().split("\n"), + }; + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + const reviewedSdk = { artifactName: "reviewed-sdk.tgz", integrity: "sha512-reviewed", @@ -512,97 +623,165 @@ printf '%s %s\\n' '6bf226944684f56c84dd014e8b979d27425c0148f61b3bd99bcc6f39e9dc ); }, ); - it("bounds and retries only Ubuntu package metadata for CLI shards", () => { + it.each([ + ["CLI shards", requiredStep(sharedActions.cliCoverageShard, "Install pinned Pi search tools")], + [ + "Advisor runtime", + requiredWorkflowStep(advisorWorkflow.jobs["build-advisor-runtime"], "Install locked runtime"), + ], + ])( + "reports an Ubuntu APT update failure before installing packages in %s (#11320)", + (_name, installStep) => { + const result = runPinnedAptFixture("update-error", installStep); + expect(result.status).toBe(86); + expect(result.stderr).toContain("APT update failed"); + expect(result.stderr).toContain("apt diagnostic 100"); + expect(result.stderr).not.toContain("apt diagnostic 1\n"); + expect(result.stderr.split("\n")).toHaveLength(62); + expect(result.runnerTempMode).toBe(0o700); + const aptCalls = result.calls.filter((call) => call.startsWith("apt-get ")); + expect(aptCalls).toHaveLength(1); + expect(result.timeoutCalls).toHaveLength(1); + expect(aptCalls[0]).toContain("Dir::Etc::sourcelist=/etc/apt/sources.list.d/ubuntu.sources"); + expect(aptCalls[0]).toContain("Dir::Etc::sourceparts=-"); + expect(aptCalls[0]).toMatch(/Dir::State::lists=\S+\/nemoclaw-apt-lists\.\S+/u); + expect(aptCalls[0]).toMatch(/ update$/u); + }, + ); + + it("uses one isolated Ubuntu package list for APT update and install (#11320)", () => { + const result = runPinnedAptFixture("success"); + expect(result.status).toBe(0); + const aptCalls = result.calls.filter((call) => call.startsWith("apt-get ")); + expect(aptCalls).toHaveLength(2); + const lists = aptCalls.map((call) => call.match(/Dir::State::lists=(\S+)/u)?.[1]); + expect(lists[0]).toMatch(/\/nemoclaw-apt-lists\.\S+$/u); + expect(lists[1]).toBe(lists[0]); + expect(aptCalls[1]).toContain("Dir::Etc::sourcelist=/etc/apt/sources.list.d/ubuntu.sources"); + expect(aptCalls[1]).toContain("Dir::Etc::sourceparts=-"); + expect(aptCalls[1]).toContain( + "install -y --no-install-recommends fd-find=9.0.0-1 ripgrep=14.1.0-1", + ); + }); + + it.each([ + ["CLI shards", requiredStep(sharedActions.cliCoverageShard, "Install pinned Pi search tools")], + [ + "Advisor runtime", + requiredWorkflowStep(advisorWorkflow.jobs["build-advisor-runtime"], "Install locked runtime"), + ], + ])("installs the pinned packages through the %s workflow step (#11320)", (_name, installStep) => { + const result = runPinnedAptFixture("success", installStep); + expect(result.status, result.stderr).toBe(0); + expect(result.runnerTempMode).toBe(0o700); + const aptCalls = result.calls.filter((call) => call.startsWith("apt-get ")); + expect(aptCalls).toHaveLength(2); + expect(aptCalls[1]).toContain( + "install -y --no-install-recommends fd-find=9.0.0-1 ripgrep=14.1.0-1", + ); + }); + + it("stops a stalled APT update within the command budget (#11320)", () => { + const result = runPinnedAptFixture("timeout"); + expect(result.status).toBe(124); + expect(result.stderr).toContain("APT update timed out"); + expect(result.timeoutCalls[0]).toMatch(/^-k 10s 300s apt-get /u); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(0); + }); + + it("retries only CLI update timeouts and then installs pinned packages (#11320)", () => { const installStep = requiredStep( sharedActions.cliCoverageShard, "Install pinned Pi search tools", ); - const temp = mkdtempSync(join(tmpdir(), "nemoclaw-ubuntu-apt-sources-")); - const fakeBin = join(temp, "bin"); - const aptArgs = join(temp, "apt-args"); - mkdirSync(fakeBin); - writeFileSync( - join(fakeBin, "sudo"), - '#!/usr/bin/env bash\nprintf "%s\\n" "$@" "__END_CALL__" >> "$APT_ARGS"\nexit 86\n', - { mode: 0o755 }, + const result = runPinnedAptFixture("recover-update", installStep); + expect(result.status, result.stderr).toBe(0); + expect(result.timeoutCalls).toHaveLength(4); + expect(result.timeoutCalls.slice(0, 3)).toEqual( + Array.from({ length: 3 }, () => expect.stringMatching(/^-k 10s 120s apt-get /u)), + ); + expect(result.timeoutCalls[3]).toMatch(/^-k 10s 180s apt-get /u); + const aptCalls = result.calls.filter((call) => call.startsWith("apt-get ")); + expect(aptCalls).toHaveLength(2); + expect(aptCalls[0]).toContain("Acquire::Retries=2"); + expect(aptCalls[0]).toContain("Acquire::http::Timeout=20"); + expect(aptCalls[0]).toContain("Dir::Etc::sourceparts=-"); + expect(aptCalls[1]).toContain("Dir::Etc::sourceparts=-"); + expect(aptCalls[1]).toContain( + "install -y --no-install-recommends fd-find=9.0.0-1 ripgrep=14.1.0-1", ); + }); - try { - const result = runWorkflowShellStep(installStep, { - APT_ARGS: aptArgs, - PATH: `${fakeBin}:${process.env.PATH ?? ""}`, - }); - const expectedCall = [ - "timeout", - "--signal=TERM", - "--kill-after=10s", - "120s", - "apt-get", - "-o", - "Acquire::Retries=2", - "-o", - "Acquire::http::Timeout=20", - "-o", - "Acquire::https::Timeout=20", - "update", - "-o", - "Dir::Etc::sourcelist=sources.list.d/ubuntu.sources", - "-o", - "Dir::Etc::sourceparts=-", - "__END_CALL__", - ]; - - expect(result.status, result.stdout + result.stderr).toBe(1); - expect(result.stdout).toContain( - "Retrying pinned Pi search-tool package index update (attempt 2/3)", - ); - expect(result.stdout).toContain( - "Retrying pinned Pi search-tool package index update (attempt 3/3)", - ); - expect(result.stdout).toContain( - "::error::Could not update pinned Pi search-tool package indexes after three bounded attempts", - ); - expect(readFileSync(aptArgs, "utf8").trim().split("\n")).toEqual( - Array.from({ length: 3 }, () => expectedCall).flat(), - ); - } finally { - rmSync(temp, { force: true, recursive: true }); - } + it("stops CLI updates after three bounded timeouts without installing (#11320)", () => { + const installStep = requiredStep( + sharedActions.cliCoverageShard, + "Install pinned Pi search tools", + ); + const result = runPinnedAptFixture("timeout", installStep); + expect(result.status).toBe(124); + expect(result.timeoutCalls).toHaveLength(3); + expect(result.timeoutCalls).toEqual( + Array.from({ length: 3 }, () => expect.stringMatching(/^-k 10s 120s apt-get /u)), + ); + expect(result.stderr).toContain("APT update timed out"); + expect(result.stderr).toContain("attempt 3/3"); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(0); }); - it("refreshes only Ubuntu package metadata for Advisor runtime", () => { - const installStep = requiredWorkflowStep( - advisorWorkflow.jobs["build-advisor-runtime"], - "Install locked runtime", + it("stops a stalled APT install after a successful update (#11320)", () => { + const result = runPinnedAptFixture("install-timeout"); + expect(result.status).toBe(124); + expect(result.stderr).toContain("APT install timed out"); + expect(result.timeoutCalls).toHaveLength(2); + expect(result.timeoutCalls[1]).toMatch(/^-k 10s 300s apt-get /u); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(1); + }); + + it("stops a stalled CLI install within the preserved limit (#11320)", () => { + const installStep = requiredStep( + sharedActions.cliCoverageShard, + "Install pinned Pi search tools", ); - const temp = mkdtempSync(join(tmpdir(), "nemoclaw-ubuntu-apt-sources-")); - const fakeBin = join(temp, "bin"); - const aptArgs = join(temp, "apt-args"); - mkdirSync(fakeBin); - writeFileSync( - join(fakeBin, "sudo"), - '#!/usr/bin/env bash\nprintf "%s\\n" "$@" > "$APT_ARGS"\nexit 86\n', - { mode: 0o755 }, + const result = runPinnedAptFixture("install-timeout", installStep); + expect(result.status).toBe(124); + expect(result.stderr).toContain("APT install timed out"); + expect(result.timeoutCalls).toHaveLength(2); + expect(result.timeoutCalls[0]).toMatch(/^-k 10s 120s apt-get /u); + expect(result.timeoutCalls[1]).toMatch(/^-k 10s 180s apt-get /u); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(1); + }); + + it("fails when the pinned package install fails (#11320)", () => { + const result = runPinnedAptFixture("install-error"); + expect(result.status).toBe(100); + expect(result.stderr).toContain("APT install failed"); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(2); + }); + + it("reports a possible force-killed timeout without claiming its cause (#11320)", () => { + const result = runPinnedAptFixture("force-killed"); + expect(result.status).toBe(137); + expect(result.stderr).toContain("APT update was force-killed"); + expect(result.stderr).toContain("may have exceeded 300s"); + expect(result.calls.filter((call) => call.startsWith("timeout "))).toHaveLength(1); + expect(result.calls.find((call) => call.startsWith("timeout "))).toMatch( + /^timeout -k 10s 300s apt-get /u, ); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(0); + }); - try { - const result = runWorkflowShellStep(installStep, { - APT_ARGS: aptArgs, - PATH: `${fakeBin}:${process.env.PATH ?? ""}`, - }); - expect(result.status).toBe(86); - expect(readFileSync(aptArgs, "utf8").trim().split("\n")).toEqual([ - "apt-get", - "update", - "-qq", - "-o", - "Dir::Etc::sourcelist=sources.list.d/ubuntu.sources", - "-o", - "Dir::Etc::sourceparts=-", - ]); - } finally { - rmSync(temp, { force: true, recursive: true }); - } + it("rejects a missing Ubuntu source before APT runs (#11320)", () => { + const result = runPinnedAptFixture("missing-source"); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Missing Ubuntu APT source"); + expect(result.calls.filter((call) => call.startsWith("apt-get "))).toHaveLength(0); + }); + + it("rejects an unpinned package before using sudo (#11320)", () => { + const result = runPinnedAptFixture("success", undefined, ["fd-find"]); + expect(result.status).toBe(2); + expect(result.stderr).toContain("Unpinned APT package"); + expect(result.calls).toEqual([""]); }); // source-shape-contract: security -- PR dependency jobs receive only the base-approved SDK archive after credential-free integrity verification