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:
DanielWalnut
2026-03-14 22:55:52 +08:00
committed by GitHub
parent 9b49a80dda
commit 76803b826f
198 changed files with 1786 additions and 941 deletions

View File

@@ -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

View File

@@ -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"])

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View 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

View File

@@ -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]

View File

@@ -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()

View 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)

View File

@@ -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")

View File

@@ -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: [])

View File

@@ -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,

View File

@@ -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():

View File

@@ -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:

View File

@@ -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": {},

View File

@@ -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

View File

@@ -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"),
)

View File

@@ -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:

View File

@@ -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,
)

View File

@@ -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):

View File

@@ -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,

View File

@@ -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:

View File

@@ -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"

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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(
{

View File

@@ -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():

View File

@@ -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",

View File

@@ -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:

View File

@@ -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": [

View File

@@ -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"):

View File

@@ -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:

View File

@@ -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"

View File

@@ -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):