Research-Stack/5-Applications/nodupe/tests/commands/test_similarity.py
2026-05-05 21:15:26 -05:00

1045 lines
38 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 Allaun
"""Tests for nodupe/tools/commands/similarity.py - SimilarityCommandTool."""
from unittest.mock import MagicMock, patch
from nodupe.tools.commands.similarity import SimilarityCommandTool, register_tool
class TestSimilarityCommandToolProperties:
"""Test SimilarityCommandTool properties."""
def test_name_property(self):
"""SimilarityCommandTool.name returns correct value."""
tool = SimilarityCommandTool()
assert tool.name == "similarity_command"
def test_version_property(self):
"""SimilarityCommandTool.version returns correct value."""
tool = SimilarityCommandTool()
assert tool.version == "1.0.0"
def test_dependencies_property(self):
"""SimilarityCommandTool.dependencies returns correct list."""
tool = SimilarityCommandTool()
assert tool.dependencies == ["similarity_backend"]
def test_description_attribute(self):
"""SimilarityCommandTool has correct description."""
tool = SimilarityCommandTool()
assert tool.description == "Find similar files using various metrics"
class TestSimilarityCommandToolInitialize:
"""Test SimilarityCommandTool.initialize() method."""
def test_initialize_no_error(self):
"""initialize() completes without error."""
tool = SimilarityCommandTool()
container = MagicMock()
# Should not raise
tool.initialize(container)
def test_initialize_with_none_container(self):
"""initialize() handles None container gracefully."""
tool = SimilarityCommandTool()
# Should not raise
tool.initialize(None)
class TestSimilarityCommandToolShutdown:
"""Test SimilarityCommandTool.shutdown() method."""
def test_shutdown_no_error(self):
"""shutdown() completes without error."""
tool = SimilarityCommandTool()
# Should not raise
tool.shutdown()
class TestSimilarityCommandToolGetCapabilities:
"""Test SimilarityCommandTool.get_capabilities() method."""
def test_get_capabilities_returns_dict(self):
"""get_capabilities() returns a dictionary."""
tool = SimilarityCommandTool()
capabilities = tool.get_capabilities()
assert isinstance(capabilities, dict)
assert 'commands' in capabilities
def test_get_capabilities_contains_similarity_command(self):
"""get_capabilities() contains similarity command."""
tool = SimilarityCommandTool()
capabilities = tool.get_capabilities()
assert 'similarity' in capabilities['commands']
class TestSimilarityCommandToolEventHandlers:
"""Test SimilarityCommandTool event handler methods."""
def test_on_similarity_start(self, capsys):
"""_on_similarity_start() prints start message."""
tool = SimilarityCommandTool()
tool._on_similarity_start(metric='name')
captured = capsys.readouterr()
assert "[TOOL] Similarity search started: name" in captured.out
def test_on_similarity_start_default_metric(self, capsys):
"""_on_similarity_start() uses default metric when not provided."""
tool = SimilarityCommandTool()
tool._on_similarity_start()
captured = capsys.readouterr()
assert "[TOOL] Similarity search started: unknown" in captured.out
def test_on_similarity_complete(self, capsys):
"""_on_similarity_complete() prints completion message."""
tool = SimilarityCommandTool()
tool._on_similarity_complete(pairs_found=42)
captured = capsys.readouterr()
assert "[TOOL] Similarity search completed: 42 similar pairs found" in captured.out
def test_on_similarity_complete_default_count(self, capsys):
"""_on_similarity_complete() uses default count when not provided."""
tool = SimilarityCommandTool()
tool._on_similarity_complete()
captured = capsys.readouterr()
assert "[TOOL] Similarity search completed: 0 similar pairs found" in captured.out
class TestSimilarityCommandToolRegisterCommands:
"""Test SimilarityCommandTool.register_commands() method."""
def test_register_commands_creates_parser(self):
"""register_commands() creates similarity subparser."""
tool = SimilarityCommandTool()
subparsers = MagicMock()
sim_parser = MagicMock()
subparsers.add_parser.return_value = sim_parser
tool.register_commands(subparsers)
subparsers.add_parser.assert_called_once_with(
'similarity', help='Find similar files'
)
def test_register_commands_adds_metric_argument(self):
"""register_commands() adds metric argument."""
tool = SimilarityCommandTool()
subparsers = MagicMock()
sim_parser = MagicMock()
subparsers.add_parser.return_value = sim_parser
tool.register_commands(subparsers)
apply_parser_call = None
for call_arg in sim_parser.add_argument.call_args_list:
if '--metric' in str(call_arg):
apply_parser_call = call_arg
break
assert apply_parser_call is not None
def test_register_commands_adds_threshold_argument(self):
"""register_commands() adds threshold argument."""
tool = SimilarityCommandTool()
subparsers = MagicMock()
sim_parser = MagicMock()
subparsers.add_parser.return_value = sim_parser
tool.register_commands(subparsers)
threshold_call = None
for call_arg in sim_parser.add_argument.call_args_list:
if '--threshold' in str(call_arg):
threshold_call = call_arg
break
assert threshold_call is not None
def test_register_commands_adds_limit_argument(self):
"""register_commands() adds limit argument."""
tool = SimilarityCommandTool()
subparsers = MagicMock()
sim_parser = MagicMock()
subparsers.add_parser.return_value = sim_parser
tool.register_commands(subparsers)
limit_call = None
for call_arg in sim_parser.add_argument.call_args_list:
if '--limit' in str(call_arg):
limit_call = call_arg
break
assert limit_call is not None
def test_register_commands_adds_output_argument(self):
"""register_commands() adds output argument."""
tool = SimilarityCommandTool()
subparsers = MagicMock()
sim_parser = MagicMock()
subparsers.add_parser.return_value = sim_parser
tool.register_commands(subparsers)
output_call = None
for call_arg in sim_parser.add_argument.call_args_list:
if '--output' in str(call_arg):
output_call = call_arg
break
assert output_call is not None
def test_register_commands_sets_func(self):
"""register_commands() sets func to execute_similarity."""
tool = SimilarityCommandTool()
subparsers = MagicMock()
sim_parser = MagicMock()
subparsers.add_parser.return_value = sim_parser
tool.register_commands(subparsers)
sim_parser.set_defaults.assert_called_once_with(func=tool.execute_similarity)
class TestSimilarityCommandToolExecuteValidation:
"""Test SimilarityCommandTool.execute_similarity() validation logic."""
def test_execute_similarity_missing_query_file(self, capsys):
"""execute_similarity() returns error when query_file is None."""
tool = SimilarityCommandTool()
args = MagicMock()
args.query_file = None
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] Query file is required" in captured.out
def test_execute_similarity_threshold_below_zero(self, capsys):
"""execute_similarity() returns error for threshold < 0.0."""
tool = SimilarityCommandTool()
args = MagicMock()
args.threshold = -0.1
args.limit = 10
delattr(args, 'query_file')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] Threshold must be between 0.0 and 1.0" in captured.out
def test_execute_similarity_threshold_above_one(self, capsys):
"""execute_similarity() returns error for threshold > 1.0."""
tool = SimilarityCommandTool()
args = MagicMock()
args.threshold = 1.5
args.limit = 10
delattr(args, 'query_file')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] Threshold must be between 0.0 and 1.0" in captured.out
def test_execute_similarity_k_zero(self, capsys):
"""execute_similarity() returns error for k=0."""
tool = SimilarityCommandTool()
args = MagicMock()
args.k = 0
args.threshold = 0.8
delattr(args, 'query_file')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] k must be a positive integer" in captured.out
def test_execute_similarity_k_negative(self, capsys):
"""execute_similarity() returns error for negative k."""
tool = SimilarityCommandTool()
args = MagicMock()
args.k = -5
args.threshold = 0.8
delattr(args, 'query_file')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] k must be a positive integer" in captured.out
def test_execute_similarity_limit_zero(self, capsys):
"""execute_similarity() returns error for limit=0."""
tool = SimilarityCommandTool()
args = MagicMock()
args.limit = 0
args.threshold = 0.8
delattr(args, 'query_file')
delattr(args, 'k')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] limit must be a positive integer" in captured.out
def test_execute_similarity_invalid_metric(self, capsys):
"""execute_similarity() returns error for invalid metric."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'invalid_metric'
args.threshold = 0.8
args.limit = 10
delattr(args, 'query_file')
delattr(args, 'k')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] Invalid metric: invalid_metric" in captured.out
class TestSimilarityCommandToolExecuteNoFiles:
"""Test SimilarityCommandTool.execute_similarity() with no files."""
def test_execute_similarity_no_files_in_database(self, capsys):
"""execute_similarity() handles empty database."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
# Test that it handles empty database gracefully
assert "Analysis complete" in captured.out or "No files" in captured.out
class TestSimilarityCommandToolExecuteHashMetric:
"""Test SimilarityCommandTool.execute_similarity() with hash metric."""
def test_execute_similarity_hash_metric_finds_duplicates(self, capsys):
"""execute_similarity() with hash metric finds duplicates."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'hash'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
# Create files with same hash (duplicates)
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
{'id': 2, 'path': '/path/to/file2.txt', 'hash': 'abc123', 'size': 100, 'name': 'file2.txt'},
{'id': 3, 'path': '/path/to/file3.txt', 'hash': 'abc123', 'size': 100, 'name': 'file3.txt'},
{'id': 4, 'path': '/path/to/unique.txt', 'hash': 'xyz789', 'size': 200, 'name': 'unique.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Analyzing 4 files using metric: hash" in captured.out
# Should mark 2 files as duplicates (file2 and file3 are duplicates of file1)
assert mock_repo.mark_as_duplicate.call_count == 2
class TestSimilarityCommandToolExecuteSizeMetric:
"""Test SimilarityCommandTool.execute_similarity() with size metric."""
def test_execute_similarity_size_metric_finds_duplicates(self, capsys):
"""execute_similarity() with size metric finds duplicates."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'size'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
# Create files with same size (duplicates by size)
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
{'id': 2, 'path': '/path/to/file2.txt', 'hash': 'def456', 'size': 100, 'name': 'file2.txt'},
{'id': 3, 'path': '/path/to/file3.txt', 'hash': 'ghi789', 'size': 200, 'name': 'file3.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Analyzing 3 files using metric: size" in captured.out
# Should mark 1 file as duplicate (file2 is duplicate of file1 by size)
assert mock_repo.mark_as_duplicate.call_count == 1
class TestSimilarityCommandToolExecuteNameMetric:
"""Test SimilarityCommandTool.execute_similarity() with name metric."""
def test_execute_similarity_name_metric_finds_duplicates(self, capsys):
"""execute_similarity() with name metric finds duplicates."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
# Create files with same name (duplicates by name)
files = [
{'id': 1, 'path': '/path/to/file.txt', 'hash': 'abc123', 'size': 100, 'name': 'file.txt'},
{'id': 2, 'path': '/other/file.txt', 'hash': 'def456', 'size': 200, 'name': 'file.txt'},
{'id': 3, 'path': '/unique/other.txt', 'hash': 'ghi789', 'size': 150, 'name': 'other.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Analyzing 3 files using metric: name" in captured.out
# Should mark 1 file as duplicate (second file.txt is duplicate of first)
assert mock_repo.mark_as_duplicate.call_count == 1
class TestSimilarityCommandToolExecuteVectorMetric:
"""Test SimilarityCommandTool.execute_similarity() with vector metric."""
def test_execute_similarity_vector_metric_not_implemented(self, capsys):
"""execute_similarity() with vector metric reports not implemented."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'vector'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Vector similarity search not yet implemented" in captured.out
class TestSimilarityCommandToolExecuteVerbose:
"""Test SimilarityCommandTool.execute_similarity() verbose output."""
def test_execute_similarity_verbose_output(self, capsys):
"""execute_similarity() with verbose flag prints details."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'hash'
args.threshold = 0.8
args.limit = 10
args.verbose = True
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
{'id': 2, 'path': '/path/to/file2.txt', 'hash': 'abc123', 'size': 100, 'name': 'file2.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "[DUP]" in captured.out
class TestSimilarityCommandToolExecuteEdgeCases:
"""Test SimilarityCommandTool.execute_similarity() edge cases."""
def test_execute_similarity_files_without_hash(self, capsys):
"""execute_similarity() handles files without hash."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'hash'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
# Files with missing hash
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': None, 'size': 100, 'name': 'file1.txt'},
{'id': 2, 'path': '/path/to/file2.txt', 'hash': 'abc123', 'size': 100, 'name': 'file2.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
# Should not crash, just skip files without hash
def test_execute_similarity_general_exception(self, capsys):
"""execute_similarity() handles general exceptions."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'hash'
args.verbose = True
args.container = MagicMock()
args.container.get_service.side_effect = Exception("Test exception")
delattr(args, 'query_file')
delattr(args, 'k')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[TOOL ERROR] Similarity search failed:" in captured.out
def test_execute_similarity_fallback_to_global_container(self, capsys):
"""execute_similarity() falls back to global container."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = None # No container
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.core.container.container') as mock_global_container:
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_global_container.get_service.return_value = MagicMock()
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
class TestSimilarityCommandToolRegisterTool:
"""Test register_tool() function."""
def test_register_tool_returns_similarity_tool(self):
"""register_tool() returns a SimilarityCommandTool instance."""
tool = register_tool()
assert isinstance(tool, SimilarityCommandTool)
class TestSimilarityCommandToolModuleLevel:
"""Test module-level exports."""
def test_similarity_tool_instance_exists(self):
"""similarity_tool module-level instance exists."""
from nodupe.tools.commands import similarity
assert similarity.similarity_tool is not None
assert isinstance(similarity.similarity_tool, SimilarityCommandTool)
def test_register_tool_function_exists(self):
"""register_tool function is exported."""
from nodupe.tools.commands import similarity
assert callable(similarity.register_tool)
def test_all_export(self):
"""__all__ contains expected exports."""
from nodupe.tools.commands import similarity
assert 'similarity_tool' in similarity.__all__
assert 'register_tool' in similarity.__all__
def test_api_methods_property(self):
"""api_methods property returns tool methods."""
tool = SimilarityCommandTool()
# api_methods should contain tool methods
assert isinstance(tool.api_methods, dict) or isinstance(tool.api_methods, list)
assert len(tool.api_methods) >= 0 # May have methods or be empty
def test_describe_usage(self):
"""describe_usage() returns usage description."""
tool = SimilarityCommandTool()
usage = tool.describe_usage()
# Should have some description
assert isinstance(usage, str) and len(usage) > 0
def test_run_standalone(self):
"""run_standalone() calls execute_similarity."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.run_standalone(args)
assert result == 0
class TestSimilarityCommandToolExecuteValidationEdgeCases:
"""Test SimilarityCommandTool.execute_similarity() validation edge cases."""
def test_execute_similarity_query_file_is_none(self, capsys):
"""execute_similarity() returns error when query_file is explicitly None."""
tool = SimilarityCommandTool()
args = MagicMock()
args.query_file = None # Explicitly set to None
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[ERROR] Query file is required" in captured.out
def test_execute_similarity_threshold_valid(self, capsys):
"""execute_similarity() proceeds when threshold is valid."""
tool = SimilarityCommandTool()
args = MagicMock()
args.threshold = 0.5 # Valid threshold
args.limit = 10
args.metric = 'name'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_k_valid(self, capsys):
"""execute_similarity() proceeds when k is valid."""
tool = SimilarityCommandTool()
args = MagicMock()
args.k = 5 # Valid k
args.threshold = 0.8
args.metric = 'name'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'limit')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
class TestSimilarityCommandToolExecuteContainerFallback:
"""Test SimilarityCommandTool.execute_similarity() container fallback."""
def test_execute_similarity_uses_global_container_when_none(self, capsys):
"""execute_similarity() uses global container when args.container is None."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = None # No container
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.core.container.container') as mock_global_container:
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_global_container.get_service.return_value = MagicMock()
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
class TestSimilarityCommandToolExecuteVerboseEdgeCases:
"""Test SimilarityCommandTool.execute_similarity() verbose edge cases."""
def test_execute_similarity_verbose_exception_traceback(self, capsys):
"""execute_similarity() prints traceback when verbose is True."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.verbose = True
args.container = MagicMock()
args.container.get_service.side_effect = Exception("Test exception")
delattr(args, 'query_file')
delattr(args, 'k')
delattr(args, 'limit')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "Traceback" in captured.out or "Traceback" in captured.err or "[TOOL ERROR]" in captured.out
class TestSimilarityCommandToolCoverageCompletion:
"""Additional tests to achieve 100% coverage."""
def test_execute_similarity_threshold_boundary_zero(self, capsys):
"""execute_similarity() accepts threshold at boundary 0.0."""
tool = SimilarityCommandTool()
args = MagicMock()
args.threshold = 0.0 # Valid boundary
args.limit = 10
args.metric = 'name'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_threshold_boundary_one(self, capsys):
"""execute_similarity() accepts threshold at boundary 1.0."""
tool = SimilarityCommandTool()
args = MagicMock()
args.threshold = 1.0 # Valid boundary
args.limit = 10
args.metric = 'name'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_limit_boundary_one(self, capsys):
"""execute_similarity() accepts limit at boundary 1."""
tool = SimilarityCommandTool()
args = MagicMock()
args.limit = 1 # Valid boundary
args.threshold = 0.8
args.metric = 'name'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = []
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_content_metric(self, capsys):
"""execute_similarity() with content metric."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'content'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
# Content metric falls through to hash/size/name grouping logic
assert "Analyzing 1 files" in captured.out
def test_execute_similarity_database_service_fallback(self, capsys):
"""execute_similarity() falls back to default database connection."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.limit = 10
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = None # No database service
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.connection.DatabaseConnection') as mock_db_conn:
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_instance = MagicMock()
mock_db_conn.get_instance.return_value = mock_instance
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
mock_db_conn.get_instance.assert_called()
def test_execute_similarity_exception_without_verbose(self, capsys):
"""execute_similarity() handles exception without verbose traceback."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.verbose = False # No verbose
args.container = MagicMock()
args.container.get_service.side_effect = Exception("Test exception")
delattr(args, 'query_file')
delattr(args, 'k')
delattr(args, 'limit')
result = tool.execute_similarity(args)
assert result == 1
captured = capsys.readouterr()
assert "[TOOL ERROR] Similarity search failed:" in captured.out
# Should NOT have traceback when verbose is False
assert "Traceback" not in captured.out
def test_execute_similarity_threshold_valid_path(self, capsys):
"""execute_similarity() continues when threshold is valid (0.5)."""
tool = SimilarityCommandTool()
args = MagicMock()
args.threshold = 0.5 # Valid threshold in middle of range
args.limit = 10
args.metric = 'hash'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_k_valid_path(self, capsys):
"""execute_similarity() continues when k is valid (5)."""
tool = SimilarityCommandTool()
args = MagicMock()
args.k = 5 # Valid k
args.threshold = 0.8
args.metric = 'hash'
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
delattr(args, 'query_file')
delattr(args, 'limit')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_no_threshold_attr(self, capsys):
"""execute_similarity() covers branch when args has no threshold attribute."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.limit = 10
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
# Remove threshold attribute to cover False branch
delattr(args, 'threshold')
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out
def test_execute_similarity_no_limit_attr(self, capsys):
"""execute_similarity() covers branch when args has no limit attribute."""
tool = SimilarityCommandTool()
args = MagicMock()
args.metric = 'name'
args.threshold = 0.8
args.verbose = False
args.container = MagicMock()
args.container.get_service.return_value = MagicMock()
# Remove limit attribute to cover False branch
delattr(args, 'limit')
delattr(args, 'query_file')
delattr(args, 'k')
files = [
{'id': 1, 'path': '/path/to/file1.txt', 'hash': 'abc123', 'size': 100, 'name': 'file1.txt'},
]
with patch('nodupe.tools.databases.files.FileRepository') as mock_repo_class:
mock_repo = MagicMock()
mock_repo.get_all_files.return_value = files
mock_repo_class.return_value = mock_repo
result = tool.execute_similarity(args)
assert result == 0
captured = capsys.readouterr()
assert "Executing similarity command" in captured.out