From 0d304c78778c2b0e3b80b6777ebd20e80678a267 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 25 Oct 2026 09:38:18 -0500 Subject: [PATCH 1/2] fix(ci): correct platform assertions and isolate wiki CLI tests Signed-off-by: phernandez --- src/basic_memory/mcp/tools/delete.py | 4 ++++ test-int/mcp/test_project_management_integration.py | 3 ++- tests/cli/test_wiki_command.py | 7 +++++++ tests/indexing/test_note_content_promotion_guard.py | 3 ++- tests/mcp/test_tool_bm_delete.py | 2 +- tests/mcp/test_tool_project_management.py | 12 +++++++----- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/basic_memory/mcp/tools/delete.py b/src/basic_memory/mcp/tools/delete.py index cb95d871e..f7b622f96 100644 --- a/src/basic_memory/mcp/tools/delete.py +++ b/src/basic_memory/mcp/tools/delete.py @@ -96,6 +96,10 @@ async def bm_delete( source = identifier if identifier is not None else path assert source is not None + # Directory deletes strip whitespace at the API boundary. Do it before routing too: + # Windows resolves ' / ' outside the project before the root-delete guard can run. + if identifier is None: + source = source.strip() # A note is resolved by identifier; a directory is a path that must stay inside. kind = "identifier" if identifier is not None else "path" async with open_project_target( diff --git a/test-int/mcp/test_project_management_integration.py b/test-int/mcp/test_project_management_integration.py index bdc5963ce..598fba0fe 100644 --- a/test-int/mcp/test_project_management_integration.py +++ b/test-int/mcp/test_project_management_integration.py @@ -152,7 +152,8 @@ async def test_create_project_duplicate_name(mcp_server, app, test_project, tmp_ "command": "create", }, ) - assert f"already exists at {original_path}, not {other_path}." in str(refused.value) + expected_paths = f"already exists at {original_path}, not {other_path}.".replace("\\", "/") + assert expected_paths in str(refused.value).replace("\\", "/") same = await client.call_tool( "bm_project", diff --git a/tests/cli/test_wiki_command.py b/tests/cli/test_wiki_command.py index 98785ceb6..6895e9917 100644 --- a/tests/cli/test_wiki_command.py +++ b/tests/cli/test_wiki_command.py @@ -6,7 +6,9 @@ import pytest from typer.testing import CliRunner +from basic_memory import db from basic_memory.cli.app import app +from basic_memory.cli.commands.command_utils import run_with_cleanup from basic_memory.cli.commands.wiki import ( WikiCommandReport, WikiProjectReport, @@ -194,7 +196,12 @@ def test_wiki_rebuild_rejects_recorded_index_failures(): _require_successful_index(result) +# The CLI opens its own sessions; reset shared Postgres state before it reads projects. +@pytest.mark.usefixtures("engine_factory") def test_wiki_rebuild_builds_local_navigation(config_home, config_manager): + # Each CLI invocation closes its engine. Start with the persistent database, + # rather than the SQLite fixture's in-memory engine that the first call would lose. + run_with_cleanup(db.shutdown_db()) note = config_home / "guides" / "start.md" note.parent.mkdir() note.write_text( diff --git a/tests/indexing/test_note_content_promotion_guard.py b/tests/indexing/test_note_content_promotion_guard.py index 2fc211091..c02c5dab8 100644 --- a/tests/indexing/test_note_content_promotion_guard.py +++ b/tests/indexing/test_note_content_promotion_guard.py @@ -413,7 +413,8 @@ async def test_local_filesystem_confirms_restored_older_version( ) note_path = Path(file_service.base_path) / sample_entity.file_path note_path.parent.mkdir(parents=True, exist_ok=True) - note_path.write_text(FIRST_ACCEPTED, encoding="utf-8") + # Confirmation hashes file bytes, so Windows must not translate LF to CRLF here. + note_path.write_bytes(FIRST_ACCEPTED.encode("utf-8")) reader = local_promotion_confirmation_reader(file_service) assert reader is file_service diff --git a/tests/mcp/test_tool_bm_delete.py b/tests/mcp/test_tool_bm_delete.py index bd34d07a8..d79db7ba4 100644 --- a/tests/mcp/test_tool_bm_delete.py +++ b/tests/mcp/test_tool_bm_delete.py @@ -211,7 +211,7 @@ async def test_invalid_arguments(client, test_project, arguments, message): @pytest.mark.asyncio -@pytest.mark.parametrize("path", [" ", " / ", "\t"]) +@pytest.mark.parametrize("path", [" ", " / ", "\t", "\t/\t"]) async def test_a_blank_path_never_deletes_the_project(client, test_project, path): """The API reads a blank directory as the project root; bm_delete refuses it.""" await bm_write(identifier="notes/keep.md", content="# Keep", project=test_project.name) diff --git a/tests/mcp/test_tool_project_management.py b/tests/mcp/test_tool_project_management.py index b59237b79..15cda1cbe 100644 --- a/tests/mcp/test_tool_project_management.py +++ b/tests/mcp/test_tool_project_management.py @@ -208,11 +208,12 @@ async def test_create_memory_project_refuses_same_name_different_path(app, tmp_p project_path=str(requested_root), ) - assert str(refused.value) == ( + # Project paths are serialized with '/', while requested paths may use '\\' on Windows. + assert str(refused.value).replace("\\", "/") == ( f"ALREADY_EXISTS: Project 'Path Mismatch Project' already exists at " - f"{existing_root}, not {requested_root}.\n" + f"{existing_root.as_posix()}, not {requested_root.as_posix()}.\n" f"Try instead: another name, or create_memory_project(project_name=" - f"'Path Mismatch Project', project_path='{existing_root}')" + f"'Path Mismatch Project', project_path='{existing_root.as_posix()}')" ) mock_index.assert_not_awaited() @@ -236,8 +237,9 @@ async def test_create_memory_project_refuses_different_path_as_json(app, tmp_pat payload = json.loads(str(refused.value)) assert payload["error"] == "ALREADY_EXISTS" - assert payload["message"] == ( - f"Project 'Path Mismatch JSON' already exists at {existing_root}, not {requested_root}." + assert payload["message"].replace("\\", "/") == ( + f"Project 'Path Mismatch JSON' already exists at " + f"{existing_root.as_posix()}, not {requested_root.as_posix()}." ) From 9ec2286bfc214952fc368b6d9bd12f9b6b688c2d Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 25 Oct 2026 09:46:38 -0500 Subject: [PATCH 2/2] test(ci): cover Windows platform regressions before merge Signed-off-by: phernandez --- .github/workflows/test.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42deb3b2b..5ffe3c5e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -125,6 +125,43 @@ jobs: name: cli-smoke path: cli-smoke.xml + test-windows-platform: + needs: changes + # The full Windows suites run only on main. Keep these path, checksum and + # CLI lifecycle regressions covered before merge without that suite's cost. + if: needs.changes.outputs.code == 'true' && github.ref_name != 'main' + name: Windows path and checksum regressions (Python 3.12) + runs-on: windows-latest + timeout-minutes: 15 + env: + UV_PYTHON: "3.12" + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + cache: "pip" + - name: Install dependencies + run: | + pip install uv + uv venv + uv pip install -e ".[dev,pdf]" + - name: Run platform regressions + run: >- + uv run pytest + tests/cli/test_wiki_command.py + tests/indexing/test_note_content_promotion_guard.py + tests/mcp/test_tool_bm_delete.py + tests/mcp/test_tool_project_management.py + test-int/mcp/test_project_management_integration.py + -q --no-cov --junitxml=windows-platform.xml + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: windows-platform + path: windows-platform.xml + test-sqlite-unit: needs: changes if: needs.changes.outputs.code == 'true'