Files
Xi Zhang fb329e4aaa perf: cut startup latency — lazy import surface, langgraph dev keepalive, indexed thread listing (#407)
* feat: add thread metadata index for improved performance in thread listing

- Implemented a new SQLite index on the `checkpoints` table to optimize thread listing queries by indexing relevant metadata fields.
- Updated the `list_threads` function to ensure the index is created if it does not exist.
- Added a test to verify the creation of the metadata index during thread listing.

feat: enhance workspace sidecar management with owner tracking

- Modified the workspace sidecar to include `owner_pids` to track the current process owners.
- Updated tests to validate the new owner tracking functionality and ensure proper behavior when managing workspace sidecars.

chore: introduce model registry for streamlined model management

- Created a new `registry.py` file to maintain a comprehensive model registry, including model names, IDs, providers, and routing tables.
- Added functions to retrieve models by provider and list available models, enhancing the modularity and maintainability of model management.

* feat: enhance workspace sidecar management and improve thread metadata indexing

* fix(tests): ensure sidecar correctly registers owner with original workspace and pid

* refactor: simplify workspace sidecar management by removing owner tracking

* feat(server): add commands to manage background langgraph dev server

- Introduced `server_app` for managing the langgraph dev server with commands to check status and stop the server.
- Enhanced workspace sidecar management to include configuration fingerprint for drift detection.
- Updated deployment functions to handle server configuration and state more effectively.

* feat(server): enhance server status command to display PID with stale record warning

* feat(langgraph_dev): exclusion-set config fingerprint, webui keepalive, unified stop guidance

* fix(cli): platform-specific manual-stop hint; document keepalive endpoint-change limitation
2026-08-11 08:37:58 +01:00

304 lines
10 KiB
Python

"""Tests for ``run_webui`` bind-host wiring.
The front-end is an external npm package (``@evoscientist/webui``) with no
``--host`` flag: its bin launcher does
``HOSTNAME: process.env.HOSTNAME || "127.0.0.1"`` and hands that to the Next
standalone server. Setting ``HOSTNAME`` on the npx env is therefore the *only*
supported way to widen the front-end's interface — these tests pin that
contract so a refactor can't quietly drop it and silently re-narrow the bind.
"""
from __future__ import annotations
import subprocess
from types import SimpleNamespace
from typing import Any
import pytest
from EvoScientist.deploy import webui as webui_mod
def _make_config(
*,
default_workdir: str = "",
langgraph_dev_port: int = 6174,
langgraph_dev_host: str = "127.0.0.1",
webui_port: int = 4716,
webui_host: str = "127.0.0.1",
langgraph_dev_keepalive: bool = False,
):
return SimpleNamespace(
default_workdir=default_workdir,
langgraph_dev_port=langgraph_dev_port,
langgraph_dev_host=langgraph_dev_host,
webui_port=webui_port,
webui_host=webui_host,
langgraph_dev_jobs_per_worker=10,
langgraph_dev_file_persistence=True,
langgraph_dev_keepalive=langgraph_dev_keepalive,
)
class _RecordingConsole:
"""A real Rich console rendering to a buffer.
Rendering for real (rather than stringifying the arguments) matters here:
the remote-backend hint lives *inside* a ``Panel``, so a naive ``str(arg)``
would only ever see ``<rich.panel.Panel object at ...>`` and the assertion
would pass or fail for the wrong reason. Width is pinned wide so the
strings under test don't wrap mid-token.
"""
def __init__(self, sink: list):
import io
from rich.console import Console
self._sink = sink
self._buf = io.StringIO()
self._console = Console(file=self._buf, width=200, no_color=True)
def print(self, *args, **kwargs):
self._buf.seek(0)
self._buf.truncate()
self._console.print(*args, **kwargs)
self._sink.append(self._buf.getvalue())
def status(self, *args, **kwargs):
class _Ctx:
def __enter__(self_inner):
return self_inner
def __exit__(self_inner, *a):
return False
return _Ctx()
class _ImmediateEvent:
"""Exits ``run_webui``'s block loop after a single iteration."""
def __init__(self):
self._called = 0
def is_set(self) -> bool:
self._called += 1
return self._called > 1
def wait(self, timeout: float | None = None):
return None
def set(self):
self._called = 99
def _run_webui_once(monkeypatch, config, *, backend_port_occupied: bool = False):
"""Run ``run_webui`` with every external dependency mocked."""
import atexit
import os
import shutil
import signal
import threading
import EvoScientist.config as config_mod
from EvoScientist.langgraph_dev import manager as lgm
captured: dict[str, Any] = {"printed": [], "npx_env": {}, "npx_args": []}
monkeypatch.setattr(config_mod, "apply_config_to_env", lambda _cfg: None)
monkeypatch.setattr(webui_mod, "console", _RecordingConsole(captured["printed"]))
monkeypatch.setattr(os, "makedirs", lambda *a, **k: None)
monkeypatch.setattr(shutil, "which", lambda _name: "/usr/bin/npx")
monkeypatch.setattr(
lgm, "_is_port_occupied", lambda _p, *_a, **_kw: backend_port_occupied
)
monkeypatch.setattr(lgm, "is_langgraph_dev_running", lambda **_kw: False)
monkeypatch.setattr(lgm, "_read_workspace_sidecar", lambda: None)
def _fake_start_langgraph_dev(workspace_dir=None, *, port=None, host=None, **_kw):
captured["backend_port"] = port
captured["backend_host"] = host
return SimpleNamespace(pid=99999)
monkeypatch.setattr(lgm, "start_langgraph_dev", _fake_start_langgraph_dev)
def _fake_stop(*_a, **_kw):
return None
monkeypatch.setattr(lgm, "stop_langgraph_dev", _fake_stop)
captured["stop_fn"] = _fake_stop
class _FakeProc:
pid = 12345
def poll(self):
return None
def wait(self, timeout=None):
return 0
def kill(self):
return None
def terminate(self):
return None
def _fake_popen(args, **kwargs):
captured["npx_args"] = args
captured["npx_env"] = kwargs.get("env", {})
return _FakeProc()
monkeypatch.setattr(subprocess, "Popen", _fake_popen)
# _stop_webui shells out to taskkill on Windows — neutralize it.
monkeypatch.setattr(webui_mod, "_stop_webui", lambda _proc: None)
captured["atexit_fns"] = []
monkeypatch.setattr(
atexit, "register", lambda fn, *a, **k: captured["atexit_fns"].append(fn) or fn
)
monkeypatch.setattr(signal, "signal", lambda _sig, _handler: lambda *a: None)
monkeypatch.setattr(threading, "Event", _ImmediateEvent)
webui_mod.run_webui(config, workspace_dir="/tmp/ws")
return captured
# =============================================================================
# Front-end bind interface (HOSTNAME)
# =============================================================================
def test_hostname_env_carries_webui_host(monkeypatch):
config = _make_config(webui_host="0.0.0.0")
captured = _run_webui_once(monkeypatch, config)
assert captured["npx_env"].get("HOSTNAME") == "0.0.0.0", (
"HOSTNAME is the package's only bind knob — without it the front-end "
"falls back to its own 127.0.0.1 default"
)
def test_hostname_env_defaults_to_loopback(monkeypatch):
"""The front-end serves the workspace file/upload and skill-install
endpoints, so it stays off the network until ``webui_host`` opts in."""
config = _make_config()
captured = _run_webui_once(monkeypatch, config)
assert captured["npx_env"].get("HOSTNAME") == "127.0.0.1"
def test_port_env_and_flag_still_set(monkeypatch):
config = _make_config(webui_port=4800)
captured = _run_webui_once(monkeypatch, config)
assert captured["npx_env"].get("PORT") == "4800"
assert "--port" in captured["npx_args"]
assert captured["npx_args"][captured["npx_args"].index("--port") + 1] == "4800"
def test_no_host_flag_passed_to_npx(monkeypatch):
"""The package's arg parser only knows ``--port``; a stray ``--host`` is at
best ignored and at worst breaks startup, so we must not emit one."""
config = _make_config()
captured = _run_webui_once(monkeypatch, config)
assert "--host" not in captured["npx_args"]
@pytest.mark.parametrize("blank", ["", " "])
def test_blank_webui_host_falls_back_to_loopback(monkeypatch, blank):
config = _make_config(webui_host=blank)
captured = _run_webui_once(monkeypatch, config)
assert captured["npx_env"].get("HOSTNAME") == "127.0.0.1"
# =============================================================================
# Backend bind interface + security warning
# =============================================================================
def test_backend_host_reaches_start_langgraph_dev(monkeypatch):
config = _make_config(langgraph_dev_host="0.0.0.0")
captured = _run_webui_once(monkeypatch, config)
assert captured["backend_host"] == "0.0.0.0"
def test_backend_defaults_to_loopback(monkeypatch):
"""The backend is an unauthenticated API whose agent can run shell, so it
stays off the network unless ``langgraph_dev_host`` opts in."""
config = _make_config()
captured = _run_webui_once(monkeypatch, config)
assert captured["backend_host"] == "127.0.0.1"
def test_public_bind_warning_when_backend_exposed(monkeypatch):
"""Users who widen the backend get told every time it is reachable
off-box — an unauthenticated, shell-capable API deserves a standing
reminder, not a one-time opt-in prompt."""
config = _make_config(langgraph_dev_host="0.0.0.0")
captured = _run_webui_once(monkeypatch, config)
assert any("PUBLIC BIND" in line for line in captured["printed"])
def test_no_public_bind_warning_when_backend_on_loopback(monkeypatch):
"""The warning must be silenceable, or it degrades into background noise
that users learn to skip past."""
config = _make_config(langgraph_dev_host="127.0.0.1")
captured = _run_webui_once(monkeypatch, config)
assert not any("PUBLIC BIND" in line for line in captured["printed"])
def test_public_bind_warning_when_frontend_exposed(monkeypatch):
"""The front-end earns its own banner: it is not a passive app shell — its
API reads, writes and uploads workspace files and installs skills."""
config = _make_config(webui_host="0.0.0.0", langgraph_dev_host="127.0.0.1")
captured = _run_webui_once(monkeypatch, config)
banner = "\n".join(captured["printed"])
assert "WebUI listening on 0.0.0.0" in banner
assert "Backend listening" not in banner
def test_remote_backend_hint_when_frontend_exposed_but_backend_is_not(monkeypatch):
"""The UI talks to the backend from the browser, so a remote visitor
cannot reach a loopback backend — say so instead of letting every request
fail silently."""
config = _make_config(webui_host="0.0.0.0", langgraph_dev_host="127.0.0.1")
captured = _run_webui_once(monkeypatch, config)
banner = "\n".join(captured["printed"])
assert "Remote visitors cannot reach" in banner
assert "langgraph_dev_host" in banner
def test_no_remote_hint_when_both_exposed(monkeypatch):
config = _make_config(webui_host="0.0.0.0", langgraph_dev_host="0.0.0.0")
captured = _run_webui_once(monkeypatch, config)
banner = "\n".join(captured["printed"])
assert "Remote visitors cannot reach" not in banner
# =============================================================================
# Backend keepalive
# =============================================================================
def test_backend_default_registers_stop_on_exit(monkeypatch):
"""Without keepalive the WebUI-started backend dies with the session."""
captured = _run_webui_once(monkeypatch, _make_config())
assert captured["stop_fn"] in captured["atexit_fns"]
def test_backend_keepalive_skips_stop_registration(monkeypatch):
"""With keepalive the backend outlives the WebUI session, so the next
same-workspace launch reuses it instead of paying the cold boot."""
captured = _run_webui_once(monkeypatch, _make_config(langgraph_dev_keepalive=True))
assert captured["stop_fn"] not in captured["atexit_fns"]