Integrate the rodney/showboat browser automation skill for headless Chrome screenshots and testing. Exclude .rodney and .agent-tests from Biome file scanning. Add picomatch override to resolve high-severity ReDoS vulnerability in knip/jscpd transitive deps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
161 lines
5.0 KiB
Python
161 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Set up a browser-interactive-testing session.
|
|
|
|
Creates the output directory, inits the showboat report, starts a browser,
|
|
and prints the DIR path. Automatically detects whether rodney can launch
|
|
its own Chromium or falls back to a system-installed browser.
|
|
"""
|
|
|
|
import datetime
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
REMOTE_DEBUG_PORT = 9222
|
|
|
|
|
|
def find_system_browser():
|
|
"""Return the path to a system Chrome/Chromium binary, or None."""
|
|
for name in ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"]:
|
|
path = shutil.which(name)
|
|
if path:
|
|
return path
|
|
return None
|
|
|
|
|
|
def port_listening(port):
|
|
"""Check if something is already listening on the given port."""
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.settimeout(1)
|
|
return s.connect_ex(("localhost", port)) == 0
|
|
|
|
|
|
def try_connect(port):
|
|
"""Try to connect rodney to a browser on the given port. Returns True on success."""
|
|
result = subprocess.run(
|
|
["uvx", "rodney", "connect", "--local", f"localhost:{port}"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
print(f"Connected to existing browser on port {port}", file=sys.stderr)
|
|
return True
|
|
return False
|
|
|
|
|
|
def launch_system_browser(browser_path):
|
|
"""Launch a system browser with remote debugging and wait for it to be ready."""
|
|
subprocess.Popen(
|
|
[
|
|
browser_path,
|
|
"--headless",
|
|
"--disable-gpu",
|
|
f"--remote-debugging-port={REMOTE_DEBUG_PORT}",
|
|
"--no-sandbox",
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
# Wait for the browser to start listening
|
|
for _ in range(20):
|
|
if port_listening(REMOTE_DEBUG_PORT):
|
|
return True
|
|
time.sleep(0.25)
|
|
return False
|
|
|
|
|
|
def start_browser():
|
|
"""Start a headless browser and connect rodney to it.
|
|
|
|
Strategy order (fastest path first):
|
|
1. Connect to an already-running browser on the debug port.
|
|
2. Launch a system Chrome/Chromium (avoids rodney's Chromium download,
|
|
which fails on some architectures like Linux ARM64).
|
|
3. Let rodney launch its own browser as a last resort.
|
|
"""
|
|
# Strategy 1: connect to an already-running browser
|
|
if port_listening(REMOTE_DEBUG_PORT) and try_connect(REMOTE_DEBUG_PORT):
|
|
return
|
|
|
|
# Strategy 2: launch a system browser (most reliable on Linux)
|
|
browser = find_system_browser()
|
|
if browser:
|
|
print(f"Launching system browser: {browser}", file=sys.stderr)
|
|
if launch_system_browser(browser):
|
|
if try_connect(REMOTE_DEBUG_PORT):
|
|
return
|
|
print("WARNING: system browser started but rodney could not connect", file=sys.stderr)
|
|
else:
|
|
print("WARNING: system browser did not start in time", file=sys.stderr)
|
|
|
|
# Strategy 3: let rodney try its built-in launcher
|
|
result = subprocess.run(
|
|
["uvx", "rodney", "start", "--local"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
print("Browser started via rodney", file=sys.stderr)
|
|
return
|
|
|
|
print(
|
|
"ERROR: Could not start a browser. Tried:\n"
|
|
f" - Connecting to localhost:{REMOTE_DEBUG_PORT} (no browser found)\n"
|
|
f" - System browser: {browser or 'not found'}\n"
|
|
" - rodney start (failed)\n"
|
|
"Install chromium or google-chrome and try again.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(1)
|
|
|
|
|
|
def ensure_gitignore_entry(entry):
|
|
"""Add entry to .gitignore if the file exists and the entry is missing."""
|
|
gitignore = ".gitignore"
|
|
if not os.path.isfile(gitignore):
|
|
return
|
|
with open(gitignore, "r") as f:
|
|
content = f.read()
|
|
# Check if the entry (with or without trailing slash/newline variations) is already present
|
|
lines = content.splitlines()
|
|
if any(line.strip() == entry or line.strip() == entry.rstrip("/") for line in lines):
|
|
return
|
|
# Append the entry
|
|
with open(gitignore, "a") as f:
|
|
if content and not content.endswith("\n"):
|
|
f.write("\n")
|
|
f.write(f"{entry}\n")
|
|
print(f"Added '{entry}' to .gitignore", file=sys.stderr)
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print(f"Usage: {sys.argv[0]} <slug> <report-title>", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
slug = sys.argv[1]
|
|
title = sys.argv[2]
|
|
|
|
# Create output directory
|
|
d = f".agent-tests/{datetime.date.today()}-{slug}"
|
|
os.makedirs(f"{d}/screenshots", exist_ok=True)
|
|
|
|
# Ensure .rodney/ is in .gitignore (rodney stores session files there)
|
|
ensure_gitignore_entry(".rodney/")
|
|
|
|
# Init showboat report
|
|
subprocess.run(["uvx", "showboat", "init", f"{d}/report.md", title], check=True)
|
|
|
|
# Start browser
|
|
start_browser()
|
|
|
|
# Print the directory path (only real stdout, everything else goes to stderr)
|
|
print(d)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|