Files
2026-04-29 15:48:19 -05:00

158 lines
5.6 KiB
Python

"""Regression tests for the live-MCP-process pre-flight in `bm reset` (#765)."""
from __future__ import annotations
import os
import psutil
import pytest
import typer
from basic_memory.cli.commands import db as db_cmd
class _FakeProc:
"""Minimal stand-in for psutil.Process; only exposes .info."""
def __init__(self, pid: int, cmdline: list[str] | None):
self.info = {"pid": pid, "cmdline": cmdline}
def _patch_iter(monkeypatch: pytest.MonkeyPatch, procs) -> None:
"""Replace psutil.process_iter with a fixed iterator.
Procs is intentionally untyped: tests pass a mix of _FakeProc and
error-raising stand-ins to exercise the per-process exception path.
"""
monkeypatch.setattr(
psutil,
"process_iter",
lambda attrs=None: iter(procs),
)
class TestFindLiveMcpProcesses:
def test_returns_empty_when_no_mcp_processes(self, monkeypatch):
_patch_iter(
monkeypatch,
[
_FakeProc(pid=11, cmdline=["python", "-m", "http.server"]),
_FakeProc(pid=22, cmdline=["bm", "sync"]),
],
)
assert db_cmd._find_live_mcp_processes() == []
def test_matches_basic_memory_mcp_invocations(self, monkeypatch):
_patch_iter(
monkeypatch,
[
# Direct `basic-memory mcp`.
_FakeProc(pid=101, cmdline=["/usr/bin/python", "basic-memory", "mcp"]),
# `bm mcp` alias entrypoint — must also match (#765 P1).
_FakeProc(pid=202, cmdline=["bm", "mcp"]),
# Python module form, underscore name.
_FakeProc(
pid=303,
cmdline=["python", "-m", "basic_memory.cli.main", "mcp"],
),
# Absolute path to the bm script.
_FakeProc(pid=404, cmdline=["/usr/local/bin/bm", "mcp"]),
# Windows-style bm.exe.
_FakeProc(pid=505, cmdline=["C:\\Users\\me\\.venv\\Scripts\\bm.exe", "mcp"]),
# Should NOT match — `mcp` is a substring of another arg, not a token.
_FakeProc(pid=606, cmdline=["python", "basic-memory", "mcp-helper"]),
# Should NOT match — has `mcp` but no basic-memory/bm signature.
_FakeProc(pid=707, cmdline=["python", "/some/other/server.py", "mcp"]),
],
)
result = db_cmd._find_live_mcp_processes()
pids = sorted(pid for pid, _ in result)
assert pids == [101, 202, 303, 404, 505]
def test_skips_current_process(self, monkeypatch):
me = os.getpid()
_patch_iter(
monkeypatch,
[
_FakeProc(pid=me, cmdline=["python", "basic-memory", "mcp"]),
],
)
# Self-match is suppressed so the helper can be called from inside
# `bm reset` without flagging the running process.
assert db_cmd._find_live_mcp_processes() == []
def test_skips_processes_with_no_cmdline(self, monkeypatch):
_patch_iter(
monkeypatch,
[
_FakeProc(pid=1, cmdline=None), # kernel-style process
_FakeProc(pid=2, cmdline=[]),
],
)
assert db_cmd._find_live_mcp_processes() == []
def test_swallows_per_process_errors(self, monkeypatch):
"""A NoSuchProcess race during iteration must not abort the scan."""
class _Raising:
@property
def info(self):
raise psutil.NoSuchProcess(pid=999)
_patch_iter(
monkeypatch,
[
_Raising(),
_FakeProc(pid=42, cmdline=["python", "basic-memory", "mcp"]),
],
)
result = db_cmd._find_live_mcp_processes()
assert [pid for pid, _ in result] == [42]
class TestAbortIfMcpProcessesAlive:
def test_no_op_when_no_processes(self, monkeypatch):
monkeypatch.setattr(db_cmd, "_find_live_mcp_processes", lambda: [])
# Must not raise — destructive work should proceed.
db_cmd._abort_if_mcp_processes_alive()
def test_exits_with_pids_when_processes_alive(self, monkeypatch, capsys):
monkeypatch.setattr(
db_cmd,
"_find_live_mcp_processes",
lambda: [(123, "python basic-memory mcp"), (456, "uv run bm mcp wrapper")],
)
with pytest.raises(typer.Exit) as exc_info:
db_cmd._abort_if_mcp_processes_alive()
assert exc_info.value.exit_code == 1
captured = capsys.readouterr()
# PIDs surface so the user can target the cleanup themselves.
assert "123" in captured.out
assert "456" in captured.out
assert "MCP processes" in captured.out
def test_prints_platform_specific_cleanup_hint_posix(self, monkeypatch, capsys):
monkeypatch.setattr(os, "name", "posix")
monkeypatch.setattr(
db_cmd,
"_find_live_mcp_processes",
lambda: [(7, "python basic-memory mcp")],
)
with pytest.raises(typer.Exit):
db_cmd._abort_if_mcp_processes_alive()
out = capsys.readouterr().out
assert "pgrep -fa 'basic-memory mcp'" in out
def test_prints_platform_specific_cleanup_hint_windows(self, monkeypatch, capsys):
monkeypatch.setattr(os, "name", "nt")
monkeypatch.setattr(
db_cmd,
"_find_live_mcp_processes",
lambda: [(7, "python basic-memory mcp")],
)
with pytest.raises(typer.Exit):
db_cmd._abort_if_mcp_processes_alive()
out = capsys.readouterr().out
assert "Get-CimInstance" in out