File size: 2,495 Bytes
daea45b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Headless verification of model-built web apps (the web `run_python`).

smolbuilder's agent writes HTML/CSS/JS but, unlike the Python path, had no way
to *run* it — so it shipped broken apps and couldn't tell. This bridges to a
small Node + jsdom checker (engine/webcheck.js) that loads the page, runs its
scripts, clicks every button, and reports JavaScript errors.

Graceful degradation is deliberate: if Node or jsdom isn't available (e.g. a
minimal Space image), we return `None` ("unverifiable") rather than failing the
build — the agent/router fall back to the structural check.
"""
from __future__ import annotations

import json
import shutil
import subprocess
import tempfile
from pathlib import Path

_CHECKER = Path(__file__).with_name("webcheck.js")


def available() -> bool:
    """True if we can actually run the headless check (Node present)."""
    return shutil.which("node") is not None and _CHECKER.exists()


def check_html(html: str, timeout: int = 20) -> tuple[bool | None, list[str]]:
    """Run the headless check on an HTML document.

    Returns (ok, errors):
      - (True, [])        the app loaded and all buttons clicked without error
      - (False, [...])    real JavaScript errors were found
      - (None, [...])     unverifiable (Node/jsdom missing, or the checker broke)
    """
    node = shutil.which("node")
    if not node or not _CHECKER.exists():
        return None, ["node/jsdom unavailable (skipped runtime check)"]

    with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as f:
        f.write(html)
        path = f.name
    try:
        proc = subprocess.run(
            [node, str(_CHECKER), path],
            capture_output=True, text=True, timeout=timeout,
        )
    except subprocess.TimeoutExpired:
        return None, [f"runtime check timed out after {timeout}s"]
    finally:
        Path(path).unlink(missing_ok=True)

    if proc.returncode == 3:        # jsdom not installed
        return None, ["jsdom not installed (skipped runtime check)"]
    line = (proc.stdout or "").strip().splitlines()
    if not line:
        return None, [f"runtime check produced no output: {proc.stderr.strip()[:200]}"]
    try:
        data = json.loads(line[-1])
    except json.JSONDecodeError:
        return None, [f"runtime check output unparseable: {line[-1][:200]}"]

    if data.get("ok") is None:
        return None, [data.get("infra", "unverifiable")]
    return bool(data.get("ok")), list(data.get("errors", []))