Skip to main content
Glama

Jinni: Bring Your Project Into Context

by smat-dev
test_utils.py20.5 kB
import pytest import os import datetime from pathlib import Path from typing import Set, List, Optional, Tuple import pathspec # Direct import, assume it's installed # Import the main orchestrator for the helper function, exceptions, and utils from jinni.core_logic import read_context # Keep for run_read_context_helper from jinni.exceptions import ContextSizeExceededError, DetailedContextSizeError # Moved exceptions from jinni.utils import _find_context_files_for_dir # Moved helper from jinni.config_system import CONTEXT_FILENAME, DEFAULT_RULES # Import constants from jinni.utils import ensure_no_nul # SEPARATOR is not directly used/tested here # --- Test Fixture --- @pytest.fixture def test_dir(tmp_path: Path) -> Path: """Creates a standard test directory structure.""" root = tmp_path / "project" root.mkdir() # Root level files and dirs (root / "README.md").write_text("# Project", encoding='utf-8') (root / "main.py").write_text("print('main')", encoding='utf-8') (root / ".env").write_text("SECRET=123", encoding='utf-8') # Should be excluded by default (root / "config.yaml").write_text("key: value", encoding='utf-8') (root / "temp.tmp").touch() # Should be excluded by default (root / "image.jpg").write_bytes(b'\xff\xd8\xff\xe0') # Binary # Src directory (root / "src").mkdir() (root / "src" / "app.py").write_text("print('app')", encoding='utf-8') (root / "src" / "utils.py").write_text("def helper(): pass", encoding='utf-8') (root / "src" / "data.json").write_text('{"data": true}', encoding='utf-8') (root / "src" / ".hidden_in_src").touch() # Hidden # Lib directory (should be excluded by default) (root / "lib").mkdir() (root / "lib" / "somelib.py").write_text("# Library code", encoding='utf-8') (root / "lib" / "binary.dll").write_bytes(b'\x4d\x5a\x90\x00') # Binary # Docs directory (root / "docs").mkdir() (root / "docs" / "index.md").write_text("Docs index", encoding='utf-8') (root / "docs" / "config").mkdir() (root / "docs" / "config" / "options.md").write_text("Config options", encoding='utf-8') # Nested directory to test hierarchy (root / "src" / "nested").mkdir() (root / "src" / "nested" / "deep.py").write_text("# Deep", encoding='utf-8') (root / "src" / "nested" / "other.txt").write_text("Nested text", encoding='utf-8') # Build directory (should be excluded by default) (root / "build").mkdir() (root / "build" / "output.bin").touch() # Symlink (if possible) symlink_target = root / "main.py" symlink_path = root / "main_link.py" if symlink_target.exists(): try: symlink_path.symlink_to(symlink_target) except OSError: print("Warning: Symlink creation failed in test setup.") return root # Helper function to run read_context and normalize output def run_read_context_helper( project_root_rel: str, # Project root relative to tmp_path (required) tmp_path: Path, target_rel: Optional[str] = None, # Target relative to tmp_path (optional) override_rules: Optional[List[str]] = None, list_only: bool = False, size_limit_mb: Optional[int] = None, debug_explain: bool = False, include_size_in_list: bool = False # Keep this arg ) -> str: """Runs read_context with absolute paths and returns normalized output.""" project_root_abs_path = str(tmp_path / project_root_rel) # Determine the target paths list for the core_logic signature if target_rel: target_abs_path_str = str(tmp_path / target_rel) target_paths_list = [target_abs_path_str] else: # If no specific target, the project root itself is the target target_paths_list = [project_root_abs_path] content = read_context( target_paths_str=target_paths_list, # Pass the list of target paths project_root_str=project_root_abs_path, # Pass the optional project root for output relativity override_rules=override_rules, list_only=list_only, size_limit_mb=size_limit_mb, debug_explain=debug_explain, include_size_in_list=include_size_in_list # Pass through ) # Normalize line endings and strip leading/trailing whitespace for comparison # For list_only, sort the lines if list_only: lines = sorted([line.rstrip() for line in content.splitlines() if line.strip()]) return "\n".join(lines) else: # For content, just normalize line endings and strip outer whitespace normalized_content = "\n".join(line.rstrip() for line in content.splitlines()).strip() return normalized_content # --- Test Cases --- def test_read_context_no_rules_defaults(test_dir: Path): """Test processing with no rules - relies on default exclusions.""" content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None # Check for files expected to be included (not excluded by defaults) assert "```path=README.md" in content assert "```path=main.py" in content assert "```path=config.yaml" in content assert "```path=src/app.py" in content assert "```path=src/utils.py" in content assert "```path=src/data.json" in content assert "```path=src/nested/deep.py" in content assert "```path=src/nested/other.txt" in content assert "```path=docs/index.md" in content assert "```path=docs/config/options.md" in content assert "```path=temp.tmp" not in content # Now excluded by default !*.tmp rule # Check for files/dirs expected to be excluded by defaults or type assert "```path=.env" not in content # Excluded by !.* assert "```path=image.jpg" not in content # Binary # assert "```path=lib/somelib.py" in content # Included by '*' default, not excluded by others - Assertion added below assert "```path=lib/binary.dll" not in content # Binary assert "```path=src/.hidden_in_src" not in content # Excluded by !.* assert "```path=build/output.bin" not in content # Excluded by !build/ assert "```path=main_link.py" not in content # Symlink def test_read_context_list_only_defaults(test_dir: Path): """Test list_only mode with default exclusions.""" content = run_read_context_helper("project", test_dir.parent, list_only=True) # Root is project, target is None expected_files = sorted([ "README.md", "main.py", "config.yaml", # "temp.tmp", # Now excluded by default !*.tmp rule "src/app.py", "src/utils.py", "src/data.json", "src/nested/deep.py", "src/nested/other.txt", "lib/somelib.py", # Included by '*' default "docs/index.md", "docs/config/options.md", ]) assert content == "\n".join(expected_files) def test_read_context_include_py_files(test_dir: Path): """Test including only Python files using a context file.""" (test_dir / CONTEXT_FILENAME).write_text("**/*.py", encoding='utf-8') content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=main.py" in content assert "```path=src/app.py" in content assert "```path=src/utils.py" in content assert "```path=src/nested/deep.py" in content assert "```path=lib/somelib.py" in content # Now included because of rule assert "```path=README.md" in content # Included by default '*' assert "```path=config.yaml" in content # Included by default '*' assert "```path=src/data.json" in content # Included by default '*' def test_read_context_exclude_overrides_include(test_dir: Path): """Test exclusion pattern overriding inclusion in the same file.""" (test_dir / CONTEXT_FILENAME).write_text("**/*.py\n!src/utils.py", encoding='utf-8') # Corrected path content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=main.py" in content assert "```path=src/app.py" in content assert "```path=src/nested/deep.py" in content assert "```path=lib/somelib.py" in content assert "```path=src/utils.py" not in content # Excluded def test_read_context_directory_exclusion(test_dir: Path): """Test excluding a directory prevents processing files within.""" (test_dir / CONTEXT_FILENAME).write_text("**/*.py\n!lib/", encoding='utf-8') # Corrected path content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=main.py" in content assert "```path=src/app.py" in content assert "```path=lib/somelib.py" not in content # Excluded via directory rule def test_read_context_hierarchy_sub_includes(test_dir: Path): """Test sub .contextfiles including files not matched by root.""" (test_dir / CONTEXT_FILENAME).write_text("project/*.md", encoding='utf-8') # Root includes only root md (test_dir / "src" / CONTEXT_FILENAME).write_text("*.json", encoding='utf-8') # Src includes json content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=README.md" in content assert "```path=docs/index.md" in content # Matched by root rule *.md assert "```path=src/data.json" in content # Included by sub rule *.json assert "```path=main.py" in content # Included by default '*' assert "```path=src/app.py" in content # Included by default '*' def test_read_context_hierarchy_sub_excludes(test_dir: Path): """Test sub .contextfiles excluding files matched by root.""" (test_dir / CONTEXT_FILENAME).write_text("**/*.py", encoding='utf-8') # Root includes all py (test_dir / "src" / CONTEXT_FILENAME).write_text("!app.py", encoding='utf-8') # Src excludes app.py content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=main.py" in content assert "```path=src/utils.py" in content # Included by root, not excluded by sub assert "```path=src/nested/deep.py" in content # Included by root assert "```path=lib/somelib.py" in content # Included by root assert "```path=src/app.py" not in content # Excluded by sub rule def test_read_context_override_rules(test_dir: Path): """Test using override rules as additions to context files.""" (test_dir / CONTEXT_FILENAME).write_text("**/*.py", encoding='utf-8') # File includes py override = ["src/app.py", "**/*.md", "**/*.py", "src/"] # Use recursive glob for markdown content = run_read_context_helper("project", test_dir.parent, override_rules=override) # Root is project, target is None assert "```path=src/app.py" in content # Included by override assert "```path=README.md" in content # Included by override assert "```path=docs/index.md" in content # Included by override assert "```path=docs/config/options.md" in content # Included by override # These are included by contextfiles and/or overrides assert "```path=main.py" in content # Included by **/*.py assert "```path=src/utils.py" in content # Included by **/*.py # config.yaml is included by default '*' rule (not excluded by any rule) assert "```path=config.yaml" in content def test_read_context_binary_skip(test_dir: Path): """Test binary files are skipped even if rules include them.""" (test_dir / CONTEXT_FILENAME).write_text("*", encoding='utf-8') # Include everything content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=image.jpg" not in content assert "```path=lib/binary.dll" not in content assert "```path=main.py" in content # Text file still included def test_read_context_symlink_skip(test_dir: Path): """Test symlinks are skipped.""" symlink_path = test_dir / "main_link.py" if not symlink_path.exists(): pytest.skip("Symlink does not exist, skipping test.") (test_dir / CONTEXT_FILENAME).write_text("*.py", encoding='utf-8') content = run_read_context_helper("project", test_dir.parent) # Root is project, target is None assert "```path=main.py" in content assert "```path=main_link.py" not in content def test_read_context_size_limit_exceeded(test_dir: Path): """Test exceeding size limit raises error.""" (test_dir / CONTEXT_FILENAME).write_text("**/*.py", encoding='utf-8') limit_mb = 0.0001 # ~100 bytes with pytest.raises(DetailedContextSizeError): run_read_context_helper("project", test_dir.parent, size_limit_mb=limit_mb) # Root is project, target is None def test_read_context_target_file(test_dir: Path): """Test processing a specific target file within the project root.""" (test_dir / CONTEXT_FILENAME).write_text("!**/*.py", encoding='utf-8') # Exclude all py # Target main.py directly, root is project content = run_read_context_helper(project_root_rel="project", tmp_path=test_dir.parent, target_rel="project/main.py") # Rule doesn't apply to target file itself, only binary/size checks assert "```path=main.py" in content # Path relative to project root assert "print('main')" in content assert "```path=src/app.py" not in content # Other files not included def test_read_context_target_dir(test_dir: Path): """Test processing a specific target directory within the project root.""" # Add a rule in the root context file that should apply when targeting src (test_dir / CONTEXT_FILENAME).write_text("!**/utils.py", encoding='utf-8') # Add a context file inside the target directory (test_dir / "src" / CONTEXT_FILENAME).write_text("!data.json", encoding='utf-8') # Exclude data.json locally # Target src directory directly, root is project content = run_read_context_helper(project_root_rel="project", tmp_path=test_dir.parent, target_rel="project/src") # Files inside should be processed with rules from project root downwards # since src is within the project. # Output paths are still relative to the original project root. assert "```path=src/app.py" in content # utils.py should be EXCLUDED because the root rule !**/utils.py applies assert "```path=src/utils.py" not in content # data.json should be EXCLUDED by the local src/.contextfiles assert "```path=src/data.json" not in content assert "```path=src/nested/deep.py" in content assert "```path=src/nested/other.txt" in content # Hidden file should still be excluded by default rules assert "```path=src/.hidden_in_src" not in content # Files outside target dir src/ should not be included assert "```path=main.py" not in content assert "```path=README.md" not in content # Test removed as core logic now handles a single effective target path # def test_read_context_multiple_targets(test_dir: Path): def test_read_context_target_file_relativity(test_dir: Path): """Test output path relativity when project_root is different from target's parent.""" (test_dir / CONTEXT_FILENAME).write_text("project/src/app.py", encoding='utf-8') # Set project root to project/, target is project/src/app.py content = run_read_context_helper( project_root_rel="project", tmp_path=test_dir.parent, target_rel="project/src/app.py" ) # Path in header should be relative to project/ assert "```path=src/app.py" in content assert "```path=app.py" not in content def test_read_context_target_outside_root_error(test_dir: Path): """Test providing a target outside the project_root raises ValueError.""" with pytest.raises(ValueError, match=r"Target path .* is outside the specified project root .*"): # Match full error run_read_context_helper( project_root_rel="project/src", # Root is src tmp_path=test_dir.parent, target_rel="project/main.py" # Target is main.py (outside src) ) def test_find_context_files_helper(test_dir: Path): """Test the _find_context_files_for_dir helper directly.""" root = test_dir src = test_dir / "src" nested = test_dir / "src" / "nested" docs = test_dir / "docs" (root / CONTEXT_FILENAME).touch() (src / CONTEXT_FILENAME).touch() (nested / CONTEXT_FILENAME).touch() # No context file in docs # Check from nested files = _find_context_files_for_dir(nested, root) assert files == [ root / CONTEXT_FILENAME, src / CONTEXT_FILENAME, nested / CONTEXT_FILENAME, ] # Check from src files = _find_context_files_for_dir(src, root) assert files == [ root / CONTEXT_FILENAME, src / CONTEXT_FILENAME, ] # Check from root files = _find_context_files_for_dir(root, root) assert files == [ root / CONTEXT_FILENAME, ] # Check from docs (should only find root) files = _find_context_files_for_dir(docs, root) assert files == [ root / CONTEXT_FILENAME, ] # Check outside root (should be empty) files = _find_context_files_for_dir(root.parent, root) assert files == [] def test_gitignore_respected_and_overridden(test_dir: Path): """Ensure .gitignore excludes files unless overridden by .contextfiles.""" (test_dir / ".gitignore").write_text("lib/\n", encoding="utf-8") # Without context override, lib/ should be excluded content = run_read_context_helper("project", test_dir.parent) assert "```path=lib/somelib.py" not in content # Add context file that re-includes lib/ (test_dir / CONTEXT_FILENAME).write_text("lib/\n", encoding="utf-8") content = run_read_context_helper("project", test_dir.parent) assert "```path=lib/somelib.py" in content def test_gitignore_directory_pattern_applies_to_subdirs(test_dir: Path): """Test that a directory pattern like 'dirtobeignored/' in top-level .gitignore applies to subdirectories.""" # Create the directory structure subdir = test_dir / "subdir" subdir.mkdir() ignored_in_root = test_dir / "dirtobeignored" ignored_in_root.mkdir() ignored_in_subdir = subdir / "dirtobeignored" ignored_in_subdir.mkdir() # Create test files (ignored_in_root / "file.txt").write_text("Should be ignored", encoding="utf-8") (ignored_in_subdir / "file.txt").write_text("Should also be ignored", encoding="utf-8") (subdir / "included.txt").write_text("Should be included", encoding="utf-8") # Create .gitignore with directory pattern (test_dir / ".gitignore").write_text("dirtobeignored/\n", encoding="utf-8") # Run jinni and check results content = run_read_context_helper("project", test_dir.parent) # Files in dirtobeignored/ at any level should be excluded assert "```path=dirtobeignored/file.txt" not in content assert "```path=subdir/dirtobeignored/file.txt" not in content # But other files should be included assert "```path=subdir/included.txt" in content # Test removed as project_root is now mandatory # def test_read_context_project_root_default(test_dir: Path): def test_read_context_project_root_invalid(test_dir: Path): """Test providing an invalid project_root raises ValueError.""" # Non-existent path with pytest.raises(ValueError, match="does not exist or is not a directory"): run_read_context_helper( project_root_rel="project/nonexistent", # Non-existent path tmp_path=test_dir.parent, target_rel=None # Target defaults to root ) # Path is a file, not a directory with pytest.raises(ValueError, match="does not exist or is not a directory"): run_read_context_helper( project_root_rel="project/main.py", # Invalid root (is a file) tmp_path=test_dir.parent, target_rel=None # Target defaults to root ) def test_read_context_target_nonexistent(test_dir: Path): """Test providing a non-existent target raises FileNotFoundError.""" with pytest.raises(FileNotFoundError, match=r"Target path does not exist: .* \(resolved to .*\)"): # Revert to single backslash escape run_read_context_helper( project_root_rel="project", tmp_path=test_dir.parent, target_rel="project/nonexistent.txt" # Non-existent target ) # --- Test ensure_no_nul utility --- def test_ensure_no_nul_utils(): # Should not raise ensure_no_nul("abc", "test-field") # Should raise ValueError on NUL import pytest with pytest.raises(ValueError): ensure_no_nul("a\x00b", "test-field")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/smat-dev/jinni'

If you have feedback or need assistance with the MCP directory API, please join our Discord server