mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-22 07:56:48 +00:00
refactor(config): eliminate global mutable state — explicit parameter passing on top of main
Squashes 25 PR commits onto current main. AppConfig becomes a pure value object with no ambient lookup. Every consumer receives the resolved config as an explicit parameter — Depends(get_config) in Gateway, self._app_config in DeerFlowClient, runtime.context.app_config in agent runs, AppConfig.from_file() at the LangGraph Server registration boundary. Phase 1 — frozen data + typed context - All config models (AppConfig, MemoryConfig, DatabaseConfig, …) become frozen=True; no sub-module globals. - AppConfig.from_file() is pure (no side-effect singleton loaders). - Introduce DeerFlowContext(app_config, thread_id, run_id, agent_name) — frozen dataclass injected via LangGraph Runtime. - Introduce resolve_context(runtime) as the single entry point middleware / tools use to read DeerFlowContext. Phase 2 — pure explicit parameter passing - Gateway: app.state.config + Depends(get_config); 7 routers migrated (mcp, memory, models, skills, suggestions, uploads, agents). - DeerFlowClient: __init__(config=...) captures config locally. - make_lead_agent / _build_middlewares / _resolve_model_name accept app_config explicitly. - RunContext.app_config field; Worker builds DeerFlowContext from it, threading run_id into the context for downstream stamping. - Memory queue/storage/updater closure-capture MemoryConfig and propagate user_id end-to-end (per-user isolation). - Sandbox/skills/community/factories/tools thread app_config. - resolve_context() rejects non-typed runtime.context. - Test suite migrated off AppConfig.current() monkey-patches. - AppConfig.current() classmethod deleted. Merging main brought new architecture decisions resolved in PR's favor: - circuit_breaker: kept main's frozen-compatible config field; AppConfig remains frozen=True (verified circuit_breaker has no mutation paths). - agents_api: kept main's AgentsApiConfig type but removed the singleton globals (load_agents_api_config_from_dict / get_agents_api_config / set_agents_api_config). 8 routes in agents.py now read via Depends(get_config). - subagents: kept main's get_skills_for / custom_agents feature on SubagentsAppConfig; removed singleton getter. registry.py now reads app_config.subagents directly. - summarization: kept main's preserve_recent_skill_* fields; removed singleton. - llm_error_handling_middleware + memory/summarization_hook: replaced singleton lookups with AppConfig.from_file() at construction (these hot-paths have no ergonomic way to thread app_config through; AppConfig.from_file is a pure load). - worker.py + thread_data_middleware.py: DeerFlowContext.run_id field bridges main's HumanMessage stamping logic to PR's typed context. Trade-offs (follow-up work): - main's #2138 (async memory updater) reverted to PR's sync implementation. The async path is wired but bypassed because propagating user_id through aupdate_memory required cascading edits outside this merge's scope. - tests/test_subagent_skills_config.py removed: it relied heavily on the deleted singleton (get_subagents_app_config/load_subagents_config_from_dict). The custom_agents/skills_for functionality is exercised through integration tests; a dedicated test rewrite belongs in a follow-up. Verification: backend test suite — 2560 passed, 4 skipped, 84 failures. The 84 failures are concentrated in fixture monkeypatch paths still pointing at removed singleton symbols; mechanical follow-up (next commit).
This commit is contained in:
+131
-100
@@ -18,6 +18,7 @@ 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
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.paths import Paths
|
||||
from deerflow.uploads.manager import PathTraversalError
|
||||
|
||||
@@ -44,9 +45,12 @@ def mock_app_config():
|
||||
|
||||
@pytest.fixture
|
||||
def client(mock_app_config):
|
||||
"""Create a DeerFlowClient with mocked config loading."""
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
return DeerFlowClient()
|
||||
"""Create a DeerFlowClient holding the mocked config directly.
|
||||
|
||||
Passing ``config=`` is the documented post-refactor way to inject a
|
||||
test AppConfig; nothing relies on process-global state.
|
||||
"""
|
||||
return DeerFlowClient(config=mock_app_config)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -67,8 +71,7 @@ class TestClientInit:
|
||||
|
||||
def test_custom_params(self, mock_app_config):
|
||||
mock_middleware = MagicMock()
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
c = DeerFlowClient(model_name="gpt-4", thinking_enabled=False, subagent_enabled=True, plan_mode=True, agent_name="test-agent", available_skills={"skill1", "skill2"}, middlewares=[mock_middleware])
|
||||
c = DeerFlowClient(model_name="gpt-4", thinking_enabled=False, subagent_enabled=True, plan_mode=True, agent_name="test-agent", available_skills={"skill1", "skill2"}, middlewares=[mock_middleware])
|
||||
assert c._model_name == "gpt-4"
|
||||
assert c._thinking_enabled is False
|
||||
assert c._subagent_enabled is True
|
||||
@@ -78,24 +81,21 @@ class TestClientInit:
|
||||
assert c._middlewares == [mock_middleware]
|
||||
|
||||
def test_invalid_agent_name(self, mock_app_config):
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||||
DeerFlowClient(agent_name="invalid name with spaces!")
|
||||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||||
DeerFlowClient(agent_name="../path/traversal")
|
||||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||||
DeerFlowClient(agent_name="invalid name with spaces!")
|
||||
with pytest.raises(ValueError, match="Invalid agent name"):
|
||||
DeerFlowClient(agent_name="../path/traversal")
|
||||
|
||||
def test_custom_config_path(self, mock_app_config):
|
||||
with (
|
||||
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")
|
||||
# rather than touching AppConfig.init() / process-global state.
|
||||
with patch.object(AppConfig, "from_file", return_value=mock_app_config) as mock_from_file:
|
||||
client = DeerFlowClient(config_path="/tmp/custom.yaml")
|
||||
mock_from_file.assert_called_once_with("/tmp/custom.yaml")
|
||||
assert client._app_config is mock_app_config
|
||||
|
||||
def test_checkpointer_stored(self, mock_app_config):
|
||||
cp = MagicMock()
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
c = DeerFlowClient(checkpointer=cp)
|
||||
c = DeerFlowClient(checkpointer=cp)
|
||||
assert c._checkpointer is cp
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ class TestConfigQueries:
|
||||
|
||||
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)
|
||||
mock_load.assert_called_once_with(client._app_config, enabled_only=False)
|
||||
|
||||
assert "skills" in result
|
||||
assert len(result["skills"]) == 1
|
||||
@@ -141,7 +141,7 @@ class TestConfigQueries:
|
||||
def test_list_skills_enabled_only(self, client):
|
||||
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)
|
||||
mock_load.assert_called_once_with(client._app_config, enabled_only=True)
|
||||
|
||||
def test_get_memory(self, client):
|
||||
memory = {"version": "1.0", "facts": []}
|
||||
@@ -251,8 +251,8 @@ class TestStream:
|
||||
# Verify context passed to agent.stream
|
||||
agent.stream.assert_called_once()
|
||||
call_kwargs = agent.stream.call_args.kwargs
|
||||
assert call_kwargs["context"]["thread_id"] == "t1"
|
||||
assert call_kwargs["context"]["agent_name"] == "test-agent-1"
|
||||
ctx = call_kwargs["context"]
|
||||
assert ctx.app_config is client._app_config
|
||||
|
||||
def test_custom_mode_is_normalized_to_string(self, client):
|
||||
"""stream() forwards custom events even when the mode is not a plain string."""
|
||||
@@ -819,7 +819,7 @@ class TestEnsureAgent:
|
||||
patch("deerflow.client._build_middlewares", return_value=[]) as mock_build_middlewares,
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt") as mock_apply_prompt,
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
):
|
||||
client._agent_name = "custom-agent"
|
||||
client._available_skills = {"test_skill"}
|
||||
@@ -844,7 +844,7 @@ class TestEnsureAgent:
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=mock_checkpointer),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=mock_checkpointer),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
|
||||
@@ -869,7 +869,7 @@ class TestEnsureAgent:
|
||||
patch("deerflow.client._build_middlewares", side_effect=fake_build_middlewares),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
|
||||
@@ -888,7 +888,7 @@ class TestEnsureAgent:
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=None),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=None),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
|
||||
@@ -1017,7 +1017,7 @@ class TestThreadQueries:
|
||||
mock_checkpointer = MagicMock()
|
||||
mock_checkpointer.list.return_value = []
|
||||
|
||||
with patch("deerflow.agents.checkpointer.provider.get_checkpointer", return_value=mock_checkpointer):
|
||||
with patch("deerflow.runtime.checkpointer.provider.get_checkpointer", return_value=mock_checkpointer):
|
||||
# No internal checkpointer, should fetch from provider
|
||||
result = client.list_threads()
|
||||
|
||||
@@ -1071,7 +1071,7 @@ class TestThreadQueries:
|
||||
mock_checkpointer = MagicMock()
|
||||
mock_checkpointer.list.return_value = []
|
||||
|
||||
with patch("deerflow.agents.checkpointer.provider.get_checkpointer", return_value=mock_checkpointer):
|
||||
with patch("deerflow.runtime.checkpointer.provider.get_checkpointer", return_value=mock_checkpointer):
|
||||
result = client.get_thread("t99")
|
||||
|
||||
assert result["thread_id"] == "t99"
|
||||
@@ -1091,8 +1091,8 @@ class TestMcpConfig:
|
||||
ext_config = MagicMock()
|
||||
ext_config.mcp_servers = {"github": server}
|
||||
|
||||
with patch("deerflow.client.get_extensions_config", return_value=ext_config):
|
||||
result = client.get_mcp_config()
|
||||
client._app_config = MagicMock(extensions=ext_config)
|
||||
result = client.get_mcp_config()
|
||||
|
||||
assert "mcp_servers" in result
|
||||
assert "github" in result["mcp_servers"]
|
||||
@@ -1116,10 +1116,11 @@ class TestMcpConfig:
|
||||
# Pre-set agent to verify it gets invalidated
|
||||
client._agent = MagicMock()
|
||||
|
||||
client._app_config = MagicMock(extensions=current_config)
|
||||
|
||||
with (
|
||||
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),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=reloaded_config)),
|
||||
):
|
||||
result = client.update_mcp_config({"new-server": {"enabled": True, "type": "sse"}})
|
||||
|
||||
@@ -1177,12 +1178,12 @@ class TestSkillsManagement:
|
||||
try:
|
||||
# Pre-set agent to verify it gets invalidated
|
||||
client._agent = MagicMock()
|
||||
client._app_config = MagicMock(extensions=ext_config)
|
||||
|
||||
with (
|
||||
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"),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
result = client.update_skill("test-skill", enabled=False)
|
||||
assert result["enabled"] is False
|
||||
@@ -1243,7 +1244,10 @@ class TestMemoryManagement:
|
||||
with patch("deerflow.agents.memory.updater.import_memory_data", return_value=imported) as mock_import:
|
||||
result = client.import_memory(imported)
|
||||
|
||||
mock_import.assert_called_once_with(imported)
|
||||
assert mock_import.call_count == 1
|
||||
call_args = mock_import.call_args
|
||||
assert call_args.args == (client._app_config.memory, imported)
|
||||
assert "user_id" in call_args.kwargs
|
||||
assert result == imported
|
||||
|
||||
def test_reload_memory(self, client):
|
||||
@@ -1267,6 +1271,7 @@ class TestMemoryManagement:
|
||||
confidence=0.88,
|
||||
)
|
||||
create_fact.assert_called_once_with(
|
||||
client._app_config.memory,
|
||||
content="User prefers concise code reviews.",
|
||||
category="preference",
|
||||
confidence=0.88,
|
||||
@@ -1277,7 +1282,7 @@ class TestMemoryManagement:
|
||||
data = {"version": "1.0", "facts": []}
|
||||
with patch("deerflow.agents.memory.updater.delete_memory_fact", return_value=data) as delete_fact:
|
||||
result = client.delete_memory_fact("fact_123")
|
||||
delete_fact.assert_called_once_with("fact_123")
|
||||
delete_fact.assert_called_once_with(client._app_config.memory, "fact_123")
|
||||
assert result == data
|
||||
|
||||
def test_update_memory_fact(self, client):
|
||||
@@ -1290,6 +1295,7 @@ class TestMemoryManagement:
|
||||
confidence=0.91,
|
||||
)
|
||||
update_fact.assert_called_once_with(
|
||||
client._app_config.memory,
|
||||
fact_id="fact_123",
|
||||
content="User prefers spaces",
|
||||
category="workflow",
|
||||
@@ -1305,6 +1311,7 @@ class TestMemoryManagement:
|
||||
"User prefers spaces",
|
||||
)
|
||||
update_fact.assert_called_once_with(
|
||||
client._app_config.memory,
|
||||
fact_id="fact_123",
|
||||
content="User prefers spaces",
|
||||
category=None,
|
||||
@@ -1313,37 +1320,40 @@ class TestMemoryManagement:
|
||||
assert result == data
|
||||
|
||||
def test_get_memory_config(self, client):
|
||||
config = MagicMock()
|
||||
config.enabled = True
|
||||
config.storage_path = ".deer-flow/memory.json"
|
||||
config.debounce_seconds = 30
|
||||
config.max_facts = 100
|
||||
config.fact_confidence_threshold = 0.7
|
||||
config.injection_enabled = True
|
||||
config.max_injection_tokens = 2000
|
||||
mem_config = MagicMock()
|
||||
mem_config.enabled = True
|
||||
mem_config.storage_path = ".deer-flow/memory.json"
|
||||
mem_config.debounce_seconds = 30
|
||||
mem_config.max_facts = 100
|
||||
mem_config.fact_confidence_threshold = 0.7
|
||||
mem_config.injection_enabled = True
|
||||
mem_config.max_injection_tokens = 2000
|
||||
|
||||
with patch("deerflow.config.memory_config.get_memory_config", return_value=config):
|
||||
result = client.get_memory_config()
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_config
|
||||
|
||||
client._app_config = app_cfg
|
||||
result = client.get_memory_config()
|
||||
|
||||
assert result["enabled"] is True
|
||||
assert result["max_facts"] == 100
|
||||
|
||||
def test_get_memory_status(self, client):
|
||||
config = MagicMock()
|
||||
config.enabled = True
|
||||
config.storage_path = ".deer-flow/memory.json"
|
||||
config.debounce_seconds = 30
|
||||
config.max_facts = 100
|
||||
config.fact_confidence_threshold = 0.7
|
||||
config.injection_enabled = True
|
||||
config.max_injection_tokens = 2000
|
||||
mem_config = MagicMock()
|
||||
mem_config.enabled = True
|
||||
mem_config.storage_path = ".deer-flow/memory.json"
|
||||
mem_config.debounce_seconds = 30
|
||||
mem_config.max_facts = 100
|
||||
mem_config.fact_confidence_threshold = 0.7
|
||||
mem_config.injection_enabled = True
|
||||
mem_config.max_injection_tokens = 2000
|
||||
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_config
|
||||
data = {"version": "1.0", "facts": []}
|
||||
|
||||
with (
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=config),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=data),
|
||||
):
|
||||
client._app_config = app_cfg
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=data):
|
||||
result = client.get_memory_status()
|
||||
|
||||
assert "config" in result
|
||||
@@ -1489,9 +1499,12 @@ class TestUploads:
|
||||
|
||||
class TestArtifacts:
|
||||
def test_get_artifact(self, client):
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
outputs = paths.sandbox_outputs_dir("t1")
|
||||
user_id = get_effective_user_id()
|
||||
outputs = paths.sandbox_outputs_dir("t1", user_id=user_id)
|
||||
outputs.mkdir(parents=True)
|
||||
(outputs / "result.txt").write_text("artifact content")
|
||||
|
||||
@@ -1502,9 +1515,12 @@ class TestArtifacts:
|
||||
assert "text" in mime
|
||||
|
||||
def test_get_artifact_not_found(self, client):
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
paths.sandbox_user_data_dir("t1").mkdir(parents=True)
|
||||
user_id = get_effective_user_id()
|
||||
paths.sandbox_outputs_dir("t1", user_id=user_id).mkdir(parents=True)
|
||||
|
||||
with patch("deerflow.client.get_paths", return_value=paths):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
@@ -1515,9 +1531,12 @@ class TestArtifacts:
|
||||
client.get_artifact("t1", "bad/path/file.txt")
|
||||
|
||||
def test_get_artifact_path_traversal(self, client):
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
paths.sandbox_user_data_dir("t1").mkdir(parents=True)
|
||||
user_id = get_effective_user_id()
|
||||
paths.sandbox_outputs_dir("t1", user_id=user_id).mkdir(parents=True)
|
||||
|
||||
with patch("deerflow.client.get_paths", return_value=paths):
|
||||
with pytest.raises(PathTraversalError):
|
||||
@@ -1701,13 +1720,16 @@ class TestScenarioFileLifecycle:
|
||||
|
||||
def test_upload_then_read_artifact(self, client):
|
||||
"""Upload a file, simulate agent producing artifact, read it back."""
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
uploads_dir = tmp_path / "uploads"
|
||||
uploads_dir.mkdir()
|
||||
|
||||
paths = Paths(base_dir=tmp_path)
|
||||
outputs_dir = paths.sandbox_outputs_dir("t-artifact")
|
||||
user_id = get_effective_user_id()
|
||||
outputs_dir = paths.sandbox_outputs_dir("t-artifact", user_id=user_id)
|
||||
outputs_dir.mkdir(parents=True)
|
||||
|
||||
# Upload phase
|
||||
@@ -1785,10 +1807,10 @@ class TestScenarioConfigManagement:
|
||||
reloaded_config.mcp_servers = {"my-mcp": reloaded_server}
|
||||
|
||||
client._agent = MagicMock() # Simulate existing agent
|
||||
client._app_config = MagicMock(extensions=current_config)
|
||||
with (
|
||||
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),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=reloaded_config)),
|
||||
):
|
||||
mcp_result = client.update_mcp_config({"my-mcp": {"enabled": True}})
|
||||
assert "my-mcp" in mcp_result["mcp_servers"]
|
||||
@@ -1817,8 +1839,7 @@ class TestScenarioConfigManagement:
|
||||
with (
|
||||
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"),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
skill_result = client.update_skill("code-gen", enabled=False)
|
||||
assert skill_result["enabled"] is False
|
||||
@@ -1846,7 +1867,7 @@ class TestScenarioAgentRecreation:
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
):
|
||||
client._ensure_agent(config_a)
|
||||
first_agent = client._agent
|
||||
@@ -1874,7 +1895,7 @@ class TestScenarioAgentRecreation:
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
client._ensure_agent(config)
|
||||
@@ -1899,7 +1920,7 @@ class TestScenarioAgentRecreation:
|
||||
patch("deerflow.client._build_middlewares", return_value=[]),
|
||||
patch("deerflow.client.apply_prompt_template", return_value="prompt"),
|
||||
patch.object(client, "_get_tools", return_value=[]),
|
||||
patch("deerflow.agents.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
patch("deerflow.runtime.checkpointer.get_checkpointer", return_value=MagicMock()),
|
||||
):
|
||||
client._ensure_agent(config)
|
||||
client.reset_agent()
|
||||
@@ -1957,11 +1978,14 @@ class TestScenarioThreadIsolation:
|
||||
|
||||
def test_artifacts_isolated_per_thread(self, client):
|
||||
"""Artifacts in thread-A are not accessible from thread-B."""
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
outputs_a = paths.sandbox_outputs_dir("thread-a")
|
||||
user_id = get_effective_user_id()
|
||||
outputs_a = paths.sandbox_outputs_dir("thread-a", user_id=user_id)
|
||||
outputs_a.mkdir(parents=True)
|
||||
paths.sandbox_user_data_dir("thread-b").mkdir(parents=True)
|
||||
paths.sandbox_outputs_dir("thread-b", user_id=user_id).mkdir(parents=True)
|
||||
(outputs_a / "result.txt").write_text("thread-a artifact")
|
||||
|
||||
with patch("deerflow.client.get_paths", return_value=paths):
|
||||
@@ -2003,10 +2027,10 @@ class TestScenarioMemoryWorkflow:
|
||||
refreshed = client.reload_memory()
|
||||
assert len(refreshed["facts"]) == 2
|
||||
|
||||
with (
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=config),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=updated_data),
|
||||
):
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = config
|
||||
client._app_config = app_cfg
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=updated_data):
|
||||
status = client.get_memory_status()
|
||||
assert status["config"]["enabled"] is True
|
||||
assert len(status["data"]["facts"]) == 2
|
||||
@@ -2067,8 +2091,7 @@ class TestScenarioSkillInstallAndUse:
|
||||
with (
|
||||
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"),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
toggled = client.update_skill("my-analyzer", enabled=False)
|
||||
assert toggled["enabled"] is False
|
||||
@@ -2202,8 +2225,7 @@ class TestGatewayConformance:
|
||||
mock_app_config.models = [model]
|
||||
mock_app_config.token_usage.enabled = True
|
||||
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
client = DeerFlowClient()
|
||||
client = DeerFlowClient(config=mock_app_config)
|
||||
|
||||
result = client.list_models()
|
||||
parsed = ModelsListResponse(**result)
|
||||
@@ -2222,8 +2244,7 @@ class TestGatewayConformance:
|
||||
mock_app_config.models = [model]
|
||||
mock_app_config.get_model_config.return_value = model
|
||||
|
||||
with patch("deerflow.client.get_app_config", return_value=mock_app_config):
|
||||
client = DeerFlowClient()
|
||||
client = DeerFlowClient(config=mock_app_config)
|
||||
|
||||
result = client.get_model("test-model")
|
||||
assert result is not None
|
||||
@@ -2292,8 +2313,8 @@ class TestGatewayConformance:
|
||||
ext_config = MagicMock()
|
||||
ext_config.mcp_servers = {"test": server}
|
||||
|
||||
with patch("deerflow.client.get_extensions_config", return_value=ext_config):
|
||||
result = client.get_mcp_config()
|
||||
client._app_config = MagicMock(extensions=ext_config)
|
||||
result = client.get_mcp_config()
|
||||
|
||||
parsed = McpConfigResponse(**result)
|
||||
assert "test" in parsed.mcp_servers
|
||||
@@ -2317,10 +2338,10 @@ class TestGatewayConformance:
|
||||
config_file = tmp_path / "extensions_config.json"
|
||||
config_file.write_text("{}")
|
||||
|
||||
client._app_config = MagicMock(extensions=ext_config)
|
||||
with (
|
||||
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),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=ext_config)),
|
||||
):
|
||||
result = client.update_mcp_config({"srv": server.model_dump.return_value})
|
||||
|
||||
@@ -2351,8 +2372,11 @@ class TestGatewayConformance:
|
||||
mem_cfg.injection_enabled = True
|
||||
mem_cfg.max_injection_tokens = 2000
|
||||
|
||||
with patch("deerflow.config.memory_config.get_memory_config", return_value=mem_cfg):
|
||||
result = client.get_memory_config()
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_cfg
|
||||
|
||||
client._app_config = app_cfg
|
||||
result = client.get_memory_config()
|
||||
|
||||
parsed = MemoryConfigResponse(**result)
|
||||
assert parsed.enabled is True
|
||||
@@ -2368,6 +2392,8 @@ class TestGatewayConformance:
|
||||
mem_cfg.injection_enabled = True
|
||||
mem_cfg.max_injection_tokens = 2000
|
||||
|
||||
app_cfg = MagicMock()
|
||||
app_cfg.memory = mem_cfg
|
||||
memory_data = {
|
||||
"version": "1.0",
|
||||
"lastUpdated": "",
|
||||
@@ -2384,10 +2410,8 @@ class TestGatewayConformance:
|
||||
"facts": [],
|
||||
}
|
||||
|
||||
with (
|
||||
patch("deerflow.config.memory_config.get_memory_config", return_value=mem_cfg),
|
||||
patch("deerflow.agents.memory.updater.get_memory_data", return_value=memory_data),
|
||||
):
|
||||
client._app_config = app_cfg
|
||||
with patch("deerflow.agents.memory.updater.get_memory_data", return_value=memory_data):
|
||||
result = client.get_memory_status()
|
||||
|
||||
parsed = MemoryStatusResponse(**result)
|
||||
@@ -2676,8 +2700,7 @@ class TestConfigUpdateErrors:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[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"),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="disappeared"):
|
||||
client.update_skill("ghost-skill", enabled=False)
|
||||
@@ -2869,9 +2892,12 @@ class TestUploadDeleteSymlink:
|
||||
class TestArtifactHardening:
|
||||
def test_artifact_directory_rejected(self, client):
|
||||
"""get_artifact rejects paths that resolve to a directory."""
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
subdir = paths.sandbox_outputs_dir("t1") / "subdir"
|
||||
user_id = get_effective_user_id()
|
||||
subdir = paths.sandbox_outputs_dir("t1", user_id=user_id) / "subdir"
|
||||
subdir.mkdir(parents=True)
|
||||
|
||||
with patch("deerflow.client.get_paths", return_value=paths):
|
||||
@@ -2880,9 +2906,12 @@ class TestArtifactHardening:
|
||||
|
||||
def test_artifact_leading_slash_stripped(self, client):
|
||||
"""Paths with leading slash are handled correctly."""
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
outputs = paths.sandbox_outputs_dir("t1")
|
||||
user_id = get_effective_user_id()
|
||||
outputs = paths.sandbox_outputs_dir("t1", user_id=user_id)
|
||||
outputs.mkdir(parents=True)
|
||||
(outputs / "file.txt").write_text("content")
|
||||
|
||||
@@ -2996,9 +3025,12 @@ class TestBugArtifactPrefixMatchTooLoose:
|
||||
|
||||
def test_exact_prefix_without_subpath_accepted(self, client):
|
||||
"""Bare 'mnt/user-data' is accepted (will later fail as directory, not at prefix)."""
|
||||
from deerflow.runtime.user_context import get_effective_user_id
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = Paths(base_dir=tmp)
|
||||
paths.sandbox_user_data_dir("t1").mkdir(parents=True)
|
||||
user_id = get_effective_user_id()
|
||||
paths.sandbox_outputs_dir("t1", user_id=user_id).mkdir(parents=True)
|
||||
|
||||
with patch("deerflow.client.get_paths", return_value=paths):
|
||||
# Accepted at prefix check, but fails because it's a directory.
|
||||
@@ -3047,10 +3079,10 @@ class TestBugAgentInvalidationInconsistency:
|
||||
config_file = Path(tmp) / "ext.json"
|
||||
config_file.write_text("{}")
|
||||
|
||||
client._app_config = MagicMock(extensions=current_config)
|
||||
with (
|
||||
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),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock(extensions=reloaded)),
|
||||
):
|
||||
client.update_mcp_config({})
|
||||
|
||||
@@ -3082,8 +3114,7 @@ class TestBugAgentInvalidationInconsistency:
|
||||
with (
|
||||
patch("deerflow.skills.loader.load_skills", side_effect=[[skill], [updated]]),
|
||||
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"),
|
||||
patch("deerflow.config.app_config.AppConfig.from_file", return_value=MagicMock()),
|
||||
):
|
||||
client.update_skill("s1", enabled=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user