tests: fix and refactor tests (#1262)

Co-authored-by: bytecii <bytecii@users.noreply.github.com>
Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
This commit is contained in:
bytecii 2026-02-13 23:16:14 -08:00 committed by GitHub
parent f3648d6057
commit 9c396ee015
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 287 additions and 253 deletions

View file

@ -168,20 +168,18 @@ def initialize_tracer_provider() -> None:
_GLOBAL_TRACER_PROVIDER = provider
def get_tracer_provider() -> TracerProvider:
def get_tracer_provider() -> TracerProvider | None:
"""Get the global TracerProvider instance.
Returns:
TracerProvider: The global tracer provider
Raises:
RuntimeError: If called before initialization
TracerProvider if initialized, None otherwise
"""
if _GLOBAL_TRACER_PROVIDER is None:
raise RuntimeError(
logger.warning(
"TracerProvider not initialized. "
"Call initialize_tracer_provider() during app startup."
)
return None
return _GLOBAL_TRACER_PROVIDER
@ -258,22 +256,28 @@ class WorkforceMetricsCallback(WorkforceMetrics):
# Get the global shared tracer provider
# This ensures only one BatchSpanProcessor is running
provider = get_tracer_provider()
# Get tracer from the shared provider
# Use CAMEL version for instrumentation versioning
self.tracer = provider.get_tracer(
TRACER_NAME_WORKFORCE, camel.__version__
)
self.root_span = self.tracer.start_span(
f"{SPAN_WORKFORCE_EXECUTION}:{task_id}"
)
# Langfuse-specific attributes
self.root_span.set_attribute(ATTR_LANGFUSE_SESSION_ID, project_id)
tags = json.dumps(DEFAULT_LANGFUSE_TAGS.copy())
self.root_span.set_attribute(ATTR_LANGFUSE_TAGS, tags)
# Custom attributes
self.root_span.set_attribute(ATTR_PROJECT_ID, project_id)
self.root_span.set_attribute(ATTR_TASK_ID, task_id)
if provider is None:
# TracerProvider not initialized (e.g., app startup not
# completed or running in test environment)
self.enabled = False
else:
# Get tracer from the shared provider
# Use CAMEL version for instrumentation versioning
self.tracer = provider.get_tracer(
TRACER_NAME_WORKFORCE, camel.__version__
)
self.root_span = self.tracer.start_span(
f"{SPAN_WORKFORCE_EXECUTION}:{task_id}"
)
# Langfuse-specific attributes
self.root_span.set_attribute(
ATTR_LANGFUSE_SESSION_ID, project_id
)
tags = json.dumps(DEFAULT_LANGFUSE_TAGS.copy())
self.root_span.set_attribute(ATTR_LANGFUSE_TAGS, tags)
# Custom attributes
self.root_span.set_attribute(ATTR_PROJECT_ID, project_id)
self.root_span.set_attribute(ATTR_TASK_ID, task_id)
# Track active spans for task execution
self.task_spans = {}

View file

@ -35,6 +35,9 @@ def test_browser_agent_creation(sample_chat_data):
_mod = "app.agent.factory.browser"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.HybridBrowserToolkit") as mock_browser_toolkit,

View file

@ -36,6 +36,9 @@ async def test_developer_agent_creation(sample_chat_data):
_mod = "app.agent.factory.developer"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit,
@ -82,6 +85,9 @@ async def test_developer_agent_with_multiple_toolkits(sample_chat_data):
_mod = "app.agent.factory.developer"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.NoteTakingToolkit") as mock_note_toolkit,

View file

@ -36,6 +36,9 @@ async def test_document_agent_creation(sample_chat_data):
_mod = "app.agent.factory.document"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.FileToolkit") as mock_file_toolkit,

View file

@ -35,6 +35,9 @@ def test_multi_modal_agent_creation(sample_chat_data):
_mod = "app.agent.factory.multi_modal"
with (
patch(f"{_mod}.agent_model") as mock_agent_model,
patch(
f"{_mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{_mod}.HumanToolkit") as mock_human_toolkit,
patch(f"{_mod}.VideoDownloaderToolkit") as mock_video_toolkit,

View file

@ -36,6 +36,9 @@ async def test_social_media_agent_creation(sample_chat_data):
mod = "app.agent.factory.social_media"
with (
patch(f"{mod}.agent_model") as mock_agent_model,
patch(
f"{mod}.get_working_directory", return_value="/tmp/test_workdir"
),
patch("asyncio.create_task"),
patch(f"{mod}.WhatsAppToolkit") as mock_whatsapp_toolkit,
patch(f"{mod}.TwitterToolkit") as mock_twitter_toolkit,

View file

@ -13,7 +13,7 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import os
from unittest.mock import MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import Response
@ -50,13 +50,14 @@ class TestChatController:
with (
patch(
"app.controller.chat_controller.create_task_lock",
"app.controller.chat_controller.get_or_create_task_lock",
return_value=mock_task_lock,
),
patch(
"app.controller.chat_controller.step_solve"
) as mock_step_solve,
patch("app.controller.chat_controller.load_dotenv"),
patch("app.controller.chat_controller.set_current_task_id"),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.home", return_value=MagicMock()),
):
@ -71,9 +72,6 @@ class TestChatController:
assert isinstance(response, StreamingResponse)
assert response.media_type == "text/event-stream"
mock_step_solve.assert_called_once_with(
chat_data, mock_request, mock_task_lock
)
@pytest.mark.asyncio
async def test_post_chat_sets_environment_variables(
@ -84,13 +82,14 @@ class TestChatController:
with (
patch(
"app.controller.chat_controller.create_task_lock",
"app.controller.chat_controller.get_or_create_task_lock",
return_value=mock_task_lock,
),
patch(
"app.controller.chat_controller.step_solve"
) as mock_step_solve,
patch("app.controller.chat_controller.load_dotenv"),
patch("app.controller.chat_controller.set_current_task_id"),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.home", return_value=MagicMock()),
patch.dict(os.environ, {}, clear=True),
@ -133,18 +132,24 @@ class TestChatController:
# put_queue is invoked when creating the coroutine passed to asyncio.run
mock_task_lock.put_queue.assert_called_once()
def test_improve_chat_task_done_error(self, mock_task_lock):
"""Test improvement fails when task is done."""
def test_improve_chat_task_done_resets_to_confirming(self, mock_task_lock):
"""Test improvement when task is done resets status to confirming."""
task_id = "test_task_123"
supplement_data = SupplementChat(question="Improve this code")
mock_task_lock.status = Status.done
with patch(
"app.controller.chat_controller.get_task_lock",
return_value=mock_task_lock,
with (
patch(
"app.controller.chat_controller.get_task_lock",
return_value=mock_task_lock,
),
patch("asyncio.run") as mock_run,
):
with pytest.raises(UserException):
improve(task_id, supplement_data)
response = improve(task_id, supplement_data)
assert mock_task_lock.status == Status.confirming
assert isinstance(response, Response)
assert response.status_code == 201
def test_supplement_chat_success(self, mock_task_lock):
"""Test successful chat supplementation."""
@ -244,16 +249,18 @@ class TestChatControllerIntegration:
"""Test chat endpoint through FastAPI test client."""
with (
patch(
"app.controller.chat_controller.create_task_lock"
"app.controller.chat_controller.get_or_create_task_lock"
) as mock_create_lock,
patch(
"app.controller.chat_controller.step_solve"
) as mock_step_solve,
patch("app.controller.chat_controller.load_dotenv"),
patch("app.controller.chat_controller.set_current_task_id"),
patch("pathlib.Path.mkdir"),
patch("pathlib.Path.home", return_value=MagicMock()),
):
mock_task_lock = MagicMock()
mock_task_lock.put_queue = AsyncMock()
mock_create_lock.return_value = mock_task_lock
async def mock_generator():
@ -455,8 +462,12 @@ class TestChatControllerErrorCases:
with (
patch(
"app.controller.chat_controller.create_task_lock"
"app.controller.chat_controller.get_or_create_task_lock"
) as mock_create_lock,
patch(
"app.controller.chat_controller.sanitize_env_path",
return_value="/tmp/fake.env",
),
patch(
"app.controller.chat_controller.load_dotenv",
side_effect=Exception("Env load failed"),

View file

@ -15,6 +15,7 @@
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.controller.model_controller import (
@ -73,11 +74,9 @@ class TestModelController:
"app.controller.model_controller.create_agent",
side_effect=Exception("Invalid model configuration"),
):
response = await validate_model(request_data)
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
assert "Invalid model name" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_step_failure(self):
@ -93,12 +92,9 @@ class TestModelController:
"app.controller.model_controller.create_agent",
return_value=mock_agent,
):
response = await validate_model(request_data)
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
assert "API call failed" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_tool_calls_false(self):
@ -130,8 +126,10 @@ class TestModelController:
@pytest.mark.asyncio
async def test_validate_model_with_minimal_parameters(self):
"""Test model validation with minimal parameters."""
request_data = ValidateModelRequest() # Uses default values
"""Test model validation with minimal parameters (no API key)."""
request_data = (
ValidateModelRequest()
) # Uses default values, api_key is None
mock_agent = MagicMock()
mock_response = MagicMock()
@ -144,12 +142,12 @@ class TestModelController:
"app.controller.model_controller.create_agent",
return_value=mock_agent,
):
# api_key is None by default, which passes the empty string check
# The agent step succeeds, so validation should pass
response = await validate_model(request_data)
assert isinstance(response, ValidateModelResponse)
assert response.is_valid is False
assert response.is_tool_calls is False
assert response.error_code is not None
assert response.error is not None
assert response.is_valid is True
assert response.is_tool_calls is True
@pytest.mark.asyncio
async def test_validate_model_no_response(self):
@ -222,13 +220,7 @@ class TestModelControllerIntegration:
):
response = client.post("/model/validate", json=request_data)
assert (
response.status_code == 200
) # Returns 200 with error in response body
response_data = response.json()
assert response_data["is_valid"] is False
assert response_data["is_tool_calls"] is False
assert "Invalid model name" in response_data["message"]
assert response.status_code == 400
@pytest.mark.model_backend
@ -267,10 +259,9 @@ class TestModelControllerErrorCases:
"app.controller.model_controller.create_agent",
side_effect=ValueError("Invalid configuration"),
):
response = await validate_model(request_data)
assert response.is_valid is False
assert "Invalid configuration" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_with_network_error(self):
@ -288,10 +279,9 @@ class TestModelControllerErrorCases:
"app.controller.model_controller.create_agent",
return_value=mock_agent,
):
response = await validate_model(request_data)
assert response.is_valid is False
assert "Network unreachable" in response.message
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_validate_model_with_malformed_tool_calls_response(self):
@ -346,36 +336,21 @@ class TestModelControllerErrorCases:
api_key="", # Empty API key
)
response = await validate_model(request_data)
assert response.is_valid is False
assert response.is_tool_calls is False
assert response.message == "Invalid key. Validation failed."
assert response.error_code == "invalid_api_key"
assert response.error is not None
assert response.error["message"] == "Invalid key. Validation failed."
assert response.error["type"] == "invalid_request_error"
assert response.error["code"] == "invalid_api_key"
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400
detail = exc_info.value.detail
assert detail["error_code"] == "invalid_api_key"
@pytest.mark.asyncio
async def test_validate_model_invalid_model_type(self):
"""Test model validation with invalid model type."""
"""Test model validation with invalid model type raises HTTPException."""
request_data = ValidateModelRequest(
model_platform="openai",
model_type="INVALID_MODEL_TYPE",
api_key="test_key",
)
response = await validate_model(request_data)
assert response.is_valid is False
assert response.is_tool_calls is False
assert response.message == "Invalid model name. Validation failed."
assert response.error_code is not None
assert "model_not_found" in response.error_code
assert response.error is not None
assert (
response.error["message"]
== "Invalid model name. Validation failed."
)
assert response.error["type"] == "invalid_request_error"
assert response.error["code"] == "model_not_found"
with pytest.raises(HTTPException) as exc_info:
await validate_model(request_data)
assert exc_info.value.status_code == 400

View file

@ -15,6 +15,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app.controller.tool_controller import install_tool
@ -37,14 +38,18 @@ class TestToolController:
return_value=mock_toolkit,
):
result = await install_tool(tool_name)
assert result == ["create_page", "update_page"]
assert result["success"] is True
assert result["tools"] == ["create_page", "update_page"]
assert result["count"] == 2
assert result["toolkit_name"] == "NotionMCPToolkit"
mock_toolkit.connect.assert_called_once()
mock_toolkit.disconnect.assert_called_once()
@pytest.mark.asyncio
async def test_install_unknown_tool(self):
result = await install_tool("unknown_tool")
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool("unknown_tool")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_notion_tool_connection_failure(self):
@ -54,8 +59,11 @@ class TestToolController:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(Exception, match="Connection failed"):
await install_tool("notion")
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
assert "warning" in result
@pytest.mark.asyncio
async def test_install_notion_tool_get_tools_failure(self):
@ -67,8 +75,11 @@ class TestToolController:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(Exception, match="Failed to get tools"):
await install_tool("notion")
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
assert "warning" in result
@pytest.mark.asyncio
async def test_install_notion_tool_disconnect_failure(self):
@ -81,8 +92,11 @@ class TestToolController:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(Exception, match="Disconnect failed"):
await install_tool("notion")
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
assert "warning" in result
@pytest.mark.asyncio
async def test_install_notion_tool_empty_tools(self):
@ -93,7 +107,9 @@ class TestToolController:
return_value=mock_toolkit,
):
result = await install_tool("notion")
assert result == []
assert result["success"] is True
assert result["tools"] == []
assert result["count"] == 0
mock_toolkit.connect.assert_called_once()
mock_toolkit.disconnect.assert_called_once()
@ -117,7 +133,9 @@ class TestToolController:
return_value=mock_toolkit,
):
result = await install_tool("notion")
assert result == names
assert result["success"] is True
assert result["tools"] == names
assert result["count"] == 4
mock_toolkit.connect.assert_called_once()
mock_toolkit.disconnect.assert_called_once()
@ -145,7 +163,10 @@ class TestToolControllerIntegration:
response = client.post(f"/install/tool/{tool_name}")
assert response.status_code == 200
assert response.json() == ["create_page", "update_page"]
data = response.json()
assert data["success"] is True
assert data["tools"] == ["create_page", "update_page"]
assert data["count"] == 2
def test_install_unknown_tool_endpoint_integration(
self, client: TestClient
@ -155,8 +176,7 @@ class TestToolControllerIntegration:
response = client.post(f"/install/tool/{tool_name}")
assert response.status_code == 200
assert response.json() == {"error": "Tool not found"}
assert response.status_code == 404
def test_install_notion_tool_endpoint_with_connection_error(
self, client: TestClient
@ -171,9 +191,12 @@ class TestToolControllerIntegration:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
# The exception should be raised by the endpoint since there's no error handling
with pytest.raises(Exception, match="Connection failed"):
client.post(f"/install/tool/{tool_name}")
response = client.post(f"/install/tool/{tool_name}")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["tools"] == []
assert "warning" in data
@pytest.mark.model_backend
@ -211,8 +234,11 @@ class TestToolControllerErrorCases:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(AttributeError):
await install_tool("notion")
# Inner except catches the AttributeError and returns success with empty tools
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert "warning" in result
@pytest.mark.asyncio
async def test_install_tool_with_none_toolkit(self):
@ -220,23 +246,29 @@ class TestToolControllerErrorCases:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=None,
):
with pytest.raises(AttributeError):
await install_tool("notion")
# Inner except catches AttributeError on None.connect()
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert "warning" in result
@pytest.mark.asyncio
async def test_install_tool_with_special_characters_in_name(self):
result = await install_tool("notion@#$%")
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool("notion@#$%")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_tool_with_empty_string_name(self):
result = await install_tool("")
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool("")
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_tool_with_none_name(self):
result = await install_tool(None)
assert result == {"error": "Tool not found"}
with pytest.raises(HTTPException) as exc_info:
await install_tool(None)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_install_notion_tool_partial_failure(self):
@ -252,5 +284,8 @@ class TestToolControllerErrorCases:
"app.controller.tool_controller.NotionMCPToolkit",
return_value=mock_toolkit,
):
with pytest.raises(AttributeError):
await install_tool("notion")
# Inner except catches the AttributeError from tools[2].func
result = await install_tool("notion")
assert result["success"] is True
assert result["tools"] == []
assert "warning" in result

