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..ead9a11 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 @@ -43,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) 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..3789081 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: @@ -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.""" 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" },