Skip to content

Commit 4fa0d94

Browse files
committed
feat library tests added
1 parent 989bfb1 commit 4fa0d94

2 files changed

Lines changed: 209 additions & 7 deletions

File tree

agentvault_server_sdk/tests/test_fastapi_integration.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,25 @@ def make_rpc_request(
193193
client: TestClient,
194194
method: str,
195195
params: Optional[Dict[str, Any]] = None,
196-
req_id: Union[str, int] = 1
196+
req_id: Union[str, int] = 1,
197+
# --- ADDED: Allow sending raw content ---
198+
raw_content: Optional[Union[str, bytes]] = None,
199+
json_payload: Optional[Dict[str, Any]] = None,
200+
# --- END ADDED ---
197201
) -> Any:
198202
"""Helper to make JSON-RPC POST requests."""
199-
payload = {"jsonrpc": "2.0", "method": method, "id": req_id}
200-
if params is not None:
201-
payload["params"] = params
202-
response = client.post("/a2a/", json=payload)
203+
# --- MODIFIED: Handle raw_content ---
204+
if raw_content is not None:
205+
headers = {"Content-Type": "application/json"} # Still set header
206+
response = client.post("/a2a/", content=raw_content, headers=headers)
207+
elif json_payload is not None:
208+
response = client.post("/a2a/", json=json_payload)
209+
# --- END MODIFIED ---
210+
else:
211+
payload = {"jsonrpc": "2.0", "method": method, "id": req_id}
212+
if params is not None:
213+
payload["params"] = params
214+
response = client.post("/a2a/", json=payload)
203215
return response
204216

205217
# --- Test Cases ---
@@ -379,3 +391,80 @@ async def test_route_tasks_sendSubscribe_generator_error(test_app_with_agent):
379391
assert error_data.get("error") == "stream_error"
380392
assert f"RuntimeError: {error_message}" in error_data.get("message", "")
381393
# --- END MODIFIED ---
394+
395+
# --- ADDED: Tests for Invalid JSON-RPC Requests ---
396+
397+
def test_invalid_json_request(test_app_with_agent):
398+
"""Test sending invalid JSON."""
399+
_, client, _ = test_app_with_agent
400+
response = make_rpc_request(client, method="", raw_content=b"{invalid json")
401+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
402+
resp_data = response.json()
403+
assert resp_data["id"] is None # No ID could be parsed
404+
assert resp_data["error"]["code"] == JSONRPC_PARSE_ERROR
405+
406+
def test_non_dict_request(test_app_with_agent):
407+
"""Test sending a JSON array instead of an object."""
408+
_, client, _ = test_app_with_agent
409+
response = make_rpc_request(client, method="", json_payload=[1, 2, 3])
410+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
411+
resp_data = response.json()
412+
assert resp_data["id"] is None # No ID could be parsed
413+
assert resp_data["error"]["code"] == JSONRPC_INVALID_REQUEST
414+
assert "Payload must be a JSON object" in resp_data["error"]["message"]
415+
416+
def test_missing_method_request(test_app_with_agent):
417+
"""Test sending a request missing the 'method' field."""
418+
_, client, _ = test_app_with_agent
419+
payload = {"jsonrpc": "2.0", "id": "m-err"}
420+
response = make_rpc_request(client, method="", json_payload=payload)
421+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
422+
resp_data = response.json()
423+
assert resp_data["id"] == "m-err"
424+
assert resp_data["error"]["code"] == JSONRPC_INVALID_REQUEST
425+
assert "'method' is required" in resp_data["error"]["message"]
426+
427+
def test_invalid_jsonrpc_version(test_app_with_agent):
428+
"""Test sending a request with the wrong 'jsonrpc' version."""
429+
_, client, _ = test_app_with_agent
430+
payload = {"jsonrpc": "1.0", "method": "test", "id": "v-err"}
431+
response = make_rpc_request(client, method="", json_payload=payload)
432+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
433+
resp_data = response.json()
434+
assert resp_data["id"] == "v-err"
435+
assert resp_data["error"]["code"] == JSONRPC_INVALID_REQUEST
436+
assert "'jsonrpc' must be '2.0'" in resp_data["error"]["message"]
437+
438+
# --- ADDED: Tests for tasks/sendSubscribe Parameter Validation ---
439+
440+
def test_route_tasks_sendSubscribe_missing_params(test_app_with_agent):
441+
"""Test tasks/sendSubscribe with missing 'params' field."""
442+
mock_agent, client, store = test_app_with_agent
443+
response = make_rpc_request(client, "tasks/sendSubscribe", params=None, req_id="sub-bad1")
444+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
445+
resp_data = response.json()
446+
assert resp_data["id"] == "sub-bad1"
447+
assert resp_data["error"]["code"] == JSONRPC_INVALID_PARAMS
448+
assert "Params must be a dictionary" in resp_data["error"]["message"]
449+
450+
def test_route_tasks_sendSubscribe_missing_id_in_params(test_app_with_agent):
451+
"""Test tasks/sendSubscribe with 'params' missing the 'id' key."""
452+
mock_agent, client, store = test_app_with_agent
453+
response = make_rpc_request(client, "tasks/sendSubscribe", params={"other": "value"}, req_id="sub-bad2")
454+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
455+
resp_data = response.json()
456+
assert resp_data["id"] == "sub-bad2"
457+
assert resp_data["error"]["code"] == JSONRPC_INVALID_PARAMS
458+
assert "'id' parameter is required" in resp_data["error"]["message"]
459+
460+
def test_route_tasks_sendSubscribe_invalid_id_type(test_app_with_agent):
461+
"""Test tasks/sendSubscribe with 'id' parameter of wrong type."""
462+
mock_agent, client, store = test_app_with_agent
463+
response = make_rpc_request(client, "tasks/sendSubscribe", params={"id": 12345}, req_id="sub-bad3")
464+
assert response.status_code == status.HTTP_200_OK # JSON-RPC error
465+
resp_data = response.json()
466+
assert resp_data["id"] == "sub-bad3"
467+
assert resp_data["error"]["code"] == JSONRPC_INVALID_PARAMS
468+
assert "'id' parameter is required" in resp_data["error"]["message"] # Error message might be generic
469+
470+
# --- END ADDED ---

