diff --git a/app/agents/checkpoint_agent.py b/app/agents/checkpoint_agent.py index 3de2eb8..91d4f56 100644 --- a/app/agents/checkpoint_agent.py +++ b/app/agents/checkpoint_agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import Any from langchain_core.messages import HumanMessage, SystemMessage diff --git a/app/agents/note_agent.py b/app/agents/note_agent.py index 5589ba1..e5c648a 100644 --- a/app/agents/note_agent.py +++ b/app/agents/note_agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import Any from langchain_core.messages import HumanMessage, SystemMessage diff --git a/app/agents/project_agent.py b/app/agents/project_agent.py index e01f1c6..ccd2ea6 100644 --- a/app/agents/project_agent.py +++ b/app/agents/project_agent.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import Any from langchain_core.messages import HumanMessage, SystemMessage diff --git a/tests/test_agents.py b/tests/test_agents.py index 33c17b9..e31813e 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -14,6 +14,56 @@ from app.agents.note_agent import NoteAgent from app.agents.project_agent import ProjectAgent from app.agents.task_agent import TaskAgent from app.core.agent_registry import registry +from app.core.ws_context import clear_client_executor, set_client_executor + + +# ── WS executor mock ────────────────────────────────────────────────── +# +# Tools call execute_on_client() which reads a ContextVar set by the WS +# handler. In unit tests there is no WS session, so we install a fake +# executor that returns plausible data for each action type. + +_FAKE_ROW: dict[str, Any] = { + "id": "fake-id", + "title": "Fake Title", + "name": "Fake Name", + "status": "todo", + "priority": "medium", + "content": "Fake content", + "date": 1700000000000, + "taskId": "fake-task-id", + "author": "Alice", + "projectId": None, +} + + +async def _fake_executor(payload: dict) -> dict: + action = payload.get("action", "") + if action == "select": + return {"rows": []} + if action == "insert": + data = payload.get("data", {}) + return {"row": {**_FAKE_ROW, **data}} + if action == "update": + data = payload.get("data", {}) + row = {**_FAKE_ROW, "id": data.get("id", "fake-id"), **data.get("updates", {})} + return {"row": row} + if action == "delete": + return {"deleted": True} + if action == "get": + data = payload.get("data", {}) + return {"row": {**_FAKE_ROW, "id": data.get("id", "fake-id")}} + if action == "vector_upsert": + return {"ok": True} + return {} + + +@pytest.fixture(autouse=True) +def ws_executor(): + """Install a fake WS executor for every test so tools can run without a real WS.""" + set_client_executor(_fake_executor) + yield + clear_client_executor() # ── Helpers ────────────────────────────────────────────────────────── @@ -148,110 +198,142 @@ class TestTaskAgentTools: @pytest.mark.asyncio async def test_list_tasks_defaults(self) -> None: from app.agents.task_agent import list_tasks - result = await list_tasks.ainvoke({}) - data = json.loads(result) - assert data["action"] == "list" - assert data["table"] == "tasks" + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_tasks.ainvoke({}) + m.assert_called_once_with( + action="select", table="tasks", + filters={"projectId": None, "status": None, "search": None, "orderBy": None}, + ) + assert result == "No tasks found matching the given filters." @pytest.mark.asyncio async def test_list_tasks_with_status_filter(self) -> None: from app.agents.task_agent import list_tasks - result = await list_tasks.ainvoke({"status": "done"}) - data = json.loads(result) - assert data["filters"]["status"] == "done" + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + await list_tasks.ainvoke({"status": "done"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["filters"]["status"] == "done" @pytest.mark.asyncio async def test_create_task_defaults(self) -> None: from app.agents.task_agent import create_task - result = await create_task.ainvoke({"title": "Test task"}) - data = json.loads(result) - assert data["action"] == "create_record" - assert data["table"] == "tasks" - assert data["data"]["title"] == "Test task" - assert data["data"]["status"] == "todo" - assert data["data"]["priority"] == "medium" + fake_row = {"id": "t1", "title": "Test task", "status": "todo", "priority": "medium"} + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await create_task.ainvoke({"title": "Test task"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "insert" + assert call_kwargs["table"] == "tasks" + assert call_kwargs["data"]["title"] == "Test task" + assert call_kwargs["data"]["status"] == "todo" + assert call_kwargs["data"]["priority"] == "medium" + assert "Test task" in result @pytest.mark.asyncio async def test_create_task_with_all_fields(self) -> None: from app.agents.task_agent import create_task - result = await create_task.ainvoke({ - "title": "Deploy", - "priority": "high", - "status": "in_progress", - "project_id": "p1", - "is_ai_suggested": 1, - }) - data = json.loads(result) - assert data["data"]["priority"] == "high" - assert data["data"]["status"] == "in_progress" - assert data["data"]["projectId"] == "p1" - assert data["data"]["isAiSuggested"] == 1 + fake_row = {"id": "t1", "title": "Deploy", "status": "in_progress", "priority": "high"} + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await create_task.ainvoke({ + "title": "Deploy", "priority": "high", "status": "in_progress", + "project_id": "p1", "is_ai_suggested": 1, + }) + call_kwargs = m.call_args.kwargs + assert call_kwargs["data"]["priority"] == "high" + assert call_kwargs["data"]["status"] == "in_progress" + assert call_kwargs["data"]["projectId"] == "p1" + assert call_kwargs["data"]["isAiSuggested"] == 1 @pytest.mark.asyncio async def test_update_task_with_status(self) -> None: from app.agents.task_agent import update_task - result = await update_task.ainvoke({"task_id": "t1", "status": "done"}) - data = json.loads(result) - assert data["action"] == "update_record" - assert data["data"]["id"] == "t1" - assert data["data"]["updates"]["status"] == "done" + fake_row = {"id": "t1", "title": "Buy groceries", "status": "done"} + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await update_task.ainvoke({"task_id": "t1", "status": "done"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "update" + assert call_kwargs["data"]["id"] == "t1" + assert call_kwargs["data"]["updates"]["status"] == "done" + assert "t1" in result @pytest.mark.asyncio async def test_update_task_empty_updates(self) -> None: from app.agents.task_agent import update_task - result = await update_task.ainvoke({"task_id": "t1"}) - data = json.loads(result) - assert data["data"]["updates"] == {} + fake_row = {"id": "t1", "title": "Task", "status": "todo"} + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await update_task.ainvoke({"task_id": "t1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["data"]["updates"] == {} @pytest.mark.asyncio async def test_delete_task(self) -> None: from app.agents.task_agent import delete_task - result = await delete_task.ainvoke({"task_id": "t1"}) - data = json.loads(result) - assert data["action"] == "delete_record" - assert data["table"] == "tasks" - assert data["data"]["id"] == "t1" + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"deleted": True} + result = await delete_task.ainvoke({"task_id": "t1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "delete" + assert call_kwargs["table"] == "tasks" + assert call_kwargs["data"]["id"] == "t1" + assert "t1" in result @pytest.mark.asyncio async def test_list_tasks_due_today(self) -> None: from app.agents.task_agent import list_tasks_due_today - result = await list_tasks_due_today.ainvoke({}) - data = json.loads(result) - assert data["action"] == "list_due_today" - assert data["table"] == "tasks" + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_tasks_due_today.ainvoke({}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "select" + assert call_kwargs["table"] == "tasks" + assert "dueDateFrom" in call_kwargs["filters"] + assert result == "No tasks are due today." @pytest.mark.asyncio async def test_list_task_comments(self) -> None: from app.agents.task_agent import list_task_comments - result = await list_task_comments.ainvoke({"task_id": "t1"}) - data = json.loads(result) - assert data["action"] == "list" - assert data["table"] == "taskComments" - assert data["filters"]["taskId"] == "t1" + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_task_comments.ainvoke({"task_id": "t1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "select" + assert call_kwargs["table"] == "taskComments" + assert call_kwargs["filters"]["taskId"] == "t1" + assert "t1" in result @pytest.mark.asyncio async def test_add_task_comment(self) -> None: from app.agents.task_agent import add_task_comment - result = await add_task_comment.ainvoke({ - "task_id": "t1", - "author": "Alice", - "content": "Looks good!", - }) - data = json.loads(result) - assert data["action"] == "create_record" - assert data["table"] == "taskComments" - assert data["data"]["taskId"] == "t1" - assert data["data"]["author"] == "Alice" - assert data["data"]["content"] == "Looks good!" + fake_row = {"id": "c1", "taskId": "t1", "author": "Alice", "content": "Looks good!"} + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await add_task_comment.ainvoke({ + "task_id": "t1", "author": "Alice", "content": "Looks good!", + }) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "insert" + assert call_kwargs["table"] == "taskComments" + assert call_kwargs["data"]["taskId"] == "t1" + assert call_kwargs["data"]["author"] == "Alice" + assert call_kwargs["data"]["content"] == "Looks good!" + assert "Alice" in result @pytest.mark.asyncio async def test_delete_task_comment(self) -> None: from app.agents.task_agent import delete_task_comment - result = await delete_task_comment.ainvoke({"comment_id": "c1"}) - data = json.loads(result) - assert data["action"] == "delete_record" - assert data["table"] == "taskComments" - assert data["data"]["id"] == "c1" + with patch("app.agents.task_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"deleted": True} + result = await delete_task_comment.ainvoke({"comment_id": "c1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "delete" + assert call_kwargs["table"] == "taskComments" + assert call_kwargs["data"]["id"] == "c1" + assert "c1" in result # ── CheckpointAgent ─────────────────────────────────────────────────── @@ -301,74 +383,86 @@ class TestCheckpointAgentTools: @pytest.mark.asyncio async def test_list_checkpoints_no_project(self) -> None: from app.agents.checkpoint_agent import list_checkpoints - result = await list_checkpoints.ainvoke({}) - data = json.loads(result) - assert data["action"] == "list" - assert data["table"] == "checkpoints" - assert data["filters"]["projectId"] is None + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_checkpoints.ainvoke({}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "select" + assert call_kwargs["table"] == "checkpoints" + assert call_kwargs["filters"]["projectId"] is None + assert result == "No checkpoints found." @pytest.mark.asyncio async def test_list_checkpoints_with_project(self) -> None: from app.agents.checkpoint_agent import list_checkpoints - result = await list_checkpoints.ainvoke({"project_id": "p1"}) - data = json.loads(result) - assert data["filters"]["projectId"] == "p1" + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + await list_checkpoints.ainvoke({"project_id": "p1"}) + assert m.call_args.kwargs["filters"]["projectId"] == "p1" @pytest.mark.asyncio async def test_create_checkpoint(self) -> None: from app.agents.checkpoint_agent import create_checkpoint - result = await create_checkpoint.ainvoke({ - "project_id": "p1", - "title": "Beta release", - "date": 1700000000000, - }) - data = json.loads(result) - assert data["action"] == "create_record" - assert data["table"] == "checkpoints" - assert data["data"]["projectId"] == "p1" - assert data["data"]["title"] == "Beta release" - assert data["data"]["date"] == 1700000000000 + fake_row = {"id": "cp1", "title": "Beta release", "date": 1700000000000} + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await create_checkpoint.ainvoke({ + "project_id": "p1", "title": "Beta release", "date": 1700000000000, + }) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "insert" + assert call_kwargs["table"] == "checkpoints" + assert call_kwargs["data"]["projectId"] == "p1" + assert call_kwargs["data"]["title"] == "Beta release" + assert call_kwargs["data"]["date"] == 1700000000000 + assert "Beta release" in result @pytest.mark.asyncio async def test_create_checkpoint_ai_suggested(self) -> None: from app.agents.checkpoint_agent import create_checkpoint - result = await create_checkpoint.ainvoke({ - "project_id": "p1", - "title": "Review", - "date": 1700000000000, - "is_ai_suggested": 1, - }) - data = json.loads(result) - assert data["data"]["isAiSuggested"] == 1 - assert data["data"]["isApproved"] == 0 + fake_row = {"id": "cp1", "title": "Review", "date": 1700000000000} + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await create_checkpoint.ainvoke({ + "project_id": "p1", "title": "Review", "date": 1700000000000, "is_ai_suggested": 1, + }) + call_kwargs = m.call_args.kwargs + assert call_kwargs["data"]["isAiSuggested"] == 1 + assert call_kwargs["data"]["isApproved"] == 0 @pytest.mark.asyncio async def test_update_checkpoint_approve(self) -> None: from app.agents.checkpoint_agent import update_checkpoint - result = await update_checkpoint.ainvoke({ - "checkpoint_id": "c1", - "is_approved": 1, - }) - data = json.loads(result) - assert data["action"] == "update_record" - assert data["data"]["id"] == "c1" - assert data["data"]["updates"]["isApproved"] == 1 + fake_row = {"id": "c1", "title": "MVP", "isApproved": 1} + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await update_checkpoint.ainvoke({"checkpoint_id": "c1", "is_approved": 1}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "update" + assert call_kwargs["data"]["id"] == "c1" + assert call_kwargs["data"]["updates"]["isApproved"] == 1 + assert "c1" in result @pytest.mark.asyncio async def test_update_checkpoint_empty_updates(self) -> None: from app.agents.checkpoint_agent import update_checkpoint - result = await update_checkpoint.ainvoke({"checkpoint_id": "c1"}) - data = json.loads(result) - assert data["data"]["updates"] == {} + fake_row = {"id": "c1", "title": "MVP"} + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await update_checkpoint.ainvoke({"checkpoint_id": "c1"}) + assert m.call_args.kwargs["data"]["updates"] == {} @pytest.mark.asyncio async def test_delete_checkpoint(self) -> None: from app.agents.checkpoint_agent import delete_checkpoint - result = await delete_checkpoint.ainvoke({"checkpoint_id": "c1"}) - data = json.loads(result) - assert data["action"] == "delete_record" - assert data["table"] == "checkpoints" - assert data["data"]["id"] == "c1" + with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"deleted": True} + result = await delete_checkpoint.ainvoke({"checkpoint_id": "c1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "delete" + assert call_kwargs["table"] == "checkpoints" + assert call_kwargs["data"]["id"] == "c1" + assert "c1" in result # ── ProjectAgent ────────────────────────────────────────────────────── @@ -425,75 +519,101 @@ class TestProjectAgentTools: @pytest.mark.asyncio async def test_list_projects_defaults(self) -> None: from app.agents.project_agent import list_projects - result = await list_projects.ainvoke({}) - data = json.loads(result) - assert data["action"] == "list" - assert data["table"] == "projects" - assert data["filters"]["includeArchived"] is False + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_projects.ainvoke({}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "select" + assert call_kwargs["table"] == "projects" + assert call_kwargs["filters"]["includeArchived"] is False + assert result == "No projects found." @pytest.mark.asyncio async def test_list_projects_include_archived(self) -> None: from app.agents.project_agent import list_projects - result = await list_projects.ainvoke({"include_archived": 1}) - data = json.loads(result) - assert data["filters"]["includeArchived"] is True + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + await list_projects.ainvoke({"include_archived": 1}) + assert m.call_args.kwargs["filters"]["includeArchived"] is True @pytest.mark.asyncio async def test_list_all_projects(self) -> None: from app.agents.project_agent import list_all_projects - result = await list_all_projects.ainvoke({}) - data = json.loads(result) - assert data["action"] == "list_all" - assert data["table"] == "projects" + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_all_projects.ainvoke({}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "select" + assert call_kwargs["table"] == "projects" + assert result == "No projects found." @pytest.mark.asyncio async def test_get_project(self) -> None: from app.agents.project_agent import get_project - result = await get_project.ainvoke({"project_id": "p1"}) - data = json.loads(result) - assert data["action"] == "get" - assert data["table"] == "projects" - assert data["data"]["id"] == "p1" + fake_row = {"id": "p1", "name": "Alpha", "status": "active", "clientId": None} + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await get_project.ainvoke({"project_id": "p1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "get" + assert call_kwargs["table"] == "projects" + assert call_kwargs["data"]["id"] == "p1" + assert "Alpha" in result @pytest.mark.asyncio async def test_create_project_name_only(self) -> None: from app.agents.project_agent import create_project - result = await create_project.ainvoke({"name": "Alpha"}) - data = json.loads(result) - assert data["action"] == "create_record" - assert data["data"]["name"] == "Alpha" - assert data["data"]["clientId"] is None + fake_row = {"id": "p1", "name": "Alpha"} + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await create_project.ainvoke({"name": "Alpha"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "insert" + assert call_kwargs["data"]["name"] == "Alpha" + assert call_kwargs["data"]["clientId"] is None + assert "Alpha" in result @pytest.mark.asyncio async def test_create_project_with_client(self) -> None: from app.agents.project_agent import create_project - result = await create_project.ainvoke({"name": "Beta", "client_id": "cl1"}) - data = json.loads(result) - assert data["data"]["clientId"] == "cl1" + fake_row = {"id": "p1", "name": "Beta"} + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await create_project.ainvoke({"name": "Beta", "client_id": "cl1"}) + assert m.call_args.kwargs["data"]["clientId"] == "cl1" @pytest.mark.asyncio async def test_update_project_archive(self) -> None: from app.agents.project_agent import update_project - result = await update_project.ainvoke({"project_id": "p1", "status": "archived"}) - data = json.loads(result) - assert data["action"] == "update_record" - assert data["data"]["id"] == "p1" - assert data["data"]["updates"]["status"] == "archived" + fake_row = {"id": "p1", "name": "Alpha", "status": "archived"} + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await update_project.ainvoke({"project_id": "p1", "status": "archived"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "update" + assert call_kwargs["data"]["id"] == "p1" + assert call_kwargs["data"]["updates"]["status"] == "archived" + assert "p1" in result @pytest.mark.asyncio async def test_update_project_empty_updates(self) -> None: from app.agents.project_agent import update_project - result = await update_project.ainvoke({"project_id": "p1"}) - data = json.loads(result) - assert data["data"]["updates"] == {} + fake_row = {"id": "p1", "name": "Alpha", "status": "active"} + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await update_project.ainvoke({"project_id": "p1"}) + assert m.call_args.kwargs["data"]["updates"] == {} @pytest.mark.asyncio async def test_delete_project(self) -> None: from app.agents.project_agent import delete_project - result = await delete_project.ainvoke({"project_id": "p1"}) - data = json.loads(result) - assert data["action"] == "delete_record" - assert data["data"]["id"] == "p1" + with patch("app.agents.project_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"deleted": True} + result = await delete_project.ainvoke({"project_id": "p1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "delete" + assert call_kwargs["data"]["id"] == "p1" + assert "p1" in result # ── NoteAgent ───────────────────────────────────────────────────────── @@ -543,78 +663,99 @@ class TestNoteAgentTools: @pytest.mark.asyncio async def test_list_notes_no_project(self) -> None: from app.agents.note_agent import list_notes - result = await list_notes.ainvoke({}) - data = json.loads(result) - assert data["action"] == "list" - assert data["table"] == "notes" - assert data["filters"]["projectId"] is None + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + result = await list_notes.ainvoke({}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "select" + assert call_kwargs["table"] == "notes" + assert call_kwargs["filters"]["projectId"] is None + assert result == "No notes found." @pytest.mark.asyncio async def test_list_notes_with_project(self) -> None: from app.agents.note_agent import list_notes - result = await list_notes.ainvoke({"project_id": "p1"}) - data = json.loads(result) - assert data["filters"]["projectId"] == "p1" + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"rows": []} + await list_notes.ainvoke({"project_id": "p1"}) + assert m.call_args.kwargs["filters"]["projectId"] == "p1" @pytest.mark.asyncio async def test_get_note(self) -> None: from app.agents.note_agent import get_note - result = await get_note.ainvoke({"note_id": "n1"}) - data = json.loads(result) - assert data["action"] == "get" - assert data["table"] == "notes" - assert data["data"]["id"] == "n1" + fake_row = {"id": "n1", "title": "Daily log", "content": "# Today\nAll good."} + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + result = await get_note.ainvoke({"note_id": "n1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "get" + assert call_kwargs["table"] == "notes" + assert call_kwargs["data"]["id"] == "n1" + assert "Daily log" in result @pytest.mark.asyncio async def test_create_note_minimal(self) -> None: from app.agents.note_agent import create_note - result = await create_note.ainvoke({ - "title": "Daily log", - "content": "# Today\nAll good.", - }) - data = json.loads(result) - assert data["action"] == "create_record" - assert data["table"] == "notes" - assert data["data"]["title"] == "Daily log" - assert data["data"]["content"] == "# Today\nAll good." - assert data["data"]["projectId"] is None + fake_row = {"id": "n1", "title": "Daily log", "projectId": None} + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m, \ + patch("app.agents.note_agent.embed", new_callable=AsyncMock) as me: + m.return_value = {"row": fake_row} + me.return_value = [0.0] * 1536 + result = await create_note.ainvoke({"title": "Daily log", "content": "# Today\nAll good."}) + # First call: insert; second call: vector_upsert + first_call = m.call_args_list[0].kwargs + assert first_call["action"] == "insert" + assert first_call["table"] == "notes" + assert first_call["data"]["title"] == "Daily log" + assert first_call["data"]["content"] == "# Today\nAll good." + assert first_call["data"]["projectId"] is None + assert "Daily log" in result @pytest.mark.asyncio async def test_create_note_with_project(self) -> None: from app.agents.note_agent import create_note - result = await create_note.ainvoke({ - "title": "Sprint notes", - "content": "## Sprint 1", - "project_id": "p1", - }) - data = json.loads(result) - assert data["data"]["projectId"] == "p1" + fake_row = {"id": "n1", "title": "Sprint notes", "projectId": "p1"} + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m, \ + patch("app.agents.note_agent.embed", new_callable=AsyncMock) as me: + m.return_value = {"row": fake_row} + me.return_value = [0.0] * 1536 + await create_note.ainvoke({"title": "Sprint notes", "content": "## Sprint 1", "project_id": "p1"}) + first_call = m.call_args_list[0].kwargs + assert first_call["data"]["projectId"] == "p1" @pytest.mark.asyncio async def test_update_note_content_only(self) -> None: from app.agents.note_agent import update_note - result = await update_note.ainvoke({ - "note_id": "n1", - "content": "# Updated content", - }) - data = json.loads(result) - assert data["action"] == "update_record" - assert data["data"]["id"] == "n1" - assert data["data"]["updates"]["content"] == "# Updated content" - assert "title" not in data["data"]["updates"] + fake_row = {"id": "n1", "title": "Daily log", "projectId": None} + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m, \ + patch("app.agents.note_agent.embed", new_callable=AsyncMock) as me: + m.return_value = {"row": fake_row} + me.return_value = [0.0] * 1536 + result = await update_note.ainvoke({"note_id": "n1", "content": "# Updated content"}) + first_call = m.call_args_list[0].kwargs + assert first_call["action"] == "update" + assert first_call["data"]["id"] == "n1" + assert first_call["data"]["updates"]["content"] == "# Updated content" + assert "title" not in first_call["data"]["updates"] + assert "n1" in result @pytest.mark.asyncio async def test_update_note_empty_updates(self) -> None: from app.agents.note_agent import update_note - result = await update_note.ainvoke({"note_id": "n1"}) - data = json.loads(result) - assert data["data"]["updates"] == {} + fake_row = {"id": "n1", "title": "Daily log", "projectId": None} + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"row": fake_row} + await update_note.ainvoke({"note_id": "n1"}) + assert m.call_args.kwargs["data"]["updates"] == {} @pytest.mark.asyncio async def test_delete_note(self) -> None: from app.agents.note_agent import delete_note - result = await delete_note.ainvoke({"note_id": "n1"}) - data = json.loads(result) - assert data["action"] == "delete_record" - assert data["table"] == "notes" - assert data["data"]["id"] == "n1" + with patch("app.agents.note_agent.execute_on_client", new_callable=AsyncMock) as m: + m.return_value = {"deleted": True} + result = await delete_note.ainvoke({"note_id": "n1"}) + call_kwargs = m.call_args.kwargs + assert call_kwargs["action"] == "delete" + assert call_kwargs["table"] == "notes" + assert call_kwargs["data"]["id"] == "n1" + assert "n1" in result