diff --git a/backend/packages/harness/deerflow/config/agents_config.py b/backend/packages/harness/deerflow/config/agents_config.py index 86b5347db..a9c9e212d 100644 --- a/backend/packages/harness/deerflow/config/agents_config.py +++ b/backend/packages/harness/deerflow/config/agents_config.py @@ -67,11 +67,13 @@ def resolve_agent_dir(name: str, *, user_id: str | None = None) -> Path: paths = get_paths() effective_user = user_id or get_effective_user_id() user_path = paths.user_agent_dir(effective_user, name) - if user_path.exists(): + # Require config.yaml to confirm this is a genuine agent directory, + # not a leftover from memory/storage writes (see #3390). + if user_path.exists() and (user_path / "config.yaml").exists(): return user_path legacy_path = paths.agent_dir(name) - if legacy_path.exists(): + if legacy_path.exists() and (legacy_path / "config.yaml").exists(): return legacy_path return user_path diff --git a/backend/tests/test_custom_agent.py b/backend/tests/test_custom_agent.py index 284908081..9f7a61ba2 100644 --- a/backend/tests/test_custom_agent.py +++ b/backend/tests/test_custom_agent.py @@ -203,6 +203,79 @@ class TestLoadAgentConfig: assert cfg.name == "legacy-agent" +# =========================================================================== +# 3b. resolve_agent_dir — memory-only directory fallback (#3390) +# =========================================================================== + + +class TestResolveAgentDirMemoryOnlyFallback: + """Regression tests for #3390. + + When memory is enabled, the first conversation creates a user-isolated + agent directory containing only ``memory.json`` (no ``config.yaml``). + On the next turn ``resolve_agent_dir`` must fall through to the legacy + shared layout instead of returning the incomplete user directory. + """ + + def test_user_dir_with_only_memory_falls_back_to_legacy(self, tmp_path): + """User dir has memory.json but no config.yaml → use legacy dir.""" + from deerflow.config.agents_config import resolve_agent_dir + + # Legacy agent with full config + legacy_dir = tmp_path / "agents" / "my-agent" + legacy_dir.mkdir(parents=True) + (legacy_dir / "config.yaml").write_text("name: my-agent\n", encoding="utf-8") + (legacy_dir / "SOUL.md").write_text("legacy soul", encoding="utf-8") + + # User dir created by memory write — no config.yaml + user_dir = tmp_path / "users" / "u1" / "agents" / "my-agent" + user_dir.mkdir(parents=True) + (user_dir / "memory.json").write_text("{}", encoding="utf-8") + + with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)), patch("deerflow.config.agents_config.get_effective_user_id", return_value="u1"): + result = resolve_agent_dir("my-agent", user_id="u1") + + assert result == legacy_dir + + def test_user_dir_with_config_takes_priority(self, tmp_path): + """User dir with config.yaml should still win over legacy.""" + from deerflow.config.agents_config import resolve_agent_dir + + # Legacy + legacy_dir = tmp_path / "agents" / "my-agent" + legacy_dir.mkdir(parents=True) + (legacy_dir / "config.yaml").write_text("name: my-agent\n", encoding="utf-8") + + # User dir with full config (migrated) + user_dir = tmp_path / "users" / "u1" / "agents" / "my-agent" + user_dir.mkdir(parents=True) + (user_dir / "config.yaml").write_text("name: my-agent\nmodel: gpt-4\n", encoding="utf-8") + (user_dir / "memory.json").write_text("{}", encoding="utf-8") + + with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)), patch("deerflow.config.agents_config.get_effective_user_id", return_value="u1"): + result = resolve_agent_dir("my-agent", user_id="u1") + + assert result == user_dir + + def test_load_config_falls_back_when_user_dir_is_memory_only(self, tmp_path): + """End-to-end: load_agent_config works when user dir only has memory.json.""" + config_dict = {"name": "my-agent", "description": "Legacy agent", "model": "deepseek-v3"} + _write_agent(tmp_path, "my-agent", config_dict) + + # Simulate memory write creating user dir without config + user_dir = tmp_path / "users" / "u1" / "agents" / "my-agent" + user_dir.mkdir(parents=True) + (user_dir / "memory.json").write_text("{}", encoding="utf-8") + + with patch("deerflow.config.agents_config.get_paths", return_value=_make_paths(tmp_path)), patch("deerflow.config.agents_config.get_effective_user_id", return_value="u1"): + from deerflow.config.agents_config import load_agent_config + + cfg = load_agent_config("my-agent", user_id="u1") + + assert cfg.name == "my-agent" + assert cfg.model == "deepseek-v3" + + # =========================================================================== # 4. load_agent_soul # ===========================================================================