From 76bdaf20421759f5d27e6c1a85e104fae3e43047 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:27:58 -0700 Subject: [PATCH] fix: pin GitHub Actions to commit SHAs, version-lock CI tools (#2983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: pin all GitHub Actions to commit SHAs and version-lock tools Addresses supply chain hardening findings from issue #2982: - Pin all 6 GitHub Actions to full commit SHAs with version comments: - actions/checkout@v4 → SHA 34e1148... - oven-sh/setup-bun@v2 → SHA 0c5077e... - actions/github-script@v7 → SHA f28e40c... - docker/login-action@v3 → SHA c94ce9f... - docker/build-push-action@v6 → SHA 10e90e3... - hashicorp/setup-packer@main → SHA c3d53c5... (v3.2.0) - Pin Packer version: latest → 1.15.0 (in packer-snapshots.yml) - Pin bun version: latest → 1.3.11 (in agent-tarballs.yml) - Pin shellcheck: replace apt-get (no version) with pinned download of v0.10.0 from GitHub releases with SHA256 integrity check These changes eliminate the primary LiteLLM-style attack vector: a compromised action maintainer can no longer force-push malicious code to an existing tag and have it run in CI. Fixes #2982 Agent: issue-fixer Co-Authored-By: Claude Sonnet 4.5 * fix: exclude import aliases from no-type-assertion lint rule The `JsNamedImportSpecifier` exclusion prevents `import { foo as bar }` patterns from being flagged as type assertions. Previously, any `as` keyword in import/export statements triggered the ban because the GritQL pattern `$value as $type` matched import specifiers as well as actual TypeScript type assertions. This also removes the `as _foo` import aliases in the script-failure-guidance test file (replaced with direct imports + distinctly-named wrapper functions) which were the original manifestation of this bug. All 1944 tests pass. Biome check clean across 169 files. Agent: issue-fixer Co-Authored-By: Claude Sonnet 4.5 --------- Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 --- .github/workflows/agent-tarballs.yml | 8 +- .github/workflows/cli-release.yml | 4 +- .github/workflows/docker.yml | 6 +- .github/workflows/gate.yml | 2 +- .github/workflows/lint.yml | 18 ++- .github/workflows/packer-snapshots.yml | 8 +- .github/workflows/test.yml | 8 +- lint/no-type-assertion.grit | 1 + .../__tests__/script-failure-guidance.test.ts | 116 +++++++++--------- 9 files changed, 87 insertions(+), 84 deletions(-) diff --git a/.github/workflows/agent-tarballs.yml b/.github/workflows/agent-tarballs.yml index 458e1ad6..df686359 100644 --- a/.github/workflows/agent-tarballs.yml +++ b/.github/workflows/agent-tarballs.yml @@ -20,7 +20,7 @@ jobs: outputs: agents: ${{ steps.set-matrix.outputs.agents }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - id: set-matrix env: @@ -58,12 +58,12 @@ jobs: - agent: claude arch: arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: latest + bun-version: "1.3.11" - name: Install agent under /root env: diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index e9436065..7d3fb301 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -22,10 +22,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install dependencies and build working-directory: packages/cli diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 4fe41648..f5ccbc00 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,15 +22,15 @@ jobs: matrix: agent: [claude, codex, openclaw, opencode, kilocode, zeroclaw, hermes, junie] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: docker/login-action@v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: sh/docker/${{ matrix.agent }}.Dockerfile diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 56e99510..ad9e07ee 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check org membership and close if external - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1ef70ad2..c8c45566 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,12 +13,18 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install ShellCheck run: | - sudo apt-get update - sudo apt-get install -y shellcheck + # Pin shellcheck v0.10.0 for reproducible CI — verifies SHA256 before install + SHELLCHECK_VERSION="0.10.0" + SHELLCHECK_SHA256="6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87" + TARBALL="shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" + curl -sSL "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/${TARBALL}" -o /tmp/${TARBALL} + echo "${SHELLCHECK_SHA256} /tmp/${TARBALL}" | sha256sum -c + tar -xJf "/tmp/${TARBALL}" -C /tmp "shellcheck-v${SHELLCHECK_VERSION}/shellcheck" + sudo mv "/tmp/shellcheck-v${SHELLCHECK_VERSION}/shellcheck" /usr/local/bin/shellcheck - name: Run ShellCheck on all bash scripts run: | @@ -41,10 +47,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install dependencies run: bun install @@ -58,7 +64,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Run macOS compat linter run: bash sh/test/macos-compat.sh diff --git a/.github/workflows/packer-snapshots.yml b/.github/workflows/packer-snapshots.yml index e5ca6788..080442b6 100644 --- a/.github/workflows/packer-snapshots.yml +++ b/.github/workflows/packer-snapshots.yml @@ -21,7 +21,7 @@ jobs: outputs: include: ${{ steps.set.outputs.include }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - id: set run: | SINGLE_AGENT="${SINGLE_AGENT_INPUT}" @@ -48,7 +48,7 @@ jobs: matrix: include: ${{ fromJson(needs.matrix.outputs.include) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Read agent config id: config @@ -61,9 +61,9 @@ jobs: AGENT_NAME: ${{ matrix.agent }} - name: Setup Packer - uses: hashicorp/setup-packer@main + uses: hashicorp/setup-packer@c3d53c525d422944e50ee27b840746d6522b08de # v3.2.0 with: - version: latest + version: "1.15.0" - name: Init Packer plugins run: packer init packer/digitalocean.pkr.hcl diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9dbeea0..f9fcd753 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,10 +15,10 @@ jobs: timeout-minutes: 5 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install dependencies run: bun install @@ -32,10 +32,10 @@ jobs: timeout-minutes: 5 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install dependencies run: bun install diff --git a/lint/no-type-assertion.grit b/lint/no-type-assertion.grit index a8c02504..b14c2ea3 100644 --- a/lint/no-type-assertion.grit +++ b/lint/no-type-assertion.grit @@ -2,5 +2,6 @@ language js(typescript) `$value as $type` as $expr where { ! $expr <: `$_ as const`, + ! $expr <: JsNamedImportSpecifier(), register_diagnostic(span=$expr, message="Type assertions (`as`) are banned. Use schema validation (parseJsonWith), type guards, or `satisfies` instead.", severity="error") } diff --git a/packages/cli/src/__tests__/script-failure-guidance.test.ts b/packages/cli/src/__tests__/script-failure-guidance.test.ts index fab99e86..0a324d97 100644 --- a/packages/cli/src/__tests__/script-failure-guidance.test.ts +++ b/packages/cli/src/__tests__/script-failure-guidance.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { - getScriptFailureGuidance as _getScriptFailureGuidance, - getSignalGuidance as _getSignalGuidance, - buildRetryCommand, -} from "../commands/index.js"; +import { buildRetryCommand, getScriptFailureGuidance, getSignalGuidance } from "../commands/index.js"; /** Strip ANSI escape codes from a string so assertions work regardless of color support. */ function stripAnsi(s: string): string { @@ -11,17 +7,17 @@ function stripAnsi(s: string): string { } /** Wrapper that strips ANSI codes from all returned lines. */ -function getScriptFailureGuidance(...args: Parameters): string[] { - return _getScriptFailureGuidance(...args).map(stripAnsi); +function stripped_getScriptFailureGuidance(...args: Parameters): string[] { + return getScriptFailureGuidance(...args).map(stripAnsi); } /** Wrapper that strips ANSI codes from all returned lines. */ -function getSignalGuidance(...args: Parameters): string[] { - return _getSignalGuidance(...args).map(stripAnsi); +function stripped_getSignalGuidance(...args: Parameters): string[] { + return getSignalGuidance(...args).map(stripAnsi); } /** - * Tests for getScriptFailureGuidance() in commands/run.ts. + * Tests for stripped_getScriptFailureGuidance() in commands/run.ts. * * This function maps exit codes from failed spawn scripts to user-facing * guidance strings. It was recently modified (PRs #450, #449) but has @@ -33,7 +29,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 127 (command not found)", () => { it("should return guidance about missing commands with required tools and cloud name", () => { - const lines = getScriptFailureGuidance(127, "hetzner"); + const lines = stripped_getScriptFailureGuidance(127, "hetzner"); const joined = lines.join("\n"); expect(lines[0]).toContain("command was not found"); expect(joined).toContain("bash"); @@ -44,14 +40,14 @@ describe("getScriptFailureGuidance", () => { }); it("should embed a different cloud name when provided", () => { - const lines = getScriptFailureGuidance(127, "vultr"); + const lines = stripped_getScriptFailureGuidance(127, "vultr"); const joined = lines.join("\n"); expect(joined).toContain("spawn vultr"); expect(joined).not.toContain("spawn hetzner"); }); it("should return exactly 3 guidance lines", () => { - const lines = getScriptFailureGuidance(127, "sprite"); + const lines = stripped_getScriptFailureGuidance(127, "sprite"); expect(lines).toHaveLength(3); }); }); @@ -60,7 +56,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 126 (permission denied)", () => { it("should mention permission denied, causes, issue link, and return 4 lines", () => { - const lines = getScriptFailureGuidance(126, "sprite"); + const lines = stripped_getScriptFailureGuidance(126, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("permission denied"); expect(joined).toContain("could not be executed"); @@ -76,7 +72,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 1 (generic failure)", () => { it("should start with Common causes, mention credentials, and reference cloud name", () => { - const lines = getScriptFailureGuidance(1, "digital-ocean"); + const lines = stripped_getScriptFailureGuidance(1, "digital-ocean"); const joined = lines.join("\n"); expect(lines[0]).toBe("Common causes:"); expect(joined).toContain("credentials"); @@ -84,7 +80,7 @@ describe("getScriptFailureGuidance", () => { }); it("should mention API error causes, provisioning failure, and return at least 4 lines", () => { - const lines = getScriptFailureGuidance(1, "sprite"); + const lines = stripped_getScriptFailureGuidance(1, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("API error"); expect(joined).toContain("quota"); @@ -97,7 +93,7 @@ describe("getScriptFailureGuidance", () => { describe("default case (unknown exit codes)", () => { it("should return common causes with credentials, rate limits, dependencies, and cloud name", () => { - const lines = getScriptFailureGuidance(42, "linode"); + const lines = stripped_getScriptFailureGuidance(42, "linode"); const joined = lines.join("\n"); expect(lines[0]).toBe("Common causes:"); expect(joined).toContain("credentials"); @@ -115,7 +111,7 @@ describe("getScriptFailureGuidance", () => { describe("null exit code", () => { it("should fall through to default case with credentials and cloud name", () => { - const lines = getScriptFailureGuidance(null, "sprite"); + const lines = stripped_getScriptFailureGuidance(null, "sprite"); const joined = lines.join("\n"); expect(lines[0]).toBe("Common causes:"); expect(joined).toContain("credentials"); @@ -128,7 +124,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 130 (user interrupt)", () => { it("should mention Ctrl+C, interruption, orphaned server warning, and return 3 lines", () => { - const lines = getScriptFailureGuidance(130, "sprite"); + const lines = stripped_getScriptFailureGuidance(130, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("Ctrl+C"); expect(joined).toContain("interrupted"); @@ -142,7 +138,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 137 (killed)", () => { it("should mention killed, timeout/OOM, larger instance suggestion, and return 4 lines", () => { - const lines = getScriptFailureGuidance(137, "sprite"); + const lines = stripped_getScriptFailureGuidance(137, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("killed"); expect(joined).toContain("timeout"); @@ -157,7 +153,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 255 (SSH failure)", () => { it("should mention SSH failure, booting, firewall, termination, and return 4 lines", () => { - const lines = getScriptFailureGuidance(255, "sprite"); + const lines = stripped_getScriptFailureGuidance(255, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("SSH connection failed"); expect(joined).toContain("still booting"); @@ -172,7 +168,7 @@ describe("getScriptFailureGuidance", () => { describe("exit code 2 (shell syntax error)", () => { it("should mention syntax error, bug report link, and return 2 lines", () => { - const lines = getScriptFailureGuidance(2, "sprite"); + const lines = stripped_getScriptFailureGuidance(2, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("Shell syntax or argument error"); expect(joined).toContain("bug in the script"); @@ -190,7 +186,7 @@ describe("getScriptFailureGuidance", () => { const savedHC = process.env.HCLOUD_TOKEN; delete process.env.OPENROUTER_API_KEY; delete process.env.HCLOUD_TOKEN; - const lines = getScriptFailureGuidance(1, "hetzner", "HCLOUD_TOKEN"); + const lines = stripped_getScriptFailureGuidance(1, "hetzner", "HCLOUD_TOKEN"); const joined = lines.join("\n"); expect(joined).toContain("HCLOUD_TOKEN"); expect(joined).toContain("OPENROUTER_API_KEY"); @@ -205,7 +201,7 @@ describe("getScriptFailureGuidance", () => { }); it("should show generic setup hint for exit code 1 when no authHint", () => { - const lines = getScriptFailureGuidance(1, "hetzner"); + const lines = stripped_getScriptFailureGuidance(1, "hetzner"); const joined = lines.join("\n"); expect(joined).toContain("spawn hetzner"); expect(joined).not.toContain("HCLOUD_TOKEN"); @@ -216,7 +212,7 @@ describe("getScriptFailureGuidance", () => { const savedDO = process.env.DO_API_TOKEN; delete process.env.OPENROUTER_API_KEY; delete process.env.DO_API_TOKEN; - const lines = getScriptFailureGuidance(42, "digitalocean", "DO_API_TOKEN"); + const lines = stripped_getScriptFailureGuidance(42, "digitalocean", "DO_API_TOKEN"); const joined = lines.join("\n"); expect(joined).toContain("DO_API_TOKEN"); expect(joined).toContain("OPENROUTER_API_KEY"); @@ -231,14 +227,14 @@ describe("getScriptFailureGuidance", () => { }); it("should show generic setup hint for default case when no authHint", () => { - const lines = getScriptFailureGuidance(42, "digitalocean"); + const lines = stripped_getScriptFailureGuidance(42, "digitalocean"); const joined = lines.join("\n"); expect(joined).toContain("spawn digitalocean"); expect(joined).not.toContain("DO_API_TOKEN"); }); it("should handle multi-credential auth hint", () => { - const lines = getScriptFailureGuidance(1, "contabo", "CONTABO_CLIENT_ID + CONTABO_CLIENT_SECRET"); + const lines = stripped_getScriptFailureGuidance(1, "contabo", "CONTABO_CLIENT_ID + CONTABO_CLIENT_SECRET"); const joined = lines.join("\n"); // Each credential var should be listed individually expect(joined).toContain("CONTABO_CLIENT_ID"); @@ -246,19 +242,19 @@ describe("getScriptFailureGuidance", () => { }); it("should not affect non-credential exit codes (130, 137, etc.)", () => { - const lines130 = getScriptFailureGuidance(130, "hetzner", "HCLOUD_TOKEN"); + const lines130 = stripped_getScriptFailureGuidance(130, "hetzner", "HCLOUD_TOKEN"); const joined130 = lines130.join("\n"); expect(joined130).not.toContain("HCLOUD_TOKEN"); expect(joined130).toContain("Ctrl+C"); - const lines255 = getScriptFailureGuidance(255, "hetzner", "HCLOUD_TOKEN"); + const lines255 = stripped_getScriptFailureGuidance(255, "hetzner", "HCLOUD_TOKEN"); const joined255 = lines255.join("\n"); expect(joined255).not.toContain("HCLOUD_TOKEN"); expect(joined255).toContain("SSH"); }); it("should include setup instruction line for exit code 1 with authHint", () => { - const lines = getScriptFailureGuidance(1, "hetzner", "HCLOUD_TOKEN"); + const lines = stripped_getScriptFailureGuidance(1, "hetzner", "HCLOUD_TOKEN"); expect(lines.length).toBeGreaterThanOrEqual(5); const joined = lines.join("\n"); expect(joined).toContain("spawn hetzner"); @@ -266,7 +262,7 @@ describe("getScriptFailureGuidance", () => { }); it("should include setup instruction line for default case with authHint", () => { - const lines = getScriptFailureGuidance(42, "hetzner", "HCLOUD_TOKEN"); + const lines = stripped_getScriptFailureGuidance(42, "hetzner", "HCLOUD_TOKEN"); expect(lines.length).toBeGreaterThanOrEqual(5); const joined = lines.join("\n"); expect(joined).toContain("spawn hetzner"); @@ -278,23 +274,23 @@ describe("getScriptFailureGuidance", () => { describe("edge cases", () => { it("should handle exit code 0 as default case", () => { - const lines = getScriptFailureGuidance(0, "sprite"); + const lines = stripped_getScriptFailureGuidance(0, "sprite"); expect(lines[0]).toBe("Common causes:"); }); it("should handle negative exit code as default case", () => { - const lines = getScriptFailureGuidance(-1, "hetzner"); + const lines = stripped_getScriptFailureGuidance(-1, "hetzner"); expect(lines[0]).toBe("Common causes:"); }); it("should handle empty cloud name", () => { - const lines = getScriptFailureGuidance(127, ""); + const lines = stripped_getScriptFailureGuidance(127, ""); const joined = lines.join("\n"); expect(joined).toContain("spawn "); }); it("should handle cloud name with special characters", () => { - const lines = getScriptFailureGuidance(1, "digital-ocean"); + const lines = stripped_getScriptFailureGuidance(1, "digital-ocean"); const joined = lines.join("\n"); expect(joined).toContain("spawn digital-ocean"); }); @@ -304,14 +300,14 @@ describe("getScriptFailureGuidance", () => { describe("return type and structure", () => { it("should produce different output for each handled exit code", () => { - const result130 = getScriptFailureGuidance(130, "sprite"); - const result137 = getScriptFailureGuidance(137, "sprite"); - const result255 = getScriptFailureGuidance(255, "sprite"); - const result127 = getScriptFailureGuidance(127, "sprite"); - const result126 = getScriptFailureGuidance(126, "sprite"); - const result2 = getScriptFailureGuidance(2, "sprite"); - const result1 = getScriptFailureGuidance(1, "sprite"); - const resultDefault = getScriptFailureGuidance(42, "sprite"); + const result130 = stripped_getScriptFailureGuidance(130, "sprite"); + const result137 = stripped_getScriptFailureGuidance(137, "sprite"); + const result255 = stripped_getScriptFailureGuidance(255, "sprite"); + const result127 = stripped_getScriptFailureGuidance(127, "sprite"); + const result126 = stripped_getScriptFailureGuidance(126, "sprite"); + const result2 = stripped_getScriptFailureGuidance(2, "sprite"); + const result1 = stripped_getScriptFailureGuidance(1, "sprite"); + const resultDefault = stripped_getScriptFailureGuidance(42, "sprite"); const all = [ result130, @@ -336,7 +332,7 @@ describe("getScriptFailureGuidance", () => { describe("getSignalGuidance", () => { describe("SIGKILL", () => { it("should mention OOM killer, larger instance size, and cloud provider dashboard", () => { - const lines = getSignalGuidance("SIGKILL"); + const lines = stripped_getSignalGuidance("SIGKILL"); const joined = lines.join("\n"); expect(joined).toContain("SIGKILL"); expect(joined).toContain("Out of memory"); @@ -347,7 +343,7 @@ describe("getSignalGuidance", () => { describe("SIGTERM", () => { it("should mention process was terminated and server shutdown", () => { - const lines = getSignalGuidance("SIGTERM"); + const lines = stripped_getSignalGuidance("SIGTERM"); const joined = lines.join("\n"); expect(joined).toContain("terminated"); expect(joined).toContain("SIGTERM"); @@ -357,7 +353,7 @@ describe("getSignalGuidance", () => { describe("SIGINT", () => { it("should mention Ctrl+C and warn about orphaned servers", () => { - const lines = getSignalGuidance("SIGINT"); + const lines = stripped_getSignalGuidance("SIGINT"); const joined = lines.join("\n"); expect(joined).toContain("Ctrl+C"); expect(joined).toContain("cloud provider dashboard"); @@ -366,7 +362,7 @@ describe("getSignalGuidance", () => { describe("SIGHUP", () => { it("should mention terminal disconnection and suggest tmux/screen", () => { - const lines = getSignalGuidance("SIGHUP"); + const lines = stripped_getSignalGuidance("SIGHUP"); const joined = lines.join("\n"); expect(joined).toContain("terminal connection"); expect(joined).toContain("SIGHUP"); @@ -376,7 +372,7 @@ describe("getSignalGuidance", () => { describe("unknown signal", () => { it("should show the signal name for unknown signals", () => { - const lines = getSignalGuidance("SIGUSR1"); + const lines = stripped_getSignalGuidance("SIGUSR1"); const joined = lines.join("\n"); expect(joined).toContain("SIGUSR1"); }); @@ -384,10 +380,10 @@ describe("getSignalGuidance", () => { describe("signal output uniqueness", () => { it("should produce different output for each handled signal", () => { - const sigkill = getSignalGuidance("SIGKILL").join("\n"); - const sigterm = getSignalGuidance("SIGTERM").join("\n"); - const sigint = getSignalGuidance("SIGINT").join("\n"); - const sighup = getSignalGuidance("SIGHUP").join("\n"); + const sigkill = stripped_getSignalGuidance("SIGKILL").join("\n"); + const sigterm = stripped_getSignalGuidance("SIGTERM").join("\n"); + const sigint = stripped_getSignalGuidance("SIGINT").join("\n"); + const sighup = stripped_getSignalGuidance("SIGHUP").join("\n"); expect(sigkill).not.toBe(sigterm); expect(sigterm).not.toBe(sigint); expect(sigint).not.toBe(sighup); @@ -489,7 +485,7 @@ describe("buildRetryCommand", () => { describe("dashboard URL in guidance", () => { describe("getScriptFailureGuidance with dashboardUrl", () => { it("should include dashboard URL for exit code 1 when provided", () => { - const lines = getScriptFailureGuidance(1, "hetzner", undefined, "https://console.hetzner.cloud/"); + const lines = stripped_getScriptFailureGuidance(1, "hetzner", undefined, "https://console.hetzner.cloud/"); const joined = lines.join("\n"); expect(joined).toContain("https://console.hetzner.cloud/"); expect(joined).toContain("dashboard"); @@ -520,13 +516,13 @@ describe("dashboard URL in guidance", () => { ], ]; for (const [code, cloud, url] of cases) { - const joined = getScriptFailureGuidance(code, cloud, undefined, url).join("\n"); + const joined = stripped_getScriptFailureGuidance(code, cloud, undefined, url).join("\n"); expect(joined, `exit code ${code}`).toContain(url); } }); it("should fall back to generic message when no dashboardUrl", () => { - const lines = getScriptFailureGuidance(130, "sprite"); + const lines = stripped_getScriptFailureGuidance(130, "sprite"); const joined = lines.join("\n"); expect(joined).toContain("cloud provider dashboard"); expect(joined).not.toContain("https://"); @@ -539,7 +535,7 @@ describe("dashboard URL in guidance", () => { 255, 2, ]) { - const lines = getScriptFailureGuidance(code, "hetzner", undefined, "https://console.hetzner.cloud/"); + const lines = stripped_getScriptFailureGuidance(code, "hetzner", undefined, "https://console.hetzner.cloud/"); const joined = lines.join("\n"); expect(joined).not.toContain("https://console.hetzner.cloud/"); } @@ -548,7 +544,7 @@ describe("dashboard URL in guidance", () => { describe("getSignalGuidance with dashboardUrl", () => { it("should include dashboard URL for SIGKILL when provided", () => { - const lines = getSignalGuidance("SIGKILL", "https://console.hetzner.cloud/"); + const lines = stripped_getSignalGuidance("SIGKILL", "https://console.hetzner.cloud/"); const joined = lines.join("\n"); expect(joined).toContain("https://console.hetzner.cloud/"); expect(joined).toContain("dashboard"); @@ -565,20 +561,20 @@ describe("dashboard URL in guidance", () => { "https://cloud.digitalocean.com/", ], ] as const) { - const joined = getSignalGuidance(signal, url).join("\n"); + const joined = stripped_getSignalGuidance(signal, url).join("\n"); expect(joined, signal).toContain(url); } }); it("should fall back to generic message when no dashboardUrl", () => { - const lines = getSignalGuidance("SIGKILL"); + const lines = stripped_getSignalGuidance("SIGKILL"); const joined = lines.join("\n"); expect(joined).toContain("cloud provider dashboard"); expect(joined).not.toContain("https://"); }); it("should not add dashboard URL for SIGHUP", () => { - const lines = getSignalGuidance("SIGHUP", "https://example.com"); + const lines = stripped_getSignalGuidance("SIGHUP", "https://example.com"); const joined = lines.join("\n"); expect(joined).not.toContain("https://example.com"); });