-
Notifications
You must be signed in to change notification settings - Fork 196
Expand file tree
/
Copy pathtest_db_reset_zombie_check.py
More file actions
157 lines (133 loc) · 5.6 KB
/
test_db_reset_zombie_check.py
File metadata and controls
157 lines (133 loc) · 5.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""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