Files
2026-07-13 13:12:33 +08:00

245 lines
8.2 KiB
Python

"""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"] == []