diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd2560..5a82399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [Unreleased] + +### Fixed +- **``tests/test_cli_help.py:_run_cli`` helper preferred the ASR-blocked + shim** — the pre-fix helper called ``shutil.which("truememory-mcp")`` + and used the bare ``.exe`` shim when present, falling back to + ``python -m`` only if the shim wasn't on PATH. On Windows hosts with + Microsoft Defender ASR rule ``01443614`` ("Block executable files + from running unless they meet a prevalence, age, or trusted list + criteria") in **Block** mode, the shim is silently killed at launch + — making 7 of the 9 tests in this file fail with + ``PermissionError [WinError 5] Access is denied`` even on a healthy + install. Rewrote the helper to always invoke + ``[sys.executable, "-m", "truememory.mcp_server"]``, routing through + the signed ``python.exe`` wrapper. Sibling fix in PR #351 covered + the 2 remaining tests in this file that bypass the helper entirely. +- **``mcp_server.py`` `__main__` block dropped exit codes under + ``python -m`` invocation** — the bottom-of-file + ``if __name__ == "__main__": main()`` discarded ``main()``'s return + value, so exit-code-2 paths (unknown flag, positional-arg typo) + silently exited 0 when the server was invoked via + ``python -m truememory.mcp_server``. The setuptools console-script + wrapper around ``truememory-mcp`` already does ``sys.exit(main())``, + so the bug was invisible until anything routed through ``-m``. + Changed to ``sys.exit(main() or 0)``. Surfaced by the ``_run_cli`` + helper rewrite above — 2 tests asserting ``returncode != 0`` were + passing under the shim and silently regressed to passing-as-0 under + ``-m`` until this line landed. + ## [0.6.8] — 2026-05-11 ### Fixed diff --git a/tests/test_cli_help.py b/tests/test_cli_help.py index 4a9edd2..156aaa1 100644 --- a/tests/test_cli_help.py +++ b/tests/test_cli_help.py @@ -16,23 +16,25 @@ from truememory import __version__ -def _truememory_mcp_bin() -> str | None: - """Locate the installed truememory-mcp console script, or None. - - Prefer the script installed by pip. Fall back to invoking via - `python -m truememory.mcp_server` — slower because it re-runs all - module-level imports, but works in any environment where truememory - is importable. - """ - return shutil.which("truememory-mcp") - - def _run_cli(args: list[str], timeout: int = 30) -> subprocess.CompletedProcess: - bin_path = _truememory_mcp_bin() - if bin_path: - cmd = [bin_path] + args - else: - cmd = [sys.executable, "-m", "truememory.mcp_server"] + args + """Run the truememory-mcp CLI via the module form. + + Always invokes ``[sys.executable, "-m", "truememory.mcp_server"]`` + rather than the bare ``truememory-mcp`` console-script shim. The + shim path is faster (cached imports) but is a setuptools / uv + trampoline with a per-install unique hash — Windows Defender's + Attack-Surface-Reduction rule ``01443614`` ("Block executable files + from running unless they meet a prevalence, age, or trusted list + criteria") silently kills it at launch on hardened Win11 baselines. + Routing through the signed ``python.exe`` wrapper passes ASR + everywhere; the per-test re-import cost is negligible at test scale. + + Replaces the pre-fix two-arm helper that called ``shutil.which`` and + preferred the shim when present — that branch fired exactly on + Block-mode ASR boxes, making 7 of the tests in this file fail with + ``PermissionError [WinError 5] Access is denied``. + """ + cmd = [sys.executable, "-m", "truememory.mcp_server"] + args return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) diff --git a/truememory/mcp_server.py b/truememory/mcp_server.py index dcbe604..ae74e4e 100644 --- a/truememory/mcp_server.py +++ b/truememory/mcp_server.py @@ -1519,4 +1519,12 @@ def main(): if __name__ == "__main__": - main() + # `sys.exit(main())` rather than bare `main()` — so exit codes + # returned from `main()` propagate when invoked via + # ``python -m truememory.mcp_server``. The setuptools console-script + # wrapper around `truememory-mcp` already does `sys.exit(main())` + # for you; `-m` invocation does not, so the bare `main()` form + # silently masked exit-code-2 paths (unknown flag, positional arg + # typo) as exit 0 under `python -m`. + import sys as _sys + _sys.exit(main() or 0)