From 6c0c2ecf62aa6bb9faf0bf8586351fb8bc53967c Mon Sep 17 00:00:00 2001 From: greatmengqi Date: Wed, 15 Apr 2026 21:54:52 +0800 Subject: [PATCH] fix(tests): update test mocks for AppConfig ContextVar refactoring - Add frozen=True to DatabaseConfig and RunEventsConfig - Update test_client_e2e.py: replace _app_config global mock with AppConfig.current staticmethod mock; remove stale _title_config, _summarization_config, get_memory_config patches; move config disablement into _make_e2e_config(); mock AppConfig.from_file for tests that trigger internal config reload - Update test_memory_storage_user_isolation.py: replace get_memory_config mock with autouse AppConfig.current fixture - Update test_model_factory.py: construct ModelConfig with stream_usage extra field instead of mutating frozen instance --- .../deerflow/config/database_config.py | 3 +- .../deerflow/config/run_events_config.py | 3 +- backend/tests/test_client_e2e.py | 52 +++++++------------ .../test_memory_storage_user_isolation.py | 48 ++++++++++------- backend/tests/test_model_factory.py | 23 ++++---- 5 files changed, 64 insertions(+), 65 deletions(-) diff --git a/backend/packages/harness/deerflow/config/database_config.py b/backend/packages/harness/deerflow/config/database_config.py index 37cfd579d..95edc84e5 100644 --- a/backend/packages/harness/deerflow/config/database_config.py +++ b/backend/packages/harness/deerflow/config/database_config.py @@ -34,10 +34,11 @@ from __future__ import annotations import os from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class DatabaseConfig(BaseModel): + model_config = ConfigDict(frozen=True) backend: Literal["memory", "sqlite", "postgres"] = Field( default="memory", description=("Storage backend for both checkpointer and application data. 'memory' for development (no persistence across restarts), 'sqlite' for single-node deployment, 'postgres' for production multi-node deployment."), diff --git a/backend/packages/harness/deerflow/config/run_events_config.py b/backend/packages/harness/deerflow/config/run_events_config.py index cddd9061f..056d0b535 100644 --- a/backend/packages/harness/deerflow/config/run_events_config.py +++ b/backend/packages/harness/deerflow/config/run_events_config.py @@ -15,10 +15,11 @@ from __future__ import annotations from typing import Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field class RunEventsConfig(BaseModel): + model_config = ConfigDict(frozen=True) backend: Literal["memory", "db", "jsonl"] = Field( default="memory", description="Storage backend for run events. 'memory' for development (no persistence), 'db' for production (SQL queries), 'jsonl' for lightweight single-node persistence.", diff --git a/backend/tests/test_client_e2e.py b/backend/tests/test_client_e2e.py index 6c688933a..456a3079d 100644 --- a/backend/tests/test_client_e2e.py +++ b/backend/tests/test_client_e2e.py @@ -56,6 +56,10 @@ def _make_e2e_config() -> AppConfig: - ``E2E_BASE_URL`` (default: ``https://ark-cn-beijing.bytedance.net/api/v3``) - ``OPENAI_API_KEY`` (required for LLM tests) """ + from deerflow.config.memory_config import MemoryConfig + from deerflow.config.summarization_config import SummarizationConfig + from deerflow.config.title_config import TitleConfig + return AppConfig( models=[ ModelConfig( @@ -73,6 +77,9 @@ def _make_e2e_config() -> AppConfig: ) ], sandbox=SandboxConfig(use="deerflow.sandbox.local:LocalSandboxProvider", allow_host_bash=True), + title=TitleConfig(enabled=False), + memory=MemoryConfig(enabled=False), + summarization=SummarizationConfig(enabled=False), ) @@ -87,7 +94,7 @@ def e2e_env(tmp_path, monkeypatch): - DEER_FLOW_HOME → tmp_path (all thread data lands in a temp dir) - Singletons reset so they pick up the new env - - Title/memory/summarization disabled to avoid extra LLM calls + - Title/memory/summarization disabled via AppConfig fields - AppConfig built programmatically (avoids config.yaml param-name issues) """ # 1. Filesystem isolation @@ -95,30 +102,12 @@ def e2e_env(tmp_path, monkeypatch): monkeypatch.setattr("deerflow.config.paths._paths", None) monkeypatch.setattr("deerflow.sandbox.sandbox_provider._default_sandbox_provider", None) - # 2. Inject a clean AppConfig via the global singleton. + # 2. Inject a clean AppConfig via the ContextVar-backed singleton. + # Title, memory, and summarization are disabled in _make_e2e_config(). config = _make_e2e_config() - monkeypatch.setattr("deerflow.config.app_config._app_config", config) - monkeypatch.setattr("deerflow.config.app_config._app_config_is_custom", True) + monkeypatch.setattr(AppConfig, "current", staticmethod(lambda: config)) - # 3. Disable title generation (extra LLM call, non-deterministic) - from deerflow.config.title_config import TitleConfig - - monkeypatch.setattr("deerflow.config.title_config._title_config", TitleConfig(enabled=False)) - - # 4. Disable memory queueing (avoids background threads & file writes) - from deerflow.config.memory_config import MemoryConfig - - monkeypatch.setattr( - "deerflow.agents.middlewares.memory_middleware.get_memory_config", - lambda: MemoryConfig(enabled=False), - ) - - # 5. Ensure summarization is off (default, but be explicit) - from deerflow.config.summarization_config import SummarizationConfig - - monkeypatch.setattr("deerflow.config.summarization_config._summarization_config", SummarizationConfig(enabled=False)) - - # 6. Exclude TitleMiddleware from the chain. + # 3. Exclude TitleMiddleware from the chain. # It triggers an extra LLM call to generate a thread title, which adds # non-determinism and cost to E2E tests (title generation is already # disabled via TitleConfig above, but the middleware still participates @@ -666,10 +655,9 @@ class TestConfigManagement: config_file.write_text(json.dumps({"mcpServers": {}, "skills": {}})) monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_file)) - # Force reload so the singleton picks up our test file - from deerflow.config.extensions_config import reload_extensions_config - - reload_extensions_config() + # Mock from_file so update_mcp_config's internal reload works without config.yaml + e2e_config = _make_e2e_config() + monkeypatch.setattr(AppConfig, "from_file", classmethod(lambda cls, path=None: e2e_config)) c = DeerFlowClient(checkpointer=None, thinking_enabled=False) # Simulate a cached agent @@ -693,9 +681,9 @@ class TestConfigManagement: config_file.write_text(json.dumps({"mcpServers": {}, "skills": {}})) monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_file)) - from deerflow.config.extensions_config import reload_extensions_config - - reload_extensions_config() + # Mock from_file so update_skill's internal reload works without config.yaml + e2e_config = _make_e2e_config() + monkeypatch.setattr(AppConfig, "from_file", classmethod(lambda cls, path=None: e2e_config)) c = DeerFlowClient(checkpointer=None, thinking_enabled=False) c._agent = "fake-agent-placeholder" @@ -721,10 +709,6 @@ class TestConfigManagement: config_file.write_text(json.dumps({"mcpServers": {}, "skills": {}})) monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_file)) - from deerflow.config.extensions_config import reload_extensions_config - - reload_extensions_config() - c = DeerFlowClient(checkpointer=None, thinking_enabled=False) with pytest.raises(ValueError, match="not found"): c.update_skill("nonexistent-skill-xyz", enabled=True) diff --git a/backend/tests/test_memory_storage_user_isolation.py b/backend/tests/test_memory_storage_user_isolation.py index a82fffa50..45b6b59c8 100644 --- a/backend/tests/test_memory_storage_user_isolation.py +++ b/backend/tests/test_memory_storage_user_isolation.py @@ -4,6 +4,14 @@ from pathlib import Path from unittest.mock import patch from deerflow.agents.memory.storage import FileMemoryStorage, create_empty_memory +from deerflow.config.app_config import AppConfig +from deerflow.config.memory_config import MemoryConfig +from deerflow.config.sandbox_config import SandboxConfig + + +def _mock_app_config() -> AppConfig: + """Build a minimal AppConfig with default (empty) memory storage_path.""" + return AppConfig(sandbox=SandboxConfig(use="test"), memory=MemoryConfig(storage_path="")) @pytest.fixture @@ -16,6 +24,14 @@ def storage() -> FileMemoryStorage: return FileMemoryStorage() +@pytest.fixture(autouse=True) +def _mock_current_config(): + """Ensure AppConfig.current() returns a minimal config for all tests.""" + cfg = _mock_app_config() + with patch.object(AppConfig, "current", return_value=cfg): + yield + + class TestUserIsolatedStorage: def test_save_and_load_per_user(self, storage: FileMemoryStorage, base_dir: Path): from deerflow.config.paths import Paths @@ -66,37 +82,33 @@ class TestUserIsolatedStorage: def test_no_user_id_uses_legacy_path(self, base_dir: Path): from deerflow.config.paths import Paths - from deerflow.config.memory_config import MemoryConfig paths = Paths(base_dir) with patch("deerflow.agents.memory.storage.get_paths", return_value=paths): - with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_path="")): - s = FileMemoryStorage() - memory = create_empty_memory() - s.save(memory, user_id=None) - expected_path = base_dir / "memory.json" - assert expected_path.exists() + s = FileMemoryStorage() + memory = create_empty_memory() + s.save(memory, user_id=None) + expected_path = base_dir / "memory.json" + assert expected_path.exists() def test_user_and_legacy_do_not_interfere(self, base_dir: Path): """user_id=None (legacy) and user_id='alice' must use different files and caches.""" from deerflow.config.paths import Paths - from deerflow.config.memory_config import MemoryConfig paths = Paths(base_dir) with patch("deerflow.agents.memory.storage.get_paths", return_value=paths): - with patch("deerflow.agents.memory.storage.get_memory_config", return_value=MemoryConfig(storage_path="")): - s = FileMemoryStorage() + s = FileMemoryStorage() - legacy_mem = create_empty_memory() - legacy_mem["user"]["workContext"]["summary"] = "legacy" - s.save(legacy_mem, user_id=None) + legacy_mem = create_empty_memory() + legacy_mem["user"]["workContext"]["summary"] = "legacy" + s.save(legacy_mem, user_id=None) - user_mem = create_empty_memory() - user_mem["user"]["workContext"]["summary"] = "alice" - s.save(user_mem, user_id="alice") + user_mem = create_empty_memory() + user_mem["user"]["workContext"]["summary"] = "alice" + s.save(user_mem, user_id="alice") - assert s.load(user_id=None)["user"]["workContext"]["summary"] == "legacy" - assert s.load(user_id="alice")["user"]["workContext"]["summary"] == "alice" + assert s.load(user_id=None)["user"]["workContext"]["summary"] == "legacy" + assert s.load(user_id="alice")["user"]["workContext"]["summary"] == "alice" def test_user_agent_memory_file_location(self, base_dir: Path): """Per-user per-agent memory uses the user_agent_memory_file path.""" diff --git a/backend/tests/test_model_factory.py b/backend/tests/test_model_factory.py index 83dd84e04..e1f05cc1b 100644 --- a/backend/tests/test_model_factory.py +++ b/backend/tests/test_model_factory.py @@ -844,7 +844,18 @@ def test_stream_usage_not_injected_for_non_openai_model(monkeypatch): def test_stream_usage_not_overridden_when_explicitly_set_in_config(monkeypatch): """If config dumps stream_usage=False, factory should respect it.""" - cfg = _make_app_config([_make_model("deepseek", use="langchain_deepseek:ChatDeepSeek")]) + # Build a ModelConfig with stream_usage=False as an extra field (extra="allow"). + model_with_stream_usage = ModelConfig( + name="deepseek", + display_name="deepseek", + description=None, + use="langchain_deepseek:ChatDeepSeek", + model="deepseek", + supports_thinking=False, + supports_vision=False, + stream_usage=False, + ) + cfg = _make_app_config([model_with_stream_usage]) _patch_factory(monkeypatch, cfg, model_class=_FakeWithStreamUsage) captured: dict = {} @@ -856,16 +867,6 @@ def test_stream_usage_not_overridden_when_explicitly_set_in_config(monkeypatch): monkeypatch.setattr(factory_module, "resolve_class", lambda path, base: CapturingModel) - # Simulate config having stream_usage explicitly set by patching model_dump - original_get_model_config = cfg.get_model_config - - def patched_get_model_config(name): - mc = original_get_model_config(name) - mc.stream_usage = False # type: ignore[attr-defined] - return mc - - monkeypatch.setattr(cfg, "get_model_config", patched_get_model_config) - factory_module.create_chat_model(name="deepseek") assert captured.get("stream_usage") is False