agentvault_server_sdk/tests/test_state.py

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
from freezegun import freeze_time
55
from typing import Union, Any, List
66
# --- ADDED: Import patch ---
7-
from unittest.mock import patch
7+
from unittest.mock import patch, MagicMock, AsyncMock # Added AsyncMock
8+
# --- END ADDED ---
9+
# --- ADDED: Import logging ---
10+
import logging
811
# --- END ADDED ---
912

1013

@@ -339,9 +342,119 @@ async def test_update_task_state_triggers_notify(task_store: InMemoryTaskStore):
339342
# Patch the notify method to check it's called
340343
with patch.object(task_store, 'notify_status_update', wraps=task_store.notify_status_update) as mock_notify:
341344
await task_store.update_task_state(task_id, TaskState.WORKING)
342-
mock_notify.assert_awaited_once_with(task_id, TaskState.WORKING)
345+
# --- MODIFIED: Correct assertion ---
346+
mock_notify.assert_awaited_once_with(task_id, TaskState.WORKING) # Removed None
347+
# --- END MODIFIED ---
343348

344349
# Check the queue still received the event via the wrapped call
345350
event = await asyncio.wait_for(q1.get(), timeout=0.1)
346351
assert isinstance(event, TaskStatusUpdateEvent)
347352
assert event.state == TaskState.WORKING
353+
354+
# --- ADDED: Tests for Edge Cases and Errors ---
355+
356+
@pytest.mark.asyncio
357+
async def test_create_task_already_exists(task_store: InMemoryTaskStore, caplog):
358+
"""Test creating a task that already exists."""
359+
task_id = "existing-task"
360+
await task_store.create_task(task_id) # Create it first
361+
initial_context = task_store._tasks[task_id]
362+
363+
with caplog.at_level(logging.WARNING):
364+
new_context = await task_store.create_task(task_id) # Try creating again
365+
366+
assert new_context is initial_context # Should return the existing one
367+
assert f"Task '{task_id}' already exists" in caplog.text
368+
369+
@pytest.mark.asyncio
370+
async def test_update_task_state_not_found(task_store: InMemoryTaskStore, caplog):
371+
"""Test updating state for a non-existent task."""
372+
task_id = "non-existent-task"
373+
with caplog.at_level(logging.WARNING):
374+
context = await task_store.update_task_state(task_id, TaskState.WORKING)
375+
376+
assert context is None
377+
assert f"Task '{task_id}' not found for state update" in caplog.text
378+
379+
@pytest.mark.asyncio
380+
async def test_delete_task_not_found(task_store: InMemoryTaskStore, caplog):
381+
"""Test deleting a non-existent task."""
382+
task_id = "non-existent-task"
383+
with caplog.at_level(logging.WARNING):
384+
result = await task_store.delete_task(task_id)
385+
386+
assert result is False
387+
assert f"Task '{task_id}' not found for deletion" in caplog.text
388+
389+
@pytest.mark.asyncio
390+
async def test_remove_listener_task_not_found(task_store: InMemoryTaskStore, caplog):
391+
"""Test removing a listener from a non-existent task."""
392+
task_id = "non-existent-task"
393+
q1 = asyncio.Queue()
394+
with caplog.at_level(logging.WARNING):
395+
await task_store.remove_listener(task_id, q1)
396+
assert f"Attempted to remove listener from non-existent task '{task_id}'" in caplog.text
397+
398+
@pytest.mark.asyncio
399+
async def test_remove_listener_queue_not_found(task_store: InMemoryTaskStore, caplog):
400+
"""Test removing a listener queue that wasn't added."""
401+
task_id = "listener-task-2"
402+
q1 = asyncio.Queue()
403+
q2 = asyncio.Queue()
404+
await task_store.add_listener(task_id, q1) # Add q1
405+
406+
with caplog.at_level(logging.WARNING):
407+
await task_store.remove_listener(task_id, q2) # Try removing q2
408+
409+
assert f"Attempted to remove a listener queue not present for task '{task_id}'" in caplog.text
410+
assert await task_store.get_listeners(task_id) == [q1] # q1 should still be there
411+
412+
@pytestmark_notify
413+
@pytest.mark.asyncio
414+
async def test_notify_listeners_queue_put_error(task_store: InMemoryTaskStore, caplog):
415+
"""Test error handling when putting event onto a listener queue fails."""
416+
task_id = "notify-queue-error"
417+
await task_store.create_task(task_id)
418+
q_ok = asyncio.Queue()
419+
q_bad = MagicMock(spec=asyncio.Queue)
420+
q_bad.put = AsyncMock(side_effect=asyncio.QueueFull("Mock Queue Full")) # Simulate error
421+
422+
await task_store.add_listener(task_id, q_ok)
423+
await task_store.add_listener(task_id, q_bad)
424+
425+
with caplog.at_level(logging.ERROR):
426+
await task_store.notify_status_update(task_id, TaskState.FAILED, "Notify Error Test")
427+
428+
# Check the good queue received the event
429+
event_ok = await asyncio.wait_for(q_ok.get(), timeout=0.1)
430+
assert isinstance(event_ok, TaskStatusUpdateEvent)
431+
assert event_ok.state == TaskState.FAILED
432+
433+
# Check the bad queue put was attempted and error logged
434+
q_bad.put.assert_awaited_once()
435+
assert f"Failed to put event onto listener queue 1 for task '{task_id}'" in caplog.text
436+
assert "Mock Queue Full" in caplog.text
437+
438+
@pytestmark_notify
439+
@pytest.mark.asyncio
440+
@patch("agentvault_server_sdk.state.TaskStatusUpdateEvent") # Patch the model class
441+
async def test_notify_status_update_event_creation_error(mock_event_cls, task_store: InMemoryTaskStore, caplog):
442+
"""Test error handling if TaskStatusUpdateEvent creation fails."""
443+
task_id = "notify-create-error"
444+
await task_store.create_task(task_id)
445+
q1 = asyncio.Queue()
446+
await task_store.add_listener(task_id, q1)
447+
448+
error_message = "Mock Pydantic validation error"
449+
mock_event_cls.side_effect = ValueError(error_message) # Simulate model init error
450+
451+
with caplog.at_level(logging.ERROR):
452+
await task_store.notify_status_update(task_id, TaskState.WORKING)
453+
454+
assert q1.empty() # Queue should be empty
455+
assert f"Failed to create TaskStatusUpdateEvent instance for task '{task_id}'" in caplog.text
456+
assert error_message in caplog.text
457+
458+
# Similar tests can be added for notify_message_event and notify_artifact_event creation errors
459+
460+
# --- END ADDED ---

0 commit comments

Comments
 (0)