Files
MyObsidianAI/tests/test_cli.py
Kodjo Sossouvi d4925f7969 Initial commit
2025-12-12 11:31:44 +01:00

537 lines
13 KiB
Python

"""
Unit tests for the CLI module.
"""
import pytest
from pathlib import Path
from typer.testing import CliRunner
from obsidian_rag.cli import app, _display_index_results, _display_results_compact
from obsidian_rag.indexer import index_vault
from obsidian_rag.searcher import SearchResult
runner = CliRunner()
@pytest.fixture
def temp_vault(tmp_path):
"""
Create a temporary vault with sample markdown files.
"""
vault_path = tmp_path / "test_vault"
vault_path.mkdir()
# Create sample files
file1 = vault_path / "python.md"
file1.write_text("""# Python Programming
Python is a high-level programming language.
## Features
Python has dynamic typing and automatic memory management.
""")
file2 = vault_path / "javascript.md"
file2.write_text("""# JavaScript
JavaScript is a scripting language for web development.
## Usage
JavaScript runs in web browsers and Node.js environments.
""")
file3 = vault_path / "cooking.md"
file3.write_text("""# Cooking Tips
Learn how to cook delicious meals.
## Basics
Start with simple recipes and basic techniques.
""")
return vault_path
# Tests for 'index' command - Passing tests
def test_i_can_index_vault_successfully(temp_vault, tmp_path):
"""
Test that we can index a vault successfully.
"""
chroma_path = tmp_path / "chroma_db"
result = runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
assert result.exit_code == 0
assert "Found 3 markdown files to index" in result.stdout
assert "Indexing completed" in result.stdout
assert "Files processed:" in result.stdout
assert "Chunks created:" in result.stdout
def test_i_can_index_with_custom_chroma_path(temp_vault, tmp_path):
"""
Test that we can specify a custom ChromaDB path.
"""
custom_chroma = tmp_path / "my_custom_db"
result = runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(custom_chroma),
])
assert result.exit_code == 0
assert custom_chroma.exists()
assert (custom_chroma / "chroma.sqlite3").exists()
def test_i_can_index_with_custom_collection_name(temp_vault, tmp_path):
"""
Test that we can use a custom collection name.
"""
chroma_path = tmp_path / "chroma_db"
collection_name = "my_custom_collection"
result = runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
"--collection", collection_name,
])
assert result.exit_code == 0
assert f"Collection: {collection_name}" in result.stdout
def test_i_can_see_errors_in_index_results(tmp_path):
"""
Test that errors during indexing are displayed.
"""
vault_path = tmp_path / "vault_with_errors"
vault_path.mkdir()
# Create a valid file
valid_file = vault_path / "valid.md"
valid_file.write_text("# Valid File\n\nThis is valid content.")
# Create an invalid file (will cause parsing error)
invalid_file = vault_path / "invalid.md"
invalid_file.write_bytes(b"\xff\xfe\x00\x00") # Invalid UTF-8
chroma_path = tmp_path / "chroma_db"
result = runner.invoke(app, [
"index",
str(vault_path),
"--chroma-path", str(chroma_path),
])
# Should still complete (exit code 0) but show errors
assert result.exit_code == 0
assert "Indexing completed" in result.stdout
# Note: Error display might vary, just check it completed
# Tests for 'index' command - Error tests
def test_i_cannot_index_nonexistent_vault(tmp_path):
"""
Test that indexing a nonexistent vault fails with clear error.
"""
nonexistent_path = tmp_path / "does_not_exist"
chroma_path = tmp_path / "chroma_db"
result = runner.invoke(app, [
"index",
str(nonexistent_path),
"--chroma-path", str(chroma_path),
])
assert result.exit_code == 1
assert "does not exist" in result.stdout
def test_i_cannot_index_file_instead_of_directory(tmp_path):
"""
Test that indexing a file (not directory) fails.
"""
file_path = tmp_path / "somefile.txt"
file_path.write_text("I am a file")
chroma_path = tmp_path / "chroma_db"
result = runner.invoke(app, [
"index",
str(file_path),
"--chroma-path", str(chroma_path),
])
assert result.exit_code == 1
assert "not a directory" in result.stdout
def test_i_can_handle_empty_vault_gracefully(tmp_path):
"""
Test that an empty vault (no .md files) is handled gracefully.
"""
empty_vault = tmp_path / "empty_vault"
empty_vault.mkdir()
# Create a non-markdown file
(empty_vault / "readme.txt").write_text("Not a markdown file")
chroma_path = tmp_path / "chroma_db"
result = runner.invoke(app, [
"index",
str(empty_vault),
"--chroma-path", str(chroma_path),
])
assert result.exit_code == 0
assert "No markdown files found" in result.stdout
# Tests for 'search' command - Passing tests
def test_i_can_search_indexed_vault(temp_vault, tmp_path):
"""
Test that we can search an indexed vault.
"""
chroma_path = tmp_path / "chroma_db"
# First, index the vault
index_result = runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
assert index_result.exit_code == 0
# Then search
search_result = runner.invoke(app, [
"search",
"Python programming",
"--chroma-path", str(chroma_path),
])
assert search_result.exit_code == 0
assert "Found" in search_result.stdout
assert "result(s) for:" in search_result.stdout
assert "python.md" in search_result.stdout
def test_i_can_search_with_limit_option(temp_vault, tmp_path):
"""
Test that the --limit option works.
"""
chroma_path = tmp_path / "chroma_db"
# Index
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
# Search with limit
result = runner.invoke(app, [
"search",
"programming",
"--chroma-path", str(chroma_path),
"--limit", "2",
])
assert result.exit_code == 0
# Count result numbers (1., 2., etc.)
result_count = result.stdout.count("[bold cyan]")
assert result_count <= 2
def test_i_can_search_with_min_score_option(temp_vault, tmp_path):
"""
Test that the --min-score option works.
"""
chroma_path = tmp_path / "chroma_db"
# Index
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
# Search with high min-score
result = runner.invoke(app, [
"search",
"Python",
"--chroma-path", str(chroma_path),
"--min-score", "0.5",
])
assert result.exit_code == 0
# Should have results (Python file should match well)
assert "Found" in result.stdout
def test_i_can_search_with_custom_collection(temp_vault, tmp_path):
"""
Test that we can search in a custom collection.
"""
chroma_path = tmp_path / "chroma_db"
collection_name = "test_collection"
# Index with custom collection
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
"--collection", collection_name,
])
# Search in same collection
result = runner.invoke(app, [
"search",
"Python",
"--chroma-path", str(chroma_path),
"--collection", collection_name,
])
assert result.exit_code == 0
assert "Found" in result.stdout
def test_i_can_handle_no_results_gracefully(temp_vault, tmp_path):
"""
Test that no results scenario is handled gracefully.
"""
chroma_path = tmp_path / "chroma_db"
# Index
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
# Search for something unlikely with high threshold
result = runner.invoke(app, [
"search",
"quantum physics relativity",
"--chroma-path", str(chroma_path),
"--min-score", "0.95",
])
assert result.exit_code == 0
assert "No results found" in result.stdout
def test_i_can_use_compact_format(temp_vault, tmp_path):
"""
Test that compact format displays correctly.
"""
chroma_path = tmp_path / "chroma_db"
# Index
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
# Search with explicit compact format
result = runner.invoke(app, [
"search",
"Python",
"--chroma-path", str(chroma_path),
"--format", "compact",
])
assert result.exit_code == 0
# Check for compact format elements
assert "Section:" in result.stdout
assert "Lines:" in result.stdout
assert "score:" in result.stdout
# Tests for 'search' command - Error tests
def test_i_cannot_search_without_index(tmp_path):
"""
Test that searching without indexing fails with clear message.
"""
chroma_path = tmp_path / "nonexistent_chroma"
result = runner.invoke(app, [
"search",
"test query",
"--chroma-path", str(chroma_path),
])
assert result.exit_code == 1
assert "not found" in result.stdout
assert "index" in result.stdout.lower()
def test_i_cannot_search_nonexistent_collection(temp_vault, tmp_path):
"""
Test that searching in a nonexistent collection fails.
"""
chroma_path = tmp_path / "chroma_db"
# Index with default collection
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
# Search in different collection
result = runner.invoke(app, [
"search",
"Python",
"--chroma-path", str(chroma_path),
"--collection", "nonexistent_collection",
])
assert result.exit_code == 1
assert "not found" in result.stdout
def test_i_cannot_use_invalid_format(temp_vault, tmp_path):
"""
Test that an invalid format is rejected.
"""
chroma_path = tmp_path / "chroma_db"
# Index
runner.invoke(app, [
"index",
str(temp_vault),
"--chroma-path", str(chroma_path),
])
# Search with invalid format
result = runner.invoke(app, [
"search",
"Python",
"--chroma-path", str(chroma_path),
"--format", "invalid_format",
])
assert result.exit_code == 1
assert "Invalid format" in result.stdout
assert "compact" in result.stdout
# Tests for helper functions
def test_i_can_display_index_results(capsys):
"""
Test that index results are displayed correctly.
"""
stats = {
"files_processed": 10,
"chunks_created": 50,
"collection_name": "test_collection",
"errors": [],
}
_display_index_results(stats)
captured = capsys.readouterr()
assert "Indexing completed" in captured.out
assert "10" in captured.out
assert "50" in captured.out
assert "test_collection" in captured.out
def test_i_can_display_index_results_with_errors(capsys):
"""
Test that index results with errors are displayed correctly.
"""
stats = {
"files_processed": 8,
"chunks_created": 40,
"collection_name": "test_collection",
"errors": [
{"file": "broken.md", "error": "Invalid encoding"},
{"file": "corrupt.md", "error": "Parse error"},
],
}
_display_index_results(stats)
captured = capsys.readouterr()
assert "Indexing completed" in captured.out
assert "2 file(s) skipped" in captured.out
assert "broken.md" in captured.out
assert "Invalid encoding" in captured.out
def test_i_can_display_results_compact(capsys):
"""
Test that compact results display correctly.
"""
results = [
SearchResult(
file_path="notes/python.md",
section_title="Introduction",
line_start=1,
line_end=5,
score=0.87,
text="Python is a high-level programming language.",
),
SearchResult(
file_path="notes/javascript.md",
section_title="Overview",
line_start=10,
line_end=15,
score=0.65,
text="JavaScript is used for web development.",
),
]
_display_results_compact(results)
captured = capsys.readouterr()
assert "python.md" in captured.out
assert "javascript.md" in captured.out
assert "0.87" in captured.out
assert "0.65" in captured.out
assert "Introduction" in captured.out
assert "Overview" in captured.out
def test_i_can_display_results_compact_with_long_text(capsys):
"""
Test that long text is truncated in compact display.
"""
long_text = "A" * 300 # Text longer than 200 characters
results = [
SearchResult(
file_path="notes/long.md",
section_title="Long Section",
line_start=1,
line_end=10,
score=0.75,
text=long_text,
),
]
_display_results_compact(results)
captured = capsys.readouterr()
assert "..." in captured.out # Should be truncated
assert len([line for line in captured.out.split('\n') if 'A' * 200 in line]) == 0 # Full text not shown