339 lines
14 KiB
Python
339 lines
14 KiB
Python
"""
|
|
Test debug tool continuation ID functionality and conversation history formatting.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from tools.debug import DebugIssueTool
|
|
from utils.conversation_memory import (
|
|
ConversationTurn,
|
|
ThreadContext,
|
|
build_conversation_history,
|
|
get_conversation_file_list,
|
|
)
|
|
|
|
|
|
class TestDebugContinuation:
|
|
"""Test debug tool continuation ID and conversation history integration."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_debug_creates_continuation_id(self):
|
|
"""Test that debug tool creates continuation ID on first step."""
|
|
tool = DebugIssueTool()
|
|
|
|
with patch("utils.conversation_memory.create_thread", return_value="debug-test-uuid-123"):
|
|
with patch("utils.conversation_memory.add_turn"):
|
|
result = await tool.execute(
|
|
{
|
|
"step": "Investigating null pointer exception",
|
|
"step_number": 1,
|
|
"total_steps": 3,
|
|
"next_step_required": True,
|
|
"findings": "Initial investigation shows null reference in UserService",
|
|
"files_checked": ["/api/UserService.java"],
|
|
}
|
|
)
|
|
|
|
assert len(result) == 1
|
|
response = json.loads(result[0].text)
|
|
assert response["status"] == "pause_for_investigation"
|
|
assert response["continuation_id"] == "debug-test-uuid-123"
|
|
assert response["investigation_required"] is True
|
|
assert "required_actions" in response
|
|
|
|
def test_debug_conversation_formatting(self):
|
|
"""Test that debug tool's structured output is properly formatted in conversation history."""
|
|
# Create a mock conversation with debug tool output
|
|
debug_output = {
|
|
"status": "investigation_in_progress",
|
|
"step_number": 2,
|
|
"total_steps": 3,
|
|
"next_step_required": True,
|
|
"investigation_status": {
|
|
"files_checked": 3,
|
|
"relevant_files": 2,
|
|
"relevant_methods": 1,
|
|
"hypotheses_formed": 1,
|
|
"images_collected": 0,
|
|
"current_confidence": "medium",
|
|
},
|
|
"output": {"instructions": "Continue systematic investigation.", "format": "systematic_investigation"},
|
|
"continuation_id": "debug-test-uuid-123",
|
|
"next_steps": "Continue investigation with step 3.",
|
|
}
|
|
|
|
context = ThreadContext(
|
|
thread_id="debug-test-uuid-123",
|
|
created_at="2025-01-01T00:00:00Z",
|
|
last_updated_at="2025-01-01T00:05:00Z",
|
|
tool_name="debug",
|
|
turns=[
|
|
ConversationTurn(
|
|
role="user",
|
|
content="Step 1: Investigating null pointer exception",
|
|
timestamp="2025-01-01T00:01:00Z",
|
|
tool_name="debug",
|
|
files=["/api/UserService.java"],
|
|
),
|
|
ConversationTurn(
|
|
role="assistant",
|
|
content=json.dumps(debug_output, indent=2),
|
|
timestamp="2025-01-01T00:02:00Z",
|
|
tool_name="debug",
|
|
files=["/api/UserService.java", "/api/UserController.java"],
|
|
),
|
|
],
|
|
initial_context={
|
|
"step": "Investigating null pointer exception",
|
|
"step_number": 1,
|
|
"total_steps": 3,
|
|
"next_step_required": True,
|
|
"findings": "Initial investigation",
|
|
},
|
|
)
|
|
|
|
# Mock file reading to avoid actual file I/O
|
|
def mock_read_file(file_path):
|
|
if file_path == "/api/UserService.java":
|
|
return "// UserService.java\npublic class UserService {\n // code...\n}", 10
|
|
elif file_path == "/api/UserController.java":
|
|
return "// UserController.java\npublic class UserController {\n // code...\n}", 10
|
|
return "", 0
|
|
|
|
# Build conversation history
|
|
from utils.model_context import ModelContext
|
|
|
|
model_context = ModelContext("flash")
|
|
history, tokens = build_conversation_history(context, model_context, read_files_func=mock_read_file)
|
|
|
|
# Verify the history contains debug-specific content
|
|
assert "=== CONVERSATION HISTORY (CONTINUATION) ===" in history
|
|
assert "Thread: debug-test-uuid-123" in history
|
|
assert "Tool: debug" in history
|
|
|
|
# Check that files are included
|
|
assert "UserService.java" in history
|
|
assert "UserController.java" in history
|
|
|
|
# Check that debug output is included
|
|
assert "investigation_in_progress" in history
|
|
assert '"step_number": 2' in history
|
|
assert '"files_checked": 3' in history
|
|
assert '"current_confidence": "medium"' in history
|
|
|
|
def test_debug_continuation_preserves_investigation_state(self):
|
|
"""Test that continuation preserves investigation state across tools."""
|
|
# Create a debug investigation context
|
|
context = ThreadContext(
|
|
thread_id="debug-test-uuid-123",
|
|
created_at="2025-01-01T00:00:00Z",
|
|
last_updated_at="2025-01-01T00:10:00Z",
|
|
tool_name="debug",
|
|
turns=[
|
|
ConversationTurn(
|
|
role="user",
|
|
content="Step 1: Initial investigation",
|
|
timestamp="2025-01-01T00:01:00Z",
|
|
tool_name="debug",
|
|
files=["/api/SessionManager.java"],
|
|
),
|
|
ConversationTurn(
|
|
role="assistant",
|
|
content=json.dumps(
|
|
{
|
|
"status": "investigation_in_progress",
|
|
"step_number": 1,
|
|
"total_steps": 4,
|
|
"next_step_required": True,
|
|
"investigation_status": {"files_checked": 1, "relevant_files": 1},
|
|
"continuation_id": "debug-test-uuid-123",
|
|
}
|
|
),
|
|
timestamp="2025-01-01T00:02:00Z",
|
|
tool_name="debug",
|
|
),
|
|
ConversationTurn(
|
|
role="user",
|
|
content="Step 2: Found dictionary modification issue",
|
|
timestamp="2025-01-01T00:03:00Z",
|
|
tool_name="debug",
|
|
files=["/api/SessionManager.java", "/api/utils.py"],
|
|
),
|
|
ConversationTurn(
|
|
role="assistant",
|
|
content=json.dumps(
|
|
{
|
|
"status": "investigation_in_progress",
|
|
"step_number": 2,
|
|
"total_steps": 4,
|
|
"next_step_required": True,
|
|
"investigation_status": {
|
|
"files_checked": 2,
|
|
"relevant_files": 1,
|
|
"relevant_methods": 1,
|
|
"hypotheses_formed": 1,
|
|
"current_confidence": "high",
|
|
},
|
|
"continuation_id": "debug-test-uuid-123",
|
|
}
|
|
),
|
|
timestamp="2025-01-01T00:04:00Z",
|
|
tool_name="debug",
|
|
),
|
|
],
|
|
initial_context={},
|
|
)
|
|
|
|
# Get file list to verify prioritization
|
|
file_list = get_conversation_file_list(context)
|
|
assert file_list == ["/api/SessionManager.java", "/api/utils.py"]
|
|
|
|
# Mock file reading
|
|
def mock_read_file(file_path):
|
|
return f"// {file_path}\n// Mock content", 5
|
|
|
|
# Build history
|
|
from utils.model_context import ModelContext
|
|
|
|
model_context = ModelContext("flash")
|
|
history, tokens = build_conversation_history(context, model_context, read_files_func=mock_read_file)
|
|
|
|
# Verify investigation progression is preserved
|
|
assert "Step 1: Initial investigation" in history
|
|
assert "Step 2: Found dictionary modification issue" in history
|
|
assert '"step_number": 1' in history
|
|
assert '"step_number": 2' in history
|
|
assert '"current_confidence": "high"' in history
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_debug_to_analyze_continuation(self):
|
|
"""Test continuation from debug tool to analyze tool."""
|
|
# Simulate debug tool creating initial investigation
|
|
debug_context = ThreadContext(
|
|
thread_id="debug-analyze-uuid-123",
|
|
created_at="2025-01-01T00:00:00Z",
|
|
last_updated_at="2025-01-01T00:10:00Z",
|
|
tool_name="debug",
|
|
turns=[
|
|
ConversationTurn(
|
|
role="user",
|
|
content="Final investigation step",
|
|
timestamp="2025-01-01T00:01:00Z",
|
|
tool_name="debug",
|
|
files=["/api/SessionManager.java"],
|
|
),
|
|
ConversationTurn(
|
|
role="assistant",
|
|
content=json.dumps(
|
|
{
|
|
"status": "calling_expert_analysis",
|
|
"investigation_complete": True,
|
|
"expert_analysis": {
|
|
"status": "analysis_complete",
|
|
"summary": "Dictionary modification during iteration bug",
|
|
"hypotheses": [
|
|
{
|
|
"name": "CONCURRENT_MODIFICATION",
|
|
"confidence": "High",
|
|
"root_cause": "Modifying dict while iterating",
|
|
"minimal_fix": "Create list of keys first",
|
|
}
|
|
],
|
|
},
|
|
"complete_investigation": {
|
|
"initial_issue": "Session validation failures",
|
|
"steps_taken": 3,
|
|
"files_examined": ["/api/SessionManager.java"],
|
|
"relevant_methods": ["SessionManager.cleanup_expired_sessions"],
|
|
},
|
|
}
|
|
),
|
|
timestamp="2025-01-01T00:02:00Z",
|
|
tool_name="debug",
|
|
),
|
|
],
|
|
initial_context={},
|
|
)
|
|
|
|
# Mock getting the thread
|
|
with patch("utils.conversation_memory.get_thread", return_value=debug_context):
|
|
# Mock file reading
|
|
def mock_read_file(file_path):
|
|
return "// SessionManager.java\n// cleanup_expired_sessions method", 10
|
|
|
|
# Build history for analyze tool
|
|
from utils.model_context import ModelContext
|
|
|
|
model_context = ModelContext("flash")
|
|
history, tokens = build_conversation_history(debug_context, model_context, read_files_func=mock_read_file)
|
|
|
|
# Verify analyze tool can see debug investigation
|
|
assert "calling_expert_analysis" in history
|
|
assert "CONCURRENT_MODIFICATION" in history
|
|
assert "Dictionary modification during iteration bug" in history
|
|
assert "SessionManager.cleanup_expired_sessions" in history
|
|
|
|
# Verify the continuation context is clear
|
|
assert "Thread: debug-analyze-uuid-123" in history
|
|
assert "Tool: debug" in history # Shows original tool
|
|
|
|
def test_debug_planner_style_formatting(self):
|
|
"""Test that debug tool uses similar formatting to planner for structured responses."""
|
|
# Create debug investigation with multiple steps
|
|
context = ThreadContext(
|
|
thread_id="debug-format-uuid-123",
|
|
created_at="2025-01-01T00:00:00Z",
|
|
last_updated_at="2025-01-01T00:15:00Z",
|
|
tool_name="debug",
|
|
turns=[
|
|
ConversationTurn(
|
|
role="user",
|
|
content="Step 1: Initial error analysis",
|
|
timestamp="2025-01-01T00:01:00Z",
|
|
tool_name="debug",
|
|
),
|
|
ConversationTurn(
|
|
role="assistant",
|
|
content=json.dumps(
|
|
{
|
|
"status": "investigation_in_progress",
|
|
"step_number": 1,
|
|
"total_steps": 3,
|
|
"next_step_required": True,
|
|
"output": {
|
|
"instructions": "Continue systematic investigation.",
|
|
"format": "systematic_investigation",
|
|
},
|
|
"continuation_id": "debug-format-uuid-123",
|
|
},
|
|
indent=2,
|
|
),
|
|
timestamp="2025-01-01T00:02:00Z",
|
|
tool_name="debug",
|
|
),
|
|
],
|
|
initial_context={},
|
|
)
|
|
|
|
# Build history
|
|
from utils.model_context import ModelContext
|
|
|
|
model_context = ModelContext("flash")
|
|
history, _ = build_conversation_history(context, model_context, read_files_func=lambda x: ("", 0))
|
|
|
|
# Verify structured format is preserved
|
|
assert '"status": "investigation_in_progress"' in history
|
|
assert '"format": "systematic_investigation"' in history
|
|
assert "--- Turn 1 (Claude using debug) ---" in history
|
|
assert "--- Turn 2 (Gemini using debug" in history
|
|
|
|
# The JSON structure should be preserved for tools to parse
|
|
# This allows other tools to understand the investigation state
|
|
turn_2_start = history.find("--- Turn 2 (Gemini using debug")
|
|
turn_2_content = history[turn_2_start:]
|
|
assert "{\n" in turn_2_content # JSON formatting preserved
|
|
assert '"continuation_id"' in turn_2_content
|