From 698c371a03615c483668ad8ba578917ace9ae4d7 Mon Sep 17 00:00:00 2001 From: Hunter Casillas Date: Sat, 16 May 2026 21:05:50 -0500 Subject: [PATCH] fix(tests,mcp): harden _run_cli + propagate exit codes under python -m MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tiny additive fixes that close the test-infrastructure gap left open by PR-2b (#351). Surfaced live by Hunter's test run hitting Defender ASR on his Block-mode Win11 box (Windows confirmed `truememory-ingest.exe` blocked at 7:44 PM). tests/test_cli_help.py:_run_cli helper - Replaced the two-arm helper (shutil.which("truememory-mcp") → bare shim, fall back to python -m) with a single `[sys.executable, "-m", "truememory.mcp_server"]` invocation. The shim path is faster (cached imports) but is a setuptools / uv trampoline with a per- install unique hash — ASR rule 01443614 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. truememory/mcp_server.py:1521 (__main__ block) - Changed `main()` to `sys.exit(main() or 0)`. The setuptools console- script wrapper around `truememory-mcp` already does `sys.exit(main())`, so exit codes propagated correctly under the shim. Under `python -m` the bare `main()` discarded the return value — exit-code-2 paths (unknown flag, positional arg typo) silently masked as exit 0. Bundled here because the _run_cli rewrite above exposed the bug: 2 of the 7 newly -m-routed tests assert `returncode != 0`. Test results on Windows: - 7 of 9 tests in test_cli_help.py pass after this PR (up from 0 of 9 on Block-mode ASR boxes pre-fix) - Remaining 2 failures (test_help_via_console_script_does_not_hang, test_ingest_version_flag_exits_cleanly) invoke the shim directly outside the helper — covered by PR-2b (#351) lines 94-98 + 137-152. Coordination context (multi-agent sweep): - Targets origin/main directly per agent-B's relay (separate region from #351's lines 94-98 + 137-152 — mechanical merge order). - mcp_server.py:1521 is unclaimed by any other agent's file ownership region. Co-Authored-By: claude-opus-4-7 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ tests/test_cli_help.py | 34 ++++++++++++++++++---------------- truememory/mcp_server.py | 10 +++++++++- 3 files changed, 56 insertions(+), 17 deletions(-) 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)