From 12c0b895408bc21f7ffb666767d3ae63d1a81193 Mon Sep 17 00:00:00 2001 From: David Miserak Date: Sun, 7 Jun 2026 13:40:40 -0400 Subject: [PATCH] fix(setup): Playwright Chromium failure becomes best-effort warn, not exit 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces both `exit 1` paths in the Playwright install/verify block with a $_PW_FAIL_REASON accumulator that prints a named warning to stderr and returns control. Previously a Chromium download failure (network blip, locked-down corporate machine, missing Node.js on Windows) would abort ./setup mid-run with no skills registered, no hooks installed, no migrations applied — and a re-run would start over from the top. Now the failure only disables browser-driven features (/qa, /design-review, make-pdf, sidebar, /pair-agent); skills, hooks, and migrations land normally. The warning names which sub-step failed (chromium-install / windows-no-node / windows-node-modules / post-install-launch) so users and bug reports can be specific, and tells the user to re-run ./setup to retry just this step. Tests (5) exercise the actual block from setup with stubbed ensure_playwright_browser and bunx, simulate install + post-install failures, and assert the script exits 0 with the named warning. One side-by-side test inlines the OLD bug shape and confirms it returned non-zero, so the exit-code difference is proven, not just asserted. Coordinates with #1838 (pins Playwright install version): both touch the same code block, so whichever lands first needs the other to rebase. The changes are complementary — #1838 fixes which Chromium build gets installed; this PR fixes what happens when the install fails. Carved out from #1883 per @jbetala7's review request. --- CHANGELOG.md | 84 ++++++++++++ VERSION | 2 +- package.json | 144 ++++++++++---------- setup | 65 +++++---- test/setup-playwright-warn-not-exit.test.ts | 105 ++++++++++++++ 5 files changed, 301 insertions(+), 99 deletions(-) create mode 100644 test/setup-playwright-warn-not-exit.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5109cc1670..8defb2f46c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,89 @@ # Changelog +## [1.57.4.0] - 2026-06-08 + +## **A failed Playwright Chromium install no longer aborts `./setup` mid-run. Skills, hooks, and migrations are already in place; you get a named warning and a retry hint.** + +The Playwright Chromium install is the riskiest step in `./setup`. It needs +network, it talks to a Microsoft CDN, on Windows it bounces through Node.js +because of a Bun pipe bug, and on locked-down machines it may not even be +allowed to run. Until now, any failure in that step was fatal: `exit 1`, and +the run dies before skills get registered, before hooks land, before +migrations run. Re-running `./setup` would start over from the top. + +That's the wrong shape. The user already has a working bun, a built browse +binary, and a tree of skill templates ready to go. The thing that failed — +Chromium availability — only affects browser-driven features (`/qa`, +`/design-review`, `make-pdf`, sidebar, `/pair-agent`). Everything else can +still work. So make the failure non-fatal. + +This release replaces both `exit 1` paths in the Playwright install/verify +block with a `$_PW_FAIL_REASON` accumulator. On install failure the script +prints a named warning telling you which sub-step failed, names the +affected features, gives Windows users the specific known-issue link, and +tells you to re-run `./setup` to retry — at which point only the Playwright +step needs to succeed, because everything else is already done. + +### The numbers that matter + +Source: `bun test test/setup-playwright-warn-not-exit.test.ts` on this branch. + +| Metric | Before | After | Δ | +|--------|--------|-------|---| +| Playwright failure aborts `./setup` | yes | no | warn instead of exit | +| Skills registered when Chromium download fails | sometimes | always | depends on which step failed first | +| Named failure reason in warning | no | yes | `chromium-install`, `windows-no-node`, `windows-node-modules`, `post-install-launch` | +| Retry guidance in failure output | no | yes | "Re-run ./setup to retry" | + +The four named reasons matter. When a Playwright install fails it's usually +not obvious whether the download died, Chromium was downloaded but won't +launch, or Node.js isn't on PATH (Windows). The warning calls it by name +so users and bug reports can be specific. + +### What this means for you + +If your network blinks during `./setup`, or you're on a corporate machine +that won't let `bunx playwright install chromium` finish, you'll now get +gstack installed with everything except browser features, plus a clear +warning. Re-run `./setup` when the network is back to enable browser +features. If you're on Windows and hit the Bun-on-Windows pipe bug, the +warning tells you exactly which `node -e` check to verify. + +### Itemized changes + +#### Changed + +- `setup`: Playwright Chromium install/verify no longer aborts on failure. + The two `exit 1` paths in the install block are replaced with a + `$_PW_FAIL_REASON` accumulator (values: `chromium-install`, + `windows-no-node`, `windows-node-modules`, `post-install-launch`). When + set, the script prints a named warning to stderr, lists the affected + browser-driven features, includes the Bun-on-Windows known-issue link + if applicable, and tells the user to re-run `./setup` to retry. All + other setup steps complete normally. + +#### For contributors + +- `test/setup-playwright-warn-not-exit.test.ts` — 5 tests: + - Two source-anchored checks confirm no `exit 1` is reachable from the + Playwright block and the `_PW_FAIL_REASON` accumulator pattern is in + place. + - Two functional tests execute the live block from `setup` with stubbed + `ensure_playwright_browser` and `bunx`, simulate install + post-install + failures, and assert the script exits 0 with the named warning. + - One side-by-side test inlines the OLD `exit 1` bug shape and confirms + it returned non-zero — proves the exit-code difference is real, not a + quoting illusion. + +#### Coordination + +- This PR is one of three carved out from #1883 per @jbetala7's review + request. It touches the same code block as #1838 (which pins the + Playwright install version), so whichever of #1838 or this PR lands + first will need a small rebase on the other. The two changes are + complementary — #1838 fixes which Chromium build gets installed; this + PR fixes what happens when the install fails. + ## [1.57.3.0] - 2026-06-07 ## **Every PR `/ship` opens gets the version stamped into its title, fork and agent PRs included.** diff --git a/VERSION b/VERSION index e97e1faf0f..283abc2ce4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.57.3.0 +1.57.4.0 diff --git a/package.json b/package.json index 7e483ae648..7d6638c8dd 100644 --- a/package.json +++ b/package.json @@ -1,74 +1,74 @@ { - "name": "gstack", - "version": "1.57.3.0", - "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", - "license": "MIT", - "type": "module", - "bin": { - "browse": "./browse/dist/browse", - "make-pdf": "./make-pdf/dist/pdf" - }, - "scripts": { - "build": "bash scripts/build.sh", - "vendor:xterm": "mkdir -p extension/lib && cp node_modules/xterm/lib/xterm.js extension/lib/xterm.js && cp node_modules/xterm/css/xterm.css extension/lib/xterm.css && cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js extension/lib/xterm-addon-fit.js", - "dev:make-pdf": "bun run make-pdf/src/cli.ts", - "dev:design": "bun run design/src/cli.ts", - "gen:skill-docs": "bun run scripts/gen-skill-docs.ts", - "gen:skill-docs:user": "bun run scripts/gen-skill-docs.ts --respect-detection", - "dev": "bun run browse/src/cli.ts", - "server": "bun run browse/src/server.ts", - "test": "bun test browse/test/ test/ make-pdf/test/ --ignore 'test/skill-e2e-*.test.ts' --ignore test/skill-llm-eval.test.ts --ignore test/skill-routing-e2e.test.ts --ignore test/codex-e2e.test.ts --ignore test/gemini-e2e.test.ts && (bun run slop:diff 2>/dev/null || true)", - "test:free": "bun run scripts/test-free-shards.ts", - "test:windows": "bun run scripts/test-free-shards.ts --windows-only", - "test:evals": "EVALS=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", - "test:evals:all": "EVALS=1 EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", - "test:e2e": "EVALS=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", - "test:e2e:all": "EVALS=1 EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", - "test:gate": "EVALS=1 EVALS_TIER=gate bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", - "test:periodic": "EVALS=1 EVALS_TIER=periodic EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", - "test:codex": "EVALS=1 bun test test/codex-e2e.test.ts", - "test:codex:all": "EVALS=1 EVALS_ALL=1 bun test test/codex-e2e.test.ts", - "test:gemini": "EVALS=1 bun test test/gemini-e2e.test.ts", - "test:gemini:all": "EVALS=1 EVALS_ALL=1 bun test test/gemini-e2e.test.ts", - "skill:check": "bun run scripts/skill-check.ts", - "dev:skill": "bun run scripts/dev-skill.ts", - "start": "bun run browse/src/server.ts", - "eval:list": "bun run scripts/eval-list.ts", - "eval:compare": "bun run scripts/eval-compare.ts", - "eval:summary": "bun run scripts/eval-summary.ts", - "eval:watch": "bun run scripts/eval-watch.ts", - "eval:select": "bun run scripts/eval-select.ts", - "analytics": "bun run scripts/analytics.ts", - "test:audit": "bun test test/audit-compliance.test.ts", - "slop": "npx slop-scan scan . 2>/dev/null || echo 'slop-scan not available (install with: npm i -g slop-scan)'", - "slop:diff": "bun run scripts/slop-diff.ts" - }, - "dependencies": { - "@huggingface/transformers": "^4.1.0", - "@ngrok/ngrok": "^1.7.0", - "diff": "^7.0.0", - "marked": "^18.0.2", - "playwright": "^1.58.2", - "puppeteer-core": "^24.40.0", - "socks": "^2.8.8" - }, - "engines": { - "bun": ">=1.0.0" - }, - "keywords": [ - "browser", - "automation", - "playwright", - "headless", - "cli", - "claude", - "ai-agent", - "devtools" - ], - "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "0.2.117", - "@anthropic-ai/sdk": "^0.78.0", - "xterm": "5", - "xterm-addon-fit": "^0.8.0" - } + "name": "gstack", + "version": "1.57.4.0", + "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", + "license": "MIT", + "type": "module", + "bin": { + "browse": "./browse/dist/browse", + "make-pdf": "./make-pdf/dist/pdf" + }, + "scripts": { + "build": "bash scripts/build.sh", + "vendor:xterm": "mkdir -p extension/lib && cp node_modules/xterm/lib/xterm.js extension/lib/xterm.js && cp node_modules/xterm/css/xterm.css extension/lib/xterm.css && cp node_modules/xterm-addon-fit/lib/xterm-addon-fit.js extension/lib/xterm-addon-fit.js", + "dev:make-pdf": "bun run make-pdf/src/cli.ts", + "dev:design": "bun run design/src/cli.ts", + "gen:skill-docs": "bun run scripts/gen-skill-docs.ts", + "gen:skill-docs:user": "bun run scripts/gen-skill-docs.ts --respect-detection", + "dev": "bun run browse/src/cli.ts", + "server": "bun run browse/src/server.ts", + "test": "bun test browse/test/ test/ make-pdf/test/ --ignore 'test/skill-e2e-*.test.ts' --ignore test/skill-llm-eval.test.ts --ignore test/skill-routing-e2e.test.ts --ignore test/codex-e2e.test.ts --ignore test/gemini-e2e.test.ts && (bun run slop:diff 2>/dev/null || true)", + "test:free": "bun run scripts/test-free-shards.ts", + "test:windows": "bun run scripts/test-free-shards.ts --windows-only", + "test:evals": "EVALS=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", + "test:evals:all": "EVALS=1 EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", + "test:e2e": "EVALS=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", + "test:e2e:all": "EVALS=1 EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", + "test:gate": "EVALS=1 EVALS_TIER=gate bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-llm-eval.test.ts test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", + "test:periodic": "EVALS=1 EVALS_TIER=periodic EVALS_ALL=1 bun test --retry 2 --concurrent --max-concurrency ${EVALS_CONCURRENCY:-15} test/skill-e2e-*.test.ts test/skill-routing-e2e.test.ts test/codex-e2e.test.ts test/gemini-e2e.test.ts", + "test:codex": "EVALS=1 bun test test/codex-e2e.test.ts", + "test:codex:all": "EVALS=1 EVALS_ALL=1 bun test test/codex-e2e.test.ts", + "test:gemini": "EVALS=1 bun test test/gemini-e2e.test.ts", + "test:gemini:all": "EVALS=1 EVALS_ALL=1 bun test test/gemini-e2e.test.ts", + "skill:check": "bun run scripts/skill-check.ts", + "dev:skill": "bun run scripts/dev-skill.ts", + "start": "bun run browse/src/server.ts", + "eval:list": "bun run scripts/eval-list.ts", + "eval:compare": "bun run scripts/eval-compare.ts", + "eval:summary": "bun run scripts/eval-summary.ts", + "eval:watch": "bun run scripts/eval-watch.ts", + "eval:select": "bun run scripts/eval-select.ts", + "analytics": "bun run scripts/analytics.ts", + "test:audit": "bun test test/audit-compliance.test.ts", + "slop": "npx slop-scan scan . 2>/dev/null || echo 'slop-scan not available (install with: npm i -g slop-scan)'", + "slop:diff": "bun run scripts/slop-diff.ts" + }, + "dependencies": { + "@huggingface/transformers": "^4.1.0", + "@ngrok/ngrok": "^1.7.0", + "diff": "^7.0.0", + "marked": "^18.0.2", + "playwright": "^1.58.2", + "puppeteer-core": "^24.40.0", + "socks": "^2.8.8" + }, + "engines": { + "bun": ">=1.0.0" + }, + "keywords": [ + "browser", + "automation", + "playwright", + "headless", + "cli", + "claude", + "ai-agent", + "devtools" + ], + "devDependencies": { + "@anthropic-ai/claude-agent-sdk": "0.2.117", + "@anthropic-ai/sdk": "^0.78.0", + "xterm": "5", + "xterm-addon-fit": "^0.8.0" + } } diff --git a/setup b/setup index 0c180f7bf5..d4a39e8323 100755 --- a/setup +++ b/setup @@ -475,44 +475,57 @@ if [ "$INSTALL_OPENCODE" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then ) fi -# 2. Ensure Playwright's Chromium is available +# 2. Ensure Playwright's Chromium is available (best-effort: warn on failure). +# +# When Chromium download or launch fails (network issue, missing sudo, +# offline mirror, Bun-on-Windows pipe bug), browser-driven features +# (/qa, /design-review, make-pdf, sidebar, /pair-agent) are unavailable +# until the user retries. All other gstack functionality (skill +# registration, hooks, migrations) is already installed by earlier steps. +# Previously this step was fatal (exit 1) and would abort the whole run +# mid-setup, leaving skills unregistered. Now it sets $_PW_FAIL_REASON, +# prints a named warning, and returns control. Re-running ./setup retries. +_PW_FAIL_REASON="" + if ! ensure_playwright_browser; then echo "Installing Playwright Chromium..." - ( - cd "$SOURCE_GSTACK_DIR" - bunx playwright install chromium - ) + if ! ( cd "$SOURCE_GSTACK_DIR" && bunx playwright install chromium ); then + _PW_FAIL_REASON="chromium-install" + fi - if [ "$IS_WINDOWS" -eq 1 ]; then + if [ -z "$_PW_FAIL_REASON" ] && [ "$IS_WINDOWS" -eq 1 ]; then # On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253). # Ensure playwright is importable by Node from the gstack directory. if ! command -v node >/dev/null 2>&1; then - echo "gstack setup failed: Node.js is required on Windows (Bun cannot launch Chromium due to a pipe bug)" >&2 - echo " Install Node.js: https://nodejs.org/" >&2 - exit 1 + _PW_FAIL_REASON="windows-no-node" + else + echo "Windows detected — verifying Node.js can load Playwright..." + if ! ( cd "$SOURCE_GSTACK_DIR" && \ + # Bun's node_modules already has playwright; verify Node can require it + { node -e "require('playwright')" 2>/dev/null || npm install --no-save playwright; } && \ + # @ngrok/ngrok is externalized in server-node.mjs and resolved at runtime. + # Verify the platform-specific native binary is installed so /pair-agent + # tunnels don't fail later with a cryptic module-not-found error. + { node -e "require('@ngrok/ngrok')" 2>/dev/null || npm install --no-save @ngrok/ngrok; } ); then + _PW_FAIL_REASON="windows-node-modules" + fi fi - echo "Windows detected — verifying Node.js can load Playwright..." - ( - cd "$SOURCE_GSTACK_DIR" - # Bun's node_modules already has playwright; verify Node can require it - node -e "require('playwright')" 2>/dev/null || npm install --no-save playwright - # @ngrok/ngrok is externalized in server-node.mjs and resolved at runtime. - # Verify the platform-specific native binary is installed so /pair-agent - # tunnels don't fail later with a cryptic module-not-found error. - node -e "require('@ngrok/ngrok')" 2>/dev/null || npm install --no-save @ngrok/ngrok - ) fi fi -if ! ensure_playwright_browser; then +if [ -z "$_PW_FAIL_REASON" ] && ! ensure_playwright_browser; then + _PW_FAIL_REASON="post-install-launch" +fi + +if [ -n "$_PW_FAIL_REASON" ]; then + echo "" >&2 + echo " warning: Playwright Chromium is unavailable ($_PW_FAIL_REASON)." >&2 + echo " Browser features (/qa, /design-review, make-pdf, sidebar, /pair-agent) are unavailable." >&2 if [ "$IS_WINDOWS" -eq 1 ]; then - echo "gstack setup failed: Playwright Chromium could not be launched via Node.js" >&2 - echo " This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2 - echo " Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2 - else - echo "gstack setup failed: Playwright Chromium could not be launched" >&2 + echo " Known Bun-on-Windows issue (oven-sh/bun#4253). Ensure Node.js is installed," >&2 + echo " then verify: node -e \"require('playwright')\"" >&2 fi - exit 1 + echo " Re-run ./setup to retry. Skills, hooks, and migrations are already installed." >&2 fi # 2b. Ensure a color-emoji font is installed so make-pdf emoji render (Linux). diff --git a/test/setup-playwright-warn-not-exit.test.ts b/test/setup-playwright-warn-not-exit.test.ts new file mode 100644 index 0000000000..abbdc0970b --- /dev/null +++ b/test/setup-playwright-warn-not-exit.test.ts @@ -0,0 +1,105 @@ +import { describe, test, expect } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const SETUP_SRC = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8'); + +function runBash(script: string): { stdout: string; stderr: string; status: number } { + const r = spawnSync('bash', ['-c', script], { encoding: 'utf-8' }); + return { stdout: r.stdout || '', stderr: r.stderr || '', status: r.status ?? -1 }; +} + +// Extract the live Playwright install block from setup. Source-anchored so +// the test is resilient to line-number drift. +function extractPlaywrightBlock(): string { + const start = SETUP_SRC.indexOf("# 2. Ensure Playwright's Chromium"); + expect(start).toBeGreaterThan(-1); + // Block ends just before the next top-level section comment. + const end = SETUP_SRC.indexOf('\n# 2b.', start); + expect(end).toBeGreaterThan(start); + return SETUP_SRC.slice(start, end); +} + +describe('setup: Playwright failure is best-effort warn, not fatal exit', () => { + const block = extractPlaywrightBlock(); + + test('no `exit 1` is reachable from the Playwright install/verify path', () => { + // The bug shape used `exit 1` twice. The fix replaces both with a + // $_PW_FAIL_REASON accumulator that prints a named warning. Either an + // explicit `exit 1` OR `exit 1` (any spaces) inside the block is the bug. + expect(block).not.toMatch(/^\s*exit\s+1\b/m); + }); + + test('uses the $_PW_FAIL_REASON accumulator + named warning', () => { + expect(block).toContain('_PW_FAIL_REASON=""'); + expect(block).toContain('_PW_FAIL_REASON="chromium-install"'); + expect(block).toContain('_PW_FAIL_REASON="post-install-launch"'); + expect(block).toMatch(/warning: Playwright Chromium is unavailable/); + // Tell users this is recoverable, not a hard fail. + expect(block).toMatch(/Re-run \.\/setup to retry/); + }); + + test('functional: install failure leaves the script with exit 0 and prints the warning', () => { + // Run a stripped harness that mimics the live block: + // - ensure_playwright_browser stubbed to always fail + // - bunx stubbed to fail (simulates Chromium download failure) + // Assert: exit 0, warning printed, NOT an exit-1 path. + const r = runBash(` + set +e + IS_WINDOWS=0 + SOURCE_GSTACK_DIR=/tmp + ensure_playwright_browser() { return 1; } # browser missing + bunx() { return 1; } # install fails + + ${block} + + RC=$? + echo "FINAL_RC=$RC" + `); + + // Script must NOT exit non-zero (the bug shape would). + expect(r.stdout).toContain('FINAL_RC=0'); + // Named warning fires with the expected reason. + expect(r.stderr).toContain('warning: Playwright Chromium is unavailable (chromium-install)'); + // User-actionable retry hint. + expect(r.stderr).toContain('Re-run ./setup to retry'); + }); + + test('functional: bug shape (with `exit 1`) would have exited non-zero', () => { + // Inline the BUGGY shape — confirms the difference is real, not an + // illusion of bash quoting. This is the form we removed. + const buggy = ` + ensure_playwright_browser() { return 1; } + bunx() { return 1; } + if ! ensure_playwright_browser; then + ( cd /tmp && bunx playwright install chromium ) + fi + if ! ensure_playwright_browser; then + echo "gstack setup failed: Playwright Chromium could not be launched" >&2 + exit 1 + fi + `; + const r = runBash(`set +e; ( ${buggy} ); echo "FINAL_RC=$?"`); + expect(r.stdout).toContain('FINAL_RC=1'); + }); + + test('functional: post-install verify failure also degrades to warn', () => { + // Simulate the install succeeding but the post-install launch still failing. + // The fixed shape sets _PW_FAIL_REASON="post-install-launch" and warns. + const r = runBash(` + set +e + IS_WINDOWS=0 + SOURCE_GSTACK_DIR=/tmp + ensure_playwright_browser() { return 1; } + bunx() { return 0; } # install succeeds + + ${block} + + echo "FINAL_RC=$?" + `); + expect(r.stdout).toContain('FINAL_RC=0'); + expect(r.stderr).toContain('warning: Playwright Chromium is unavailable (post-install-launch)'); + }); +});