"""Opt-in real-browser smoke for the Control UI. The default test suite skips this file. Run it with: OPENSQUILLA_WEBUI_BROWSER_E2E=1 uv run pytest tests/functional/test_webui_browser_e2e.py -q -s """ from __future__ import annotations import json import os import socket import subprocess import sys import textwrap import time from pathlib import Path import httpx import pytest pytestmark = pytest.mark.webui_browser def _free_port() -> int: with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) return int(sock.getsockname()[1]) def _npm() -> str: return "npm.cmd" if os.name == "nt" else "npm" def _node() -> str: return "node.exe" if os.name == "nt" else "node" def _install_playwright(work_dir: Path) -> None: result = subprocess.run( [_npm(), "--prefix", str(work_dir), "install", "playwright"], cwd=Path.cwd(), check=False, capture_output=True, text=True, timeout=120, ) assert result.returncode == 0, result.stderr or result.stdout browser_result = subprocess.run( [_npm(), "--prefix", str(work_dir), "exec", "playwright", "install", "chromium"], cwd=Path.cwd(), check=False, capture_output=True, text=True, timeout=180, ) assert browser_result.returncode == 0, browser_result.stderr or browser_result.stdout def _wait_for_health(port: int, server: subprocess.Popen[str]) -> None: url = f"http://127.0.0.1:{port}/health" deadline = time.monotonic() + 20.0 last_error = "" while time.monotonic() < deadline: if server.poll() is not None: stdout = server.stdout.read() if server.stdout else "" stderr = server.stderr.read() if server.stderr else "" raise AssertionError( f"gateway exited early code={server.returncode}\nstdout={stdout}\nstderr={stderr}" ) try: response = httpx.get(url, timeout=1.0) if response.status_code == 200 and response.json().get("ok") is True: return except Exception as exc: # noqa: BLE001 - included in timeout assertion. last_error = str(exc) time.sleep(0.1) raise AssertionError(f"gateway did not become healthy: {last_error}") def _stop_process(process: subprocess.Popen[str]) -> None: process.terminate() try: process.wait(timeout=8) except subprocess.TimeoutExpired: process.kill() process.wait(timeout=8) def test_control_ui_loads_in_real_browser(tmp_path: Path) -> None: if os.environ.get("OPENSQUILLA_WEBUI_BROWSER_E2E") != "1": pytest.skip("set OPENSQUILLA_WEBUI_BROWSER_E2E=1 to run browser smoke") port = _free_port() server_script = tmp_path / "webui_smoke_server.py" browser_script = tmp_path / "webui_smoke_browser.js" state_dir = tmp_path / "state" proposal_dir = state_dir / "proposals" / "deadbeef" proposal_dir.mkdir(parents=True) (proposal_dir / "SKILL.md").write_text( "---\n" "name: browser-audit-proposal\n" "kind: meta\n" "description: Browser smoke proposal.\n" "triggers: [browser audit proposal]\n" "composition:\n" " steps:\n" " - id: classify\n" " kind: llm_classify\n" " output_choices: [A, B]\n" " with: {text: x}\n" "---\n" "# browser-audit-proposal\n", encoding="utf-8", ) (proposal_dir / "gates.json").write_text( json.dumps( { "auto_enable_eligible": True, "auto_enable": { "status": "skipped", "reason": "risk_too_high", "risk_level": "medium", "max_risk": "low", "details": { "validation_profile": "static-safety-v2", "skills": ["artifact-writer"], "tools": [], "reasons": ["capability:artifact-writer:filesystem-write"], }, }, } ), encoding="utf-8", ) server_script.write_text( textwrap.dedent( f""" import uvicorn from opensquilla.gateway.app import create_gateway_app from opensquilla.gateway.config import AuthConfig, GatewayConfig config = GatewayConfig( host="127.0.0.1", port={port}, auth=AuthConfig(mode="none"), ) app = create_gateway_app(config) if __name__ == "__main__": uvicorn.run(app, host="127.0.0.1", port={port}, log_level="warning") """ ), encoding="utf-8", ) browser_script.write_text( textwrap.dedent( """ const { chromium } = require("playwright"); (async () => { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage(); const errors = []; page.on("pageerror", err => errors.push(String(err))); const response = await page.goto(process.env.TARGET_URL, { waitUntil: "domcontentloaded", timeout: 30000, }); await page.waitForFunction( () => document.querySelector("#conn-pill")?.textContent === "Connected", { timeout: 15000 } ); await page.locator('a[data-path="/skills"]').click(); await page.waitForSelector('[data-proposal-show="deadbeef"]', { state: "attached", timeout: 15000, }); await page.locator('[data-proposal-show="deadbeef"]').click(); await page.waitForSelector('dialog[open] .sk-audit-grid', { timeout: 15000 }); const auditText = await page.locator('dialog[open]').innerText(); const result = { status: response ? response.status() : 0, title: await page.title(), appCount: await page.locator("#app").count(), basePath: await page.locator("#opensquilla-data").getAttribute("data-base-path"), authMode: await page.locator("#opensquilla-data").getAttribute("data-auth-mode"), proposalButtons: await page.locator('[data-proposal-show="deadbeef"]').count(), auditText, pageErrors: errors, }; await browser.close(); console.log(JSON.stringify(result)); })().catch(err => { console.error(err && err.stack ? err.stack : String(err)); process.exit(1); }); """ ), encoding="utf-8", ) env = os.environ.copy() env["OPENSQUILLA_STATE_DIR"] = str(state_dir) server = subprocess.Popen( [sys.executable, str(server_script)], cwd=Path.cwd(), env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8", errors="replace", ) try: _wait_for_health(port, server) _install_playwright(tmp_path) browser_env = dict(env, TARGET_URL=f"http://127.0.0.1:{port}/control/") result = subprocess.run( [_node(), str(browser_script)], cwd=tmp_path, env=browser_env, check=False, capture_output=True, text=True, timeout=60, ) assert result.returncode == 0, result.stderr or result.stdout payload = json.loads(result.stdout.strip().splitlines()[-1]) finally: _stop_process(server) assert payload["status"] == 200 assert payload["title"] == "OpenSquilla Control" assert payload["appCount"] == 1 assert payload["basePath"] == "/control" assert payload["authMode"] == "none" assert payload["proposalButtons"] == 1 assert "Auto-enable Audit" in payload["auditText"] assert "static-safety-v2" in payload["auditText"] assert "medium / low" in payload["auditText"] assert "capability:artifact-writer:filesystem-write" in payload["auditText"] assert payload["pageErrors"] == []