Files
liaohch3--claude-tap/tests/test_shared_dashboard.py
wehub-resource-sync f3d80b4628
Auto Release / auto-release (push) Failing after 1s
CI / lint (push) Failing after 0s
CI / screenshot-quality (push) Failing after 3s
CI / pr-policy (push) Has been skipped
CI / test (3.11) (push) Failing after 0s
CI / test (3.12) (push) Failing after 0s
CI / test (3.13) (push) Failing after 3s
CI / coverage (push) Failing after 1s
Legibility / legibility (push) Failing after 3s
chore: import upstream snapshot with attribution
2026-07-13 12:31:48 +08:00

783 lines
29 KiB
Python

from __future__ import annotations
import signal
import subprocess
from pathlib import Path
import pytest
from aiohttp import web
from claude_tap.shared_dashboard import (
CLAUDE_TAP_VERSION,
DEFAULT_DASHBOARD_PORT,
_dashboard_listening_pids_for_port,
_dashboard_lock_path,
_dashboard_process_command,
_dashboard_spawn_lock,
_looks_like_legacy_dashboard_command,
_spawn_dashboard_subprocess,
_sync_dashboard_healthy_for_current_db,
_terminate_legacy_dashboard_pids,
dashboard_connect_host,
dashboard_url,
ensure_shared_dashboard,
is_dashboard_healthy,
is_legacy_dashboard_healthy,
resolve_dashboard_port,
stop_dashboard_service,
stop_legacy_dashboard_process,
stop_shared_dashboard,
)
from claude_tap.trace_store import resolve_db_path
async def _start_test_app(app: web.Application) -> tuple[web.AppRunner, int]:
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, "127.0.0.1", 0)
await site.start()
port = site._server.sockets[0].getsockname()[1]
return runner, port
def test_resolve_dashboard_port_defaults_to_shared_port() -> None:
assert resolve_dashboard_port(0) == DEFAULT_DASHBOARD_PORT
assert resolve_dashboard_port(None) == DEFAULT_DASHBOARD_PORT
def test_resolve_dashboard_port_honors_explicit_port() -> None:
assert resolve_dashboard_port(3000) == 3000
def test_resolve_dashboard_port_honors_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("CLOUDTAP_DASHBOARD_PORT", "8765")
assert resolve_dashboard_port(0) == 8765
@pytest.mark.parametrize("value", ["0", "-1", "not-a-port"])
def test_resolve_dashboard_port_ignores_invalid_env(monkeypatch: pytest.MonkeyPatch, value: str) -> None:
monkeypatch.setenv("CLOUDTAP_DASHBOARD_PORT", value)
assert resolve_dashboard_port(0) == DEFAULT_DASHBOARD_PORT
def test_dashboard_url() -> None:
assert dashboard_connect_host("localhost") == "localhost"
assert dashboard_connect_host(" ") == "127.0.0.1"
assert dashboard_connect_host("0.0.0.0") == "127.0.0.1"
assert dashboard_connect_host("::") == "::1"
assert dashboard_connect_host("[::]") == "::1"
assert dashboard_url("127.0.0.1", 1234) == "http://127.0.0.1:1234"
assert dashboard_url("0.0.0.0", 1234) == "http://127.0.0.1:1234"
assert dashboard_url("::", 1234) == "http://[::1]:1234"
assert dashboard_url("::1", 1234) == "http://[::1]:1234"
assert dashboard_url("[::1]", 1234) == "http://[::1]:1234"
def test_sync_dashboard_health_uses_proxyless_opener(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
class FakeResponse:
status = 200
def __enter__(self) -> "FakeResponse":
return self
def __exit__(self, *_args: object) -> None:
return None
def read(self) -> bytes:
return json_bytes
class FakeOpener:
def open(self, url: str, *, timeout: float) -> FakeResponse:
calls.append((url, timeout))
return FakeResponse()
db_path = tmp_path / "health.sqlite3"
json_bytes = f'{{"ok":true,"db_path":"{db_path}","version":"{CLAUDE_TAP_VERSION}"}}'.encode()
calls: list[tuple[str, float]] = []
monkeypatch.setenv("CLOUDTAP_DB", str(db_path))
monkeypatch.setattr("claude_tap.shared_dashboard._LOCAL_DASHBOARD_OPENER", FakeOpener())
assert _sync_dashboard_healthy_for_current_db("127.0.0.1", 19527) is True
assert calls and calls[0][0] == "http://127.0.0.1:19527/dashboard/health"
def test_sync_dashboard_health_rejects_stale_version(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
class FakeResponse:
status = 200
def __enter__(self) -> "FakeResponse":
return self
def __exit__(self, *_args: object) -> None:
return None
def read(self) -> bytes:
return json_bytes
class FakeOpener:
def open(self, _url: str, *, timeout: float) -> FakeResponse:
assert timeout > 0
return FakeResponse()
db_path = tmp_path / "health.sqlite3"
json_bytes = f'{{"ok":true,"db_path":"{db_path}","version":"0.1.106"}}'.encode()
monkeypatch.setenv("CLOUDTAP_DB", str(db_path))
monkeypatch.setattr("claude_tap.shared_dashboard._LOCAL_DASHBOARD_OPENER", FakeOpener())
assert _sync_dashboard_healthy_for_current_db("127.0.0.1", 19527) is False
def test_dashboard_lock_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "test.sqlite3"))
assert _dashboard_lock_path() == tmp_path / "dashboard.lock"
def test_dashboard_spawn_lock(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "test.sqlite3"))
with _dashboard_spawn_lock():
pass
with _dashboard_spawn_lock():
pass
def test_spawn_dashboard_subprocess(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
spawned_args: list[tuple[list[str], dict[str, object]]] = []
class FakePopen:
def __init__(self, cmd: list[str], **kwargs: object) -> None:
spawned_args.append((cmd, kwargs))
self.pid = 99999
monkeypatch.setattr(subprocess, "Popen", FakePopen)
_spawn_dashboard_subprocess("127.0.0.1", 19527, tmp_path)
assert len(spawned_args) == 1
cmd, kwargs = spawned_args[0]
assert "dashboard" in cmd
assert "--tap-live-port" in cmd
assert "19527" in cmd
assert str(tmp_path) in cmd
assert kwargs["stdin"] == subprocess.DEVNULL
assert kwargs["stdout"] == subprocess.DEVNULL
assert kwargs["stderr"] == subprocess.DEVNULL
assert kwargs["start_new_session"] is True
def test_spawn_dashboard_subprocess_hides_windows_console(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
captured: dict[str, object] = {}
class FakeStartupInfo:
def __init__(self) -> None:
self.dwFlags = 0
self.wShowWindow: int | None = None
class FakePopen:
def __init__(self, cmd: list[str], **kwargs: object) -> None:
captured["cmd"] = cmd
captured["kwargs"] = kwargs
self.pid = 99999
scripts_dir = tmp_path / "uv" / "tools" / "claude-tap" / "Scripts"
scripts_dir.mkdir(parents=True)
python_exe = scripts_dir / "python.exe"
pythonw_exe = scripts_dir / "pythonw.exe"
python_exe.touch()
pythonw_exe.touch()
monkeypatch.setattr("claude_tap.shared_dashboard.sys.platform", "win32")
monkeypatch.setattr("claude_tap.shared_dashboard.sys.executable", str(python_exe))
monkeypatch.setattr(subprocess, "Popen", FakePopen)
monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", 0x1000, raising=False)
monkeypatch.setattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0x2000, raising=False)
monkeypatch.setattr(subprocess, "STARTF_USESHOWWINDOW", 0x4000, raising=False)
monkeypatch.setattr(subprocess, "SW_HIDE", 0, raising=False)
monkeypatch.setattr(subprocess, "STARTUPINFO", FakeStartupInfo, raising=False)
_spawn_dashboard_subprocess("0.0.0.0", 19527, tmp_path)
cmd = captured["cmd"]
kwargs = captured["kwargs"]
assert isinstance(cmd, list)
assert isinstance(kwargs, dict)
assert cmd[0] == str(pythonw_exe)
assert cmd[-2:] == ["--tap-host", "0.0.0.0"]
assert kwargs["stdin"] == subprocess.DEVNULL
assert kwargs["stdout"] == subprocess.DEVNULL
assert kwargs["stderr"] == subprocess.DEVNULL
assert kwargs["creationflags"] == 0x1000 | 0x2000
assert "start_new_session" not in kwargs
startupinfo = kwargs["startupinfo"]
assert isinstance(startupinfo, FakeStartupInfo)
assert startupinfo.dwFlags == 0x4000
assert startupinfo.wShowWindow == 0
@pytest.mark.asyncio
async def test_is_dashboard_healthy_real_server(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
import aiohttp
from claude_tap.live import LiveViewerServer
from claude_tap.shared_dashboard import wait_for_dashboard_healthy
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "dashboard.sqlite3"))
# Before starting, it should be unhealthy
assert await is_dashboard_healthy("127.0.0.1", 54321) is False
assert await wait_for_dashboard_healthy("127.0.0.1", 54321, timeout=0.2, interval=0.05) is False
# Start real server
server = LiveViewerServer(port=0, migrate_from=tmp_path, dashboard_mode=True)
port = await server.start()
try:
async with aiohttp.ClientSession() as session:
async with session.get(f"http://127.0.0.1:{port}/dashboard/health") as resp:
assert resp.status == 200
payload = await resp.json()
assert payload["ok"] is True
assert payload["db_path"] == str(resolve_db_path())
assert payload["dashboard_mode"] is True
assert payload["version"] == CLAUDE_TAP_VERSION
assert isinstance(payload["quit_token"], str)
assert payload["quit_token"]
assert await is_dashboard_healthy("127.0.0.1", port) is True
assert await wait_for_dashboard_healthy("127.0.0.1", port, timeout=1.0) is True
finally:
await server.stop()
@pytest.mark.asyncio
async def test_stop_shared_dashboard_stops_real_server(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
from claude_tap.live import LiveViewerServer
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "dashboard.sqlite3"))
server = LiveViewerServer(port=0, migrate_from=tmp_path, dashboard_mode=True)
port = await server.start()
try:
assert await stop_shared_dashboard("127.0.0.1", port) is True
assert await is_dashboard_healthy("127.0.0.1", port, require_current_db=False) is False
finally:
await server.stop()
@pytest.mark.asyncio
async def test_bind_all_dashboard_uses_loopback_for_local_controls(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
from claude_tap.live import LiveViewerServer
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "dashboard.sqlite3"))
server = LiveViewerServer(port=0, host="0.0.0.0", migrate_from=tmp_path, dashboard_mode=True)
port = await server.start()
try:
assert server.url == f"http://127.0.0.1:{port}"
assert await is_dashboard_healthy("0.0.0.0", port) is True
assert await stop_shared_dashboard("0.0.0.0", port) is True
assert await is_dashboard_healthy("0.0.0.0", port, require_current_db=False) is False
finally:
await server.stop()
@pytest.mark.asyncio
async def test_stop_shared_dashboard_requires_health_token() -> None:
quit_called = False
async def health(request: web.Request) -> web.Response:
return web.json_response({"ok": True})
async def quit_dashboard(request: web.Request) -> web.Response:
nonlocal quit_called
quit_called = True
return web.json_response({"ok": True})
app = web.Application()
app.router.add_get("/dashboard/health", health)
app.router.add_post("/dashboard/quit", quit_dashboard)
runner, port = await _start_test_app(app)
try:
assert await stop_shared_dashboard("127.0.0.1", port) is False
assert quit_called is False
finally:
await runner.cleanup()
@pytest.mark.asyncio
async def test_stop_shared_dashboard_rejects_unhealthy_or_forbidden_server() -> None:
async def unhealthy(request: web.Request) -> web.Response:
return web.json_response({"ok": False}, status=500)
unhealthy_app = web.Application()
unhealthy_app.router.add_get("/dashboard/health", unhealthy)
unhealthy_runner, unhealthy_port = await _start_test_app(unhealthy_app)
try:
assert await stop_shared_dashboard("127.0.0.1", unhealthy_port) is False
finally:
await unhealthy_runner.cleanup()
async def health(request: web.Request) -> web.Response:
return web.json_response({"ok": True, "quit_token": "test-token"})
async def forbidden_quit(request: web.Request) -> web.Response:
assert request.headers["X-Claude-Tap-Dashboard-Token"] == "test-token"
return web.json_response({"ok": False}, status=403)
forbidden_app = web.Application()
forbidden_app.router.add_get("/dashboard/health", health)
forbidden_app.router.add_post("/dashboard/quit", forbidden_quit)
forbidden_runner, forbidden_port = await _start_test_app(forbidden_app)
try:
assert await stop_shared_dashboard("127.0.0.1", forbidden_port) is False
finally:
await forbidden_runner.cleanup()
@pytest.mark.asyncio
async def test_stop_shared_dashboard_handles_post_client_error(monkeypatch: pytest.MonkeyPatch) -> None:
import aiohttp
async def healthy(*_args: object, **_kwargs: object) -> tuple[int, dict[str, str]]:
return 200, {"quit_token": "test-token"}
class FailingSession:
def __init__(self, *_args: object, **_kwargs: object) -> None:
pass
async def __aenter__(self) -> "FailingSession":
return self
async def __aexit__(self, *_args: object) -> None:
return None
def post(self, *_args: object, **_kwargs: object) -> object:
raise aiohttp.ClientError("post failed")
monkeypatch.setattr("claude_tap.shared_dashboard._dashboard_get_status_and_payload", healthy)
monkeypatch.setattr("claude_tap.shared_dashboard.aiohttp.ClientSession", FailingSession)
assert await stop_shared_dashboard("127.0.0.1", 19527) is False
@pytest.mark.asyncio
async def test_stop_dashboard_service_falls_back_to_legacy_process(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []
async def fake_stop_shared_dashboard(_host: str, _port: int) -> bool:
calls.append("shared")
return False
async def fake_is_legacy_dashboard_healthy(_host: str, _port: int) -> bool:
calls.append("legacy")
return True
async def fake_stop_legacy_dashboard_process(_host: str, _port: int) -> bool:
calls.append("process")
return True
monkeypatch.setattr("claude_tap.shared_dashboard.stop_shared_dashboard", fake_stop_shared_dashboard)
monkeypatch.setattr("claude_tap.shared_dashboard.is_legacy_dashboard_healthy", fake_is_legacy_dashboard_healthy)
monkeypatch.setattr("claude_tap.shared_dashboard.stop_legacy_dashboard_process", fake_stop_legacy_dashboard_process)
assert await stop_dashboard_service("127.0.0.1", 19527) is True
assert calls == ["shared", "legacy", "process"]
def test_looks_like_legacy_dashboard_command_is_strict() -> None:
assert _looks_like_legacy_dashboard_command(
"/usr/bin/python -m claude_tap dashboard --tap-live-port 19527",
19527,
)
assert not _looks_like_legacy_dashboard_command(
"/usr/bin/python -m claude_tap dashboard --tap-live-port 3000",
19527,
)
assert not _looks_like_legacy_dashboard_command(
"/usr/bin/python -m claude_tap proxy --tap-live-port 19527",
19527,
)
assert not _looks_like_legacy_dashboard_command(
"/usr/bin/python -m other_app dashboard --tap-live-port 19527",
19527,
)
def test_dashboard_listening_pids_uses_lsof(monkeypatch: pytest.MonkeyPatch) -> None:
class Result:
returncode = 0
stdout = "111\nnot-a-pid\n222\n"
calls: list[list[str]] = []
def fake_which(name: str) -> str | None:
return f"/usr/bin/{name}" if name == "lsof" else None
def fake_run(cmd: list[str], **_kwargs: object) -> Result:
calls.append(cmd)
return Result()
monkeypatch.setattr("claude_tap.shared_dashboard.shutil.which", fake_which)
monkeypatch.setattr("claude_tap.shared_dashboard.subprocess.run", fake_run)
assert _dashboard_listening_pids_for_port(19527) == [111, 222]
assert calls == [["/usr/bin/lsof", "-nP", "-iTCP:19527", "-sTCP:LISTEN", "-t"]]
def test_dashboard_listening_pids_falls_back_to_ss(monkeypatch: pytest.MonkeyPatch) -> None:
class Result:
returncode = 0
stdout = 'LISTEN 0 128 127.0.0.1:19527 *:* users:(("python",pid=333,fd=6),("python",pid=444,fd=7))'
def fake_which(name: str) -> str | None:
return "/usr/bin/ss" if name == "ss" else None
monkeypatch.setattr("claude_tap.shared_dashboard.shutil.which", fake_which)
monkeypatch.setattr("claude_tap.shared_dashboard.subprocess.run", lambda *_args, **_kwargs: Result())
assert _dashboard_listening_pids_for_port(19527) == [333, 444]
def test_dashboard_listening_pids_handles_missing_tools(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("claude_tap.shared_dashboard.shutil.which", lambda _name: None)
assert _dashboard_listening_pids_for_port(0) == []
assert _dashboard_listening_pids_for_port(19527) == []
def test_dashboard_process_command_reads_linux_proc(monkeypatch: pytest.MonkeyPatch) -> None:
def fake_read_bytes(path: Path) -> bytes:
assert str(path) == "/proc/123/cmdline"
return b"python\0-m\0claude_tap\0dashboard\0"
monkeypatch.setattr("claude_tap.shared_dashboard.sys.platform", "linux")
monkeypatch.setattr("pathlib.Path.read_bytes", fake_read_bytes)
assert _dashboard_process_command(123) == "python -m claude_tap dashboard"
def test_dashboard_process_command_falls_back_to_ps(monkeypatch: pytest.MonkeyPatch) -> None:
class Result:
returncode = 0
stdout = "claude-tap dashboard --tap-live-port 19527\n"
def fake_which(name: str) -> str | None:
return "/bin/ps" if name == "ps" else None
monkeypatch.setattr("claude_tap.shared_dashboard.sys.platform", "darwin")
monkeypatch.setattr("claude_tap.shared_dashboard.shutil.which", fake_which)
monkeypatch.setattr("claude_tap.shared_dashboard.subprocess.run", lambda *_args, **_kwargs: Result())
assert _dashboard_process_command(123) == "claude-tap dashboard --tap-live-port 19527"
def test_terminate_legacy_dashboard_pids_filters_commands(monkeypatch: pytest.MonkeyPatch) -> None:
commands = {
111: "/usr/bin/python -m claude_tap dashboard --tap-live-port 19527",
222: "/usr/bin/python -m claude_tap proxy --tap-live-port 19527",
}
killed: list[tuple[int, signal.Signals]] = []
monkeypatch.setattr("claude_tap.shared_dashboard._dashboard_process_command", commands.__getitem__)
monkeypatch.setattr("claude_tap.shared_dashboard.os.kill", lambda pid, sig: killed.append((pid, sig)))
assert _terminate_legacy_dashboard_pids([111, 222], 19527) is True
assert killed == [(111, signal.SIGTERM)]
@pytest.mark.asyncio
async def test_stop_legacy_dashboard_process_terminates_and_waits(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[tuple[str, object]] = []
def fake_listening_pids(port: int) -> list[int]:
calls.append(("pids", port))
return [111]
def fake_terminate(pids: list[int], port: int) -> bool:
calls.append(("terminate", (pids, port)))
return True
async def fake_wait_stopped(host: str, port: int) -> bool:
calls.append(("wait", (host, port)))
return True
monkeypatch.setattr("claude_tap.shared_dashboard._dashboard_listening_pids_for_port", fake_listening_pids)
monkeypatch.setattr("claude_tap.shared_dashboard._terminate_legacy_dashboard_pids", fake_terminate)
monkeypatch.setattr("claude_tap.shared_dashboard.wait_for_dashboard_stopped", fake_wait_stopped)
assert await stop_legacy_dashboard_process("127.0.0.1", 19527) is True
assert calls == [
("pids", 19527),
("terminate", ([111], 19527)),
("wait", ("127.0.0.1", 19527)),
]
@pytest.mark.asyncio
async def test_is_dashboard_healthy_prefers_lightweight_health_route(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "health.sqlite3"))
sessions_seen = False
app = web.Application()
async def health(request: web.Request) -> web.Response:
return web.json_response({"ok": True, "db_path": str(resolve_db_path()), "version": CLAUDE_TAP_VERSION})
async def sessions(request: web.Request) -> web.Response:
nonlocal sessions_seen
sessions_seen = True
return web.json_response({"sessions": []})
app.router.add_get("/dashboard/health", health)
app.router.add_get("/api/sessions", sessions)
runner, port = await _start_test_app(app)
try:
assert await is_dashboard_healthy("127.0.0.1", port) is True
assert sessions_seen is False
finally:
await runner.cleanup()
@pytest.mark.asyncio
async def test_is_dashboard_healthy_rejects_stale_version(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "current.sqlite3"))
app = web.Application()
async def health(request: web.Request) -> web.Response:
return web.json_response({"ok": True, "db_path": str(resolve_db_path()), "version": "0.1.106"})
app.router.add_get("/dashboard/health", health)
runner, port = await _start_test_app(app)
try:
assert await is_dashboard_healthy("127.0.0.1", port) is False
assert await is_dashboard_healthy("127.0.0.1", port, require_current_db=False) is True
assert await is_legacy_dashboard_healthy("127.0.0.1", port) is False
finally:
await runner.cleanup()
@pytest.mark.asyncio
async def test_is_dashboard_healthy_rejects_different_database(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "current.sqlite3"))
app = web.Application()
async def health(request: web.Request) -> web.Response:
return web.json_response(
{"ok": True, "db_path": str(tmp_path / "other.sqlite3"), "version": CLAUDE_TAP_VERSION}
)
app.router.add_get("/dashboard/health", health)
runner, port = await _start_test_app(app)
try:
assert await is_dashboard_healthy("127.0.0.1", port) is False
assert await is_dashboard_healthy("127.0.0.1", port, require_current_db=False) is True
assert await is_legacy_dashboard_healthy("127.0.0.1", port) is False
finally:
await runner.cleanup()
@pytest.mark.asyncio
async def test_is_dashboard_healthy_falls_back_for_legacy_dashboard() -> None:
app = web.Application()
async def sessions(request: web.Request) -> web.Response:
return web.json_response({"sessions": []})
app.router.add_get("/api/sessions", sessions)
runner, port = await _start_test_app(app)
try:
assert await is_dashboard_healthy("127.0.0.1", port) is False
assert await is_dashboard_healthy("127.0.0.1", port, require_current_db=False) is True
assert await is_legacy_dashboard_healthy("127.0.0.1", port) is True
finally:
await runner.cleanup()
@pytest.mark.asyncio
async def test_ensure_shared_dashboard_already_healthy_does_not_reopen_browser(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
async def mock_true(h: str, p: int) -> bool:
return True
migrated: list[Path] = []
monkeypatch.setattr("claude_tap.shared_dashboard.is_dashboard_healthy", mock_true)
monkeypatch.setattr("claude_tap.history.migrate_legacy_traces", migrated.append)
opened = []
def fake_open(url: str) -> None:
opened.append(url)
url, spawned = await ensure_shared_dashboard(
host="127.0.0.1",
port=19527,
output_dir=tmp_path,
open_browser=True,
open_browser_fn=fake_open,
)
assert url == "http://127.0.0.1:19527"
assert spawned is False
assert opened == []
assert migrated == [tmp_path]
@pytest.mark.asyncio
async def test_ensure_shared_dashboard_stops_stale_dashboard_before_spawn(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
calls: list[tuple[str, object]] = []
async def fake_is_dashboard_healthy(_host: str, _port: int, *, require_current_db: bool = True) -> bool:
calls.append(("health", require_current_db))
return not require_current_db
async def fake_stop_dashboard_service(_host: str, _port: int) -> bool:
calls.append(("stop", None))
return True
def fake_spawn_if_needed(_host: str, _port: int, _output_dir: Path) -> bool:
calls.append(("spawn", None))
return True
async def fake_wait_for_dashboard_healthy(_host: str, _port: int) -> bool:
calls.append(("wait", None))
return True
monkeypatch.setattr("claude_tap.shared_dashboard.is_dashboard_healthy", fake_is_dashboard_healthy)
monkeypatch.setattr("claude_tap.shared_dashboard.stop_dashboard_service", fake_stop_dashboard_service)
monkeypatch.setattr("claude_tap.shared_dashboard._spawn_dashboard_subprocess_if_needed", fake_spawn_if_needed)
monkeypatch.setattr("claude_tap.shared_dashboard.wait_for_dashboard_healthy", fake_wait_for_dashboard_healthy)
url, spawned = await ensure_shared_dashboard(
host="127.0.0.1",
port=19527,
output_dir=tmp_path,
open_browser=False,
open_browser_fn=lambda _url: None,
)
assert url == "http://127.0.0.1:19527"
assert spawned is True
assert calls == [("health", True), ("health", False), ("stop", None), ("spawn", None), ("wait", None)]
@pytest.mark.asyncio
async def test_ensure_shared_dashboard_reports_unstoppable_stale_dashboard(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
async def fake_is_dashboard_healthy(_host: str, _port: int, *, require_current_db: bool = True) -> bool:
return not require_current_db
async def fake_stop_dashboard_service(_host: str, _port: int) -> bool:
return False
monkeypatch.setattr("claude_tap.shared_dashboard.is_dashboard_healthy", fake_is_dashboard_healthy)
monkeypatch.setattr("claude_tap.shared_dashboard.stop_dashboard_service", fake_stop_dashboard_service)
with pytest.raises(RuntimeError, match="outdated claude-tap dashboard"):
await ensure_shared_dashboard(
host="127.0.0.1",
port=19527,
output_dir=tmp_path,
open_browser=False,
open_browser_fn=lambda _url: None,
)
@pytest.mark.asyncio
async def test_ensure_shared_dashboard_migrates_after_lock_time_reuse(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
async def mock_false(h: str, p: int, *, require_current_db: bool = True) -> bool:
return False
migrated: list[Path] = []
monkeypatch.setattr("claude_tap.shared_dashboard.is_dashboard_healthy", mock_false)
monkeypatch.setattr("claude_tap.shared_dashboard.is_legacy_dashboard_healthy", mock_false)
monkeypatch.setattr("claude_tap.shared_dashboard._spawn_dashboard_subprocess_if_needed", lambda h, p, d: False)
monkeypatch.setattr("claude_tap.shared_dashboard._migrate_legacy_traces", migrated.append)
opened: list[str] = []
url, spawned = await ensure_shared_dashboard(
host="127.0.0.1",
port=19527,
output_dir=tmp_path,
open_browser=True,
open_browser_fn=opened.append,
)
assert url == "http://127.0.0.1:19527"
assert spawned is False
assert opened == []
assert migrated == [tmp_path]
@pytest.mark.asyncio
async def test_ensure_shared_dashboard_spawns(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "test.sqlite3"))
health_calls: list[int] = []
async def mock_health(h: str, p: int, *, require_current_db: bool = True) -> bool:
if len(health_calls) < 2:
health_calls.append(1)
return False
return True
async def mock_legacy_false(h: str, p: int) -> bool:
return False
monkeypatch.setattr("claude_tap.shared_dashboard.is_dashboard_healthy", mock_health)
monkeypatch.setattr("claude_tap.shared_dashboard.is_legacy_dashboard_healthy", mock_legacy_false)
monkeypatch.setattr("claude_tap.shared_dashboard._spawn_dashboard_subprocess", lambda h, p, d: None)
opened = []
def fake_open(url: str) -> None:
opened.append(url)
url, spawned = await ensure_shared_dashboard(
host="127.0.0.1",
port=19527,
output_dir=tmp_path,
open_browser=True,
open_browser_fn=fake_open,
)
assert url == "http://127.0.0.1:19527"
assert spawned is True
assert opened == ["http://127.0.0.1:19527"]
@pytest.mark.asyncio
async def test_ensure_shared_dashboard_timeout_raises_error(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("CLOUDTAP_DB", str(tmp_path / "test.sqlite3"))
async def mock_false(h: str, p: int, *, require_current_db: bool = True) -> bool:
return False
async def mock_wait_false(h: str, p: int, **kw: object) -> bool:
return False
monkeypatch.setattr("claude_tap.shared_dashboard.is_dashboard_healthy", mock_false)
monkeypatch.setattr("claude_tap.shared_dashboard.is_legacy_dashboard_healthy", mock_false)
monkeypatch.setattr("claude_tap.shared_dashboard.wait_for_dashboard_healthy", mock_wait_false)
monkeypatch.setattr("claude_tap.shared_dashboard._spawn_dashboard_subprocess", lambda h, p, d: None)
with pytest.raises(RuntimeError, match="Failed to start shared dashboard"):
await ensure_shared_dashboard(
host="127.0.0.1",
port=19527,
output_dir=tmp_path,
open_browser=False,
open_browser_fn=lambda u: None,
)