diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3d2ab3f..6d48023 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -90,3 +90,26 @@ jobs: case "$out" in *Traceback*) echo "::error::base install printed a traceback"; exit 1 ;; esac + - name: the installed server must find an index relative to the user's directory + # The tests run from the source tree, and the steps above only check that + # the commands start. Neither reproduces how people actually launch the MCP + # server: from their OWN directory, with a relative BRAIN_INDEX_DIR, against + # the installed wheel. The recall child used to run with cwd set to the + # package directory inside site-packages, so "index" resolved there and the + # user's index was never found. CI stayed green on exactly that bug, so this + # step reproduces it: a minimal index in /tmp/userdir, launch from there. + # Index found => brain_ask gets past loading and stops at the missing + # [embeddings] extra. Index NOT found => "need an embedding index". + run: | + mkdir -p /tmp/userdir/index + cd /tmp/userdir + /tmp/clean/bin/python -c "import numpy as np, pickle; np.save('index/_brain_e5.npy', np.zeros((1, 4), dtype='float32')); pickle.dump([{'path': 'a.md', 'chunk_idx': 0}], open('index/_brain_e5_meta.pkl', 'wb'))" + out=$(env -u PYTHONPATH BRAIN_INDEX_DIR=index /tmp/clean/bin/sgm-mcp --test "agent memory" 2>&1 || true) + echo "$out" + case "$out" in + *"need an embedding index"*) echo "::error::installed server looked for the index outside the user's directory"; exit 1 ;; + esac + case "$out" in + *"sqlite-graph-memory[embeddings]"*) echo "index found from the user's directory" ;; + *) echo "::error::unexpected output from the installed server"; exit 1 ;; + esac diff --git a/src/sqlite_graph_memory/mcp_server.py b/src/sqlite_graph_memory/mcp_server.py index 1cf2ee4..dd0d7d7 100644 --- a/src/sqlite_graph_memory/mcp_server.py +++ b/src/sqlite_graph_memory/mcp_server.py @@ -131,7 +131,6 @@ def run_recall(query: str, mode: str = "associative") -> tuple[str, bool]: call_env["PYTHONPATH"] = f"{src_dir}{os.pathsep}{existing_pp}" if existing_pp else src_dir res = subprocess.run( cmd, - cwd=str(ROOT_DIR), env=call_env, capture_output=True, text=True, diff --git a/tests/test_mcp_script_mode.py b/tests/test_mcp_script_mode.py new file mode 100644 index 0000000..86f4b20 --- /dev/null +++ b/tests/test_mcp_script_mode.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +"""Script-mode and working-directory behaviour of the MCP server. + +Kept in its own file so it does not collide with edits to test_mcp_server.py. +Runs offline: no model, no index, no network. +""" +import json +import os +import subprocess +import sys +from pathlib import Path + +SRC = Path(__file__).resolve().parents[1] / "src" +sys.path.insert(0, str(SRC)) +from sqlite_graph_memory import __version__ # noqa: E402 +from sqlite_graph_memory import mcp_server # noqa: E402 + +SCRIPT = SRC / "sqlite_graph_memory" / "mcp_server.py" + + +def test_script_mode_starts_without_installed_package(tmp_path): + """`python src/sqlite_graph_memory/mcp_server.py` must not need the package on sys.path. + + Python puts the script's own directory (not src/) on sys.path, so an + absolute `from sqlite_graph_memory import ...` at import time crashed with + ModuleNotFoundError before the server answered anything (issue #24). + `-S` drops site-packages so an installed or editable copy cannot mask it. + """ + env = {k: v for k, v in os.environ.items() if k != "PYTHONPATH"} + req = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}} + res = subprocess.run( + [sys.executable, "-S", str(SCRIPT)], + input=json.dumps(req) + "\n", + capture_output=True, text=True, cwd=str(tmp_path), env=env, timeout=30, + ) + assert res.returncode == 0, res.stderr + resp = json.loads(res.stdout.strip().splitlines()[-1]) + assert resp["result"]["serverInfo"]["version"] == __version__ + + +def test_recall_subprocess_inherits_server_cwd(tmp_path, monkeypatch): + """Relative BRAIN_INDEX_DIR / TURNSTATE_DB must resolve against the user's cwd. + + On an installed wheel the package dir is site-packages/sqlite_graph_memory; + running brain_ask there made `BRAIN_INDEX_DIR=index` miss the user's ./index. + """ + work = tmp_path / "work" + work.mkdir() + stub = tmp_path / "stub_brain_ask.py" + stub.write_text( + "import os, pathlib\n" + "pathlib.Path(os.environ['BRAIN_ANSWER_OUT']).write_text(os.getcwd(), encoding='utf-8')\n", + encoding="utf-8", + ) + monkeypatch.setattr(mcp_server, "_brain_ask_cmd", lambda: [sys.executable, str(stub)]) + monkeypatch.chdir(work) + text, is_error = mcp_server.run_recall("anything") + assert is_error is False, text + assert Path(text).resolve() == work.resolve()