|
|
|
|
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
import app.agents # noqa: F401 — triggers @registry.register decorators
|
|
|
|
|
from app.agents.checkpoint_agent import CheckpointAgent
|
|
|
|
|
from app.agents.timeline_agent import TimelineAgent
|
|
|
|
|
from app.agents.note_agent import NoteAgent
|
|
|
|
|
from app.agents.project_agent import ProjectAgent
|
|
|
|
|
from app.agents.task_agent import TaskAgent
|
|
|
|
|
@@ -110,12 +110,12 @@ class TestAgentRegistration:
|
|
|
|
|
def test_all_agents_registered(self) -> None:
|
|
|
|
|
names = {a["name"] for a in registry.list_agents()}
|
|
|
|
|
assert {
|
|
|
|
|
"task_agent", "checkpoint_agent", "project_agent", "note_agent"
|
|
|
|
|
"task_agent", "timeline_agent", "project_agent", "note_agent"
|
|
|
|
|
}.issubset(names)
|
|
|
|
|
|
|
|
|
|
def test_registry_returns_correct_types(self) -> None:
|
|
|
|
|
assert isinstance(registry.get("task_agent"), TaskAgent)
|
|
|
|
|
assert isinstance(registry.get("checkpoint_agent"), CheckpointAgent)
|
|
|
|
|
assert isinstance(registry.get("timeline_agent"), TimelineAgent)
|
|
|
|
|
assert isinstance(registry.get("project_agent"), ProjectAgent)
|
|
|
|
|
assert isinstance(registry.get("note_agent"), NoteAgent)
|
|
|
|
|
|
|
|
|
|
@@ -336,94 +336,94 @@ class TestTaskAgentTools:
|
|
|
|
|
assert "c1" in result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── CheckpointAgent ───────────────────────────────────────────────────
|
|
|
|
|
# ── TimelineAgent ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCheckpointAgent:
|
|
|
|
|
class TestTimelineAgent:
|
|
|
|
|
def test_name(self) -> None:
|
|
|
|
|
assert CheckpointAgent().get_name() == "checkpoint_agent"
|
|
|
|
|
assert TimelineAgent().get_name() == "timeline_agent"
|
|
|
|
|
|
|
|
|
|
def test_description(self) -> None:
|
|
|
|
|
assert CheckpointAgent().get_description() == "Manages project checkpoints (milestones): list, create, update, delete"
|
|
|
|
|
assert TimelineAgent().get_description() == "Manages project timelines (milestones): list, create, update, delete"
|
|
|
|
|
|
|
|
|
|
def test_get_tools_count(self) -> None:
|
|
|
|
|
assert len(CheckpointAgent().get_tools()) == 4
|
|
|
|
|
assert len(TimelineAgent().get_tools()) == 4
|
|
|
|
|
|
|
|
|
|
def test_tool_names(self) -> None:
|
|
|
|
|
names = {t.name for t in CheckpointAgent().get_tools()}
|
|
|
|
|
assert names == {"list_checkpoints", "create_checkpoint", "update_checkpoint", "delete_checkpoint"}
|
|
|
|
|
names = {t.name for t in TimelineAgent().get_tools()}
|
|
|
|
|
assert names == {"list_timelines", "create_timeline", "update_timeline", "delete_timeline"}
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_handle_no_tool_calls(self) -> None:
|
|
|
|
|
with patch("app.agents.checkpoint_agent.get_llm") as mock_cls:
|
|
|
|
|
mock_cls.return_value = _mock_llm("No checkpoints found.")
|
|
|
|
|
result = await CheckpointAgent().handle("list checkpoints", {})
|
|
|
|
|
assert result == "No checkpoints found."
|
|
|
|
|
with patch("app.agents.timeline_agent.get_llm") as mock_cls:
|
|
|
|
|
mock_cls.return_value = _mock_llm("No timelines found.")
|
|
|
|
|
result = await TimelineAgent().handle("list timelines", {})
|
|
|
|
|
assert result == "No timelines found."
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_handle_with_create_tool_call(self) -> None:
|
|
|
|
|
with patch("app.agents.checkpoint_agent.get_llm") as mock_cls:
|
|
|
|
|
with patch("app.agents.timeline_agent.get_llm") as mock_cls:
|
|
|
|
|
mock_cls.return_value = _mock_llm_with_tool_call(
|
|
|
|
|
"create_checkpoint",
|
|
|
|
|
"create_timeline",
|
|
|
|
|
{"project_id": "p1", "title": "MVP Launch", "date": 1700000000000},
|
|
|
|
|
"Checkpoint 'MVP Launch' created.",
|
|
|
|
|
"Timeline 'MVP Launch' created.",
|
|
|
|
|
)
|
|
|
|
|
result = await CheckpointAgent().handle("add MVP checkpoint", {})
|
|
|
|
|
assert result == "Checkpoint 'MVP Launch' created."
|
|
|
|
|
result = await TimelineAgent().handle("add MVP timeline", {})
|
|
|
|
|
assert result == "Timeline 'MVP Launch' created."
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_handle_accepts_empty_context(self) -> None:
|
|
|
|
|
with patch("app.agents.checkpoint_agent.get_llm") as mock_cls:
|
|
|
|
|
with patch("app.agents.timeline_agent.get_llm") as mock_cls:
|
|
|
|
|
mock_cls.return_value = _mock_llm("Done.")
|
|
|
|
|
result = await CheckpointAgent().handle("show milestones", {})
|
|
|
|
|
result = await TimelineAgent().handle("show milestones", {})
|
|
|
|
|
assert isinstance(result, str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCheckpointAgentTools:
|
|
|
|
|
class TestTimelineAgentTools:
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_checkpoints_no_project(self) -> None:
|
|
|
|
|
from app.agents.checkpoint_agent import list_checkpoints
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
async def test_list_timelines_no_project(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import list_timelines
|
|
|
|
|
with patch("app.agents.timeline_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
m.return_value = {"rows": []}
|
|
|
|
|
result = await list_checkpoints.ainvoke({})
|
|
|
|
|
result = await list_timelines.ainvoke({})
|
|
|
|
|
call_kwargs = m.call_args.kwargs
|
|
|
|
|
assert call_kwargs["action"] == "select"
|
|
|
|
|
assert call_kwargs["table"] == "checkpoints"
|
|
|
|
|
assert call_kwargs["table"] == "timelines"
|
|
|
|
|
assert call_kwargs["filters"]["projectId"] is None
|
|
|
|
|
assert result == "No checkpoints found."
|
|
|
|
|
assert result == "No timelines found."
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_list_checkpoints_with_project(self) -> None:
|
|
|
|
|
from app.agents.checkpoint_agent import list_checkpoints
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
async def test_list_timelines_with_project(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import list_timelines
|
|
|
|
|
with patch("app.agents.timeline_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
m.return_value = {"rows": []}
|
|
|
|
|
await list_checkpoints.ainvoke({"project_id": "p1"})
|
|
|
|
|
await list_timelines.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
|
|
|
|
|
async def test_create_timeline(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import create_timeline
|
|
|
|
|
fake_row = {"id": "cp1", "title": "Beta release", "date": 1700000000000}
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
with patch("app.agents.timeline_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
m.return_value = {"row": fake_row}
|
|
|
|
|
result = await create_checkpoint.ainvoke({
|
|
|
|
|
result = await create_timeline.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["table"] == "timelines"
|
|
|
|
|
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
|
|
|
|
|
async def test_create_timeline_ai_suggested(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import create_timeline
|
|
|
|
|
fake_row = {"id": "cp1", "title": "Review", "date": 1700000000000}
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
with patch("app.agents.timeline_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
m.return_value = {"row": fake_row}
|
|
|
|
|
await create_checkpoint.ainvoke({
|
|
|
|
|
await create_timeline.ainvoke({
|
|
|
|
|
"project_id": "p1", "title": "Review", "date": 1700000000000, "is_ai_suggested": 1,
|
|
|
|
|
})
|
|
|
|
|
call_kwargs = m.call_args.kwargs
|
|
|
|
|
@@ -431,12 +431,12 @@ class TestCheckpointAgentTools:
|
|
|
|
|
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
|
|
|
|
|
async def test_update_timeline_approve(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import update_timeline
|
|
|
|
|
fake_row = {"id": "c1", "title": "MVP", "isApproved": 1}
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
with patch("app.agents.timeline_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})
|
|
|
|
|
result = await update_timeline.ainvoke({"timeline_id": "c1", "is_approved": 1})
|
|
|
|
|
call_kwargs = m.call_args.kwargs
|
|
|
|
|
assert call_kwargs["action"] == "update"
|
|
|
|
|
assert call_kwargs["data"]["id"] == "c1"
|
|
|
|
|
@@ -444,23 +444,23 @@ class TestCheckpointAgentTools:
|
|
|
|
|
assert "c1" in result
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_update_checkpoint_empty_updates(self) -> None:
|
|
|
|
|
from app.agents.checkpoint_agent import update_checkpoint
|
|
|
|
|
async def test_update_timeline_empty_updates(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import update_timeline
|
|
|
|
|
fake_row = {"id": "c1", "title": "MVP"}
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
with patch("app.agents.timeline_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
m.return_value = {"row": fake_row}
|
|
|
|
|
await update_checkpoint.ainvoke({"checkpoint_id": "c1"})
|
|
|
|
|
await update_timeline.ainvoke({"timeline_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
|
|
|
|
|
with patch("app.agents.checkpoint_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
async def test_delete_timeline(self) -> None:
|
|
|
|
|
from app.agents.timeline_agent import delete_timeline
|
|
|
|
|
with patch("app.agents.timeline_agent.execute_on_client", new_callable=AsyncMock) as m:
|
|
|
|
|
m.return_value = {"deleted": True}
|
|
|
|
|
result = await delete_checkpoint.ainvoke({"checkpoint_id": "c1"})
|
|
|
|
|
result = await delete_timeline.ainvoke({"timeline_id": "c1"})
|
|
|
|
|
call_kwargs = m.call_args.kwargs
|
|
|
|
|
assert call_kwargs["action"] == "delete"
|
|
|
|
|
assert call_kwargs["table"] == "checkpoints"
|
|
|
|
|
assert call_kwargs["table"] == "timelines"
|
|
|
|
|
assert call_kwargs["data"]["id"] == "c1"
|
|
|
|
|
assert "c1" in result
|
|
|
|
|
|
|
|
|
|
|