deer-flow/backend/tests/test_memory_router.py
greatmengqi 84dccef230 refactor(config): Phase 2 — eliminate AppConfig.current() ambient lookup
Finish Phase 2 of the config refactor: production code no longer calls
AppConfig.current() anywhere. AppConfig now flows as an explicit parameter
down every consumer lane.

Call-site migrations
--------------------
- Memory subsystem (queue/updater/storage): MemoryConfig captured at
  enqueue time so the Timer closure survives the ContextVar boundary.
- Sandbox layer: tools.py, security.py, sandbox_provider.py, local_sandbox_provider,
  aio_sandbox_provider all take app_config explicitly. Module-level
  caching in tools.py's path helpers is removed — pure parameter flow.
- Skills layer: manager.py + loader.py + lead_agent.prompt cache refresh
  all thread app_config; cache worker closes over it.
- Community tools (tavily, jina, firecrawl, exa, ddg, image_search,
  infoquest, aio_sandbox): read runtime.context.app_config.
- Subagents registry: get_subagent_config / list_subagents /
  get_available_subagent_names require app_config.
- Runtime worker: requires RunContext.app_config; no fallback.
- Gateway routers (uploads, skills): add Depends(get_config).
- Channels feishu: uses AppConfig.from_file() (pure) at its sync boundary.
- LangGraph Server bootstrap (make_lead_agent): falls back to
  AppConfig.from_file() — pure load, not ambient lookup.

Context resolution
------------------
- resolve_context(runtime) now raises on non-DeerFlowContext runtime.context.
  Every entry point attaches typed context; dict/None shapes are rejected
  loudly instead of being papered over with an ambient AppConfig lookup.

AppConfig lifecycle
-------------------
- AppConfig.current() kept as a deprecated slot that raises RuntimeError,
  purely so legacy tests that still run `patch.object(AppConfig, "current")`
  don't trip AttributeError at teardown. Production never calls it.
- conftest autouse fixture no longer monkey-patches `current` — it only
  stubs `from_file()` so tests don't need a real config.yaml.

Design refs
-----------
- docs/plans/2026-04-12-config-refactor-plan.md (Phase 2: P2-6..P2-10)
- docs/plans/2026-04-12-config-refactor-design.md §8

All 2338 non-e2e tests pass. Zero AppConfig.current() call sites remain
in backend/packages or backend/app (docstrings in deps.py excepted).
2026-04-17 11:14:13 +08:00

306 lines
10 KiB
Python

