mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-18 20:14:44 +08:00
refactor: split backend into harness (deerflow.*) and app (app.*) (#1131)
* refactor: extract shared utils to break harness→app cross-layer imports Move _validate_skill_frontmatter to src/skills/validation.py and CONVERTIBLE_EXTENSIONS + convert_file_to_markdown to src/utils/file_conversion.py. This eliminates the two reverse dependencies from client.py (harness layer) into gateway/routers/ (app layer), preparing for the harness/app package split. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: split backend/src into harness (deerflow.*) and app (app.*) Physically split the monolithic backend/src/ package into two layers: - **Harness** (`packages/harness/deerflow/`): publishable agent framework package with import prefix `deerflow.*`. Contains agents, sandbox, tools, models, MCP, skills, config, and all core infrastructure. - **App** (`app/`): unpublished application code with import prefix `app.*`. Contains gateway (FastAPI REST API) and channels (IM integrations). Key changes: - Move 13 harness modules to packages/harness/deerflow/ via git mv - Move gateway + channels to app/ via git mv - Rename all imports: src.* → deerflow.* (harness) / app.* (app layer) - Set up uv workspace with deerflow-harness as workspace member - Update langgraph.json, config.example.yaml, all scripts, Docker files - Add build-system (hatchling) to harness pyproject.toml - Add PYTHONPATH=. to gateway startup commands for app.* resolution - Update ruff.toml with known-first-party for import sorting - Update all documentation to reflect new directory structure Boundary rule enforced: harness code never imports from app. All 429 tests pass. Lint clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add harness→app boundary check test and update docs Add test_harness_boundary.py that scans all Python files in packages/harness/deerflow/ and fails if any `from app.*` or `import app.*` statement is found. This enforces the architectural rule that the harness layer never depends on the app layer. Update CLAUDE.md to document the harness/app split architecture, import conventions, and the boundary enforcement test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add config versioning with auto-upgrade on startup When config.example.yaml schema changes, developers' local config.yaml files can silently become outdated. This adds a config_version field and auto-upgrade mechanism so breaking changes (like src.* → deerflow.* renames) are applied automatically before services start. - Add config_version: 1 to config.example.yaml - Add startup version check warning in AppConfig.from_file() - Add scripts/config-upgrade.sh with migration registry for value replacements - Add `make config-upgrade` target - Auto-run config-upgrade in serve.sh and start-daemon.sh before starting services - Add config error hints in service failure messages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix comments * fix: update src.* import in test_sandbox_tools_security to deerflow.* Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: handle empty config and search parent dirs for config.example.yaml Address Copilot review comments on PR #1131: - Guard against yaml.safe_load() returning None for empty config files - Search parent directories for config.example.yaml instead of only looking next to config.yaml, fixing detection in common setups Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct skills root path depth and config_version type coercion - loader.py: fix get_skills_root_path() to use 5 parent levels (was 3) after harness split, file lives at packages/harness/deerflow/skills/ so parent×3 resolved to backend/packages/harness/ instead of backend/ - app_config.py: coerce config_version to int() before comparison in _check_config_version() to prevent TypeError when YAML stores value as string (e.g. config_version: "1") - tests: add regression tests for both fixes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update test imports from src.* to deerflow.*/app.* after harness refactor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,19 +8,19 @@ import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
# Make 'src' importable from any working directory
|
||||
# Make 'app' and 'deerflow' importable from any working directory
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# Break the circular import chain that exists in production code:
|
||||
# src.subagents.__init__
|
||||
# deerflow.subagents.__init__
|
||||
# -> .executor (SubagentExecutor, SubagentResult)
|
||||
# -> src.agents.thread_state
|
||||
# -> src.agents.__init__
|
||||
# -> deerflow.agents.thread_state
|
||||
# -> deerflow.agents.__init__
|
||||
# -> lead_agent.agent
|
||||
# -> subagent_limit_middleware
|
||||
# -> src.subagents.executor <-- circular!
|
||||
# -> deerflow.subagents.executor <-- circular!
|
||||
#
|
||||
# By injecting a mock for src.subagents.executor *before* any test module
|
||||
# By injecting a mock for deerflow.subagents.executor *before* any test module
|
||||
# triggers the import, __init__.py's "from .executor import ..." succeeds
|
||||
# immediately without running the real executor module.
|
||||
_executor_mock = MagicMock()
|
||||
@@ -30,4 +30,4 @@ _executor_mock.SubagentStatus = MagicMock
|
||||
_executor_mock.MAX_CONCURRENT_SUBAGENTS = 3
|
||||
_executor_mock.get_background_task_result = MagicMock()
|
||||
|
||||
sys.modules["src.subagents.executor"] = _executor_mock
|
||||
sys.modules["deerflow.subagents.executor"] = _executor_mock
|
||||
|
||||
@@ -6,8 +6,8 @@ import asyncio
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.channels.base import Channel
|
||||
from src.channels.message_bus import MessageBus, OutboundMessage, ResolvedAttachment
|
||||
from app.channels.base import Channel
|
||||
from app.channels.message_bus import MessageBus, OutboundMessage, ResolvedAttachment
|
||||
|
||||
|
||||
def _run(coro):
|
||||
@@ -102,7 +102,7 @@ class TestOutboundMessageAttachments:
|
||||
class TestResolveAttachments:
|
||||
def test_resolves_existing_file(self, tmp_path):
|
||||
"""Successfully resolves a virtual path to an existing file."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
# Create the directory structure: threads/{thread_id}/user-data/outputs/
|
||||
thread_id = "test-thread-123"
|
||||
@@ -115,7 +115,7 @@ class TestResolveAttachments:
|
||||
mock_paths.resolve_virtual_path.return_value = test_file
|
||||
mock_paths.sandbox_outputs_dir.return_value = outputs_dir
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments(thread_id, ["/mnt/user-data/outputs/report.pdf"])
|
||||
|
||||
assert len(result) == 1
|
||||
@@ -126,7 +126,7 @@ class TestResolveAttachments:
|
||||
|
||||
def test_resolves_image_file(self, tmp_path):
|
||||
"""Images are detected by MIME type."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
thread_id = "test-thread"
|
||||
outputs_dir = tmp_path / "threads" / thread_id / "user-data" / "outputs"
|
||||
@@ -138,7 +138,7 @@ class TestResolveAttachments:
|
||||
mock_paths.resolve_virtual_path.return_value = img
|
||||
mock_paths.sandbox_outputs_dir.return_value = outputs_dir
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments(thread_id, ["/mnt/user-data/outputs/chart.png"])
|
||||
|
||||
assert len(result) == 1
|
||||
@@ -147,7 +147,7 @@ class TestResolveAttachments:
|
||||
|
||||
def test_skips_missing_file(self, tmp_path):
|
||||
"""Missing files are skipped with a warning."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
outputs_dir = tmp_path / "outputs"
|
||||
outputs_dir.mkdir()
|
||||
@@ -156,30 +156,30 @@ class TestResolveAttachments:
|
||||
mock_paths.resolve_virtual_path.return_value = outputs_dir / "nonexistent.txt"
|
||||
mock_paths.sandbox_outputs_dir.return_value = outputs_dir
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments("t1", ["/mnt/user-data/outputs/nonexistent.txt"])
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_skips_invalid_path(self):
|
||||
"""Invalid paths (ValueError from resolve) are skipped."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.resolve_virtual_path.side_effect = ValueError("bad path")
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments("t1", ["/invalid/path"])
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_rejects_uploads_path(self):
|
||||
"""Paths under /mnt/user-data/uploads/ are rejected (security)."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
mock_paths = MagicMock()
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments("t1", ["/mnt/user-data/uploads/secret.pdf"])
|
||||
|
||||
assert result == []
|
||||
@@ -187,11 +187,11 @@ class TestResolveAttachments:
|
||||
|
||||
def test_rejects_workspace_path(self):
|
||||
"""Paths under /mnt/user-data/workspace/ are rejected (security)."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
mock_paths = MagicMock()
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments("t1", ["/mnt/user-data/workspace/config.py"])
|
||||
|
||||
assert result == []
|
||||
@@ -199,7 +199,7 @@ class TestResolveAttachments:
|
||||
|
||||
def test_rejects_path_traversal_escape(self, tmp_path):
|
||||
"""Paths that escape the outputs directory after resolution are rejected."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
thread_id = "t1"
|
||||
outputs_dir = tmp_path / "threads" / thread_id / "user-data" / "outputs"
|
||||
@@ -213,14 +213,14 @@ class TestResolveAttachments:
|
||||
mock_paths.resolve_virtual_path.return_value = escaped_file
|
||||
mock_paths.sandbox_outputs_dir.return_value = outputs_dir
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments(thread_id, ["/mnt/user-data/outputs/../uploads/stolen.txt"])
|
||||
|
||||
assert result == []
|
||||
|
||||
def test_multiple_artifacts_partial_resolution(self, tmp_path):
|
||||
"""Mixed valid/invalid artifacts: only valid ones are returned."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
thread_id = "t1"
|
||||
outputs_dir = tmp_path / "outputs"
|
||||
@@ -238,7 +238,7 @@ class TestResolveAttachments:
|
||||
|
||||
mock_paths.resolve_virtual_path.side_effect = resolve_side_effect
|
||||
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments(
|
||||
thread_id,
|
||||
["/mnt/user-data/outputs/data.csv", "/mnt/user-data/outputs/missing.txt"],
|
||||
@@ -417,17 +417,17 @@ class TestBaseChannelOnOutbound:
|
||||
class TestManagerArtifactResolution:
|
||||
def test_handle_chat_populates_attachments(self):
|
||||
"""Verify _resolve_attachments is importable and works with the manager module."""
|
||||
from src.channels.manager import _resolve_attachments
|
||||
from app.channels.manager import _resolve_attachments
|
||||
|
||||
# Basic smoke test: empty artifacts returns empty list
|
||||
mock_paths = MagicMock()
|
||||
with patch("src.config.paths.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.config.paths.get_paths", return_value=mock_paths):
|
||||
result = _resolve_attachments("t1", [])
|
||||
assert result == []
|
||||
|
||||
def test_format_artifact_text_for_unresolved(self):
|
||||
"""_format_artifact_text produces expected output."""
|
||||
from src.channels.manager import _format_artifact_text
|
||||
from app.channels.manager import _format_artifact_text
|
||||
|
||||
assert "report.pdf" in _format_artifact_text(["/mnt/user-data/outputs/report.pdf"])
|
||||
result = _format_artifact_text(["/mnt/user-data/outputs/a.txt", "/mnt/user-data/outputs/b.txt"])
|
||||
|
||||
@@ -11,9 +11,9 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.channels.base import Channel
|
||||
from src.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage
|
||||
from src.channels.store import ChannelStore
|
||||
from app.channels.base import Channel
|
||||
from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage
|
||||
from app.channels.store import ChannelStore
|
||||
|
||||
|
||||
def _run(coro):
|
||||
@@ -277,19 +277,19 @@ class TestChannelBase:
|
||||
|
||||
class TestExtractResponseText:
|
||||
def test_string_content(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {"messages": [{"type": "ai", "content": "hello"}]}
|
||||
assert _extract_response_text(result) == "hello"
|
||||
|
||||
def test_list_content_blocks(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {"messages": [{"type": "ai", "content": [{"type": "text", "text": "hello"}, {"type": "text", "text": " world"}]}]}
|
||||
assert _extract_response_text(result) == "hello world"
|
||||
|
||||
def test_picks_last_ai_message(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -301,24 +301,24 @@ class TestExtractResponseText:
|
||||
assert _extract_response_text(result) == "second"
|
||||
|
||||
def test_empty_messages(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
assert _extract_response_text({"messages": []}) == ""
|
||||
|
||||
def test_no_ai_messages(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {"messages": [{"type": "human", "content": "hi"}]}
|
||||
assert _extract_response_text(result) == ""
|
||||
|
||||
def test_list_result(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = [{"type": "ai", "content": "from list"}]
|
||||
assert _extract_response_text(result) == "from list"
|
||||
|
||||
def test_skips_empty_ai_content(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -329,7 +329,7 @@ class TestExtractResponseText:
|
||||
assert _extract_response_text(result) == "actual response"
|
||||
|
||||
def test_clarification_tool_message(self):
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -342,7 +342,7 @@ class TestExtractResponseText:
|
||||
|
||||
def test_clarification_over_empty_ai(self):
|
||||
"""When AI content is empty but ask_clarification tool message exists, use the tool message."""
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -354,7 +354,7 @@ class TestExtractResponseText:
|
||||
|
||||
def test_does_not_leak_previous_turn_text(self):
|
||||
"""When current turn AI has no text (only tool calls), do not return previous turn's text."""
|
||||
from src.channels.manager import _extract_response_text
|
||||
from app.channels.manager import _extract_response_text
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -415,7 +415,7 @@ def _make_async_iterator(items):
|
||||
|
||||
class TestChannelManager:
|
||||
def test_handle_chat_creates_thread(self):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -459,7 +459,7 @@ class TestChannelManager:
|
||||
_run(go())
|
||||
|
||||
def test_handle_chat_uses_channel_session_overrides(self):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -506,7 +506,7 @@ class TestChannelManager:
|
||||
_run(go())
|
||||
|
||||
def test_handle_chat_uses_user_session_overrides(self):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -565,9 +565,9 @@ class TestChannelManager:
|
||||
_run(go())
|
||||
|
||||
def test_handle_feishu_chat_streams_multiple_outbound_updates(self, monkeypatch):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
monkeypatch.setattr("src.channels.manager.STREAM_UPDATE_MIN_INTERVAL_SECONDS", 0.0)
|
||||
monkeypatch.setattr("app.channels.manager.STREAM_UPDATE_MIN_INTERVAL_SECONDS", 0.0)
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -634,9 +634,9 @@ class TestChannelManager:
|
||||
|
||||
def test_handle_feishu_stream_error_still_sends_final(self, monkeypatch):
|
||||
"""When the stream raises mid-way, a final outbound with is_final=True must still be published."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
monkeypatch.setattr("src.channels.manager.STREAM_UPDATE_MIN_INTERVAL_SECONDS", 0.0)
|
||||
monkeypatch.setattr("app.channels.manager.STREAM_UPDATE_MIN_INTERVAL_SECONDS", 0.0)
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -685,7 +685,7 @@ class TestChannelManager:
|
||||
_run(go())
|
||||
|
||||
def test_handle_command_help(self):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -718,7 +718,7 @@ class TestChannelManager:
|
||||
_run(go())
|
||||
|
||||
def test_handle_command_new(self):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -761,7 +761,7 @@ class TestChannelManager:
|
||||
|
||||
def test_each_topic_creates_new_thread(self):
|
||||
"""Messages with distinct topic_ids should each create a new DeerFlow thread."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -813,7 +813,7 @@ class TestChannelManager:
|
||||
|
||||
def test_same_topic_reuses_thread(self):
|
||||
"""Messages with the same topic_id should reuse the same DeerFlow thread."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -857,7 +857,7 @@ class TestChannelManager:
|
||||
|
||||
def test_none_topic_reuses_thread(self):
|
||||
"""Messages with topic_id=None should reuse the same thread (e.g. Telegram private chat)."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -901,7 +901,7 @@ class TestChannelManager:
|
||||
|
||||
def test_different_topics_get_different_threads(self):
|
||||
"""Messages with different topic_ids should create separate threads."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -951,7 +951,7 @@ class TestChannelManager:
|
||||
|
||||
class TestExtractArtifacts:
|
||||
def test_extracts_from_present_files_tool_call(self):
|
||||
from src.channels.manager import _extract_artifacts
|
||||
from app.channels.manager import _extract_artifacts
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -969,7 +969,7 @@ class TestExtractArtifacts:
|
||||
assert _extract_artifacts(result) == ["/mnt/user-data/outputs/report.md"]
|
||||
|
||||
def test_empty_when_no_present_files(self):
|
||||
from src.channels.manager import _extract_artifacts
|
||||
from app.channels.manager import _extract_artifacts
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -980,14 +980,14 @@ class TestExtractArtifacts:
|
||||
assert _extract_artifacts(result) == []
|
||||
|
||||
def test_empty_for_list_result_no_tool_calls(self):
|
||||
from src.channels.manager import _extract_artifacts
|
||||
from app.channels.manager import _extract_artifacts
|
||||
|
||||
result = [{"type": "ai", "content": "hello"}]
|
||||
assert _extract_artifacts(result) == []
|
||||
|
||||
def test_only_extracts_after_last_human_message(self):
|
||||
"""Artifacts from previous turns (before the last human message) should be ignored."""
|
||||
from src.channels.manager import _extract_artifacts
|
||||
from app.channels.manager import _extract_artifacts
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -1015,7 +1015,7 @@ class TestExtractArtifacts:
|
||||
assert _extract_artifacts(result) == ["/mnt/user-data/outputs/chart.png"]
|
||||
|
||||
def test_multiple_files_in_single_call(self):
|
||||
from src.channels.manager import _extract_artifacts
|
||||
from app.channels.manager import _extract_artifacts
|
||||
|
||||
result = {
|
||||
"messages": [
|
||||
@@ -1034,13 +1034,13 @@ class TestExtractArtifacts:
|
||||
|
||||
class TestFormatArtifactText:
|
||||
def test_single_artifact(self):
|
||||
from src.channels.manager import _format_artifact_text
|
||||
from app.channels.manager import _format_artifact_text
|
||||
|
||||
text = _format_artifact_text(["/mnt/user-data/outputs/report.md"])
|
||||
assert text == "Created File: 📎 report.md"
|
||||
|
||||
def test_multiple_artifacts(self):
|
||||
from src.channels.manager import _format_artifact_text
|
||||
from app.channels.manager import _format_artifact_text
|
||||
|
||||
text = _format_artifact_text(
|
||||
["/mnt/user-data/outputs/a.txt", "/mnt/user-data/outputs/b.csv"],
|
||||
@@ -1050,7 +1050,7 @@ class TestFormatArtifactText:
|
||||
|
||||
class TestHandleChatWithArtifacts:
|
||||
def test_artifacts_appended_to_text(self):
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1097,7 +1097,7 @@ class TestHandleChatWithArtifacts:
|
||||
|
||||
def test_artifacts_only_no_text(self):
|
||||
"""When agent produces artifacts but no text, the artifacts should be the response."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1145,7 +1145,7 @@ class TestHandleChatWithArtifacts:
|
||||
|
||||
def test_only_last_turn_artifacts_returned(self):
|
||||
"""Only artifacts from the current turn's present_files calls should be included."""
|
||||
from src.channels.manager import ChannelManager
|
||||
from app.channels.manager import ChannelManager
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1228,7 +1228,7 @@ class TestHandleChatWithArtifacts:
|
||||
|
||||
class TestFeishuChannel:
|
||||
def test_prepare_inbound_publishes_without_waiting_for_running_card(self):
|
||||
from src.channels.feishu import FeishuChannel
|
||||
from app.channels.feishu import FeishuChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1270,7 +1270,7 @@ class TestFeishuChannel:
|
||||
_run(go())
|
||||
|
||||
def test_prepare_inbound_and_send_share_running_card_task(self):
|
||||
from src.channels.feishu import FeishuChannel
|
||||
from app.channels.feishu import FeishuChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1339,7 +1339,7 @@ class TestFeishuChannel:
|
||||
ReplyMessageRequestBody,
|
||||
)
|
||||
|
||||
from src.channels.feishu import FeishuChannel
|
||||
from app.channels.feishu import FeishuChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1402,7 +1402,7 @@ class TestFeishuChannel:
|
||||
|
||||
class TestChannelService:
|
||||
def test_get_status_no_channels(self):
|
||||
from src.channels.service import ChannelService
|
||||
from app.channels.service import ChannelService
|
||||
|
||||
async def go():
|
||||
service = ChannelService(channels_config={})
|
||||
@@ -1419,7 +1419,7 @@ class TestChannelService:
|
||||
_run(go())
|
||||
|
||||
def test_disabled_channels_are_skipped(self):
|
||||
from src.channels.service import ChannelService
|
||||
from app.channels.service import ChannelService
|
||||
|
||||
async def go():
|
||||
service = ChannelService(
|
||||
@@ -1434,7 +1434,7 @@ class TestChannelService:
|
||||
_run(go())
|
||||
|
||||
def test_session_config_is_forwarded_to_manager(self):
|
||||
from src.channels.service import ChannelService
|
||||
from app.channels.service import ChannelService
|
||||
|
||||
service = ChannelService(
|
||||
channels_config={
|
||||
@@ -1465,7 +1465,7 @@ class TestChannelService:
|
||||
|
||||
class TestSlackSendRetry:
|
||||
def test_retries_on_failure_then_succeeds(self):
|
||||
from src.channels.slack import SlackChannel
|
||||
from app.channels.slack import SlackChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1491,7 +1491,7 @@ class TestSlackSendRetry:
|
||||
_run(go())
|
||||
|
||||
def test_raises_after_all_retries_exhausted(self):
|
||||
from src.channels.slack import SlackChannel
|
||||
from app.channels.slack import SlackChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1517,7 +1517,7 @@ class TestSlackSendRetry:
|
||||
|
||||
class TestTelegramSendRetry:
|
||||
def test_retries_on_failure_then_succeeds(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1547,7 +1547,7 @@ class TestTelegramSendRetry:
|
||||
_run(go())
|
||||
|
||||
def test_raises_after_all_retries_exhausted(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1594,7 +1594,7 @@ class TestTelegramPrivateChatThread:
|
||||
"""Verify that private chats use topic_id=None (single thread per chat)."""
|
||||
|
||||
def test_private_chat_no_reply_uses_none_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1610,7 +1610,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_private_chat_with_reply_still_uses_none_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1626,7 +1626,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_group_chat_no_reply_uses_msg_id_as_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1642,7 +1642,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_group_chat_reply_uses_reply_msg_id_as_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1658,7 +1658,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_supergroup_chat_uses_msg_id_as_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1674,7 +1674,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_cmd_generic_private_chat_uses_none_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1691,7 +1691,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_cmd_generic_group_chat_uses_msg_id_as_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1708,7 +1708,7 @@ class TestTelegramPrivateChatThread:
|
||||
_run(go())
|
||||
|
||||
def test_cmd_generic_group_chat_reply_uses_reply_msg_id_as_topic(self):
|
||||
from src.channels.telegram import TelegramChannel
|
||||
from app.channels.telegram import TelegramChannel
|
||||
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
@@ -1734,20 +1734,20 @@ class TestSlackMarkdownConversion:
|
||||
"""Verify that the SlackChannel.send() path applies mrkdwn conversion."""
|
||||
|
||||
def test_bold_converted(self):
|
||||
from src.channels.slack import _slack_md_converter
|
||||
from app.channels.slack import _slack_md_converter
|
||||
|
||||
result = _slack_md_converter.convert("this is **bold** text")
|
||||
assert "*bold*" in result
|
||||
assert "**" not in result
|
||||
|
||||
def test_link_converted(self):
|
||||
from src.channels.slack import _slack_md_converter
|
||||
from app.channels.slack import _slack_md_converter
|
||||
|
||||
result = _slack_md_converter.convert("[click](https://example.com)")
|
||||
assert "<https://example.com|click>" in result
|
||||
|
||||
def test_heading_converted(self):
|
||||
from src.channels.slack import _slack_md_converter
|
||||
from app.channels.slack import _slack_md_converter
|
||||
|
||||
result = _slack_md_converter.convert("# Title")
|
||||
assert "*Title*" in result
|
||||
|
||||
@@ -5,8 +5,8 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.checkpointer import get_checkpointer, reset_checkpointer
|
||||
from src.config.checkpointer_config import (
|
||||
from deerflow.agents.checkpointer import get_checkpointer, reset_checkpointer
|
||||
from deerflow.config.checkpointer_config import (
|
||||
CheckpointerConfig,
|
||||
get_checkpointer_config,
|
||||
load_checkpointer_config_from_dict,
|
||||
@@ -195,7 +195,7 @@ class TestClientCheckpointerFallback:
|
||||
"""DeerFlowClient._ensure_agent falls back to get_checkpointer() when checkpointer=None."""
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from src.client import DeerFlowClient
|
||||
from deerflow.client import DeerFlowClient
|
||||
|
||||
load_checkpointer_config_from_dict({"type": "memory"})
|
||||
|
||||
@@ -212,12 +212,12 @@ class TestClientCheckpointerFallback:
|
||||
config_mock.checkpointer = None
|
||||
|
||||
with (
|
||||
patch("src.client.get_app_config", return_value=config_mock),
|
||||
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("src.client.create_chat_model", return_value=MagicMock()),
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value=""),
|
||||
patch("src.client.DeerFlowClient._get_tools", return_value=[]),
|
||||
patch("deerflow.client.get_app_config", return_value=config_mock),
|
||||
patch("deerflow.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("deerflow.client.create_chat_model", return_value=MagicMock()),
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value=""),
|
||||
patch("deerflow.client.DeerFlowClient._get_tools", return_value=[]),
|
||||
):
|
||||
client = DeerFlowClient(checkpointer=None)
|
||||
config = client._get_runnable_config("test-thread")
|
||||
@@ -228,7 +228,7 @@ class TestClientCheckpointerFallback:
|
||||
|
||||
def test_client_explicit_checkpointer_takes_precedence(self):
|
||||
"""An explicitly provided checkpointer is used even when config checkpointer is set."""
|
||||
from src.client import DeerFlowClient
|
||||
from deerflow.client import DeerFlowClient
|
||||
|
||||
load_checkpointer_config_from_dict({"type": "memory"})
|
||||
|
||||
@@ -246,12 +246,12 @@ class TestClientCheckpointerFallback:
|
||||
config_mock.checkpointer = None
|
||||
|
||||
with (
|
||||
patch("src.client.get_app_config", return_value=config_mock),
|
||||
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("src.client.create_chat_model", return_value=MagicMock()),
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value=""),
|
||||
patch("src.client.DeerFlowClient._get_tools", return_value=[]),
|
||||
patch("deerflow.client.get_app_config", return_value=config_mock),
|
||||
patch("deerflow.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("deerflow.client.create_chat_model", return_value=MagicMock()),
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value=""),
|
||||
patch("deerflow.client.DeerFlowClient._get_tools", return_value=[]),
|
||||
):
|
||||
client = DeerFlowClient(checkpointer=explicit_cp)
|
||||
config = client._get_runnable_config("test-thread")
|
||||
|
||||
@@ -12,13 +12,13 @@ class TestCheckpointerNoneFix:
|
||||
@pytest.mark.anyio
|
||||
async def test_async_make_checkpointer_returns_in_memory_saver_when_not_configured(self):
|
||||
"""make_checkpointer should return InMemorySaver when config.checkpointer is None."""
|
||||
from src.agents.checkpointer.async_provider import make_checkpointer
|
||||
from deerflow.agents.checkpointer.async_provider import make_checkpointer
|
||||
|
||||
# Mock get_app_config to return a config with checkpointer=None
|
||||
mock_config = MagicMock()
|
||||
mock_config.checkpointer = None
|
||||
|
||||
with patch("src.agents.checkpointer.async_provider.get_app_config", return_value=mock_config):
|
||||
with patch("deerflow.agents.checkpointer.async_provider.get_app_config", return_value=mock_config):
|
||||
async with make_checkpointer() as checkpointer:
|
||||
# Should return InMemorySaver, not None
|
||||
assert checkpointer is not None
|
||||
@@ -35,13 +35,13 @@ class TestCheckpointerNoneFix:
|
||||
|
||||
def test_sync_checkpointer_context_returns_in_memory_saver_when_not_configured(self):
|
||||
"""checkpointer_context should return InMemorySaver when config.checkpointer is None."""
|
||||
from src.agents.checkpointer.provider import checkpointer_context
|
||||
from deerflow.agents.checkpointer.provider import checkpointer_context
|
||||
|
||||
# Mock get_app_config to return a config with checkpointer=None
|
||||
mock_config = MagicMock()
|
||||
mock_config.checkpointer = None
|
||||
|
||||
with patch("src.agents.checkpointer.provider.get_app_config", return_value=mock_config):
|
||||
with patch("deerflow.agents.checkpointer.provider.get_app_config", return_value=mock_config):
|
||||
with checkpointer_context() as checkpointer:
|
||||
# Should return InMemorySaver, not None
|
||||
assert checkpointer is not None
|
||||
|
||||
@@ -11,12 +11,12 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage # noqa: F401
|
||||
|
||||
from src.client import DeerFlowClient
|
||||
from src.gateway.routers.mcp import McpConfigResponse
|
||||
from src.gateway.routers.memory import MemoryConfigResponse, MemoryStatusResponse
|
||||
from src.gateway.routers.models import ModelResponse, ModelsListResponse
|
||||
from src.gateway.routers.skills import SkillInstallResponse, SkillResponse, SkillsListResponse
|
||||
from src.gateway.routers.uploads import UploadResponse
|
||||
from app.gateway.routers.mcp import McpConfigResponse
|
||||
from app.gateway.routers.memory import MemoryConfigResponse, MemoryStatusResponse
|
||||
from app.gateway.routers.models import ModelResponse, ModelsListResponse
|
||||
from app.gateway.routers.skills import SkillInstallResponse, SkillResponse, SkillsListResponse
|
||||
from app.gateway.routers.uploads import UploadResponse
|
||||
from deerflow.client import DeerFlowClient
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
@@ -40,7 +40,7 @@ def mock_app_config():
|
||||
@pytest.fixture
|
||||
def client(mock_app_config):
|
||||
"""Create a DeerFlowClient with mocked config loading."""
|
||||
with patch("src.client.get_app_config", return_value=mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
return DeerFlowClient()
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class TestClientInit:
|
||||
assert client._agent is None
|
||||
|
||||
def test_custom_params(self, mock_app_config):
|
||||
with patch("src.client.get_app_config", return_value=mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
c = DeerFlowClient(
|
||||
model_name="gpt-4",
|
||||
thinking_enabled=False,
|
||||
@@ -73,15 +73,15 @@ class TestClientInit:
|
||||
|
||||
def test_custom_config_path(self, mock_app_config):
|
||||
with (
|
||||
patch("src.client.reload_app_config") as mock_reload,
|
||||
patch("src.client.get_app_config", return_value=mock_app_config),
|
||||
patch("deerflow.client.reload_app_config") as mock_reload,
|
||||
patch("deerflow.client.get_app_config", return_value=mock_app_config),
|
||||
):
|
||||
DeerFlowClient(config_path="/tmp/custom.yaml")
|
||||
mock_reload.assert_called_once_with("/tmp/custom.yaml")
|
||||
|
||||
def test_checkpointer_stored(self, mock_app_config):
|
||||
cp = MagicMock()
|
||||
with patch("src.client.get_app_config", return_value=mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
c = DeerFlowClient(checkpointer=cp)
|
||||
assert c._checkpointer is cp
|
||||
|
||||
@@ -109,7 +109,7 @@ class TestConfigQueries:
|
||||
skill.category = "public"
|
||||
skill.enabled = True
|
||||
|
||||
with patch("src.skills.loader.load_skills", return_value=[skill]) as mock_load:
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[skill]) as mock_load:
|
||||
result = client.list_skills()
|
||||
mock_load.assert_called_once_with(enabled_only=False)
|
||||
|
||||
@@ -124,13 +124,13 @@ class TestConfigQueries:
|
||||
}
|
||||
|
||||
def test_list_skills_enabled_only(self, client):
|
||||
with patch("src.skills.loader.load_skills", return_value=[]) as mock_load:
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[]) as mock_load:
|
||||
client.list_skills(enabled_only=True)
|
||||
mock_load.assert_called_once_with(enabled_only=True)
|
||||
|
||||
def test_get_memory(self, client):
|
||||
memory = {"version": "1.0", "facts": []}
|
||||
with patch("src.agents.memory.updater.get_memory_data", return_value=memory) as mock_mem:
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=memory) as mock_mem:
|
||||
result = client.get_memory()
|
||||
mock_mem.assert_called_once()
|
||||
assert result == memory
|
||||
@@ -355,10 +355,10 @@ class TestEnsureAgent:
|
||||
config = client._get_runnable_config("t1")
|
||||
|
||||
with (
|
||||
patch("src.client.create_chat_model"),
|
||||
patch("src.client.create_agent", return_value=mock_agent),
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value="prompt"),
|
||||
patch("deerflow.client.create_chat_model"),
|
||||
patch("deerflow.client.create_agent", return_value=mock_agent),
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
@@ -371,12 +371,12 @@ class TestEnsureAgent:
|
||||
config = client._get_runnable_config("t1")
|
||||
|
||||
with (
|
||||
patch("src.client.create_chat_model"),
|
||||
patch("src.client.create_agent", return_value=mock_agent) as mock_create_agent,
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value="prompt"),
|
||||
patch("deerflow.client.create_chat_model"),
|
||||
patch("deerflow.client.create_agent", return_value=mock_agent) as mock_create_agent,
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("src.agents.checkpointer.get_checkpointer", return_value=mock_checkpointer),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=mock_checkpointer),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
|
||||
@@ -387,12 +387,12 @@ class TestEnsureAgent:
|
||||
config = client._get_runnable_config("t1")
|
||||
|
||||
with (
|
||||
patch("src.client.create_chat_model"),
|
||||
patch("src.client.create_agent", return_value=mock_agent) as mock_create_agent,
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value="prompt"),
|
||||
patch("deerflow.client.create_chat_model"),
|
||||
patch("deerflow.client.create_agent", return_value=mock_agent) as mock_create_agent,
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("src.agents.checkpointer.get_checkpointer", return_value=None),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=None),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
|
||||
@@ -452,7 +452,7 @@ class TestMcpConfig:
|
||||
ext_config = MagicMock()
|
||||
ext_config.mcp_servers = {"github": server}
|
||||
|
||||
with patch("src.client.get_extensions_config", return_value=ext_config):
|
||||
with patch("deerflow.client.get_extensions_config", return_value=ext_config):
|
||||
result = client.get_mcp_config()
|
||||
|
||||
assert "mcp_servers" in result
|
||||
@@ -478,9 +478,9 @@ class TestMcpConfig:
|
||||
client._agent = MagicMock()
|
||||
|
||||
with (
|
||||
patch("src.client.ExtensionsConfig.resolve_config_path", return_value=tmp_path),
|
||||
patch("src.client.get_extensions_config", return_value=current_config),
|
||||
patch("src.client.reload_extensions_config", return_value=reloaded_config),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=tmp_path),
|
||||
patch("deerflow.client.get_extensions_config", return_value=current_config),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=reloaded_config),
|
||||
):
|
||||
result = client.update_mcp_config({"new-server": {"enabled": True, "type": "sse"}})
|
||||
|
||||
@@ -513,13 +513,13 @@ class TestSkillsManagement:
|
||||
|
||||
def test_get_skill_found(self, client):
|
||||
skill = self._make_skill()
|
||||
with patch("src.skills.loader.load_skills", return_value=[skill]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[skill]):
|
||||
result = client.get_skill("test-skill")
|
||||
assert result is not None
|
||||
assert result["name"] == "test-skill"
|
||||
|
||||
def test_get_skill_not_found(self, client):
|
||||
with patch("src.skills.loader.load_skills", return_value=[]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[]):
|
||||
result = client.get_skill("nonexistent")
|
||||
assert result is None
|
||||
|
||||
@@ -540,10 +540,10 @@ class TestSkillsManagement:
|
||||
client._agent = MagicMock()
|
||||
|
||||
with (
|
||||
patch("src.skills.loader.load_skills", side_effect=[[skill], [updated_skill]]),
|
||||
patch("src.client.ExtensionsConfig.resolve_config_path", return_value=tmp_path),
|
||||
patch("src.client.get_extensions_config", return_value=ext_config),
|
||||
patch("src.client.reload_extensions_config"),
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], [updated_skill]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=tmp_path),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
):
|
||||
result = client.update_skill("test-skill", enabled=False)
|
||||
assert result["enabled"] is False
|
||||
@@ -552,7 +552,7 @@ class TestSkillsManagement:
|
||||
tmp_path.unlink()
|
||||
|
||||
def test_update_skill_not_found(self, client):
|
||||
with patch("src.skills.loader.load_skills", return_value=[]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[]):
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
client.update_skill("nonexistent", enabled=True)
|
||||
|
||||
@@ -573,8 +573,8 @@ class TestSkillsManagement:
|
||||
(skills_root / "custom").mkdir(parents=True)
|
||||
|
||||
with (
|
||||
patch("src.skills.loader.get_skills_root_path", return_value=skills_root),
|
||||
patch("src.gateway.routers.skills._validate_skill_frontmatter", return_value=(True, "OK", "my-skill")),
|
||||
patch("deerflow.skills.loader.get_skills_root_path", return_value=skills_root),
|
||||
patch("deerflow.skills.validation._validate_skill_frontmatter", return_value=(True, "OK", "my-skill")),
|
||||
):
|
||||
result = client.install_skill(archive_path)
|
||||
|
||||
@@ -604,7 +604,7 @@ class TestSkillsManagement:
|
||||
class TestMemoryManagement:
|
||||
def test_reload_memory(self, client):
|
||||
data = {"version": "1.0", "facts": []}
|
||||
with patch("src.agents.memory.updater.reload_memory_data", return_value=data):
|
||||
with patch("deerflow.agents.memory.updater.reload_memory_data", return_value=data):
|
||||
result = client.reload_memory()
|
||||
assert result == data
|
||||
|
||||
@@ -618,7 +618,7 @@ class TestMemoryManagement:
|
||||
config.injection_enabled = True
|
||||
config.max_injection_tokens = 2000
|
||||
|
||||
with patch("src.config.memory_config.get_memory_config", return_value=config):
|
||||
with patch("deerflow.config.memory_config.get_memory_config", return_value=config):
|
||||
result = client.get_memory_config()
|
||||
|
||||
assert result["enabled"] is True
|
||||
@@ -637,8 +637,8 @@ class TestMemoryManagement:
|
||||
data = {"version": "1.0", "facts": []}
|
||||
|
||||
with (
|
||||
patch("src.config.memory_config.get_memory_config", return_value=config),
|
||||
patch("src.agents.memory.updater.get_memory_data", return_value=data),
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=config),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=data),
|
||||
):
|
||||
result = client.get_memory_status()
|
||||
|
||||
@@ -720,8 +720,8 @@ class TestUploads:
|
||||
|
||||
with (
|
||||
patch.object(DeerFlowClient, "_get_uploads_dir", return_value=uploads_dir),
|
||||
patch("src.gateway.routers.uploads.CONVERTIBLE_EXTENSIONS", {".pdf"}),
|
||||
patch("src.gateway.routers.uploads.convert_file_to_markdown", side_effect=fake_convert),
|
||||
patch("deerflow.utils.file_conversion.CONVERTIBLE_EXTENSIONS", {".pdf"}),
|
||||
patch("deerflow.utils.file_conversion.convert_file_to_markdown", side_effect=fake_convert),
|
||||
patch("concurrent.futures.ThreadPoolExecutor", FakeExecutor),
|
||||
):
|
||||
result = asyncio.run(call_upload())
|
||||
@@ -793,7 +793,7 @@ class TestArtifacts:
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.sandbox_user_data_dir.return_value = user_data_dir
|
||||
|
||||
with patch("src.client.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.client.get_paths", return_value=mock_paths):
|
||||
content, mime = client.get_artifact("t1", "mnt/user-data/outputs/result.txt")
|
||||
|
||||
assert content == b"artifact content"
|
||||
@@ -807,7 +807,7 @@ class TestArtifacts:
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.sandbox_user_data_dir.return_value = user_data_dir
|
||||
|
||||
with patch("src.client.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.client.get_paths", return_value=mock_paths):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
client.get_artifact("t1", "mnt/user-data/outputs/nope.txt")
|
||||
|
||||
@@ -823,7 +823,7 @@ class TestArtifacts:
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.sandbox_user_data_dir.return_value = user_data_dir
|
||||
|
||||
with patch("src.client.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.client.get_paths", return_value=mock_paths):
|
||||
with pytest.raises(PermissionError):
|
||||
client.get_artifact("t1", "mnt/user-data/../../../etc/passwd")
|
||||
|
||||
@@ -1028,7 +1028,7 @@ class TestScenarioFileLifecycle:
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.sandbox_user_data_dir.return_value = user_data_dir
|
||||
|
||||
with patch("src.client.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.client.get_paths", return_value=mock_paths):
|
||||
content, mime = client.get_artifact("t-artifact", "mnt/user-data/outputs/analysis.json")
|
||||
|
||||
assert json.loads(content) == {"result": "processed"}
|
||||
@@ -1064,12 +1064,12 @@ class TestScenarioConfigManagement:
|
||||
skill.category = "public"
|
||||
skill.enabled = True
|
||||
|
||||
with patch("src.skills.loader.load_skills", return_value=[skill]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[skill]):
|
||||
skills_result = client.list_skills()
|
||||
assert len(skills_result["skills"]) == 1
|
||||
|
||||
# Get specific skill
|
||||
with patch("src.skills.loader.load_skills", return_value=[skill]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[skill]):
|
||||
detail = client.get_skill("web-search")
|
||||
assert detail is not None
|
||||
assert detail["enabled"] is True
|
||||
@@ -1091,9 +1091,9 @@ class TestScenarioConfigManagement:
|
||||
|
||||
client._agent = MagicMock() # Simulate existing agent
|
||||
with (
|
||||
patch("src.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("src.client.get_extensions_config", return_value=current_config),
|
||||
patch("src.client.reload_extensions_config", return_value=reloaded_config),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=current_config),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=reloaded_config),
|
||||
):
|
||||
mcp_result = client.update_mcp_config({"my-mcp": {"enabled": True}})
|
||||
assert "my-mcp" in mcp_result["mcp_servers"]
|
||||
@@ -1120,10 +1120,10 @@ class TestScenarioConfigManagement:
|
||||
|
||||
client._agent = MagicMock() # Simulate re-created agent
|
||||
with (
|
||||
patch("src.skills.loader.load_skills", side_effect=[[skill], [toggled]]),
|
||||
patch("src.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("src.client.get_extensions_config", return_value=ext_config),
|
||||
patch("src.client.reload_extensions_config"),
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], [toggled]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
):
|
||||
skill_result = client.update_skill("code-gen", enabled=False)
|
||||
assert skill_result["enabled"] is False
|
||||
@@ -1146,10 +1146,10 @@ class TestScenarioAgentRecreation:
|
||||
config_b = client._get_runnable_config("t1", model_name="claude-3")
|
||||
|
||||
with (
|
||||
patch("src.client.create_chat_model"),
|
||||
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value="prompt"),
|
||||
patch("deerflow.client.create_chat_model"),
|
||||
patch("deerflow.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
):
|
||||
client._ensure_agent(config_a)
|
||||
@@ -1173,10 +1173,10 @@ class TestScenarioAgentRecreation:
|
||||
config = client._get_runnable_config("t1", model_name="gpt-4")
|
||||
|
||||
with (
|
||||
patch("src.client.create_chat_model"),
|
||||
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value="prompt"),
|
||||
patch("deerflow.client.create_chat_model"),
|
||||
patch("deerflow.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
@@ -1197,10 +1197,10 @@ class TestScenarioAgentRecreation:
|
||||
config = client._get_runnable_config("t1")
|
||||
|
||||
with (
|
||||
patch("src.client.create_chat_model"),
|
||||
patch("src.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("src.client._build_middlewares", return_value=[]),
|
||||
patch("src.client.apply_prompt_template", return_value="prompt"),
|
||||
patch("deerflow.client.create_chat_model"),
|
||||
patch("deerflow.client.create_agent", side_effect=fake_create_agent),
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
@@ -1271,7 +1271,7 @@ class TestScenarioThreadIsolation:
|
||||
mock_paths = MagicMock()
|
||||
mock_paths.sandbox_user_data_dir.side_effect = lambda tid: data_a if tid == "thread-a" else data_b
|
||||
|
||||
with patch("src.client.get_paths", return_value=mock_paths):
|
||||
with patch("deerflow.client.get_paths", return_value=mock_paths):
|
||||
content, _ = client.get_artifact("thread-a", "mnt/user-data/outputs/result.txt")
|
||||
assert content == b"thread-a artifact"
|
||||
|
||||
@@ -1302,17 +1302,17 @@ class TestScenarioMemoryWorkflow:
|
||||
config.injection_enabled = True
|
||||
config.max_injection_tokens = 2000
|
||||
|
||||
with patch("src.agents.memory.updater.get_memory_data", return_value=initial_data):
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=initial_data):
|
||||
mem = client.get_memory()
|
||||
assert len(mem["facts"]) == 1
|
||||
|
||||
with patch("src.agents.memory.updater.reload_memory_data", return_value=updated_data):
|
||||
with patch("deerflow.agents.memory.updater.reload_memory_data", return_value=updated_data):
|
||||
refreshed = client.reload_memory()
|
||||
assert len(refreshed["facts"]) == 2
|
||||
|
||||
with (
|
||||
patch("src.config.memory_config.get_memory_config", return_value=config),
|
||||
patch("src.agents.memory.updater.get_memory_data", return_value=updated_data),
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=config),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=updated_data),
|
||||
):
|
||||
status = client.get_memory_status()
|
||||
assert status["config"]["enabled"] is True
|
||||
@@ -1340,8 +1340,8 @@ class TestScenarioSkillInstallAndUse:
|
||||
|
||||
# Step 1: Install
|
||||
with (
|
||||
patch("src.skills.loader.get_skills_root_path", return_value=skills_root),
|
||||
patch("src.gateway.routers.skills._validate_skill_frontmatter", return_value=(True, "OK", "my-analyzer")),
|
||||
patch("deerflow.skills.loader.get_skills_root_path", return_value=skills_root),
|
||||
patch("deerflow.skills.validation._validate_skill_frontmatter", return_value=(True, "OK", "my-analyzer")),
|
||||
):
|
||||
result = client.install_skill(archive)
|
||||
assert result["success"] is True
|
||||
@@ -1355,7 +1355,7 @@ class TestScenarioSkillInstallAndUse:
|
||||
installed_skill.category = "custom"
|
||||
installed_skill.enabled = True
|
||||
|
||||
with patch("src.skills.loader.load_skills", return_value=[installed_skill]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[installed_skill]):
|
||||
skills_result = client.list_skills()
|
||||
assert any(s["name"] == "my-analyzer" for s in skills_result["skills"])
|
||||
|
||||
@@ -1375,10 +1375,10 @@ class TestScenarioSkillInstallAndUse:
|
||||
config_file.write_text("{}")
|
||||
|
||||
with (
|
||||
patch("src.skills.loader.load_skills", side_effect=[[installed_skill], [disabled_skill]]),
|
||||
patch("src.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("src.client.get_extensions_config", return_value=ext_config),
|
||||
patch("src.client.reload_extensions_config"),
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[installed_skill], [disabled_skill]]),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.reload_extensions_config"),
|
||||
):
|
||||
toggled = client.update_skill("my-analyzer", enabled=False)
|
||||
assert toggled["enabled"] is False
|
||||
@@ -1475,8 +1475,8 @@ class TestScenarioEdgeCases:
|
||||
|
||||
with (
|
||||
patch.object(DeerFlowClient, "_get_uploads_dir", return_value=uploads_dir),
|
||||
patch("src.gateway.routers.uploads.CONVERTIBLE_EXTENSIONS", {".pdf"}),
|
||||
patch("src.gateway.routers.uploads.convert_file_to_markdown", side_effect=Exception("conversion failed")),
|
||||
patch("deerflow.utils.file_conversion.CONVERTIBLE_EXTENSIONS", {".pdf"}),
|
||||
patch("deerflow.utils.file_conversion.convert_file_to_markdown", side_effect=Exception("conversion failed")),
|
||||
):
|
||||
result = client.upload_files("t-pdf-fail", [pdf_file])
|
||||
|
||||
@@ -1508,7 +1508,7 @@ class TestGatewayConformance:
|
||||
model.supports_thinking = False
|
||||
mock_app_config.models = [model]
|
||||
|
||||
with patch("src.client.get_app_config", return_value=mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
client = DeerFlowClient()
|
||||
|
||||
result = client.list_models()
|
||||
@@ -1525,7 +1525,7 @@ class TestGatewayConformance:
|
||||
mock_app_config.models = [model]
|
||||
mock_app_config.get_model_config.return_value = model
|
||||
|
||||
with patch("src.client.get_app_config", return_value=mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
client = DeerFlowClient()
|
||||
|
||||
result = client.get_model("test-model")
|
||||
@@ -1541,7 +1541,7 @@ class TestGatewayConformance:
|
||||
skill.category = "public"
|
||||
skill.enabled = True
|
||||
|
||||
with patch("src.skills.loader.load_skills", return_value=[skill]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[skill]):
|
||||
result = client.list_skills()
|
||||
|
||||
parsed = SkillsListResponse(**result)
|
||||
@@ -1556,7 +1556,7 @@ class TestGatewayConformance:
|
||||
skill.category = "public"
|
||||
skill.enabled = True
|
||||
|
||||
with patch("src.skills.loader.load_skills", return_value=[skill]):
|
||||
with patch("deerflow.skills.loader.load_skills", return_value=[skill]):
|
||||
result = client.get_skill("web-search")
|
||||
|
||||
assert result is not None
|
||||
@@ -1574,7 +1574,7 @@ class TestGatewayConformance:
|
||||
|
||||
custom_dir = tmp_path / "custom"
|
||||
custom_dir.mkdir()
|
||||
with patch("src.skills.loader.get_skills_root_path", return_value=tmp_path):
|
||||
with patch("deerflow.skills.loader.get_skills_root_path", return_value=tmp_path):
|
||||
result = client.install_skill(archive)
|
||||
|
||||
parsed = SkillInstallResponse(**result)
|
||||
@@ -1596,7 +1596,7 @@ class TestGatewayConformance:
|
||||
ext_config = MagicMock()
|
||||
ext_config.mcp_servers = {"test": server}
|
||||
|
||||
with patch("src.client.get_extensions_config", return_value=ext_config):
|
||||
with patch("deerflow.client.get_extensions_config", return_value=ext_config):
|
||||
result = client.get_mcp_config()
|
||||
|
||||
parsed = McpConfigResponse(**result)
|
||||
@@ -1622,9 +1622,9 @@ class TestGatewayConformance:
|
||||
config_file.write_text("{}")
|
||||
|
||||
with (
|
||||
patch("src.client.get_extensions_config", return_value=ext_config),
|
||||
patch("src.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("src.client.reload_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.get_extensions_config", return_value=ext_config),
|
||||
patch("deerflow.client.ExtensionsConfig.resolve_config_path", return_value=config_file),
|
||||
patch("deerflow.client.reload_extensions_config", return_value=ext_config),
|
||||
):
|
||||
result = client.update_mcp_config({"srv": server.model_dump.return_value})
|
||||
|
||||
@@ -1655,7 +1655,7 @@ class TestGatewayConformance:
|
||||
mem_cfg.injection_enabled = True
|
||||
mem_cfg.max_injection_tokens = 2000
|
||||
|
||||
with patch("src.config.memory_config.get_memory_config", return_value=mem_cfg):
|
||||
with patch("deerflow.config.memory_config.get_memory_config", return_value=mem_cfg):
|
||||
result = client.get_memory_config()
|
||||
|
||||
parsed = MemoryConfigResponse(**result)
|
||||
@@ -1689,8 +1689,8 @@ class TestGatewayConformance:
|
||||
}
|
||||
|
||||
with (
|
||||
patch("src.config.memory_config.get_memory_config", return_value=mem_cfg),
|
||||
patch("src.agents.memory.updater.get_memory_data", return_value=memory_data),
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=mem_cfg),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=memory_data),
|
||||
):
|
||||
result = client.get_memory_status()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.client import DeerFlowClient, StreamEvent
|
||||
from deerflow.client import DeerFlowClient, StreamEvent
|
||||
|
||||
# Skip entire module in CI or when no config.yaml exists
|
||||
_skip_reason = None
|
||||
|
||||
125
backend/tests/test_config_version.py
Normal file
125
backend/tests/test_config_version.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Tests for config version check and upgrade logic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from deerflow.config.app_config import AppConfig
|
||||
|
||||
|
||||
def _make_config_files(tmpdir: Path, user_config: dict, example_config: dict) -> Path:
|
||||
"""Write user config.yaml and config.example.yaml to a temp dir, return config path."""
|
||||
config_path = tmpdir / "config.yaml"
|
||||
example_path = tmpdir / "config.example.yaml"
|
||||
|
||||
# Minimal valid config needs sandbox
|
||||
defaults = {
|
||||
"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"},
|
||||
}
|
||||
for cfg in (user_config, example_config):
|
||||
for k, v in defaults.items():
|
||||
cfg.setdefault(k, v)
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(user_config, f)
|
||||
with open(example_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(example_config, f)
|
||||
|
||||
return config_path
|
||||
|
||||
|
||||
def test_missing_version_treated_as_zero(caplog):
|
||||
"""Config without config_version should be treated as version 0."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = _make_config_files(
|
||||
Path(tmpdir),
|
||||
user_config={}, # no config_version
|
||||
example_config={"config_version": 1},
|
||||
)
|
||||
with caplog.at_level(logging.WARNING, logger="deerflow.config.app_config"):
|
||||
AppConfig._check_config_version(
|
||||
{"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}},
|
||||
config_path,
|
||||
)
|
||||
assert "outdated" in caplog.text
|
||||
assert "version 0" in caplog.text
|
||||
assert "version is 1" in caplog.text
|
||||
|
||||
|
||||
def test_matching_version_no_warning(caplog):
|
||||
"""Config with matching version should not emit a warning."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = _make_config_files(
|
||||
Path(tmpdir),
|
||||
user_config={"config_version": 1},
|
||||
example_config={"config_version": 1},
|
||||
)
|
||||
with caplog.at_level(logging.WARNING, logger="deerflow.config.app_config"):
|
||||
AppConfig._check_config_version(
|
||||
{"config_version": 1},
|
||||
config_path,
|
||||
)
|
||||
assert "outdated" not in caplog.text
|
||||
|
||||
|
||||
def test_outdated_version_emits_warning(caplog):
|
||||
"""Config with lower version should emit a warning."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = _make_config_files(
|
||||
Path(tmpdir),
|
||||
user_config={"config_version": 1},
|
||||
example_config={"config_version": 2},
|
||||
)
|
||||
with caplog.at_level(logging.WARNING, logger="deerflow.config.app_config"):
|
||||
AppConfig._check_config_version(
|
||||
{"config_version": 1},
|
||||
config_path,
|
||||
)
|
||||
assert "outdated" in caplog.text
|
||||
assert "version 1" in caplog.text
|
||||
assert "version is 2" in caplog.text
|
||||
|
||||
|
||||
def test_no_example_file_no_warning(caplog):
|
||||
"""If config.example.yaml doesn't exist, no warning should be emitted."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = Path(tmpdir) / "config.yaml"
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump({"sandbox": {"use": "test"}}, f)
|
||||
# No config.example.yaml created
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="deerflow.config.app_config"):
|
||||
AppConfig._check_config_version({}, config_path)
|
||||
assert "outdated" not in caplog.text
|
||||
|
||||
|
||||
def test_string_config_version_does_not_raise_type_error(caplog):
|
||||
"""config_version stored as a YAML string should not raise TypeError on comparison."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = _make_config_files(
|
||||
Path(tmpdir),
|
||||
user_config={"config_version": "1"}, # string, as YAML can produce
|
||||
example_config={"config_version": 2},
|
||||
)
|
||||
# Must not raise TypeError: '<' not supported between instances of 'str' and 'int'
|
||||
AppConfig._check_config_version({"config_version": "1"}, config_path)
|
||||
|
||||
|
||||
def test_newer_user_version_no_warning(caplog):
|
||||
"""If user has a newer version than example (edge case), no warning."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = _make_config_files(
|
||||
Path(tmpdir),
|
||||
user_config={"config_version": 3},
|
||||
example_config={"config_version": 2},
|
||||
)
|
||||
with caplog.at_level(logging.WARNING, logger="deerflow.config.app_config"):
|
||||
AppConfig._check_config_version(
|
||||
{"config_version": 3},
|
||||
config_path,
|
||||
)
|
||||
assert "outdated" not in caplog.text
|
||||
@@ -16,7 +16,7 @@ from fastapi.testclient import TestClient
|
||||
|
||||
def _make_paths(base_dir: Path):
|
||||
"""Return a Paths instance pointing to base_dir."""
|
||||
from src.config.paths import Paths
|
||||
from deerflow.config.paths import Paths
|
||||
|
||||
return Paths(base_dir=base_dir)
|
||||
|
||||
@@ -72,7 +72,7 @@ class TestPaths:
|
||||
|
||||
class TestAgentConfig:
|
||||
def test_minimal_config(self):
|
||||
from src.config.agents_config import AgentConfig
|
||||
from deerflow.config.agents_config import AgentConfig
|
||||
|
||||
cfg = AgentConfig(name="my-agent")
|
||||
assert cfg.name == "my-agent"
|
||||
@@ -81,7 +81,7 @@ class TestAgentConfig:
|
||||
assert cfg.tool_groups is None
|
||||
|
||||
def test_full_config(self):
|
||||
from src.config.agents_config import AgentConfig
|
||||
from deerflow.config.agents_config import AgentConfig
|
||||
|
||||
cfg = AgentConfig(
|
||||
name="code-reviewer",
|
||||
@@ -94,7 +94,7 @@ class TestAgentConfig:
|
||||
assert cfg.tool_groups == ["file:read", "bash"]
|
||||
|
||||
def test_config_from_dict(self):
|
||||
from src.config.agents_config import AgentConfig
|
||||
from deerflow.config.agents_config import AgentConfig
|
||||
|
||||
data = {"name": "test-agent", "description": "A test", "model": "gpt-4"}
|
||||
cfg = AgentConfig(**data)
|
||||
@@ -113,8 +113,8 @@ class TestLoadAgentConfig:
|
||||
config_dict = {"name": "code-reviewer", "description": "Code review agent", "model": "deepseek-v3"}
|
||||
_write_agent(tmp_path, "code-reviewer", config_dict)
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import load_agent_config
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
cfg = load_agent_config("code-reviewer")
|
||||
|
||||
@@ -123,8 +123,8 @@ class TestLoadAgentConfig:
|
||||
assert cfg.model == "deepseek-v3"
|
||||
|
||||
def test_load_missing_agent_raises(self, tmp_path):
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import load_agent_config
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_agent_config("nonexistent-agent")
|
||||
@@ -133,8 +133,8 @@ class TestLoadAgentConfig:
|
||||
# Create directory without config.yaml
|
||||
(tmp_path / "agents" / "broken-agent").mkdir(parents=True)
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import load_agent_config
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_agent_config("broken-agent")
|
||||
@@ -146,8 +146,8 @@ class TestLoadAgentConfig:
|
||||
(agent_dir / "config.yaml").write_text("description: My agent\n")
|
||||
(agent_dir / "SOUL.md").write_text("Hello")
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import load_agent_config
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
cfg = load_agent_config("inferred-name")
|
||||
|
||||
@@ -157,8 +157,8 @@ class TestLoadAgentConfig:
|
||||
config_dict = {"name": "restricted", "tool_groups": ["file:read", "file:write"]}
|
||||
_write_agent(tmp_path, "restricted", config_dict)
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import load_agent_config
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
cfg = load_agent_config("restricted")
|
||||
|
||||
@@ -171,8 +171,8 @@ class TestLoadAgentConfig:
|
||||
(agent_dir / "config.yaml").write_text("name: legacy-agent\nprompt_file: system.md\n")
|
||||
(agent_dir / "SOUL.md").write_text("Soul content")
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import load_agent_config
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import load_agent_config
|
||||
|
||||
cfg = load_agent_config("legacy-agent")
|
||||
|
||||
@@ -189,8 +189,8 @@ class TestLoadAgentSoul:
|
||||
expected_soul = "You are a specialized code review expert."
|
||||
_write_agent(tmp_path, "code-reviewer", {"name": "code-reviewer"}, soul=expected_soul)
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import AgentConfig, load_agent_soul
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import AgentConfig, load_agent_soul
|
||||
|
||||
cfg = AgentConfig(name="code-reviewer")
|
||||
soul = load_agent_soul(cfg.name)
|
||||
@@ -203,8 +203,8 @@ class TestLoadAgentSoul:
|
||||
(agent_dir / "config.yaml").write_text("name: no-soul\n")
|
||||
# No SOUL.md created
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import AgentConfig, load_agent_soul
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import AgentConfig, load_agent_soul
|
||||
|
||||
cfg = AgentConfig(name="no-soul")
|
||||
soul = load_agent_soul(cfg.name)
|
||||
@@ -217,8 +217,8 @@ class TestLoadAgentSoul:
|
||||
(agent_dir / "config.yaml").write_text("name: empty-soul\n")
|
||||
(agent_dir / "SOUL.md").write_text(" \n ")
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import AgentConfig, load_agent_soul
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import AgentConfig, load_agent_soul
|
||||
|
||||
cfg = AgentConfig(name="empty-soul")
|
||||
soul = load_agent_soul(cfg.name)
|
||||
@@ -233,8 +233,8 @@ class TestLoadAgentSoul:
|
||||
|
||||
class TestListCustomAgents:
|
||||
def test_empty_when_no_agents_dir(self, tmp_path):
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import list_custom_agents
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import list_custom_agents
|
||||
|
||||
agents = list_custom_agents()
|
||||
|
||||
@@ -244,8 +244,8 @@ class TestListCustomAgents:
|
||||
_write_agent(tmp_path, "agent-a", {"name": "agent-a"})
|
||||
_write_agent(tmp_path, "agent-b", {"name": "agent-b", "description": "B"})
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import list_custom_agents
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import list_custom_agents
|
||||
|
||||
agents = list_custom_agents()
|
||||
|
||||
@@ -259,8 +259,8 @@ class TestListCustomAgents:
|
||||
# Invalid dir (no config.yaml)
|
||||
(tmp_path / "agents" / "invalid-dir").mkdir(parents=True)
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import list_custom_agents
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import list_custom_agents
|
||||
|
||||
agents = list_custom_agents()
|
||||
|
||||
@@ -274,8 +274,8 @@ class TestListCustomAgents:
|
||||
(agents_dir / "not-a-dir.txt").write_text("hello")
|
||||
_write_agent(tmp_path, "real-agent", {"name": "real-agent"})
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import list_custom_agents
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import list_custom_agents
|
||||
|
||||
agents = list_custom_agents()
|
||||
|
||||
@@ -287,8 +287,8 @@ class TestListCustomAgents:
|
||||
_write_agent(tmp_path, "a-agent", {"name": "a-agent"})
|
||||
_write_agent(tmp_path, "m-agent", {"name": "m-agent"})
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from src.config.agents_config import list_custom_agents
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)):
|
||||
from deerflow.config.agents_config import list_custom_agents
|
||||
|
||||
agents = list_custom_agents()
|
||||
|
||||
@@ -304,35 +304,35 @@ class TestListCustomAgents:
|
||||
class TestMemoryFilePath:
|
||||
def test_global_memory_path(self, tmp_path):
|
||||
"""None agent_name should return global memory file."""
|
||||
import src.agents.memory.updater as updater_mod
|
||||
from src.config.memory_config import MemoryConfig
|
||||
import deerflow.agents.memory.updater as updater_mod
|
||||
from deerflow.config.memory_config import MemoryConfig
|
||||
|
||||
with (
|
||||
patch("src.agents.memory.updater.get_paths", return_value=_make_paths(tmp_path)),
|
||||
patch("src.agents.memory.updater.get_memory_config", return_value=MemoryConfig(storage_path="")),
|
||||
patch("deerflow.agents.memory.updater.get_paths", return_value=_make_paths(tmp_path)),
|
||||
patch("deerflow.agents.memory.updater.get_memory_config", return_value=MemoryConfig(storage_path="")),
|
||||
):
|
||||
path = updater_mod._get_memory_file_path(None)
|
||||
assert path == tmp_path / "memory.json"
|
||||
|
||||
def test_agent_memory_path(self, tmp_path):
|
||||
"""Providing agent_name should return per-agent memory file."""
|
||||
import src.agents.memory.updater as updater_mod
|
||||
from src.config.memory_config import MemoryConfig
|
||||
import deerflow.agents.memory.updater as updater_mod
|
||||
from deerflow.config.memory_config import MemoryConfig
|
||||
|
||||
with (
|
||||
patch("src.agents.memory.updater.get_paths", return_value=_make_paths(tmp_path)),
|
||||
patch("src.agents.memory.updater.get_memory_config", return_value=MemoryConfig(storage_path="")),
|
||||
patch("deerflow.agents.memory.updater.get_paths", return_value=_make_paths(tmp_path)),
|
||||
patch("deerflow.agents.memory.updater.get_memory_config", return_value=MemoryConfig(storage_path="")),
|
||||
):
|
||||
path = updater_mod._get_memory_file_path("code-reviewer")
|
||||
assert path == tmp_path / "agents" / "code-reviewer" / "memory.json"
|
||||
|
||||
def test_different_paths_for_different_agents(self, tmp_path):
|
||||
import src.agents.memory.updater as updater_mod
|
||||
from src.config.memory_config import MemoryConfig
|
||||
import deerflow.agents.memory.updater as updater_mod
|
||||
from deerflow.config.memory_config import MemoryConfig
|
||||
|
||||
with (
|
||||
patch("src.agents.memory.updater.get_paths", return_value=_make_paths(tmp_path)),
|
||||
patch("src.agents.memory.updater.get_memory_config", return_value=MemoryConfig(storage_path="")),
|
||||
patch("deerflow.agents.memory.updater.get_paths", return_value=_make_paths(tmp_path)),
|
||||
patch("deerflow.agents.memory.updater.get_memory_config", return_value=MemoryConfig(storage_path="")),
|
||||
):
|
||||
path_global = updater_mod._get_memory_file_path(None)
|
||||
path_a = updater_mod._get_memory_file_path("agent-a")
|
||||
@@ -352,7 +352,7 @@ def _make_test_app(tmp_path: Path):
|
||||
"""Create a FastAPI app with the agents router, patching paths to tmp_path."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
from src.gateway.routers.agents import router
|
||||
from app.gateway.routers.agents import router
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
@@ -364,7 +364,7 @@ def agent_client(tmp_path):
|
||||
"""TestClient with agents router, using tmp_path as base_dir."""
|
||||
paths_instance = _make_paths(tmp_path)
|
||||
|
||||
with patch("src.config.agents_config.get_paths", return_value=paths_instance), patch("src.gateway.routers.agents.get_paths", return_value=paths_instance):
|
||||
with patch("deerflow.config.agents_config.get_paths", return_value=paths_instance), patch("app.gateway.routers.agents.get_paths", return_value=paths_instance):
|
||||
app = _make_test_app(tmp_path)
|
||||
with TestClient(app) as client:
|
||||
client._tmp_path = tmp_path # type: ignore[attr-defined]
|
||||
|
||||
@@ -39,7 +39,7 @@ def test_detect_mode_local_provider():
|
||||
"""Local sandbox provider should map to local mode."""
|
||||
config = """
|
||||
sandbox:
|
||||
use: src.sandbox.local:LocalSandboxProvider
|
||||
use: deerflow.sandbox.local:LocalSandboxProvider
|
||||
""".strip()
|
||||
|
||||
assert _detect_mode_with_config(config) == "local"
|
||||
@@ -49,7 +49,7 @@ def test_detect_mode_aio_without_provisioner_url():
|
||||
"""AIO sandbox without provisioner_url should map to aio mode."""
|
||||
config = """
|
||||
sandbox:
|
||||
use: src.community.aio_sandbox:AioSandboxProvider
|
||||
use: deerflow.community.aio_sandbox:AioSandboxProvider
|
||||
""".strip()
|
||||
|
||||
assert _detect_mode_with_config(config) == "aio"
|
||||
@@ -59,7 +59,7 @@ def test_detect_mode_provisioner_with_url():
|
||||
"""AIO sandbox with provisioner_url should map to provisioner mode."""
|
||||
config = """
|
||||
sandbox:
|
||||
use: src.community.aio_sandbox:AioSandboxProvider
|
||||
use: deerflow.community.aio_sandbox:AioSandboxProvider
|
||||
provisioner_url: http://provisioner:8002
|
||||
""".strip()
|
||||
|
||||
@@ -70,7 +70,7 @@ def test_detect_mode_ignores_commented_provisioner_url():
|
||||
"""Commented provisioner_url should not activate provisioner mode."""
|
||||
config = """
|
||||
sandbox:
|
||||
use: src.community.aio_sandbox:AioSandboxProvider
|
||||
use: deerflow.community.aio_sandbox:AioSandboxProvider
|
||||
# provisioner_url: http://provisioner:8002
|
||||
""".strip()
|
||||
|
||||
|
||||
46
backend/tests/test_harness_boundary.py
Normal file
46
backend/tests/test_harness_boundary.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Boundary check: harness layer must not import from app layer.
|
||||
|
||||
The deerflow-harness package (packages/harness/deerflow/) is a standalone,
|
||||
publishable agent framework. It must never depend on the app layer (app/).
|
||||
|
||||
This test scans all Python files in the harness package and fails if any
|
||||
``from app.`` or ``import app.`` statement is found.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
HARNESS_ROOT = Path(__file__).parent.parent / "packages" / "harness" / "deerflow"
|
||||
|
||||
BANNED_PREFIXES = ("app.",)
|
||||
|
||||
|
||||
def _collect_imports(filepath: Path) -> list[tuple[int, str]]:
|
||||
"""Return (line_number, module_path) for every import in *filepath*."""
|
||||
source = filepath.read_text(encoding="utf-8")
|
||||
try:
|
||||
tree = ast.parse(source, filename=str(filepath))
|
||||
except SyntaxError:
|
||||
return []
|
||||
|
||||
results: list[tuple[int, str]] = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
results.append((node.lineno, alias.name))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
results.append((node.lineno, node.module))
|
||||
return results
|
||||
|
||||
|
||||
def test_harness_does_not_import_app():
|
||||
violations: list[str] = []
|
||||
|
||||
for py_file in sorted(HARNESS_ROOT.rglob("*.py")):
|
||||
for lineno, module in _collect_imports(py_file):
|
||||
if any(module == prefix.rstrip(".") or module.startswith(prefix) for prefix in BANNED_PREFIXES):
|
||||
rel = py_file.relative_to(HARNESS_ROOT.parent.parent.parent)
|
||||
violations.append(f" {rel}:{lineno} imports {module}")
|
||||
|
||||
assert not violations, "Harness layer must not import from app layer:\n" + "\n".join(violations)
|
||||
@@ -3,8 +3,8 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from src.community.infoquest import tools
|
||||
from src.community.infoquest.infoquest_client import InfoQuestClient
|
||||
from deerflow.community.infoquest import tools
|
||||
from deerflow.community.infoquest.infoquest_client import InfoQuestClient
|
||||
|
||||
|
||||
class TestInfoQuestClient:
|
||||
@@ -24,7 +24,7 @@ class TestInfoQuestClient:
|
||||
assert client.fetch_navigation_timeout == 60
|
||||
assert client.search_time_range == 24
|
||||
|
||||
@patch("src.community.infoquest.infoquest_client.requests.post")
|
||||
@patch("deerflow.community.infoquest.infoquest_client.requests.post")
|
||||
def test_fetch_success(self, mock_post):
|
||||
"""Test successful fetch operation."""
|
||||
mock_response = MagicMock()
|
||||
@@ -42,7 +42,7 @@ class TestInfoQuestClient:
|
||||
assert kwargs["json"]["url"] == "https://example.com"
|
||||
assert kwargs["json"]["format"] == "HTML"
|
||||
|
||||
@patch("src.community.infoquest.infoquest_client.requests.post")
|
||||
@patch("deerflow.community.infoquest.infoquest_client.requests.post")
|
||||
def test_fetch_non_200_status(self, mock_post):
|
||||
"""Test fetch operation with non-200 status code."""
|
||||
mock_response = MagicMock()
|
||||
@@ -55,7 +55,7 @@ class TestInfoQuestClient:
|
||||
|
||||
assert result == "Error: fetch API returned status 404: Not Found"
|
||||
|
||||
@patch("src.community.infoquest.infoquest_client.requests.post")
|
||||
@patch("deerflow.community.infoquest.infoquest_client.requests.post")
|
||||
def test_fetch_empty_response(self, mock_post):
|
||||
"""Test fetch operation with empty response."""
|
||||
mock_response = MagicMock()
|
||||
@@ -68,7 +68,7 @@ class TestInfoQuestClient:
|
||||
|
||||
assert result == "Error: no result found"
|
||||
|
||||
@patch("src.community.infoquest.infoquest_client.requests.post")
|
||||
@patch("deerflow.community.infoquest.infoquest_client.requests.post")
|
||||
def test_web_search_raw_results_success(self, mock_post):
|
||||
"""Test successful web_search_raw_results operation."""
|
||||
mock_response = MagicMock()
|
||||
@@ -85,7 +85,7 @@ class TestInfoQuestClient:
|
||||
assert args[0] == "https://search.infoquest.bytepluses.com"
|
||||
assert kwargs["json"]["query"] == "test query"
|
||||
|
||||
@patch("src.community.infoquest.infoquest_client.requests.post")
|
||||
@patch("deerflow.community.infoquest.infoquest_client.requests.post")
|
||||
def test_web_search_success(self, mock_post):
|
||||
"""Test successful web_search operation."""
|
||||
mock_response = MagicMock()
|
||||
@@ -133,7 +133,7 @@ class TestInfoQuestClient:
|
||||
assert cleaned[0]["thumbnail_url"] == "https://example.com/thumb1.jpg"
|
||||
assert cleaned[0]["url"] == "https://example.com/page1"
|
||||
|
||||
@patch("src.community.infoquest.tools._get_infoquest_client")
|
||||
@patch("deerflow.community.infoquest.tools._get_infoquest_client")
|
||||
def test_web_search_tool(self, mock_get_client):
|
||||
"""Test web_search_tool function."""
|
||||
mock_client = MagicMock()
|
||||
@@ -146,7 +146,7 @@ class TestInfoQuestClient:
|
||||
mock_get_client.assert_called_once()
|
||||
mock_client.web_search.assert_called_once_with("test query")
|
||||
|
||||
@patch("src.community.infoquest.tools._get_infoquest_client")
|
||||
@patch("deerflow.community.infoquest.tools._get_infoquest_client")
|
||||
def test_web_fetch_tool(self, mock_get_client):
|
||||
"""Test web_fetch_tool function."""
|
||||
mock_client = MagicMock()
|
||||
@@ -159,7 +159,7 @@ class TestInfoQuestClient:
|
||||
mock_get_client.assert_called_once()
|
||||
mock_client.fetch.assert_called_once_with("https://example.com")
|
||||
|
||||
@patch("src.community.infoquest.tools.get_app_config")
|
||||
@patch("deerflow.community.infoquest.tools.get_app_config")
|
||||
def test_get_infoquest_client(self, mock_get_app_config):
|
||||
"""Test _get_infoquest_client function with config."""
|
||||
mock_config = MagicMock()
|
||||
@@ -173,7 +173,7 @@ class TestInfoQuestClient:
|
||||
assert client.fetch_timeout == 30
|
||||
assert client.fetch_navigation_timeout == 60
|
||||
|
||||
@patch("src.community.infoquest.infoquest_client.requests.post")
|
||||
@patch("deerflow.community.infoquest.infoquest_client.requests.post")
|
||||
def test_web_search_api_error(self, mock_post):
|
||||
"""Test web_search operation with API error."""
|
||||
mock_post.side_effect = Exception("Connection error")
|
||||
|
||||
@@ -4,16 +4,16 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.lead_agent import agent as lead_agent_module
|
||||
from src.config.app_config import AppConfig
|
||||
from src.config.model_config import ModelConfig
|
||||
from src.config.sandbox_config import SandboxConfig
|
||||
from deerflow.agents.lead_agent import agent as lead_agent_module
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
from deerflow.config.sandbox_config import SandboxConfig
|
||||
|
||||
|
||||
def _make_app_config(models: list[ModelConfig]) -> AppConfig:
|
||||
return AppConfig(
|
||||
models=models,
|
||||
sandbox=SandboxConfig(use="src.sandbox.local:LocalSandboxProvider"),
|
||||
sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"),
|
||||
)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def test_resolve_model_name_raises_when_no_models_configured(monkeypatch):
|
||||
def test_make_lead_agent_disables_thinking_when_model_does_not_support_it(monkeypatch):
|
||||
app_config = _make_app_config([_make_model("safe-model", supports_thinking=False)])
|
||||
|
||||
import src.tools as tools_module
|
||||
import deerflow.tools as tools_module
|
||||
|
||||
monkeypatch.setattr(lead_agent_module, "get_app_config", lambda: app_config)
|
||||
monkeypatch.setattr(tools_module, "get_available_tools", lambda **kwargs: [])
|
||||
|
||||
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from langchain_core.messages import AIMessage, SystemMessage
|
||||
|
||||
from src.agents.middlewares.loop_detection_middleware import (
|
||||
from deerflow.agents.middlewares.loop_detection_middleware import (
|
||||
_HARD_STOP_MSG,
|
||||
LoopDetectionMiddleware,
|
||||
_hash_tool_calls,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config.extensions_config import ExtensionsConfig, McpServerConfig
|
||||
from src.mcp.client import build_server_params, build_servers_config
|
||||
from deerflow.config.extensions_config import ExtensionsConfig, McpServerConfig
|
||||
from deerflow.mcp.client import build_server_params, build_servers_config
|
||||
|
||||
|
||||
def test_build_server_params_stdio_success():
|
||||
|
||||
@@ -5,8 +5,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from src.config.extensions_config import ExtensionsConfig
|
||||
from src.mcp.oauth import OAuthTokenManager, build_oauth_tool_interceptor, get_initial_oauth_headers
|
||||
from deerflow.config.extensions_config import ExtensionsConfig
|
||||
from deerflow.mcp.oauth import OAuthTokenManager, build_oauth_tool_interceptor, get_initial_oauth_headers
|
||||
|
||||
|
||||
class _MockResponse:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import math
|
||||
|
||||
from src.agents.memory.prompt import _coerce_confidence, format_memory_for_injection
|
||||
from deerflow.agents.memory.prompt import _coerce_confidence, format_memory_for_injection
|
||||
|
||||
|
||||
def test_format_memory_includes_facts_section() -> None:
|
||||
@@ -39,7 +39,7 @@ def test_format_memory_sorts_facts_by_confidence_desc() -> None:
|
||||
|
||||
def test_format_memory_respects_budget_when_adding_facts(monkeypatch) -> None:
|
||||
# Make token counting deterministic for this test by counting characters.
|
||||
monkeypatch.setattr("src.agents.memory.prompt._count_tokens", lambda text, encoding_name="cl100k_base": len(text))
|
||||
monkeypatch.setattr("deerflow.agents.memory.prompt._count_tokens", lambda text, encoding_name="cl100k_base": len(text))
|
||||
|
||||
memory_data = {
|
||||
"user": {},
|
||||
|
||||
@@ -9,8 +9,8 @@ persisting in long-term memory:
|
||||
|
||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
|
||||
from src.agents.memory.updater import _strip_upload_mentions_from_memory
|
||||
from src.agents.middlewares.memory_middleware import _filter_messages_for_memory
|
||||
from deerflow.agents.memory.updater import _strip_upload_mentions_from_memory
|
||||
from deerflow.agents.middlewares.memory_middleware import _filter_messages_for_memory
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Tests for src.models.factory.create_chat_model."""
|
||||
"""Tests for deerflow.models.factory.create_chat_model."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from langchain.chat_models import BaseChatModel
|
||||
|
||||
from src.config.app_config import AppConfig
|
||||
from src.config.model_config import ModelConfig
|
||||
from src.config.sandbox_config import SandboxConfig
|
||||
from src.models import factory as factory_module
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.model_config import ModelConfig
|
||||
from deerflow.config.sandbox_config import SandboxConfig
|
||||
from deerflow.models import factory as factory_module
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -18,7 +18,7 @@ from src.models import factory as factory_module
|
||||
def _make_app_config(models: list[ModelConfig]) -> AppConfig:
|
||||
return AppConfig(
|
||||
models=models,
|
||||
sandbox=SandboxConfig(use="src.sandbox.local:LocalSandboxProvider"),
|
||||
sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import importlib
|
||||
from types import SimpleNamespace
|
||||
|
||||
present_file_tool_module = importlib.import_module("src.tools.builtins.present_file_tool")
|
||||
present_file_tool_module = importlib.import_module("deerflow.tools.builtins.present_file_tool")
|
||||
|
||||
|
||||
def _make_runtime(outputs_path: str) -> SimpleNamespace:
|
||||
|
||||
@@ -4,7 +4,7 @@ import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from src.utils.readability import ReadabilityExtractor
|
||||
from deerflow.utils.readability import ReadabilityExtractor
|
||||
|
||||
|
||||
def test_extract_article_falls_back_when_readability_js_fails(monkeypatch):
|
||||
@@ -23,7 +23,7 @@ def test_extract_article_falls_back_when_readability_js_fails(monkeypatch):
|
||||
return {"title": "Fallback Title", "content": "<p>Fallback Content</p>"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.utils.readability.simple_json_from_html_string",
|
||||
"deerflow.utils.readability.simple_json_from_html_string",
|
||||
_fake_simple_json_from_html_string,
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ def test_extract_article_re_raises_unexpected_exception(monkeypatch):
|
||||
return {"title": "Should Not Reach Fallback", "content": "<p>Fallback</p>"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.utils.readability.simple_json_from_html_string",
|
||||
"deerflow.utils.readability.simple_json_from_html_string",
|
||||
_fake_simple_json_from_html_string,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.reflection import resolvers
|
||||
from src.reflection.resolvers import resolve_variable
|
||||
from deerflow.reflection import resolvers
|
||||
from deerflow.reflection.resolvers import resolve_variable
|
||||
|
||||
|
||||
def test_resolve_variable_reports_install_hint_for_missing_google_provider(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.sandbox.tools import (
|
||||
from deerflow.sandbox.tools import (
|
||||
VIRTUAL_PATH_PREFIX,
|
||||
mask_local_paths_in_output,
|
||||
replace_virtual_path,
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src.gateway.routers.skills import _resolve_skill_dir_from_archive_root
|
||||
from app.gateway.routers.skills import _resolve_skill_dir_from_archive_root
|
||||
|
||||
|
||||
def _write_skill(skill_dir: Path) -> None:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from src.skills.loader import load_skills
|
||||
from deerflow.skills.loader import get_skills_root_path, load_skills
|
||||
|
||||
|
||||
def _write_skill(skill_dir: Path, name: str, description: str) -> None:
|
||||
@@ -12,6 +12,15 @@ def _write_skill(skill_dir: Path, name: str, description: str) -> None:
|
||||
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def test_get_skills_root_path_points_to_project_root_skills():
|
||||
"""get_skills_root_path() should point to deer-flow/skills (sibling of backend/), not backend/packages/skills."""
|
||||
path = get_skills_root_path()
|
||||
assert path.name == "skills", f"Expected 'skills', got '{path.name}'"
|
||||
assert (path.parent / "backend").is_dir(), (
|
||||
f"Expected skills path's parent to be project root containing 'backend/', but got {path}"
|
||||
)
|
||||
|
||||
|
||||
def test_load_skills_discovers_nested_skills_and_sets_container_paths(tmp_path: Path):
|
||||
"""Nested skills should be discovered recursively with correct container paths."""
|
||||
skills_root = tmp_path / "skills"
|
||||
|
||||
@@ -2,11 +2,11 @@ from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import src.gateway.routers.skills as skills_router
|
||||
from deerflow.skills.validation import _validate_skill_frontmatter
|
||||
|
||||
VALIDATE_SKILL_FRONTMATTER = cast(
|
||||
Callable[[Path], tuple[bool, str, str | None]],
|
||||
getattr(skills_router, "_validate_skill_frontmatter"),
|
||||
_validate_skill_frontmatter,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Covers:
|
||||
- Async tool support (MCP tools)
|
||||
|
||||
Note: Due to circular import issues in the main codebase, conftest.py mocks
|
||||
src.subagents.executor. This test file uses delayed import via fixture to test
|
||||
deerflow.subagents.executor. This test file uses delayed import via fixture to test
|
||||
the real implementation in isolation.
|
||||
"""
|
||||
|
||||
@@ -21,13 +21,13 @@ import pytest
|
||||
|
||||
# Module names that need to be mocked to break circular imports
|
||||
_MOCKED_MODULE_NAMES = [
|
||||
"src.agents",
|
||||
"src.agents.thread_state",
|
||||
"src.agents.middlewares",
|
||||
"src.agents.middlewares.thread_data_middleware",
|
||||
"src.sandbox",
|
||||
"src.sandbox.middleware",
|
||||
"src.models",
|
||||
"deerflow.agents",
|
||||
"deerflow.agents.thread_state",
|
||||
"deerflow.agents.middlewares",
|
||||
"deerflow.agents.middlewares.thread_data_middleware",
|
||||
"deerflow.sandbox",
|
||||
"deerflow.sandbox.middleware",
|
||||
"deerflow.models",
|
||||
]
|
||||
|
||||
|
||||
@@ -40,11 +40,11 @@ def _setup_executor_classes():
|
||||
"""
|
||||
# Save original modules
|
||||
original_modules = {name: sys.modules.get(name) for name in _MOCKED_MODULE_NAMES}
|
||||
original_executor = sys.modules.get("src.subagents.executor")
|
||||
original_executor = sys.modules.get("deerflow.subagents.executor")
|
||||
|
||||
# Remove mocked executor if exists (from conftest.py)
|
||||
if "src.subagents.executor" in sys.modules:
|
||||
del sys.modules["src.subagents.executor"]
|
||||
if "deerflow.subagents.executor" in sys.modules:
|
||||
del sys.modules["deerflow.subagents.executor"]
|
||||
|
||||
# Set up mocks
|
||||
for name in _MOCKED_MODULE_NAMES:
|
||||
@@ -53,8 +53,8 @@ def _setup_executor_classes():
|
||||
# Import real classes inside fixture
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from src.subagents.config import SubagentConfig
|
||||
from src.subagents.executor import (
|
||||
from deerflow.subagents.config import SubagentConfig
|
||||
from deerflow.subagents.executor import (
|
||||
SubagentExecutor,
|
||||
SubagentResult,
|
||||
SubagentStatus,
|
||||
@@ -81,9 +81,9 @@ def _setup_executor_classes():
|
||||
|
||||
# Restore executor module (conftest.py mock)
|
||||
if original_executor is not None:
|
||||
sys.modules["src.subagents.executor"] = original_executor
|
||||
elif "src.subagents.executor" in sys.modules:
|
||||
del sys.modules["src.subagents.executor"]
|
||||
sys.modules["deerflow.subagents.executor"] = original_executor
|
||||
elif "deerflow.subagents.executor" in sys.modules:
|
||||
del sys.modules["deerflow.subagents.executor"]
|
||||
|
||||
|
||||
# Helper classes that wrap real classes for testing
|
||||
@@ -641,7 +641,7 @@ class TestCleanupBackgroundTask:
|
||||
# Re-import to get the real module with cleanup_background_task
|
||||
import importlib
|
||||
|
||||
from src.subagents import executor
|
||||
from deerflow.subagents import executor
|
||||
|
||||
return importlib.reload(executor)
|
||||
|
||||
@@ -749,9 +749,7 @@ class TestCleanupBackgroundTask:
|
||||
# Should not raise
|
||||
executor_module.cleanup_background_task("nonexistent-task")
|
||||
|
||||
def test_cleanup_removes_task_with_completed_at_even_if_running(
|
||||
self, executor_module, classes
|
||||
):
|
||||
def test_cleanup_removes_task_with_completed_at_even_if_running(self, executor_module, classes):
|
||||
"""Test that cleanup removes task if completed_at is set, even if status is RUNNING.
|
||||
|
||||
This is a safety net: if completed_at is set, the task is considered done
|
||||
|
||||
@@ -11,13 +11,13 @@ Covers:
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config.subagents_config import (
|
||||
from deerflow.config.subagents_config import (
|
||||
SubagentOverrideConfig,
|
||||
SubagentsAppConfig,
|
||||
get_subagents_app_config,
|
||||
load_subagents_config_from_dict,
|
||||
)
|
||||
from src.subagents.config import SubagentConfig
|
||||
from deerflow.subagents.config import SubagentConfig
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -195,32 +195,32 @@ class TestRegistryGetSubagentConfig:
|
||||
_reset_subagents_config()
|
||||
|
||||
def test_returns_none_for_unknown_agent(self):
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
assert get_subagent_config("nonexistent") is None
|
||||
|
||||
def test_returns_config_for_builtin_agents(self):
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
assert get_subagent_config("general-purpose") is not None
|
||||
assert get_subagent_config("bash") is not None
|
||||
|
||||
def test_default_timeout_preserved_when_no_config(self):
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
_reset_subagents_config(timeout_seconds=900)
|
||||
config = get_subagent_config("general-purpose")
|
||||
assert config.timeout_seconds == 900
|
||||
|
||||
def test_global_timeout_override_applied(self):
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
_reset_subagents_config(timeout_seconds=1800)
|
||||
config = get_subagent_config("general-purpose")
|
||||
assert config.timeout_seconds == 1800
|
||||
|
||||
def test_per_agent_timeout_override_applied(self):
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
load_subagents_config_from_dict(
|
||||
{
|
||||
@@ -232,7 +232,7 @@ class TestRegistryGetSubagentConfig:
|
||||
assert bash_config.timeout_seconds == 120
|
||||
|
||||
def test_per_agent_override_does_not_affect_other_agents(self):
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
load_subagents_config_from_dict(
|
||||
{
|
||||
@@ -245,8 +245,8 @@ class TestRegistryGetSubagentConfig:
|
||||
|
||||
def test_builtin_config_object_is_not_mutated(self):
|
||||
"""Registry must return a new object, leaving the builtin default intact."""
|
||||
from src.subagents.builtins import BUILTIN_SUBAGENTS
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.builtins import BUILTIN_SUBAGENTS
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
original_timeout = BUILTIN_SUBAGENTS["bash"].timeout_seconds
|
||||
load_subagents_config_from_dict({"timeout_seconds": 42})
|
||||
@@ -257,8 +257,8 @@ class TestRegistryGetSubagentConfig:
|
||||
|
||||
def test_config_preserves_other_fields(self):
|
||||
"""Applying timeout override must not change other SubagentConfig fields."""
|
||||
from src.subagents.builtins import BUILTIN_SUBAGENTS
|
||||
from src.subagents.registry import get_subagent_config
|
||||
from deerflow.subagents.builtins import BUILTIN_SUBAGENTS
|
||||
from deerflow.subagents.registry import get_subagent_config
|
||||
|
||||
_reset_subagents_config(timeout_seconds=300)
|
||||
original = BUILTIN_SUBAGENTS["general-purpose"]
|
||||
@@ -282,21 +282,21 @@ class TestRegistryListSubagents:
|
||||
_reset_subagents_config()
|
||||
|
||||
def test_lists_both_builtin_agents(self):
|
||||
from src.subagents.registry import list_subagents
|
||||
from deerflow.subagents.registry import list_subagents
|
||||
|
||||
names = {cfg.name for cfg in list_subagents()}
|
||||
assert "general-purpose" in names
|
||||
assert "bash" in names
|
||||
|
||||
def test_all_returned_configs_get_global_override(self):
|
||||
from src.subagents.registry import list_subagents
|
||||
from deerflow.subagents.registry import list_subagents
|
||||
|
||||
_reset_subagents_config(timeout_seconds=123)
|
||||
for cfg in list_subagents():
|
||||
assert cfg.timeout_seconds == 123, f"{cfg.name} has wrong timeout"
|
||||
|
||||
def test_per_agent_overrides_reflected_in_list(self):
|
||||
from src.subagents.registry import list_subagents
|
||||
from deerflow.subagents.registry import list_subagents
|
||||
|
||||
load_subagents_config_from_dict(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from src.gateway.routers import suggestions
|
||||
from app.gateway.routers import suggestions
|
||||
|
||||
|
||||
def test_strip_markdown_code_fence_removes_wrapping():
|
||||
|
||||
@@ -5,10 +5,10 @@ from enum import Enum
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from src.subagents.config import SubagentConfig
|
||||
from deerflow.subagents.config import SubagentConfig
|
||||
|
||||
# Use module import so tests can patch the exact symbols referenced inside task_tool().
|
||||
task_tool_module = importlib.import_module("src.tools.builtins.task_tool")
|
||||
task_tool_module = importlib.import_module("deerflow.tools.builtins.task_tool")
|
||||
|
||||
|
||||
class FakeSubagentStatus(Enum):
|
||||
@@ -110,8 +110,8 @@ def test_task_tool_emits_running_and_completed_events(monkeypatch):
|
||||
monkeypatch.setattr(task_tool_module, "get_background_task_result", lambda _: next(responses))
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
# task_tool lazily imports from src.tools at call time, so patch that module-level function.
|
||||
monkeypatch.setattr("src.tools.get_available_tools", get_available_tools)
|
||||
# task_tool lazily imports from deerflow.tools at call time, so patch that module-level function.
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", get_available_tools)
|
||||
|
||||
output = task_tool_module.task_tool.func(
|
||||
runtime=runtime,
|
||||
@@ -156,7 +156,7 @@ def test_task_tool_returns_failed_message(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
|
||||
output = task_tool_module.task_tool.func(
|
||||
runtime=_make_runtime(),
|
||||
@@ -190,7 +190,7 @@ def test_task_tool_returns_timed_out_message(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
|
||||
output = task_tool_module.task_tool.func(
|
||||
runtime=_make_runtime(),
|
||||
@@ -226,7 +226,7 @@ def test_task_tool_polling_safety_timeout(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
|
||||
output = task_tool_module.task_tool.func(
|
||||
runtime=_make_runtime(),
|
||||
@@ -262,7 +262,7 @@ def test_cleanup_called_on_completed(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr(
|
||||
task_tool_module,
|
||||
"cleanup_background_task",
|
||||
@@ -302,7 +302,7 @@ def test_cleanup_called_on_failed(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr(
|
||||
task_tool_module,
|
||||
"cleanup_background_task",
|
||||
@@ -342,7 +342,7 @@ def test_cleanup_called_on_timed_out(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr(
|
||||
task_tool_module,
|
||||
"cleanup_background_task",
|
||||
@@ -389,7 +389,7 @@ def test_cleanup_not_called_on_polling_safety_timeout(monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append)
|
||||
monkeypatch.setattr(task_tool_module.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr("src.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr("deerflow.tools.get_available_tools", lambda **kwargs: [])
|
||||
monkeypatch.setattr(
|
||||
task_tool_module,
|
||||
"cleanup_background_task",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from src.config.title_config import TitleConfig, get_title_config, set_title_config
|
||||
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from deerflow.config.title_config import TitleConfig, get_title_config, set_title_config
|
||||
|
||||
|
||||
class TestTitleConfig:
|
||||
|
||||
@@ -5,8 +5,8 @@ from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from src.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from src.config.title_config import TitleConfig, get_title_config, set_title_config
|
||||
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from deerflow.config.title_config import TitleConfig, get_title_config, set_title_config
|
||||
|
||||
|
||||
def _clone_title_config(config: TitleConfig) -> TitleConfig:
|
||||
@@ -78,7 +78,7 @@ class TestTitleMiddlewareCoreLogic:
|
||||
middleware = TitleMiddleware()
|
||||
fake_model = MagicMock()
|
||||
fake_model.ainvoke = AsyncMock(return_value=MagicMock(content='"A very long generated title"'))
|
||||
monkeypatch.setattr("src.agents.middlewares.title_middleware.create_chat_model", lambda **kwargs: fake_model)
|
||||
monkeypatch.setattr("deerflow.agents.middlewares.title_middleware.create_chat_model", lambda **kwargs: fake_model)
|
||||
|
||||
state = {
|
||||
"messages": [
|
||||
@@ -97,7 +97,7 @@ class TestTitleMiddlewareCoreLogic:
|
||||
middleware = TitleMiddleware()
|
||||
fake_model = MagicMock()
|
||||
fake_model.ainvoke = AsyncMock(side_effect=RuntimeError("LLM unavailable"))
|
||||
monkeypatch.setattr("src.agents.middlewares.title_middleware.create_chat_model", lambda **kwargs: fake_model)
|
||||
monkeypatch.setattr("deerflow.agents.middlewares.title_middleware.create_chat_model", lambda **kwargs: fake_model)
|
||||
|
||||
state = {
|
||||
"messages": [
|
||||
|
||||
@@ -4,7 +4,7 @@ import pytest
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langgraph.errors import GraphInterrupt
|
||||
|
||||
from src.agents.middlewares.tool_error_handling_middleware import ToolErrorHandlingMiddleware
|
||||
from deerflow.agents.middlewares.tool_error_handling_middleware import ToolErrorHandlingMiddleware
|
||||
|
||||
|
||||
def _request(name: str = "web_search", tool_call_id: str | None = "tc-1"):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Tests for src.config.tracing_config."""
|
||||
"""Tests for deerflow.config.tracing_config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from src.config import tracing_config as tracing_module
|
||||
from deerflow.config import tracing_config as tracing_module
|
||||
|
||||
|
||||
def _reset_tracing_cache() -> None:
|
||||
|
||||
@@ -12,8 +12,8 @@ from unittest.mock import MagicMock
|
||||
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
|
||||
from src.agents.middlewares.uploads_middleware import UploadsMiddleware
|
||||
from src.config.paths import Paths
|
||||
from deerflow.agents.middlewares.uploads_middleware import UploadsMiddleware
|
||||
from deerflow.config.paths import Paths
|
||||
|
||||
THREAD_ID = "thread-abc123"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from src.gateway.routers import uploads
|
||||
from app.gateway.routers import uploads
|
||||
|
||||
|
||||
def test_upload_files_writes_thread_storage_and_skips_local_sandbox_sync(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user