diff --git a/hermes_cli/web_routers/chat_ws.py b/hermes_cli/web_routers/chat_ws.py index 95ec9dcbbb..00252b9df1 100644 --- a/hermes_cli/web_routers/chat_ws.py +++ b/hermes_cli/web_routers/chat_ws.py @@ -438,6 +438,7 @@ async def _pty_fail(ws: WebSocket, exc: BaseException) -> None: @router.websocket("/api/pty") async def pty_ws(ws: WebSocket) -> None: from hermes_cli.web_server_chat import PTY_REGISTRY, PtyBridge, PtyUnavailableError, _PTY_BRIDGE_AVAILABLE, _RESIZE_RE + from pm.package import InstallError gate = await _ws_gate(ws, "pty") if gate is None: return @@ -503,6 +504,9 @@ async def pty_ws(ws: WebSocket) -> None: except SystemExit as exc: # _make_tui_argv sys.exit(1)s when node/npm is missing await _pty_fail(ws, exc) return + except InstallError as exc: # PM could not provide node; its remedy names the fix + await _pty_fail(ws, exc) + return attach_token = ws.query_params.get("attach") or None registry_resume = raw_resume diff --git a/tests/e2e/core/_pm_dependencies.py b/tests/e2e/core/_pm_dependencies.py index f8056137f3..5c5ea2731d 100644 --- a/tests/e2e/core/_pm_dependencies.py +++ b/tests/e2e/core/_pm_dependencies.py @@ -25,13 +25,16 @@ def _prepare_test_tools() -> tuple[Path, dict] | None: if not (source / "facts.json").is_file(): return None # A system/Nix test interpreter has no selected PM tool store. facts = Facts(source / "facts.json", strict=True) - records = {name: facts.get(name) for name in ("uv", "python")} + # node runs the TUI behind the dashboard's /api/pty; carry it when the + # runner's store has it, or PM in the sandbox reports it not installed. + names = ("uv", "python") + (("node",) if facts.get("node") else ()) + records = {name: facts.get(name) for name in names} python = records["python"] if not python or (source / python["entry"]).resolve() != Path(sys.base_prefix).resolve(): raise AssertionError(f"test interpreter is not from the selected PM store: {source}") target = current_target() lock = Lockfile(lockfile_path()) - verified_tools(("uv", "python"), source_store=source, target=target, lock=lock) + verified_tools(names, source_store=source, target=target, lock=lock) # The source may live under the real ~/.hermes. Per-test guards must never # read it, nor follow a sandbox tool symlink back into it. Copy the verified @@ -44,7 +47,7 @@ def _prepare_test_tools() -> tuple[Path, dict] | None: (prepared / "facts.json").write_text( json.dumps({"schema": 1, "packages": records}), encoding="utf-8" ) - verified_tools(("uv", "python"), source_store=prepared, target=target, lock=lock) + verified_tools(names, source_store=prepared, target=target, lock=lock) except BaseException: shutil.rmtree(prepared, ignore_errors=True) raise