diff --git a/pyproject.toml b/pyproject.toml index 01f38c2..b55e72e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wisdomgraph" -version = "0.3.2" +version = "0.3.3" description = "Graph-native persistent cognition for AI agents using Neo4j, DIKW, and MCP" readme = "README.md" license = {text = "MIT"} diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 36cc3e4..660bec2 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -844,3 +844,56 @@ def test_try_autostart_local_starts_backend_when_docker_present(): patch("wisdom.local.up") as up: assert mcp._try_autostart_local() is True up.assert_called_once_with(connect=True) + + +# ── version / stale-install self-check ───────────────────────────────────────── + +def test_version_command_prints_code_version(monkeypatch, capsys): + import wisdom.__main__ as m + + monkeypatch.setattr(sys, "argv", ["wisdom", "--version"]) + m.main() + assert f"wisdomgraph {m._CODE_VERSION}" in capsys.readouterr().out + + +def test_code_version_matches_pyproject(): + import re + from pathlib import Path + import wisdom.__main__ as m + + root = Path(__file__).resolve().parent.parent + text = (root / "pyproject.toml").read_text(encoding="utf-8") + match = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text) + assert match, "version not found in pyproject.toml" + assert match.group(1) == m._CODE_VERSION, "bump _CODE_VERSION to match pyproject.toml" + + +def test_warn_if_stale_warns_on_version_mismatch(capsys): + import wisdom.__main__ as m + + with patch.object(m, "_dist_version", return_value="0.2.0"): + m._warn_if_stale() + err = capsys.readouterr().err + assert "0.2.0" in err + assert "pip install -U wisdomgraph" in err + + +def test_warn_if_stale_silent_when_matching_or_unknown(capsys): + import wisdom.__main__ as m + + with patch.object(m, "_dist_version", return_value=m._CODE_VERSION): + m._warn_if_stale() + with patch.object(m, "_dist_version", return_value="unknown"): + m._warn_if_stale() + assert capsys.readouterr().err == "" + + +def test_main_skips_stale_check_for_mcp(monkeypatch): + import wisdom.__main__ as m + + monkeypatch.setattr(sys, "argv", ["wisdom", "mcp"]) + with patch.object(m, "_warn_if_stale") as warn, \ + patch("wisdom.mcp.run_mcp_server") as run: + m.main() + warn.assert_not_called() + run.assert_called_once() diff --git a/wisdom/__main__.py b/wisdom/__main__.py index 667747b..f5d0449 100644 --- a/wisdom/__main__.py +++ b/wisdom/__main__.py @@ -8,6 +8,11 @@ import sys from pathlib import Path +# Baked into the source so it identifies the code actually running, independent +# of whatever distribution metadata happens to resolve on sys.path. Keep in sync +# with pyproject.toml (guarded by test_code_version_matches_pyproject). +_CODE_VERSION = "0.3.3" + try: from importlib.metadata import version as _pkg_version __version__ = _pkg_version("wisdomgraph") @@ -15,6 +20,41 @@ __version__ = "unknown" +def _dist_version() -> str: + """Version of the installed `wisdomgraph` distribution metadata (may differ + from the running code if more than one install is on PATH).""" + try: + from importlib.metadata import version + return version("wisdomgraph") + except Exception: + return "unknown" + + +def _print_version() -> None: + print(f"wisdomgraph {_CODE_VERSION}") + dist = _dist_version() + if dist not in (_CODE_VERSION, "unknown"): + print( + f" note: installed distribution metadata reports {dist} — " + "more than one wisdomgraph install is on PATH.", + file=sys.stderr, + ) + + +def _warn_if_stale() -> None: + """Warn when the running code and the installed distribution disagree — the + classic 'wisdom on PATH is an old shim' trap.""" + dist = _dist_version() + if dist not in (_CODE_VERSION, "unknown"): + print( + f"warning: this 'wisdom' runs code v{_CODE_VERSION} but the installed " + f"wisdomgraph distribution is v{dist} — you likely have more than one " + "install on PATH.", + file=sys.stderr, + ) + print(" Fix: pip install -U wisdomgraph (check with: which -a wisdom)", file=sys.stderr) + + # ── Hook / registration strings ────────────────────────────────────────────── _SETTINGS_HOOK = { @@ -380,6 +420,15 @@ def main() -> None: cmd = sys.argv[1] + if cmd in ("--version", "-V", "version"): + _print_version() + return + + # Surface stale-install mismatches, but never on `mcp` — that speaks JSON-RPC + # over stdio and its stderr is reserved for the host's logs. + if cmd != "mcp": + _warn_if_stale() + if cmd == "install": default_p = "windows" if _platform_mod.system() == "Windows" else "claude" chosen = default_p @@ -635,6 +684,8 @@ def _print_explain(result: dict) -> None: def _print_help() -> None: print("Usage: wisdom ") print() + print(" --version print the running wisdomgraph version") + print() print("Setup:") print(" quickstart [--storage local|aura|existing] [--host claude|codex|all|none]") print(" doctor check Neo4j, local backend, and MCP host readiness")