File size: 9,834 Bytes
674d56a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | """Regression tests for install.sh browser setup.
Browser automation is optional. The installer should not leave Hermes
half-installed just because Playwright's managed Chromium download hangs on an
unsupported distribution.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
INSTALL_SH = REPO_ROOT / "scripts" / "install.sh"
def test_install_script_honors_explicit_browser_override_only() -> None:
"""find_system_browser consults only an explicit AGENT_BROWSER_EXECUTABLE_PATH."""
text = INSTALL_SH.read_text()
assert 'override="${AGENT_BROWSER_EXECUTABLE_PATH:-}"' in text
# An explicit override still skips the bundled download (override, not fallback).
assert "Skipping bundled Chromium download" in text
def test_playwright_installs_are_timeout_guarded() -> None:
text = INSTALL_SH.read_text()
# The timeout wrapper still exists and is used internally by the install
# wrapper, so every Playwright download remains bounded.
assert "run_browser_install_with_timeout()" in text
# Playwright installs now go through run_playwright_install(), which wraps
# run_browser_install_with_timeout (timeout-guarded) and adds an
# unrecognized-platform fallback retry.
assert "run_playwright_install 600 npx playwright install chromium" in text
# --with-deps is still invoked on apt-based systems, but only when sudo
# is available non-interactively (root or passwordless sudo). Non-sudo
# service users fall back to the browser-only install — see
# install_node_deps() in install.sh.
assert "run_playwright_install 600 npx playwright install --with-deps chromium" in text
# The wrapper still bounds the download with the timeout helper.
assert 'run_browser_install_with_timeout "$timeout_seconds" "$@"' in text
def test_install_script_supports_skip_browser_flag() -> None:
"""--skip-browser (and --no-playwright alias) skips the Playwright install."""
text = INSTALL_SH.read_text()
assert "--skip-browser|--no-playwright)" in text
assert "SKIP_BROWSER=true" in text
assert 'if [ "$SKIP_BROWSER" = true ]; then' in text
assert "--skip-browser Skip Playwright/Chromium install" in text
def test_browser_install_timeout_stays_interruptible() -> None:
"""The Playwright download must stay Ctrl+C-able and force-kill if wedged.
GNU `timeout` runs the child in its own process group, so a terminal Ctrl+C
reaches `timeout` but never the download — it looks frozen and ignores
Ctrl+C (#35166). `--foreground` keeps it in the shell's foreground group;
`-k 10` guarantees a SIGKILL after the deadline. Both are GNU-only, so the
installer probes support once and falls back to plain `timeout`.
"""
text = INSTALL_SH.read_text()
# GNU-flag probe + the guarded invocation must both be present. The timeout
# binary is parameterized ($timeout_bin) so macOS gtimeout works too (#39219).
assert '"$timeout_bin" --foreground -k 10 1 true' in text
assert '"$timeout_bin" --foreground -k 10 "$timeout_seconds" "$@"' in text
# Plain-timeout fallback preserved for BusyBox/non-GNU.
assert '"$timeout_bin" "$timeout_seconds" "$@"' in text
# ---------------------------------------------------------------------------
# Behavioral tests: source the install.sh helpers in a stubbed shell and assert
# the override retry fires ONLY on a too-new apt release (#35166), and not on a
# host Playwright already supports.
# ---------------------------------------------------------------------------
import subprocess
def _run_install_fn(distro: str, version: str, *, native_fails: bool,
arch: str = "x86_64", operator_override: str = "") -> dict:
"""Source the relevant functions from install.sh and drive run_playwright_install.
Stubs `npx` (the install command) to fail/succeed, `uname -m` for arch, and
`log_warn`/`log_info` to no-ops. Returns parsed observations: how many times
the install command ran, and the override value seen on each run.
"""
# Extract the functions we need so we don't execute the whole installer.
# run_browser_install_with_timeout delegates to run_with_timeout (#39219),
# so the helper must be pulled in too or the install command never runs.
fn_names = [
"run_browser_install_with_timeout",
"run_with_timeout",
"playwright_host_unrecognized",
"playwright_fallback_platform",
"run_playwright_install",
]
src = INSTALL_SH.read_text()
import re
extracted = []
for name in fn_names:
m = re.search(rf"^{re.escape(name)}\(\) \{{.*?^\}}", src, re.MULTILINE | re.DOTALL)
assert m, f"could not extract {name}() from install.sh"
extracted.append(m.group(0))
body = "\n\n".join(extracted)
native_rc = 1 if native_fails else 0
harness = f"""
set -u
DISTRO={distro!r}
DISTRO_VERSION={version!r}
export PLAYWRIGHT_HOST_PLATFORM_OVERRIDE={operator_override!r}
[ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ] && unset PLAYWRIGHT_HOST_PLATFORM_OVERRIDE
log_warn() {{ :; }}
log_info() {{ :; }}
# Stub `uname -m` for arch control without touching the real binary.
uname() {{ if [ "$1" = "-m" ]; then echo {arch!r}; else command uname "$@"; fi }}
# Stub `timeout`: just run the command, ignoring flags/duration. We only care
# about how the npx stub behaves, not real timeout semantics here.
timeout() {{
while [ $# -gt 0 ]; do
case "$1" in -*|[0-9]*) shift ;; *) break ;; esac
done
"$@"
}}
# Stub the install command. Record each invocation + the override in effect.
npx() {{
echo "RUN override=${{PLAYWRIGHT_HOST_PLATFORM_OVERRIDE:-<none>}}" >>"$RUNLOG"
# First run reflects native_fails; the override retry (if any) succeeds.
if [ -n "${{PLAYWRIGHT_HOST_PLATFORM_OVERRIDE:-}}" ]; then return 0; fi
return {native_rc}
}}
{body}
run_playwright_install 600 npx playwright install --with-deps chromium
echo "FINAL_RC=$?"
"""
import tempfile, os
with tempfile.NamedTemporaryFile("w", suffix=".log", delete=False) as lf:
runlog = lf.name
try:
env = dict(os.environ, RUNLOG=runlog)
proc = subprocess.run(["bash", "-c", harness], capture_output=True,
text=True, env=env)
runs = Path(runlog).read_text().strip().splitlines()
final_rc = None
for line in proc.stdout.splitlines():
if line.startswith("FINAL_RC="):
final_rc = int(line.split("=", 1)[1])
return {"runs": runs, "final_rc": final_rc, "stderr": proc.stderr}
finally:
Path(runlog).unlink(missing_ok=True)
def test_override_retry_fires_on_ubuntu_26() -> None:
"""Ubuntu 26.04 (too new) → native fails → retry with ubuntu24.04 override."""
r = _run_install_fn("ubuntu", "26.04", native_fails=True)
assert len(r["runs"]) == 2, r["runs"]
assert "override=<none>" in r["runs"][0]
assert "override=ubuntu24.04-x64" in r["runs"][1]
assert r["final_rc"] == 0
def test_override_retry_fires_on_debian_14() -> None:
"""Debian 14 (> 13) is the too-new apt case → retry with override."""
r = _run_install_fn("debian", "14", native_fails=True)
assert len(r["runs"]) == 2, r["runs"]
assert "override=ubuntu24.04-x64" in r["runs"][1]
assert r["final_rc"] == 0
def test_no_retry_when_native_succeeds_on_ubuntu_26() -> None:
"""Even on Ubuntu 26.04, a successful native install is never retried."""
r = _run_install_fn("ubuntu", "26.04", native_fails=False)
assert len(r["runs"]) == 1, r["runs"]
assert "override=<none>" in r["runs"][0]
assert r["final_rc"] == 0
import re
def _extract_function_body(source: str, name: str) -> str:
m = re.search(rf"^{re.escape(name)}\(\) \{{.*?^\}}", source, re.MULTILINE | re.DOTALL)
assert m, f"could not extract {name}() from install.sh"
return m.group(0)
def test_ensure_browser_no_longer_npm_installs_agent_browser() -> None:
"""agent-browser resolves lazily via npx everywhere else in the system
(tools/browser_tool.py::_find_agent_browser); this was the last place
that still eagerly npm-installed a second, separately version-pinned
copy of it. Removed: agent-browser acquisition now happens only via
`hermes update`'s npx cache warm or an actual browser-tool call's lazy
npx resolution (PR #44772 review)."""
body = _extract_function_body(INSTALL_SH.read_text(), "ensure_browser")
assert "agent-browser@" not in body
assert "Installing Chromium via agent-browser install" not in body
# camofox is unrelated to this change and must still be installed here.
assert "@askjo/camofox-browser@^1.5.2" in body
# System-browser detection is still cheap/valuable without agent-browser.
assert "find_system_browser" in body
assert "configure_browser_env_from_system_browser" in body
def test_ensure_browser_still_ignore_scripts_and_timeout_guarded() -> None:
"""The removal of agent-browser must not have also dropped the
supply-chain and hang-protection hardening that still applies to the
remaining camofox install."""
body = _extract_function_body(INSTALL_SH.read_text(), "ensure_browser")
assert "--ignore-scripts" in body
assert "run_with_timeout" in body
def test_ensure_browser_no_longer_references_agent_browser_binary_path() -> None:
"""No dangling reference to a local agent-browser binary path should
remain now that this function never installs it — a leftover reference
would be dead code pointing at a binary that no longer gets placed
there by this function."""
body = _extract_function_body(INSTALL_SH.read_text(), "ensure_browser")
assert "$HERMES_HOME/node/bin/agent-browser" not in body
|