"""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()