View file

@ -71,7 +71,6 @@ class TestCollectPreviousTaskContext:
assert "Previous Task Result:" in result
assert "Successfully created script.py" in result
assert "=== END OF PREVIOUS TASK CONTEXT ===" in result
assert "=== NEW TASK ===" in result
def test_collect_previous_task_context_with_generated_files(
self, temp_dir
@ -208,7 +207,6 @@ class TestCollectPreviousTaskContext:
# Should still have the structural elements
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "=== END OF PREVIOUS TASK CONTEXT ===" in result
assert "=== NEW TASK ===" in result
# Should not have content sections for empty inputs
assert "Previous Task:" not in result
@ -289,7 +287,6 @@ class TestBuildContextForWorkforce:
# Create mock TaskLock
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = [
{"role": "user", "content": "Create a Python script"},
{
"role": "assistant",
"content": "I will create a Python script for you",
@ -304,14 +301,10 @@ class TestBuildContextForWorkforce:
result = build_context_for_workforce(task_lock, options)
# Should include conversation history
# Should include conversation history header
assert "=== CONVERSATION HISTORY ===" in result
assert "user: Create a Python script" in result
assert "assistant: I will create a Python script for you" in result
# Should include previous task context
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "Script created successfully" in result
# build_conversation_context only processes assistant and task_result roles
assert "I will create a Python script for you" in result
def test_build_context_for_workforce_empty_history(self, temp_dir):
"""Test build_context_for_workforce with empty conversation history."""
@ -329,15 +322,17 @@ class TestBuildContextForWorkforce:
assert result == ""
def test_build_context_for_workforce_task_result_role(self, temp_dir):
"""Test build_context_for_workforce handles 'task_result' role specially."""
"""Test build_context_for_workforce handles 'task_result' role."""
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = [
{"role": "user", "content": "First question"},
{
"role": "task_result",
"content": "Full task context from previous task",
},
{"role": "user", "content": "Second question"},
{
"role": "assistant",
"content": "Task completed successfully",
},
]
task_lock.last_task_result = "Final result"
task_lock.last_task_summary = "Task summary"
@ -347,22 +342,18 @@ class TestBuildContextForWorkforce:
result = build_context_for_workforce(task_lock, options)
# Should simplify task_result display
assert "[Previous Task Completed]" in result
assert (
"Full task context from previous task" not in result
) # Should not show full content
assert "user: First question" in result
assert "user: Second question" in result
# build_conversation_context appends string task_result content directly
assert "Full task context from previous task" in result
assert "Task completed successfully" in result
def test_build_context_for_workforce_with_last_task_result(self, temp_dir):
"""Test build_context_for_workforce includes last task result context."""
# Create some files in temp directory
(temp_dir / "output.txt").write_text("Task output")
"""Test build_context_for_workforce with assistant entries."""
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = [
{"role": "user", "content": "Test question"}
{
"role": "assistant",
"content": "Task completed with output.txt",
},
]
task_lock.last_task_result = "Task completed with output.txt"
task_lock.last_task_summary = "File creation task"
@ -372,13 +363,9 @@ class TestBuildContextForWorkforce:
result = build_context_for_workforce(task_lock, options)
# Should include conversation history and task context
# Should include conversation history
assert "=== CONVERSATION HISTORY ===" in result
assert "user: Test question" in result
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "Task completed with output.txt" in result
assert "File creation task" in result
assert "output.txt" in result # Generated file should be listed
@pytest.mark.unit
@ -586,17 +573,14 @@ class TestChatServiceAgentOperations:
@pytest.mark.asyncio
async def test_question_confirm_simple_query(self, mock_camel_agent):
"""Test question_confirm with simple query that gets direct response."""
mock_camel_agent.step.return_value.msgs[
0
].content = "Hello! How can I help you today?"
"""Test question_confirm with simple query returns False."""
mock_camel_agent.step.return_value.msgs[0].content = "no"
mock_camel_agent.chat_history = []
result = await question_confirm(mock_camel_agent, "hello")
# Should return SSE formatted response for simple queries
assert "wait_confirm" in result
assert "Hello! How can I help you today?" in result
# Should return False for simple queries (no "yes" in response)
assert result is False
@pytest.mark.asyncio
async def test_question_confirm_complex_task(self, mock_camel_agent):
@ -666,6 +650,10 @@ class TestChatServiceAgentOperations:
with (
patch("app.service.chat_service.agent_model") as mock_agent_model,
patch(
"app.service.chat_service.get_working_directory",
return_value="/tmp/test_workdir",
),
patch(
"app.service.chat_service.Workforce",
return_value=mock_workforce,
@ -682,6 +670,10 @@ class TestChatServiceAgentOperations:
"app.agent.toolkit.human_toolkit.get_task_lock",
return_value=mock_task_lock,
),
patch(
"app.service.chat_service.WorkforceMetricsCallback",
return_value=MagicMock(),
),
):
mock_agent_model.return_value = MagicMock()
@ -738,23 +730,15 @@ class TestChatServiceIntegration:
"def hello(): print('Hello World')"
)
# Mock file_save_path method to return our temp directory
with patch.object(
Chat, "file_save_path", return_value=str(working_dir)
):
# Test the context building directly
context = build_context_for_workforce(task_lock, options)
# Test the context building directly
# build_context_for_workforce now only calls build_conversation_context
# which only processes assistant and task_result roles
context = build_context_for_workforce(task_lock, options)
# Verify context includes conversation history
assert "=== CONVERSATION HISTORY ===" in context
assert "user: Create a Python script" in context
assert "assistant: Script created successfully" in context
# Verify context includes task context with files
assert "=== CONTEXT FROM PREVIOUS TASK ===" in context
assert "def hello(): print('Hello World')" in context
assert "Python Hello World Script" in context
assert "script.py" in context
# Verify context includes conversation history header
assert "=== CONVERSATION HISTORY ===" in context
# assistant entries are included
assert "Script created successfully" in context
@pytest.mark.asyncio
async def test_step_solve_new_task_state_context_collection(
@ -793,7 +777,6 @@ class TestChatServiceIntegration:
assert "main.py" in result
assert "config.json" in result
assert "=== END OF PREVIOUS TASK CONTEXT ===" in result
assert "=== NEW TASK ===" in result
@pytest.mark.asyncio
async def test_step_solve_end_action_context_collection(
@ -1008,26 +991,23 @@ class TestChatServiceErrorCases:
# Should log warning
mock_logger.warning.assert_called_once()
def test_collect_previous_task_context_relpath_exception(self, temp_dir):
"""Test collect_previous_task_context handles os.path.relpath exceptions."""
def test_collect_previous_task_context_abspath_used(self, temp_dir):
"""Test collect_previous_task_context uses absolute paths for files."""
working_directory = str(temp_dir)
# Create a test file
(temp_dir / "test.txt").write_text("test content")
with patch("os.path.relpath", side_effect=ValueError("Invalid path")):
with patch("app.service.chat_service.logger") as mock_logger:
result = collect_previous_task_context(
working_directory=working_directory,
previous_task_content="Test task",
previous_task_result="Test result",
previous_summary="Test summary",
)
result = collect_previous_task_context(
working_directory=working_directory,
previous_task_content="Test task",
previous_task_result="Test result",
previous_summary="Test summary",
)
# Should handle the exception gracefully
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
# Should log warning about file collection failure
mock_logger.warning.assert_called_once()
# Should include absolute path for the file
assert "=== CONTEXT FROM PREVIOUS TASK ===" in result
assert "test.txt" in result
def test_build_context_for_workforce_missing_attributes(self, temp_dir):
"""Test build_context_for_workforce handles missing attributes gracefully."""
@ -1045,20 +1025,18 @@ class TestChatServiceErrorCases:
# Should handle missing attributes gracefully
assert result == ""
def test_build_context_for_workforce_file_save_path_exception(self):
"""Test build_context_for_workforce handles file_save_path exceptions."""
def test_build_context_for_workforce_empty_conversation(self):
"""Test build_context_for_workforce returns empty for empty conversation."""
task_lock = MagicMock(spec=TaskLock)
task_lock.conversation_history = []
task_lock.last_task_result = "Test result"
task_lock.last_task_summary = "Test summary"
options = MagicMock()
options.file_save_path.side_effect = Exception("Path error")
with patch("app.service.chat_service.logger") as mock_logger:
# Should handle exception when getting file path
with pytest.raises(Exception, match="Path error"):
build_context_for_workforce(task_lock, options)
# Should return empty string for empty conversation history
result = build_context_for_workforce(task_lock, options)
assert result == ""
def test_collect_previous_task_context_unicode_handling(self, temp_dir):
"""Test collect_previous_task_context handles unicode content correctly."""
@ -1216,6 +1194,22 @@ class TestChatServiceErrorCases:
"app.service.chat_service.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.developer.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.browser.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.document.agent_model",
side_effect=Exception("Agent creation failed"),
),
patch(
"app.agent.factory.multi_modal.agent_model",
side_effect=Exception("Agent creation failed"),
),
):
with pytest.raises(Exception, match="Agent creation failed"):
await construct_workforce(options)

View file

@ -519,32 +519,29 @@ class TestPeriodicCleanup:
@pytest.mark.asyncio
async def test_periodic_cleanup_handles_exceptions(self):
"""Test that periodic cleanup handles exceptions gracefully."""
import app.service.task as task_module
# Create a stale task lock
task_lock = create_task_lock("test_task")
task_lock.last_accessed = datetime.now() - timedelta(hours=3)
# Mock delete_task_lock to raise exception
# Mock delete_task_lock to raise exception and call through module
with (
patch(
"app.service.task.delete_task_lock",
patch.object(
task_module,
"delete_task_lock",
side_effect=Exception("Test error"),
),
patch(
"app.service.task.logger.error",
) as mock_logger,
patch.object(task_module, "logger") as mock_logger,
):
# Directly call the cleanup logic
# that should trigger the exception
# Simulate what _periodic_cleanup does when encountering an error
try:
await delete_task_lock("test_task")
await task_module.delete_task_lock("test_task")
except Exception as e:
import logging
task_logger = logging.getLogger("task_service")
task_logger.error(f"Error during task cleanup: {e}")
task_module.logger.error(f"Error in periodic cleanup: {e}")
# Should have logged the error
mock_logger.assert_called()
mock_logger.error.assert_called()
@pytest.mark.integration

View file

@ -33,6 +33,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "worker_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker description",
@ -57,6 +58,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "worker_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -123,6 +125,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "worker_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -178,6 +181,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -247,6 +251,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker", worker=mock_worker
@ -280,6 +285,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -333,6 +339,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -382,6 +389,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -431,6 +439,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "test_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test worker",
@ -476,6 +485,7 @@ class TestSingleAgentWorker:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(description="Test", worker=mock_worker)
assert isinstance(worker, BaseSingleAgentWorker)
@ -491,6 +501,7 @@ class TestSingleAgentWorkerIntegration:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.role_name = "integration_worker"
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "integration_worker"
worker = SingleAgentWorker(
description="Integration test worker",
@ -568,6 +579,7 @@ class TestSingleAgentWorkerErrorCases:
"""Test _process_task when agent returns None response."""
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test",
worker=mock_worker,
@ -600,6 +612,7 @@ class TestSingleAgentWorkerErrorCases:
"""Test _process_task with malformed response structure."""
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test",
worker=mock_worker,
@ -637,6 +650,7 @@ class TestSingleAgentWorkerErrorCases:
mock_worker = MagicMock(spec=ListenChatAgent)
mock_worker.agent_id = "test_agent_123"
mock_worker.role_name = "test_worker"
mock_worker.agent_name = "test_worker"
worker = SingleAgentWorker(
description="Test",
worker=mock_worker,

View file

@ -344,6 +344,20 @@ async def async_mock_agent() -> AsyncGenerator[AsyncMock, None]:
yield agent
# Safety net: clean up any MagicMock-named directories that tests may
# accidentally create when mock objects are used as file paths.
@pytest.fixture(autouse=True, scope="session")
def _cleanup_magicmock_dirs():
"""Remove MagicMock-named directories from backend/ after test session."""
yield
import shutil
backend_dir = Path(__file__).parent.parent
for entry in backend_dir.iterdir():
if "MagicMock" in entry.name:
shutil.rmtree(entry, ignore_errors=True)
# Markers for test categorization
pytest_plugins = ["pytest_asyncio"]