test_workflow_file_embedding.pyโข10.2 kB
"""
Unit tests for workflow file embedding behavior
Tests the critical file embedding logic for workflow tools:
- Intermediate steps: Only reference file names (save Claude's context)
- Final steps: Embed full file content for expert analysis
"""
import os
import tempfile
from unittest.mock import Mock, patch
import pytest
from tools.workflow.workflow_mixin import BaseWorkflowMixin
class TestWorkflowFileEmbedding:
"""Test workflow file embedding behavior"""
def setup_method(self):
"""Set up test fixtures"""
# Create a mock workflow tool
self.mock_tool = Mock()
self.mock_tool.get_name.return_value = "test_workflow"
# Bind the methods we want to test - use bound methods
self.mock_tool._should_embed_files_in_workflow_step = (
BaseWorkflowMixin._should_embed_files_in_workflow_step.__get__(self.mock_tool)
)
self.mock_tool._force_embed_files_for_expert_analysis = (
BaseWorkflowMixin._force_embed_files_for_expert_analysis.__get__(self.mock_tool)
)
# Create test files
self.test_files = []
for i in range(2):
fd, path = tempfile.mkstemp(suffix=f"_test_{i}.py")
with os.fdopen(fd, "w") as f:
f.write(f"# Test file {i}\nprint('hello world {i}')\n")
self.test_files.append(path)
def teardown_method(self):
"""Clean up test files"""
for file_path in self.test_files:
try:
os.unlink(file_path)
except OSError:
pass
def test_intermediate_step_no_embedding(self):
"""Test that intermediate steps only reference files, don't embed"""
# Intermediate step: step_number=1, next_step_required=True
step_number = 1
continuation_id = None # New conversation
is_final_step = False # next_step_required=True
should_embed = self.mock_tool._should_embed_files_in_workflow_step(step_number, continuation_id, is_final_step)
assert should_embed is False, "Intermediate steps should NOT embed files"
def test_intermediate_step_with_continuation_no_embedding(self):
"""Test that intermediate steps with continuation only reference files"""
# Intermediate step with continuation: step_number=2, next_step_required=True
step_number = 2
continuation_id = "test-thread-123" # Continuing conversation
is_final_step = False # next_step_required=True
should_embed = self.mock_tool._should_embed_files_in_workflow_step(step_number, continuation_id, is_final_step)
assert should_embed is False, "Intermediate steps with continuation should NOT embed files"
def test_final_step_embeds_files(self):
"""Test that final steps embed full file content for expert analysis"""
# Final step: any step_number, next_step_required=False
step_number = 3
continuation_id = "test-thread-123"
is_final_step = True # next_step_required=False
should_embed = self.mock_tool._should_embed_files_in_workflow_step(step_number, continuation_id, is_final_step)
assert should_embed is True, "Final steps SHOULD embed files for expert analysis"
def test_final_step_new_conversation_embeds_files(self):
"""Test that final steps in new conversations embed files"""
# Final step in new conversation (rare but possible): step_number=1, next_step_required=False
step_number = 1
continuation_id = None # New conversation
is_final_step = True # next_step_required=False (one-step workflow)
should_embed = self.mock_tool._should_embed_files_in_workflow_step(step_number, continuation_id, is_final_step)
assert should_embed is True, "Final steps in new conversations SHOULD embed files"
@patch("utils.file_utils.read_files")
@patch("utils.file_utils.expand_paths")
@patch("utils.conversation_memory.get_thread")
@patch("utils.conversation_memory.get_conversation_file_list")
def test_comprehensive_file_collection_for_expert_analysis(
self, mock_get_conversation_file_list, mock_get_thread, mock_expand_paths, mock_read_files
):
"""Test that expert analysis collects relevant files from current workflow and conversation history"""
# Setup test files for different sources
conversation_files = [self.test_files[0]] # relevant_files from conversation history
current_relevant_files = [
self.test_files[0],
self.test_files[1],
] # current step's relevant_files (overlap with conversation)
# Setup mocks
mock_thread_context = Mock()
mock_get_thread.return_value = mock_thread_context
mock_get_conversation_file_list.return_value = conversation_files
mock_expand_paths.return_value = self.test_files
mock_read_files.return_value = "# File content\nprint('test')"
# Mock model context for token allocation
mock_model_context = Mock()
mock_token_allocation = Mock()
mock_token_allocation.file_tokens = 100000
mock_model_context.calculate_token_allocation.return_value = mock_token_allocation
# Set up the tool methods and state
self.mock_tool.get_current_model_context.return_value = mock_model_context
self.mock_tool.wants_line_numbers_by_default.return_value = True
self.mock_tool.get_name.return_value = "test_workflow"
# Set up consolidated findings
self.mock_tool.consolidated_findings = Mock()
self.mock_tool.consolidated_findings.relevant_files = set(current_relevant_files)
# Set up current arguments with continuation
self.mock_tool._current_arguments = {"continuation_id": "test-thread-123"}
self.mock_tool.get_current_arguments.return_value = {"continuation_id": "test-thread-123"}
# Bind the method we want to test
self.mock_tool._prepare_files_for_expert_analysis = (
BaseWorkflowMixin._prepare_files_for_expert_analysis.__get__(self.mock_tool)
)
self.mock_tool._force_embed_files_for_expert_analysis = (
BaseWorkflowMixin._force_embed_files_for_expert_analysis.__get__(self.mock_tool)
)
# Call the method
file_content = self.mock_tool._prepare_files_for_expert_analysis()
# Verify it collected files from conversation history
mock_get_thread.assert_called_once_with("test-thread-123")
mock_get_conversation_file_list.assert_called_once_with(mock_thread_context)
# Verify it called read_files with ALL unique relevant files
# Should include files from: conversation_files + current_relevant_files
# But deduplicated: [test_files[0], test_files[1]] (unique set)
expected_unique_files = list(set(conversation_files + current_relevant_files))
# The actual call will be with whatever files were collected and deduplicated
mock_read_files.assert_called_once()
call_args = mock_read_files.call_args
called_files = call_args[0][0] # First positional argument
# Verify all expected files are included
for expected_file in expected_unique_files:
assert expected_file in called_files, f"Expected file {expected_file} not found in {called_files}"
# Verify return value
assert file_content == "# File content\nprint('test')"
@patch("utils.file_utils.read_files")
@patch("utils.file_utils.expand_paths")
def test_force_embed_bypasses_conversation_history(self, mock_expand_paths, mock_read_files):
"""Test that _force_embed_files_for_expert_analysis bypasses conversation filtering"""
# Setup mocks
mock_expand_paths.return_value = self.test_files
mock_read_files.return_value = "# File content\nprint('test')"
# Mock model context for token allocation
mock_model_context = Mock()
mock_token_allocation = Mock()
mock_token_allocation.file_tokens = 100000
mock_model_context.calculate_token_allocation.return_value = mock_token_allocation
# Set up the tool methods
self.mock_tool.get_current_model_context.return_value = mock_model_context
self.mock_tool.wants_line_numbers_by_default.return_value = True
# Call the method
file_content, processed_files = self.mock_tool._force_embed_files_for_expert_analysis(self.test_files)
# Verify it called read_files directly (bypassing conversation history filtering)
mock_read_files.assert_called_once_with(
self.test_files,
max_tokens=100000,
reserve_tokens=1000,
include_line_numbers=True,
)
# Verify it expanded paths to get individual files
mock_expand_paths.assert_called_once_with(self.test_files)
# Verify return values
assert file_content == "# File content\nprint('test')"
assert processed_files == self.test_files
def test_embedding_decision_logic_comprehensive(self):
"""Comprehensive test of the embedding decision logic"""
test_cases = [
# (step_number, continuation_id, is_final_step, expected_embed, description)
(1, None, False, False, "Step 1 new conversation, intermediate"),
(1, None, True, True, "Step 1 new conversation, final (one-step workflow)"),
(2, "thread-123", False, False, "Step 2 with continuation, intermediate"),
(2, "thread-123", True, True, "Step 2 with continuation, final"),
(5, "thread-456", False, False, "Step 5 with continuation, intermediate"),
(5, "thread-456", True, True, "Step 5 with continuation, final"),
]
for step_number, continuation_id, is_final_step, expected_embed, description in test_cases:
should_embed = self.mock_tool._should_embed_files_in_workflow_step(
step_number, continuation_id, is_final_step
)
assert should_embed == expected_embed, f"Failed for: {description}"
if __name__ == "__main__":
pytest.main([__file__])