mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
1045 lines
38 KiB
Python
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
|