From 84d06f42732e47c6562b08e20cccb89d120e4c24 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 28 Dec 2025 21:51:23 +0800 Subject: [PATCH] fix(registry): normalize path case for comparison on Windows Adds case normalization for path comparison on Windows to handle case-insensitive filesystem behavior. Preserves case-sensitivity on Unix. Fixes: ISS-1766921318981-13 Solution-ID: SOL-1735386000-13 Issue-ID: ISS-1766921318981-13 Task-ID: T1 --- codex-lens/src/codexlens/storage/registry.py | 41 ++++++++---- codex-lens/tests/test_registry.py | 69 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 codex-lens/tests/test_registry.py diff --git a/codex-lens/src/codexlens/storage/registry.py b/codex-lens/src/codexlens/storage/registry.py index 2f1b2cc8..6a4469ab 100644 --- a/codex-lens/src/codexlens/storage/registry.py +++ b/codex-lens/src/codexlens/storage/registry.py @@ -2,6 +2,7 @@ from __future__ import annotations +import platform import sqlite3 import threading import time @@ -153,6 +154,17 @@ class RegistryStore: except sqlite3.DatabaseError as exc: raise StorageError(f"Failed to initialize registry schema: {exc}") from exc + def _normalize_path_for_comparison(self, path: Path) -> str: + """Normalize paths for comparisons and storage. + + Windows paths are treated as case-insensitive, so normalize to lowercase. + Unix platforms preserve case sensitivity. + """ + path_str = str(path) + if platform.system() == "Windows": + return path_str.lower() + return path_str + # === Project Operations === def register_project(self, source_root: Path, index_root: Path) -> ProjectInfo: @@ -167,7 +179,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_root_str = str(source_root.resolve()) + source_root_str = self._normalize_path_for_comparison(source_root.resolve()) index_root_str = str(index_root.resolve()) now = time.time() @@ -205,7 +217,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_root_str = str(source_root.resolve()) + source_root_str = self._normalize_path_for_comparison(source_root.resolve()) row = conn.execute( "SELECT id FROM projects WHERE source_root=?", (source_root_str,) @@ -229,7 +241,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_root_str = str(source_root.resolve()) + source_root_str = self._normalize_path_for_comparison(source_root.resolve()) row = conn.execute( "SELECT * FROM projects WHERE source_root=?", (source_root_str,) @@ -291,7 +303,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_root_str = str(source_root.resolve()) + source_root_str = self._normalize_path_for_comparison(source_root.resolve()) conn.execute( """ @@ -312,7 +324,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_root_str = str(source_root.resolve()) + source_root_str = self._normalize_path_for_comparison(source_root.resolve()) conn.execute( "UPDATE projects SET status=? WHERE source_root=?", @@ -344,7 +356,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_path_str = str(source_path.resolve()) + source_path_str = self._normalize_path_for_comparison(source_path.resolve()) index_path_str = str(index_path.resolve()) now = time.time() @@ -385,7 +397,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_path_str = str(source_path.resolve()) + source_path_str = self._normalize_path_for_comparison(source_path.resolve()) row = conn.execute( "SELECT id FROM dir_mapping WHERE source_path=?", (source_path_str,) @@ -409,7 +421,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_path_str = str(source_path.resolve()) + source_path_str = self._normalize_path_for_comparison(source_path.resolve()) row = conn.execute( "SELECT index_path FROM dir_mapping WHERE source_path=?", @@ -441,7 +453,7 @@ class RegistryStore: paths_to_check = [] current = source_path_resolved while True: - paths_to_check.append(str(current)) + paths_to_check.append(self._normalize_path_for_comparison(current)) parent = current.parent if parent == current: # Reached filesystem root break @@ -476,7 +488,8 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_path_resolved = str(Path(source_path).resolve()) + resolved_path = Path(source_path).resolve() + source_path_resolved = self._normalize_path_for_comparison(resolved_path) # First try exact match on projects table row = conn.execute( @@ -494,9 +507,9 @@ class RegistryStore: # Try finding project that contains this path # Build list of all parent paths paths_to_check = [] - current = Path(source_path_resolved) + current = resolved_path while True: - paths_to_check.append(str(current)) + paths_to_check.append(self._normalize_path_for_comparison(current)) parent = current.parent if parent == current: break @@ -552,7 +565,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_path_str = str(source_path.resolve()) + source_path_str = self._normalize_path_for_comparison(source_path.resolve()) # First get the parent's depth parent_row = conn.execute( @@ -587,7 +600,7 @@ class RegistryStore: """ with self._lock: conn = self._get_connection() - source_path_str = str(source_path.resolve()) + source_path_str = self._normalize_path_for_comparison(source_path.resolve()) conn.execute( """ diff --git a/codex-lens/tests/test_registry.py b/codex-lens/tests/test_registry.py new file mode 100644 index 00000000..e8f54d0d --- /dev/null +++ b/codex-lens/tests/test_registry.py @@ -0,0 +1,69 @@ +"""Tests for RegistryStore path handling.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from codexlens.storage.registry import RegistryStore + + +def _swap_case(path: Path) -> str: + return str(path).swapcase() + + +def test_path_case_normalization_windows(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """On Windows, path comparisons should be case-insensitive.""" + import codexlens.storage.registry as registry + + monkeypatch.setattr(registry.platform, "system", lambda: "Windows") + + db_path = tmp_path / "registry.db" + source_root = tmp_path / "MyProject" + index_root = tmp_path / "indexes" + + with RegistryStore(db_path=db_path) as store: + store.register_project(source_root, index_root) + + result = store.find_by_source_path(_swap_case(source_root)) + assert result is not None + assert result["source_root"] == str(source_root.resolve()).lower() + + +def test_path_case_sensitivity_non_windows(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """On Unix, path comparisons should remain case-sensitive.""" + import codexlens.storage.registry as registry + + monkeypatch.setattr(registry.platform, "system", lambda: "Linux") + + db_path = tmp_path / "registry.db" + source_root = tmp_path / "MyProject" + index_root = tmp_path / "indexes" + + with RegistryStore(db_path=db_path) as store: + store.register_project(source_root, index_root) + assert store.find_by_source_path(_swap_case(source_root)) is None + + +def test_find_nearest_index(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Nearest ancestor lookup should be case-insensitive on Windows.""" + import codexlens.storage.registry as registry + + monkeypatch.setattr(registry.platform, "system", lambda: "Windows") + + db_path = tmp_path / "registry.db" + source_root = tmp_path / "MyProject" + index_root = tmp_path / "indexes" + index_db = index_root / "_index.db" + + with RegistryStore(db_path=db_path) as store: + project = store.register_project(source_root, index_root) + mapping = store.register_dir(project.id, source_root, index_db, depth=0) + + query_path = Path(_swap_case(source_root)) / "SubDir" / "file.py" + found = store.find_nearest_index(query_path) + + assert found is not None + assert found.id == mapping.id +