from unittest.mock import patch
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.gateway.routers import memory
from deerflow.config.app_config import AppConfig
from deerflow.config.sandbox_config import SandboxConfig
_TEST_APP_CONFIG = AppConfig(sandbox=SandboxConfig(use="test"))
def _make_app() -> FastAPI:
"""Build a memory-router app pre-populated with a minimal AppConfig."""
app = FastAPI()
app.state.config = _TEST_APP_CONFIG
app.include_router(memory.router)
return app
def _sample_memory(facts: list[dict] | None = None) -> dict:
return {
"version": "1.0",
"lastUpdated": "2026-03-26T12:00:00Z",
"user": {
"workContext": {"summary": "", "updatedAt": ""},
"personalContext": {"summary": "", "updatedAt": ""},
"topOfMind": {"summary": "", "updatedAt": ""},
},
"history": {
"recentMonths": {"summary": "", "updatedAt": ""},
"earlierContext": {"summary": "", "updatedAt": ""},
"longTermBackground": {"summary": "", "updatedAt": ""},
},
"facts": facts or [],
}
def test_export_memory_route_returns_current_memory() -> None:
app = _make_app()
exported_memory = _sample_memory(
facts=[
{
"id": "fact_export",
"content": "User prefers concise responses.",
"category": "preference",
"confidence": 0.9,
"createdAt": "2026-03-20T00:00:00Z",
"source": "thread-1",
}
]
)
with patch("app.gateway.routers.memory.get_memory_data", return_value=exported_memory):
with TestClient(app) as client:
response = client.get("/api/memory/export")
assert response.status_code == 200
assert response.json()["facts"] == exported_memory["facts"]
def test_import_memory_route_returns_imported_memory() -> None:
app = _make_app()
imported_memory = _sample_memory(
facts=[
{
"id": "fact_import",
"content": "User works on DeerFlow.",
"category": "context",
"confidence": 0.87,
"createdAt": "2026-03-20T00:00:00Z",
"source": "manual",
}
]
)
with patch("app.gateway.routers.memory.import_memory_data", return_value=imported_memory):
with TestClient(app) as client:
response = client.post("/api/memory/import", json=imported_memory)
assert response.status_code == 200
assert response.json()["facts"] == imported_memory["facts"]
def test_export_memory_route_preserves_source_error() -> None:
app = _make_app()
exported_memory = _sample_memory(
facts=[
{
"id": "fact_correction",
"content": "Use make dev for local development.",
"category": "correction",
"confidence": 0.95,
"createdAt": "2026-03-20T00:00:00Z",
"source": "thread-1",
"sourceError": "The agent previously suggested npm start.",
}
]
)
with patch("app.gateway.routers.memory.get_memory_data", return_value=exported_memory):
with TestClient(app) as client:
response = client.get("/api/memory/export")
assert response.status_code == 200
assert response.json()["facts"][0]["sourceError"] == "The agent previously suggested npm start."
def test_import_memory_route_preserves_source_error() -> None:
app = _make_app()
imported_memory = _sample_memory(
facts=[
{
"id": "fact_correction",
"content": "Use make dev for local development.",
"category": "correction",
"confidence": 0.95,
"createdAt": "2026-03-20T00:00:00Z",
"source": "thread-1",
"sourceError": "The agent previously suggested npm start.",
}
]
)
with patch("app.gateway.routers.memory.import_memory_data", return_value=imported_memory):
with TestClient(app) as client:
response = client.post("/api/memory/import", json=imported_memory)
assert response.status_code == 200
assert response.json()["facts"][0]["sourceError"] == "The agent previously suggested npm start."
def test_clear_memory_route_returns_cleared_memory() -> None:
app = _make_app()
with patch("app.gateway.routers.memory.clear_memory_data", return_value=_sample_memory()):
with TestClient(app) as client:
response = client.delete("/api/memory")
assert response.status_code == 200
assert response.json()["facts"] == []
def test_create_memory_fact_route_returns_updated_memory() -> None:
app = _make_app()
updated_memory = _sample_memory(
facts=[
{
"id": "fact_new",
"content": "User prefers concise code reviews.",
"category": "preference",
"confidence": 0.88,
"createdAt": "2026-03-20T00:00:00Z",
"source": "manual",
}
]
)
with patch("app.gateway.routers.memory.create_memory_fact", return_value=updated_memory):
with TestClient(app) as client:
response = client.post(
"/api/memory/facts",
json={
"content": "User prefers concise code reviews.",
"category": "preference",
"confidence": 0.88,
},
)
assert response.status_code == 200
assert response.json()["facts"] == updated_memory["facts"]
def test_delete_memory_fact_route_returns_updated_memory() -> None:
app = _make_app()
updated_memory = _sample_memory(
facts=[
{
"id": "fact_keep",
"content": "User likes Python",
"category": "preference",
"confidence": 0.9,
"createdAt": "2026-03-20T00:00:00Z",
"source": "thread-1",
}
]
)
with patch("app.gateway.routers.memory.delete_memory_fact", return_value=updated_memory):
with TestClient(app) as client:
response = client.delete("/api/memory/facts/fact_delete")
assert response.status_code == 200
assert response.json()["facts"] == updated_memory["facts"]
def test_delete_memory_fact_route_returns_404_for_missing_fact() -> None:
app = _make_app()
with patch("app.gateway.routers.memory.delete_memory_fact", side_effect=KeyError("fact_missing")):
with TestClient(app) as client:
response = client.delete("/api/memory/facts/fact_missing")
assert response.status_code == 404
assert response.json()["detail"] == "Memory fact 'fact_missing' not found."
def test_update_memory_fact_route_returns_updated_memory() -> None:
app = _make_app()
updated_memory = _sample_memory(
facts=[
{
"id": "fact_edit",
"content": "User prefers spaces",
"category": "workflow",
"confidence": 0.91,
"createdAt": "2026-03-20T00:00:00Z",
"source": "manual",
}
]
)
with patch("app.gateway.routers.memory.update_memory_fact", return_value=updated_memory):
with TestClient(app) as client:
response = client.patch(
"/api/memory/facts/fact_edit",
json={
"content": "User prefers spaces",
"category": "workflow",
"confidence": 0.91,
},
)
assert response.status_code == 200
assert response.json()["facts"] == updated_memory["facts"]
def test_update_memory_fact_route_preserves_omitted_fields() -> None:
app = _make_app()
updated_memory = _sample_memory(
facts=[
{
"id": "fact_edit",
"content": "User prefers spaces",
"category": "preference",
"confidence": 0.8,
"createdAt": "2026-03-20T00:00:00Z",
"source": "manual",
}
]
)
with patch("app.gateway.routers.memory.update_memory_fact", return_value=updated_memory) as update_fact:
with TestClient(app) as client:
response = client.patch(
"/api/memory/facts/fact_edit",
json={
"content": "User prefers spaces",
},
)
assert response.status_code == 200
assert update_fact.call_count == 1
call_kwargs = update_fact.call_args.kwargs
assert call_kwargs.get("fact_id") == "fact_edit"
assert call_kwargs.get("content") == "User prefers spaces"
assert call_kwargs.get("category") is None
assert call_kwargs.get("confidence") is None
assert "user_id" in call_kwargs
assert response.json()["facts"] == updated_memory["facts"]
def test_update_memory_fact_route_returns_404_for_missing_fact() -> None:
app = _make_app()
with patch("app.gateway.routers.memory.update_memory_fact", side_effect=KeyError("fact_missing")):
with TestClient(app) as client:
response = client.patch(
"/api/memory/facts/fact_missing",
json={
"content": "User prefers spaces",
"category": "workflow",
"confidence": 0.91,
},
)
assert response.status_code == 404
assert response.json()["detail"] == "Memory fact 'fact_missing' not found."
def test_update_memory_fact_route_returns_specific_error_for_invalid_confidence() -> None:
app = _make_app()
with patch("app.gateway.routers.memory.update_memory_fact", side_effect=ValueError("confidence")):
with TestClient(app) as client:
response = client.patch(
"/api/memory/facts/fact_edit",
json={
"content": "User prefers spaces",
"confidence": 0.91,
},
)
assert response.status_code == 400
assert response.json()["detail"] == "Invalid confidence value; must be between 0 and 1."