From db7f7f8b81cb48271faae95fae7a19b48960fa73 Mon Sep 17 00:00:00 2001 From: Kapil Lamba Date: Mon, 11 May 2026 18:17:32 +0530 Subject: [PATCH 1/3] Restructure as code_memory package and bump to 1.0.32 - Move sources into a `code_memory/` package so `pip install code-memory` stops shipping top-level modules (`db`, `parser`, `queries`, etc.) into users' site-packages. Convert intra-package imports to relative; tests now use absolute `from code_memory import ...`. Remove sys.path hacks from conftest.py and test_dead_code.py. - Delete unused test_pydantic.py debug script that previously shipped in the wheel. - Fix author name typo (Lambert -> Lamba) in pyproject.toml. - Expand CI mypy from 3 files to the whole `code_memory` package, with per-module `ignore_errors` overrides for 4 modules pending type cleanup (db, doc_parser, git_search, parser). - Expand CI test matrix to ubuntu, macos, and windows runners with fail-fast disabled. - Extract `_hint_if_unindexed` helper in server.py and remove all 6 stale `# type: ignore[typeddict-unknown-key]` annotations (the `hint` field is already declared as NotRequired in api_types). - Bump version 1.0.31 -> 1.0.32 in pyproject.toml, server.json (x2), uv.lock. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 6 +- CONTRIBUTING.md | 32 +++++----- README.md | 30 ++++----- code-memory.spec | 22 ++++--- code_memory/__init__.py | 1 + api_types.py => code_memory/api_types.py | 0 db.py => code_memory/db.py | 0 doc_parser.py => code_memory/doc_parser.py | 4 +- errors.py => code_memory/errors.py | 2 +- git_search.py => code_memory/git_search.py | 2 +- .../logging_config.py | 0 parser.py => code_memory/parser.py | 2 +- queries.py => code_memory/queries.py | 4 +- server.py => code_memory/server.py | 62 +++++++++---------- validation.py => code_memory/validation.py | 2 +- pyproject.toml | 20 ++++-- server.json | 4 +- test_pydantic.py | 57 ----------------- tests/conftest.py | 4 -- tests/test_dead_code.py | 25 ++++---- tests/test_errors.py | 2 +- tests/test_logging.py | 2 +- tests/test_main.py | 4 +- tests/test_tools.py | 22 +++---- tests/test_validation.py | 4 +- uv.lock | 2 +- 26 files changed, 134 insertions(+), 181 deletions(-) create mode 100644 code_memory/__init__.py rename api_types.py => code_memory/api_types.py (100%) rename db.py => code_memory/db.py (100%) rename doc_parser.py => code_memory/doc_parser.py (99%) rename errors.py => code_memory/errors.py (99%) rename git_search.py => code_memory/git_search.py (99%) rename logging_config.py => code_memory/logging_config.py (100%) rename parser.py => code_memory/parser.py (99%) rename queries.py => code_memory/queries.py (99%) rename server.py => code_memory/server.py (95%) rename validation.py => code_memory/validation.py (99%) delete mode 100644 test_pydantic.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0b4f30..8446140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,9 +8,11 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: + os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.13"] steps: @@ -29,7 +31,7 @@ jobs: run: uv run ruff check . - name: Run type checking - run: uv run mypy api_types.py errors.py server.py + run: uv run mypy code_memory - name: Run tests run: uv run pytest tests/ -v --tb=short diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 428d34f..1cc6c9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,10 +24,10 @@ uv sync --all-extras ```bash # Run the MCP server -uv run mcp run server.py +uv run mcp run code_memory/server.py # Run with MCP Inspector for debugging -uv run mcp dev server.py +uv run mcp dev code_memory/server.py ``` ## Development Workflow @@ -73,18 +73,20 @@ uv build ``` code-memory/ -├── server.py # MCP server entry point -├── db.py # SQLite database layer -├── parser.py # Tree-sitter code parser -├── doc_parser.py # Markdown documentation parser -├── queries.py # Hybrid retrieval query layer -├── git_search.py # Git history search module -├── errors.py # Custom exception hierarchy -├── validation.py # Input validation functions -├── logging_config.py # Structured logging configuration -├── tests/ # Test suite -├── prompts/ # Milestone prompt files -└── pyproject.toml # Project configuration +├── code_memory/ # Package source +│ ├── server.py # MCP server entry point +│ ├── db.py # SQLite database layer +│ ├── parser.py # Tree-sitter code parser +│ ├── doc_parser.py # Markdown documentation parser +│ ├── queries.py # Hybrid retrieval query layer +│ ├── git_search.py # Git history search module +│ ├── errors.py # Custom exception hierarchy +│ ├── validation.py # Input validation functions +│ ├── logging_config.py # Structured logging configuration +│ └── api_types.py # MCP response TypedDicts +├── tests/ # Test suite +├── prompts/ # Milestone prompt files +└── pyproject.toml # Project configuration ``` ## Pull Request Process @@ -109,7 +111,7 @@ code-memory/ ### Adding a New Tool -1. Add the tool function in `server.py` with the `@mcp.tool()` decorator +1. Add the tool function in `code_memory/server.py` with the `@mcp.tool()` decorator 2. Add input validation using functions from `validation.py` 3. Wrap the implementation in error handling 4. Add logging using `ToolLogger` diff --git a/README.md b/README.md index 9d64639..3649e91 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ cd code-memory uv sync # Run the MCP server (stdio transport) -uv run mcp run server.py +uv run mcp run code_memory/server.py ``` ### Pre-built Binaries (Standalone) @@ -119,7 +119,7 @@ uvx code-memory ```bash # Run with the MCP Inspector for interactive debugging -uv run mcp dev server.py +uv run mcp dev code_memory/server.py # Run tests uv run pytest tests/ -v @@ -382,18 +382,20 @@ search_history(query="server.py", search_type="blame", target_file="server.py", ``` code-memory/ -├── server.py # MCP server entry point (FastMCP) -├── db.py # SQLite database layer with sqlite-vec -├── parser.py # Tree-sitter-based code parser -├── doc_parser.py # Markdown documentation parser -├── queries.py # Hybrid retrieval query layer -├── git_search.py # Git history search module -├── errors.py # Custom exception hierarchy -├── validation.py # Input validation functions -├── logging_config.py # Structured logging configuration -├── tests/ # Test suite -├── pyproject.toml # Project metadata & dependencies -└── prompts/ # Milestone prompt engineering files +├── code_memory/ # Package source +│ ├── server.py # MCP server entry point (FastMCP) +│ ├── db.py # SQLite database layer with sqlite-vec +│ ├── parser.py # Tree-sitter-based code parser +│ ├── doc_parser.py # Markdown documentation parser +│ ├── queries.py # Hybrid retrieval query layer +│ ├── git_search.py # Git history search module +│ ├── errors.py # Custom exception hierarchy +│ ├── validation.py # Input validation functions +│ ├── logging_config.py # Structured logging configuration +│ └── api_types.py # MCP response TypedDicts +├── tests/ # Test suite +├── pyproject.toml # Project metadata & dependencies +└── prompts/ # Milestone prompt engineering files ``` ## Troubleshooting diff --git a/code-memory.spec b/code-memory.spec index 32b172e..c16c279 100644 --- a/code-memory.spec +++ b/code-memory.spec @@ -48,15 +48,17 @@ hidden_imports = [ # Tree-sitter core 'tree_sitter', # Local modules - 'server', - 'db', - 'parser', - 'doc_parser', - 'queries', - 'git_search', - 'errors', - 'validation', - 'logging_config', + 'code_memory', + 'code_memory.server', + 'code_memory.db', + 'code_memory.parser', + 'code_memory.doc_parser', + 'code_memory.queries', + 'code_memory.git_search', + 'code_memory.errors', + 'code_memory.validation', + 'code_memory.logging_config', + 'code_memory.api_types', # GitPython dependencies 'git', 'gitdb', @@ -101,7 +103,7 @@ except ImportError: pass a = Analysis( - ['server.py'], + ['code_memory/server.py'], pathex=[str(PROJECT_ROOT)], binaries=[], datas=datas, diff --git a/code_memory/__init__.py b/code_memory/__init__.py new file mode 100644 index 0000000..339b226 --- /dev/null +++ b/code_memory/__init__.py @@ -0,0 +1 @@ +"""code-memory: a deterministic, high-precision code intelligence MCP server.""" diff --git a/api_types.py b/code_memory/api_types.py similarity index 100% rename from api_types.py rename to code_memory/api_types.py diff --git a/db.py b/code_memory/db.py similarity index 100% rename from db.py rename to code_memory/db.py diff --git a/doc_parser.py b/code_memory/doc_parser.py similarity index 99% rename from doc_parser.py rename to code_memory/doc_parser.py index dd0f979..4be96b9 100644 --- a/doc_parser.py +++ b/code_memory/doc_parser.py @@ -13,8 +13,8 @@ from markdown_it import MarkdownIt -import db as db_mod -from parser import SKIP_DIRS, GitignoreMatcher +from . import db as db_mod +from .parser import SKIP_DIRS, GitignoreMatcher logger = logging.getLogger(__name__) diff --git a/errors.py b/code_memory/errors.py similarity index 99% rename from errors.py rename to code_memory/errors.py index b483505..c30dcd1 100644 --- a/errors.py +++ b/code_memory/errors.py @@ -11,7 +11,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - import api_types + from . import api_types class CodeMemoryError(Exception): diff --git a/git_search.py b/code_memory/git_search.py similarity index 99% rename from git_search.py rename to code_memory/git_search.py index 7ccc90a..cbf7f4b 100644 --- a/git_search.py +++ b/code_memory/git_search.py @@ -22,7 +22,7 @@ import git from git.exc import InvalidGitRepositoryError, NoSuchPathError -import errors +from . import errors # --------------------------------------------------------------------------- # Helpers diff --git a/logging_config.py b/code_memory/logging_config.py similarity index 100% rename from logging_config.py rename to code_memory/logging_config.py diff --git a/parser.py b/code_memory/parser.py similarity index 99% rename from parser.py rename to code_memory/parser.py index b55d145..6f45818 100644 --- a/parser.py +++ b/code_memory/parser.py @@ -18,7 +18,7 @@ import pathspec from tree_sitter import Language, Node, Parser -import db as db_mod +from . import db as db_mod logger = logging.getLogger(__name__) diff --git a/queries.py b/code_memory/queries.py similarity index 99% rename from queries.py rename to code_memory/queries.py index c8f8108..f86e103 100644 --- a/queries.py +++ b/code_memory/queries.py @@ -11,8 +11,8 @@ import logging import struct -import db as db_mod -import validation as val +from . import db as db_mod +from . import validation as val logger = logging.getLogger(__name__) diff --git a/server.py b/code_memory/server.py similarity index 95% rename from server.py rename to code_memory/server.py index f93f6f6..103637f 100644 --- a/server.py +++ b/code_memory/server.py @@ -14,23 +14,37 @@ import argparse import asyncio -from typing import Literal, cast +from typing import Any, Literal, cast from mcp.server.fastmcp import Context, FastMCP -import api_types -import db as db_mod -import doc_parser as doc_parser_mod -import errors -import logging_config -import parser as parser_mod -import queries -import validation as val +from . import api_types, errors, logging_config, queries +from . import db as db_mod +from . import doc_parser as doc_parser_mod +from . import parser as parser_mod +from . import validation as val # ── Initialize logging ─────────────────────────────────────────────────── logger = logging_config.setup_logging() tool_logger = logging_config.get_logger("tools") +# ── Hint messages shown when a tool returns no results because nothing is indexed yet ── +_HINT_CODE_NOT_INDEXED = ( + "No results. Codebase may not be indexed. Call index_codebase(directory) first." +) +_HINT_DOCS_NOT_INDEXED = ( + "No results. Documentation may not be indexed. Call index_codebase(directory) first." +) + + +def _hint_if_unindexed( + response: Any, db: Any, *, table: Literal["symbols", "doc_chunks"], hint: str +) -> None: + """Attach `hint` to `response` if the given table has zero rows.""" + if db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] == 0: + response["hint"] = hint + + # ── Lazy warmup state ──────────────────────────────────────────────────── _warmup_done = False @@ -306,9 +320,7 @@ def search_code( "results": results, }) if not results: - symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] - if symbols_count == 0: - topic_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key] + _hint_if_unindexed(topic_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED) return topic_response elif validated_search_type == "definition": @@ -321,9 +333,7 @@ def search_code( "results": results, }) if not results: - symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] - if symbols_count == 0: - def_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key] + _hint_if_unindexed(def_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED) return def_response elif validated_search_type == "references": @@ -336,9 +346,7 @@ def search_code( "results": results, }) if not results: - symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] - if symbols_count == 0: - ref_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key] + _hint_if_unindexed(ref_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED) return ref_response elif validated_search_type == "file_structure": @@ -351,9 +359,7 @@ def search_code( "results": results, }) if not results: - symbols_count = database.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] - if symbols_count == 0: - struct_response["hint"] = "No results. Codebase may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key] + _hint_if_unindexed(struct_response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED) return struct_response return errors.format_error(errors.ValidationError(f"Unknown search_type: {search_type}")) @@ -627,9 +633,7 @@ def search_docs(query: str, directory: str, top_k: int = 10) -> api_types.Search }) if not results: - doc_chunks_count = database.execute("SELECT COUNT(*) FROM doc_chunks").fetchone()[0] - if doc_chunks_count == 0: - response["hint"] = "No results. Documentation may not be indexed. Call index_codebase(directory) first." # type: ignore[typeddict-unknown-key] + _hint_if_unindexed(response, database, table="doc_chunks", hint=_HINT_DOCS_NOT_INDEXED) return response @@ -707,7 +711,7 @@ def search_history( try: from git.exc import InvalidGitRepositoryError, NoSuchPathError - import git_search as gs + from . import git_search as gs # Validate inputs validated_search_type = val.validate_search_type( @@ -908,13 +912,7 @@ def find_dead_code( }) if result["total_symbols"] == 0: - symbols_count = database.execute( - "SELECT COUNT(*) FROM symbols" - ).fetchone()[0] - if symbols_count == 0: - response["hint"] = ( # type: ignore[typeddict-unknown-key] - "Codebase may not be indexed. Call index_codebase(directory) first." - ) + _hint_if_unindexed(response, database, table="symbols", hint=_HINT_CODE_NOT_INDEXED) return response diff --git a/validation.py b/code_memory/validation.py similarity index 99% rename from validation.py rename to code_memory/validation.py index 1580db5..a8d5b3c 100644 --- a/validation.py +++ b/code_memory/validation.py @@ -10,7 +10,7 @@ import re from pathlib import Path -from errors import ValidationError +from .errors import ValidationError def validate_directory(path: str, must_exist: bool = True) -> Path: diff --git a/pyproject.toml b/pyproject.toml index efaba9c..0aa52c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "hatchling.build" [project] name = "code-memory" -version = "1.0.31" +version = "1.0.32" description = "A deterministic, high-precision code intelligence MCP server" readme = "README.md" license = "MIT" requires-python = ">=3.13" authors = [ - {name = "Kapil Lambert", email = "kapillamba4@gmail.com"} + {name = "Kapil Lamba", email = "kapillamba4@gmail.com"} ] classifiers = [ "Development Status :: 4 - Beta", @@ -52,7 +52,7 @@ dev = [ ] [project.scripts] -code-memory = "server:main" +code-memory = "code_memory.server:main" [project.urls] Homepage = "https://github.com/kapillamba4/code-memory" @@ -61,7 +61,7 @@ Repository = "https://github.com/kapillamba4/code-memory.git" Issues = "https://github.com/kapillamba4/code-memory/issues" [tool.hatch.build.targets.wheel] -packages = ["."] +packages = ["code_memory"] [tool.pytest.ini_options] testpaths = ["tests"] @@ -83,8 +83,18 @@ warn_return_any = true warn_unused_configs = true ignore_missing_imports = true +# TODO: clean up pre-existing type issues in these modules and remove the override. +[[tool.mypy.overrides]] +module = [ + "code_memory.db", + "code_memory.doc_parser", + "code_memory.git_search", + "code_memory.parser", +] +ignore_errors = true + [tool.coverage.run] -source = ["."] +source = ["code_memory"] omit = ["tests/*", ".venv/*"] [tool.coverage.report] diff --git a/server.json b/server.json index d49e582..46f7aea 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/kapillamba4/code-memory", "source": "github" }, - "version": "1.0.31", + "version": "1.0.32", "packages": [ { "registryType": "pypi", "identifier": "code-memory", - "version": "1.0.31", + "version": "1.0.32", "transport": { "type": "stdio" }, diff --git a/test_pydantic.py b/test_pydantic.py deleted file mode 100644 index 3efdd83..0000000 --- a/test_pydantic.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Literal, NotRequired - -from pydantic import TypeAdapter -from typing_extensions import TypedDict - - -class ContextChunk(TypedDict): - type: Literal["previous", "current", "next"] - content: str - -class DocSearchResult(TypedDict): - content: str - source_file: str - section_title: str | None - line_start: int - line_end: int - doc_type: str - score: float - context: NotRequired[list[ContextChunk]] - -class SearchDocsResponse(TypedDict): - status: Literal["ok"] - query: str - results: list[DocSearchResult] - count: int - hint: NotRequired[str] - -class ErrorResponse(TypedDict): - error: Literal[True] - error_type: str - message: str - details: str | dict | None - -ToolOutput = SearchDocsResponse | ErrorResponse - -adapter = TypeAdapter(ToolOutput) -try: - dict_val = { - "status": "ok", - "query": "test", - "count": 1, - "results": [ - { - "content": "2. Configure...", - "source_file": "path/to/file", - "section_title": None, - "line_start": 1, - "line_end": 10, - "score": 0.0, - "doc_type": "markdown", - } - ] - } - adapter.validate_python(dict_val) - print("VALIDATION SUCCESS") -except Exception as e: - print("ERROR:", e) diff --git a/tests/conftest.py b/tests/conftest.py index 3341954..52a9ecd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,13 +6,9 @@ import os import sqlite3 -import sys import tempfile from pathlib import Path -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - import pytest diff --git a/tests/test_dead_code.py b/tests/test_dead_code.py index 693dd75..f6ffc8d 100644 --- a/tests/test_dead_code.py +++ b/tests/test_dead_code.py @@ -3,16 +3,13 @@ from __future__ import annotations import sqlite3 -import sys from pathlib import Path import pytest -sys.path.insert(0, str(Path(__file__).parent.parent)) - -import db as db_mod -import queries -from queries import ( +from code_memory import db as db_mod +from code_memory import queries +from code_memory.queries import ( _has_decorator_above, _is_excluded_from_dead_code, _is_test_path, @@ -569,40 +566,40 @@ def test_shared_name_alive_via_either_caller(self, dc_db): class TestFindDeadCodeServerValidation: def test_nonexistent_directory_returns_error(self): - import server + from code_memory import server result = server.find_dead_code("/nonexistent/path") assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") def test_min_confidence_above_one_returns_error(self, temp_dir): - import server + from code_memory import server result = server.find_dead_code(str(temp_dir), min_confidence=1.5) assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") def test_negative_min_confidence_returns_error(self, temp_dir): - import server + from code_memory import server result = server.find_dead_code(str(temp_dir), min_confidence=-0.1) assert result.get("error") is True def test_invalid_kind_returns_error(self, temp_dir): - import server + from code_memory import server result = server.find_dead_code(str(temp_dir), kinds=["function", "variable"]) assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") def test_empty_kinds_list_returns_error(self, temp_dir): - import server + from code_memory import server result = server.find_dead_code(str(temp_dir), kinds=[]) assert result.get("error") is True def test_top_k_too_large_returns_error(self, temp_dir): - import server + from code_memory import server result = server.find_dead_code(str(temp_dir), top_k=10000) assert result.get("error") is True @@ -647,7 +644,7 @@ def prepopulated_directory(temp_dir): class TestFindDeadCodeServerEndToEnd: def test_returns_candidates(self, prepopulated_directory): - import server + from code_memory import server directory, conn = prepopulated_directory src_path = str(directory / "src.py") @@ -678,7 +675,7 @@ def test_returns_candidates(self, prepopulated_directory): assert "directory" in result def test_empty_index_returns_hint(self, prepopulated_directory): - import server + from code_memory import server directory, _ = prepopulated_directory result = server.find_dead_code(str(directory)) diff --git a/tests/test_errors.py b/tests/test_errors.py index 17c0d2a..a7e5db8 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -2,7 +2,7 @@ from __future__ import annotations -from errors import ( +from code_memory.errors import ( CodeMemoryError, DatabaseError, EmbeddingError, diff --git a/tests/test_logging.py b/tests/test_logging.py index ef5e3be..cd20e90 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -5,7 +5,7 @@ import io import logging -import logging_config +from code_memory import logging_config class TestSetupLogging: diff --git a/tests/test_main.py b/tests/test_main.py index 250ec5d..ba8069d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -7,8 +7,8 @@ import pytest -import server as server_mod -from server import build_arg_parser +from code_memory import server as server_mod +from code_memory.server import build_arg_parser class TestMainArgParsing: diff --git a/tests/test_tools.py b/tests/test_tools.py index 25b4f81..22c9420 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -17,14 +17,14 @@ class TestSearchCodeValidation: def test_empty_query_returns_error(self): """Test that empty query returns structured error.""" - import server + from code_memory import server result = server.search_code("", "definition", "/tmp") assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") def test_invalid_search_type_returns_error(self): """Test that invalid search_type returns structured error.""" - import server + from code_memory import server result = server.search_code("test", "invalid_type", "/tmp") assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") @@ -35,14 +35,14 @@ class TestSearchDocsValidation: def test_empty_query_returns_error(self): """Test that empty query returns structured error.""" - import server + from code_memory import server result = server.search_docs("", "/tmp") assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") def test_invalid_top_k_returns_error(self): """Test that invalid top_k returns structured error.""" - import server + from code_memory import server result = server.search_docs("test", "/tmp", top_k=-1) assert result.get("error") is True @@ -52,14 +52,14 @@ class TestSearchHistoryValidation: def test_invalid_search_type_returns_error(self): """Test that invalid search_type returns structured error.""" - import server + from code_memory import server result = server.search_history("test", "/tmp", search_type="invalid") assert result.get("error") is True assert "ValidationError" in result.get("error_type", "") def test_file_history_requires_target_file(self): """Test that file_history requires target_file.""" - import server + from code_memory import server # Use current directory (which is a git repo) to get past git validation result = server.search_history("test", ".", search_type="file_history", target_file=None) assert result.get("error") is True @@ -67,7 +67,7 @@ def test_file_history_requires_target_file(self): def test_blame_requires_target_file(self): """Test that blame requires target_file.""" - import server + from code_memory import server # Use current directory (which is a git repo) to get past git validation result = server.search_history("test", ".", search_type="blame", target_file=None) assert result.get("error") is True @@ -75,7 +75,7 @@ def test_blame_requires_target_file(self): def test_invalid_line_range_returns_error(self): """Test that invalid line range returns error.""" - import server + from code_memory import server # This should work since we're in a git repo, but line_start > line_end result = server.search_history( "test", @@ -95,7 +95,7 @@ def test_nonexistent_directory_returns_error(self): """Test that nonexistent directory returns structured error.""" import asyncio - import server + from code_memory import server ctx = MockContext() async def run_test(): @@ -112,7 +112,7 @@ class TestToolResponseStructure: def test_success_response_has_status(self): """Test that successful responses have status field.""" - import server + from code_memory import server # search_docs should work even without indexed content result = server.search_docs("test query", "/tmp") # Either it succeeds or fails gracefully @@ -123,7 +123,7 @@ def test_success_response_has_status(self): def test_error_response_structure(self): """Test that error responses have consistent structure.""" - import server + from code_memory import server result = server.search_code("", "definition", "/tmp") assert "error" in result assert result["error"] is True diff --git a/tests/test_validation.py b/tests/test_validation.py index 489c2c5..878e689 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -4,8 +4,8 @@ import pytest -import validation as val -from errors import ValidationError +from code_memory import validation as val +from code_memory.errors import ValidationError class TestValidateQuery: diff --git a/uv.lock b/uv.lock index c50e728..6aac91b 100644 --- a/uv.lock +++ b/uv.lock @@ -109,7 +109,7 @@ wheels = [ [[package]] name = "code-memory" -version = "1.0.30" +version = "1.0.32" source = { editable = "." } dependencies = [ { name = "einops" }, From eef911e3a0fb19556f3881faa493fde11062b1dc Mon Sep 17 00:00:00 2001 From: Kapil Lamba Date: Mon, 11 May 2026 18:21:06 +0530 Subject: [PATCH 2/3] Fix path-equality tests on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `validate_directory`/`validate_file` resolve symlinks (intentional — collapses `..` and normalises). On macOS, `/tmp` is a symlink to `/private/tmp`, so the resolved result diverges from the unresolved tempdir path. Compare against `.resolve()` instead. Surfaced by the new ubuntu/macos/windows CI matrix. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_validation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_validation.py b/tests/test_validation.py index 878e689..3789081 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -140,7 +140,8 @@ class TestValidateDirectory: def test_existing_directory(self, temp_dir): """Test that existing directories pass.""" result = val.validate_directory(str(temp_dir)) - assert result == temp_dir + # validate_directory resolves symlinks; on macOS /tmp -> /private/tmp. + assert result == temp_dir.resolve() def test_nonexistent_fails(self): """Test that nonexistent directories raise ValidationError.""" @@ -164,7 +165,8 @@ def test_existing_file(self, temp_dir): test_file = temp_dir / "test.txt" test_file.write_text("test") result = val.validate_file(str(test_file)) - assert result == test_file + # validate_file resolves symlinks; on macOS /tmp -> /private/tmp. + assert result == test_file.resolve() def test_nonexistent_fails(self): """Test that nonexistent files raise ValidationError.""" From d1ff74ef006cef6aa372b86dcbbafbca0234fccb Mon Sep 17 00:00:00 2001 From: Kapil Lamba Date: Mon, 11 May 2026 18:23:16 +0530 Subject: [PATCH 3/3] Tolerate Windows tempdir cleanup errors in tests `get_db()` opens a sqlite3 connection but tools (e.g. find_dead_code) don't explicitly close it. On Linux/macOS this is harmless since the OS happily unlinks open files, but on Windows the .db file stays locked until the Connection is GC'd, which can outlive the fixture and break `TemporaryDirectory.cleanup()`. Pass `ignore_cleanup_errors=True` so the teardown is best-effort. Fixing the underlying connection leak is a separate concern. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 52a9ecd..ead9a11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,10 @@ def temp_db(): @pytest.fixture def temp_dir(): """Provide a temporary directory for file tests.""" - with tempfile.TemporaryDirectory() as tmpdir: + # ignore_cleanup_errors=True: on Windows, sqlite3 keeps the .db file + # locked until the Connection is GC'd, which can outlive the fixture + # and break TemporaryDirectory.cleanup(). + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: yield Path(tmpdir)