from __future__ import annotations
import base64
from datetime import UTC, datetime
from pathlib import Path
import pytest
from core.models import Task
from device.manager import DeviceManager
from runtime.task import TaskRunner, TaskRunnerConfig
from storage.artifact_store import ArtifactStore
from storage.device_config import DeviceConfigStore
from storage.task_metadata import TaskMetadataStore
from storage.timeline import Timeline
from tests.fakes import PNG_10X20, FakeDriver
def _client(tmp_path, *, manager=None, runner=None, config_store=None, timeline=None):
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
from api.rest import create_app
metadata_store = TaskMetadataStore(tmp_path / "tasks.sqlite3")
app = create_app(
manager=manager or DeviceManager(),
metadata_store=metadata_store,
task_runner=runner,
device_config_store=config_store
or DeviceConfigStore(tmp_path / "device_config.sqlite3"),
timeline=timeline or Timeline(ArtifactStore(tmp_path / "history")),
)
return TestClient(app), metadata_store
_TEMPLATE_NAMES = [
"base.html",
"dashboard.html",
"_status_fragment.html",
"tasks.html",
"task_detail.html",
"config.html",
]
# -- 5.2 Template tests ------------------------------------------------------
def test_module_jinja_environment_autoescapes_html_and_xml() -> None:
from api.console_web import _ENV
# select_autoescape(["html", "xml"]) returns a callable used by Jinja2.
assert callable(_ENV.autoescape)
assert _ENV.autoescape("foo.html") is True
assert _ENV.autoescape("foo.xml") is True
def test_every_console_template_is_known_and_loadable() -> None:
from api.console_web import _ENV
for name in _TEMPLATE_NAMES:
assert _ENV.get_template(name) is not None
def test_no_template_uses_safe_filter_bypass() -> None:
template_dir = (
Path(__file__).resolve().parent.parent / "api" / "templates" / "runtime_console"
)
for path in template_dir.glob("*.html"):
source = path.read_text(encoding="utf-8")
assert "| safe" not in source, f"{path.name} uses |safe bypass"
assert "|safe" not in source, f"{path.name} uses |safe bypass"
def test_dashboard_escapes_untrusted_device_name(tmp_path) -> None:
manager = DeviceManager()
manager.register_device(
"dev-xss",
lambda: FakeDriver(),
name="",
driver_type="wda",
)
client, _ = _client(tmp_path, manager=manager)
body = client.get("/ui/").text
assert "<script>" in body
assert "" not in body
def test_tasks_list_escapes_untrusted_goal(tmp_path) -> None:
client, metadata_store = _client(tmp_path)
metadata_store.create_task(
Task(
id="task-xss",
goal="",
device_id="dev-1",
)
)
body = client.get("/ui/tasks").text
assert "<script>" in body
assert "" not in body
def test_task_detail_escapes_failure_reason_and_structured_output(tmp_path) -> None:
timeline = Timeline(ArtifactStore(tmp_path / "history"))
client, metadata_store = _client(tmp_path, timeline=timeline)
metadata_store.create_task(
Task(
id="task-detail",
goal="do thing",
device_id="dev-1",
failure_reason="",
)
)
timeline.append(
task_id="task-detail",
scene={"screen": {"width": 10, "height": 20}, "elements": []},
prompt="do thing",
tool_call={"action": ""},
screenshot=PNG_10X20,
)
body = client.get("/ui/tasks/task-detail").text
assert "" not in body
assert "",
"driver_type": "bad-driver",
},
)
assert response.status_code == 400
assert "" not in response.text
assert "<script>" in response.text
# -- 5.3 TestClient page-route coverage --------------------------------------
def test_root_redirects_to_ui(tmp_path) -> None:
client, _ = _client(tmp_path)
response = client.get("/", follow_redirects=False)
assert response.status_code == 307
assert response.headers["location"] == "/ui/"
def test_dashboard_serves_html_with_empty_state(tmp_path) -> None:
client, _ = _client(tmp_path)
response = client.get("/ui/")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "No devices registered" in response.text
def test_dashboard_shows_populated_metrics_and_devices(tmp_path) -> None:
manager = DeviceManager()
manager.register_device(
"iphone-1",
lambda: FakeDriver(),
name="Desk iPhone",
driver_type="wda",
)
client, metadata_store = _client(tmp_path, manager=manager)
metadata_store.create_task(
Task(id="t-running", goal="run", device_id="iphone-1", status="running")
)
metadata_store.create_task(
Task(id="t-failed", goal="fail", device_id="iphone-1", status="failed")
)
body = client.get("/ui/").text
assert "Desk iPhone" in body
assert "iphone-1" in body
def test_status_fragment_endpoint_returns_html_partial(tmp_path) -> None:
client, _ = _client(tmp_path)
response = client.get("/ui/_status_fragment")
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
assert "Device Status" in response.text
def test_tasks_page_supports_device_and_status_filters(tmp_path) -> None:
manager = DeviceManager()
manager.register_device(
"iphone-1",
lambda: FakeDriver(),
name="Desk",
driver_type="wda",
)
client, metadata_store = _client(tmp_path, manager=manager)
older = Task(
id="task-old",
goal="open settings",
device_id="iphone-1",
created_at=datetime(2026, 1, 1, tzinfo=UTC),
updated_at=datetime(2026, 1, 1, tzinfo=UTC),
)
newer = Task(
id="task-new",
goal="search",
device_id="iphone-2",
status="running",
created_at=datetime(2026, 1, 2, tzinfo=UTC),
updated_at=datetime(2026, 1, 2, tzinfo=UTC),
)
metadata_store.create_task(older)
metadata_store.create_task(newer)
body_all = client.get("/ui/tasks").text
assert "task-old" in body_all
assert "task-new" in body_all
body_filtered = client.get("/ui/tasks?device_id=iphone-1").text
assert "task-old" in body_filtered
assert "task-new" not in body_filtered
body_status = client.get("/ui/tasks?status=running").text
assert "task-new" in body_status
assert "task-old" not in body_status
def test_task_detail_renders_timeline_with_screenshot(tmp_path) -> None:
timeline = Timeline(ArtifactStore(tmp_path / "history"))
client, metadata_store = _client(tmp_path, timeline=timeline)
metadata_store.create_task(
Task(id="task-with-timeline", goal="tap search", device_id="iphone-1")
)
timeline.append(
task_id="task-with-timeline",
scene={"screen": {"width": 10, "height": 20}, "elements": []},
prompt="tap search",
tool_call={"action": "tap", "args": {"x": 1, "y": 2}},
result={"ok": True},
screenshot=PNG_10X20,
)
body = client.get("/ui/tasks/task-with-timeline").text
expected_data_uri = "data:image/png;base64," + base64.b64encode(PNG_10X20).decode(
"ascii"
)
assert expected_data_uri in body
assert "tap" in body
def test_task_detail_404_for_unknown_task(tmp_path) -> None:
client, _ = _client(tmp_path)
response = client.get("/ui/tasks/does-not-exist")
assert response.status_code == 404
def test_config_page_lists_supported_drivers_and_current_max_steps(tmp_path) -> None:
config_store = DeviceConfigStore(tmp_path / "device_config.sqlite3")
config_store.set_setting("max_steps", 5)
runner = TaskRunner(config=TaskRunnerConfig(max_steps=1))
client, _ = _client(tmp_path, runner=runner, config_store=config_store)
body = client.get("/ui/config").text
assert "wda" in body
assert 'value="5"' in body
def test_register_device_prg_redirects_and_persists(tmp_path) -> None:
config_store = DeviceConfigStore(tmp_path / "device_config.sqlite3")
client, _ = _client(tmp_path, config_store=config_store)
response = client.post(
"/ui/config/devices",
data={
"name": "Desk iPhone",
"driver_type": "wda",
"server_url": "http://127.0.0.1:4723",
"udid": "abc123",
"wda_local_port": "8100",
},
follow_redirects=False,
)
assert response.status_code == 303
assert response.headers["location"].endswith("/ui/config")
assert len(config_store.list()) == 1
def test_register_device_rejects_bad_driver_without_partial_mutation(tmp_path) -> None:
config_store = DeviceConfigStore(tmp_path / "device_config.sqlite3")
client, _ = _client(tmp_path, config_store=config_store)
response = client.post(
"/ui/config/devices",
data={"name": "Bad", "driver_type": "android"},
)
assert response.status_code == 400
assert config_store.list() == []
assert "unsupported driver_type" in response.text
def test_register_device_rejects_non_numeric_port_without_partial_mutation(
tmp_path,
) -> None:
config_store = DeviceConfigStore(tmp_path / "device_config.sqlite3")
client, _ = _client(tmp_path, config_store=config_store)
response = client.post(
"/ui/config/devices",
data={
"name": "Bad Port",
"driver_type": "wda",
"wda_local_port": "not-a-number",
},
)
assert response.status_code == 400
assert config_store.list() == []
assert "wda_local_port must be a number" in response.text
def test_remove_device_prg_redirects_and_removes(tmp_path) -> None:
config_store = DeviceConfigStore(tmp_path / "device_config.sqlite3")
config_store.add(
device_id="removable-1",
name="To Remove",
driver_type="wda",
connection_info={"udid": "abc"},
)
client, _ = _client(tmp_path, config_store=config_store)
response = client.post(
"/ui/config/devices/removable-1/delete",
follow_redirects=False,
)
assert response.status_code == 303
assert config_store.list() == []
def test_update_max_steps_prg_redirects_and_applies(tmp_path) -> None:
runner = TaskRunner(config=TaskRunnerConfig(max_steps=1))
client, _ = _client(tmp_path, runner=runner)
response = client.post(
"/ui/config/max-steps",
data={"max_steps": "25"},
follow_redirects=False,
)
assert response.status_code == 303
assert runner.config.max_steps == 25
def test_update_max_steps_rejects_non_positive_without_partial_mutation(
tmp_path,
) -> None:
config_store = DeviceConfigStore(tmp_path / "device_config.sqlite3")
config_store.set_setting("max_steps", 10)
runner = TaskRunner(config=TaskRunnerConfig(max_steps=1))
client, _ = _client(tmp_path, runner=runner, config_store=config_store)
response = client.post("/ui/config/max-steps", data={"max_steps": "0"})
assert response.status_code == 400
assert runner.config.max_steps == 10
assert "max_steps must be positive" in response.text
def test_update_max_steps_rejects_non_integer(tmp_path) -> None:
client, _ = _client(tmp_path)
response = client.post("/ui/config/max-steps", data={"max_steps": "abc"})
assert response.status_code == 400
assert "max_steps must be an integer" in response.text
def test_static_assets_are_served(tmp_path) -> None:
client, _ = _client(tmp_path)
assert client.get("/ui/assets/console.css").status_code == 200
assert client.get("/ui/assets/dashboard.js").status_code == 200
# -- 5.4 Regression: no SPA static dir, no wildcard CORS ---------------------
def test_ui_works_without_runtime_console_static_dir(tmp_path, monkeypatch) -> None:
monkeypatch.delenv("RUNTIME_CONSOLE_STATIC_DIR", raising=False)
client, _ = _client(tmp_path)
assert client.get("/ui/").status_code == 200
def test_runtime_app_does_not_register_wildcard_cors(tmp_path) -> None:
client, _ = _client(tmp_path)
# A same-origin browser client must not require CORS preflight. If wildcard
# CORS were still registered, an explicit Origin header would produce
# access-control-allow-origin in the response; assert it is absent for an
# arbitrary same-origin page request.
response = client.get(
"/ui/",
headers={"Origin": "http://127.0.0.1:8000"},
)
assert response.status_code == 200
assert "access-control-allow-origin" not in {k.lower() for k in response.headers}