110 lines
4.8 KiB
Python
110 lines
4.8 KiB
Python
"""Full browser -> deterministic assistant -> schedule trial/save, without hardware."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import threading
|
|
from http import HTTPStatus
|
|
from http.server import ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
from urllib.parse import urlparse
|
|
|
|
import pytest
|
|
|
|
from cognitive_agent.application.codex_assistant import CodexAssistantService
|
|
from cognitive_agent.application.seat_service import SeatDemoService
|
|
from cognitive_agent.auth import AuthConfig, AuthRuntime
|
|
from cognitive_agent.server import REPOSITORY_ROOT, SeatDemoHandler
|
|
|
|
|
|
def test_mode_schedule_real_browser_preview_trial_and_save(tmp_path: Path) -> None:
|
|
node = shutil.which("node")
|
|
if not node:
|
|
runtime = Path.home() / ".cache" / "codex-runtimes" / "codex-primary-runtime" / "dependencies" / "node" / "bin"
|
|
node = next((str(path) for path in (runtime / "node.exe", runtime / "node") if path.is_file()), None)
|
|
if not node:
|
|
pytest.skip("Node.js is required for the schedule browser regression")
|
|
env = dict(os.environ)
|
|
modules = Path(node).resolve().parent.parent / "node_modules"
|
|
if modules.is_dir():
|
|
env["NODE_PATH"] = os.pathsep.join(value for value in (str(modules), env.get("NODE_PATH")) if value)
|
|
probe = subprocess.run([node, "-e", "require.resolve('playwright')"], env=env, capture_output=True, encoding="utf-8", check=False)
|
|
if probe.returncode:
|
|
pytest.skip("Playwright is required for the schedule browser regression")
|
|
|
|
with patch.dict(os.environ, {
|
|
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "false",
|
|
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED": "false",
|
|
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED": "false",
|
|
"COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED": "false",
|
|
"COGNITIVE_AGENT_EXECUTION_MODE": "dry_run",
|
|
"COGNITIVE_AGENT_MODE_PROFILE_ROOT": str(tmp_path / "user-modes"),
|
|
}):
|
|
service = SeatDemoService(REPOSITORY_ROOT)
|
|
service.artifact_root = tmp_path / "artifacts"
|
|
# A failed deterministic route must fail visibly, never launch a model or
|
|
# make network calls to an external assistant during the browser test.
|
|
client = MagicMock()
|
|
client.available = False
|
|
client.status = "test-offline"
|
|
assistant = CodexAssistantService(client, fast_implicit_controls=True)
|
|
forbidden = []
|
|
|
|
class BrowserHandler(SeatDemoHandler):
|
|
auth = AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
|
|
|
|
def do_GET(self) -> None: # noqa: N802
|
|
path = urlparse(self.path).path
|
|
if path in {"/api/hardware/seat-state", "/api/hardware/vehicle-state"}:
|
|
# Both legacy GET paths can touch physical adapters or firmware.
|
|
self._json({"status": "unavailable", "hardware_execution_allowed": False})
|
|
return
|
|
if path == "/api/health":
|
|
self._json({"status": "ok", "assistant": {"status": "ready", "available": True}})
|
|
return
|
|
super().do_GET()
|
|
|
|
def do_POST(self) -> None: # noqa: N802
|
|
path = urlparse(self.path).path
|
|
if path == "/api/extension/display-sync":
|
|
self._json({"status": "ok"})
|
|
return
|
|
if path not in {"/api/assistant/stream", "/api/mode-profile-drafts/trial", "/api/mode-profiles"}:
|
|
forbidden.append(path)
|
|
self._json({"error": "browser_test_write_forbidden"}, HTTPStatus.FORBIDDEN)
|
|
return
|
|
super().do_POST()
|
|
|
|
def _read_json_body(self, **kwargs):
|
|
payload = super()._read_json_body(**kwargs)
|
|
if payload.get("execution_mode", "dry_run") != "dry_run":
|
|
forbidden.append(f"execution_mode:{payload.get('execution_mode')}")
|
|
raise ValueError("Browser regression permits dry_run only")
|
|
return payload
|
|
|
|
def log_message(self, *_args: object) -> None:
|
|
pass
|
|
|
|
BrowserHandler.service = service
|
|
BrowserHandler.assistant = assistant
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), BrowserHandler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
completed = subprocess.run([
|
|
node, str(Path(__file__).with_name("mode_schedule_browser.cjs")),
|
|
f"http://127.0.0.1:{server.server_port}", str(tmp_path),
|
|
], env=env, capture_output=True, encoding="utf-8", timeout=180, check=False)
|
|
assert completed.returncode == 0, completed.stdout + completed.stderr
|
|
assert not forbidden, forbidden
|
|
assert len(list((tmp_path / "user-modes").rglob("*.yaml"))) == 2
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=3)
|
|
assistant.close()
|
|
service.close()
|