Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ docs-build:
mkdocs build --strict

test-cov:
uv run pytest --cov=agentflow-cli --cov-report=html --cov-report=term-missing --cov-report=xml -v
# Ensure pytest-cov is available
uv pip install pytest-cov
uv run pytest --cov=agentflow_cli --cov-report=html --cov-report=term-missing --cov-report=xml -v
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ def _merge_states(
base: dict[str, Any] = {}
if old_state is not None:
# Keep full dump so we can preserve existing fields
base = old_state.model_dump()
# Use serialize_as_any=True to include subclass fields
base = old_state.model_dump(serialize_as_any=True)

merged: dict[str, Any] = {**base}

Expand Down
10 changes: 5 additions & 5 deletions agentflow_cli/src/app/routers/graph/services/graph_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ async def invoke_graph(

return GraphInvokeOutputSchema(
messages=messages,
state=raw_state.model_dump() if raw_state else None,
state=raw_state.model_dump(serialize_as_any=True) if raw_state else None,
context=context,
summary=context_summary,
meta=meta,
Expand Down Expand Up @@ -295,7 +295,7 @@ async def stream_graph(
mt = chunk.metadata or {}
mt.update(meta)
chunk.metadata = mt
yield chunk.model_dump_json()
yield chunk.model_dump_json(serialize_as_any=True)
if (
self.config.thread_name_generator_path
and meta["is_new_thread"]
Expand All @@ -317,7 +317,7 @@ async def stream_graph(
event=StreamEvent.UPDATES,
data={"status": "completed"},
metadata=meta,
).model_dump_json()
).model_dump_json(serialize_as_any=True)

except Exception as e:
logger.error(f"Graph streaming failed: {e}")
Expand Down Expand Up @@ -416,7 +416,7 @@ async def fix_graph(
"success": True,
"message": "No messages found in state",
"removed_count": 0,
"state": state.model_dump_json(),
"state": state.model_dump_json(serialize_as_any=True),
}

filtered = [m for m in messages if not self._has_empty_tool_call(m)]
Expand All @@ -433,7 +433,7 @@ async def fix_graph(
"success": True,
"message": message,
"removed_count": removed_count,
"state": state.model_dump_json(),
"state": state.model_dump_json(serialize_as_any=True),
}
except Exception as e:
logger.error(f"Fix graph operation failed: {e}")
Expand Down
8 changes: 4 additions & 4 deletions agentflow_cli/src/app/utils/parse_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

def parse_state_output(settings: Settings, response: BaseModel) -> dict[str, Any]:
# if settings.IS_DEBUG:
# return response.model_dump(exclude={"execution_meta"})
return response.model_dump()
# return response.model_dump(exclude={"execution_meta"}, serialize_as_any=True)
return response.model_dump(serialize_as_any=True)


def parse_message_output(settings: Settings, response: BaseModel) -> dict[str, Any]:
# if settings.IS_DEBUG:
# return response.model_dump(exclude={"raw"})
return response.model_dump()
# return response.model_dump(exclude={"raw"}, serialize_as_any=True)
return response.model_dump(serialize_as_any=True)
4 changes: 2 additions & 2 deletions tests/cli/test_cli_api_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def silent_output():
def test_api_command_with_env_file(monkeypatch, tmp_path, silent_output):
# Prepare a fake config file and .env
cfg = tmp_path / "agentflow.json"
# Provide minimal valid configuration expected by validation (include 'graphs')
cfg.write_text('{"graphs": {"default": "graph/react.py"}}', encoding="utf-8")
# Provide minimal valid configuration expected by current validation (top-level 'agent')
cfg.write_text('{"agent": "graph/react.py"}', encoding="utf-8")
env_file = tmp_path / ".env.dev"
env_file.write_text("FOO=BAR\n", encoding="utf-8")

Expand Down
4 changes: 0 additions & 4 deletions tests/cli/test_utils_parse_and_callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ def test_parse_state_output(is_debug: bool):
settings = Settings(IS_DEBUG=is_debug)
model = _StateModel(a=1, b="x", execution_meta={"duration": 123})
out = parse_state_output(settings, model)
# Since parse_state_output doesn't filter execution_meta (commented out),
# it should always be present regardless of debug mode
assert out["execution_meta"] == {"duration": 123}
assert out["a"] == 1 and out["b"] == "x"

Expand All @@ -39,8 +37,6 @@ def test_parse_message_output(is_debug: bool):
settings = Settings(IS_DEBUG=is_debug)
model = _MessageModel(content="hello", raw={"tokens": 5})
out = parse_message_output(settings, model)
# Since parse_message_output doesn't filter raw (commented out),
# it should always be present regardless of debug mode
assert out["raw"] == {"tokens": 5}
assert out["content"] == "hello"

Expand Down
112 changes: 95 additions & 17 deletions tests/integration_tests/store/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Shared fixtures for store integration tests."""

from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import uuid4

import pytest
Expand All @@ -9,7 +9,9 @@
from fastapi.testclient import TestClient

from agentflow_cli.src.app.core.config.setup_middleware import setup_middleware
from agentflow_cli.src.app.routers.store.router import router as store_router
from agentflow_cli.src.app.core.config.graph_config import GraphConfig
from injectq import InjectQ
from injectq.integrations.fastapi import setup_fastapi


@pytest.fixture
Expand All @@ -31,26 +33,46 @@ def mock_auth_user():
@pytest.fixture
def app(mock_store, mock_auth_user):
"""FastAPI test app with store router."""
# Import early before binding
from agentflow_cli.src.app.core.auth.base_auth import BaseAuth

app = FastAPI()
setup_middleware(app)
app.include_router(store_router)

# Mock the dependency injection for StoreService
with patch("agentflow_cli.src.app.routers.store.router.InjectAPI") as mock_inject:
from agentflow_cli.src.app.routers.store.services.store_service import (
StoreService,
)
# Create a fresh container for this test
container = InjectQ()
container.bind_instance(BaseStore, mock_store)

class _NoAuthConfig:
def auth_config(self):
return None

container.bind_instance(GraphConfig, _NoAuthConfig())

# Create a mock BaseAuth instance
mock_auth = MagicMock(spec=BaseAuth)
mock_auth.authenticate.return_value = mock_auth_user
container.bind_instance(BaseAuth, mock_auth)

# Setup FastAPI with the container
setup_fastapi(container, app)

# Mock authentication to provide a user
with patch(
"agentflow_cli.src.app.core.auth.auth_backend.verify_current_user",
return_value=mock_auth_user,
):
from agentflow_cli.src.app.routers.store.router import router as store_router

app.include_router(store_router)

# Create a StoreService with the mocked store
mock_service = StoreService(store=mock_store)
mock_inject.return_value = mock_service
# Debug: Check OpenAPI
openapi = app.openapi()
if "/v1/store/memories" in openapi.get("paths", {}):
endpoint = openapi["paths"]["/v1/store/memories"]["post"]
print(f"DEBUG: /v1/store/memories parameters: {endpoint.get('parameters', [])}")

# Mock authentication
with patch(
"agentflow_cli.src.app.routers.store.router.verify_current_user",
return_value=mock_auth_user,
):
yield app
yield app


@pytest.fixture
Expand All @@ -59,6 +81,62 @@ def client(app):
return TestClient(app)


@pytest.fixture
def unauth_app(mock_store):
"""FastAPI test app without auth patch, but with DI patched and safe defaults.

This allows testing endpoints without authentication while avoiding
serialization issues from AsyncMock default returns.
"""
# Import early before binding
from agentflow_cli.src.app.core.auth.base_auth import BaseAuth

app = FastAPI()
setup_middleware(app)

# Provide safe default return values to avoid pydantic/serialization issues
mock_store.astore.return_value = str(uuid4())
mock_store.asearch.return_value = []
mock_store.aget.return_value = None
mock_store.aget_all.return_value = []
mock_store.aupdate.return_value = {"updated": True}
mock_store.adelete.return_value = {"deleted": True}
mock_store.aforget_memory.return_value = {"count": 0}

# Setup InjectQ container and bind BaseStore to mocked store
container = InjectQ()
container.bind_instance(BaseStore, mock_store)

class _NoAuthConfig:
def auth_config(self):
return None

container.bind_instance(GraphConfig, _NoAuthConfig())

# Create a mock BaseAuth instance
mock_auth = MagicMock(spec=BaseAuth)
mock_auth.authenticate.return_value = {}
container.bind_instance(BaseAuth, mock_auth)

setup_fastapi(container, app)

# Patch auth to no-op so BaseAuth DI is not required in unauthenticated tests
with patch(
"agentflow_cli.src.app.core.auth.auth_backend.verify_current_user",
return_value={},
):
from agentflow_cli.src.app.routers.store.router import router as store_router

app.include_router(store_router)
yield app


@pytest.fixture
def unauth_client(unauth_app):
"""Test client without auth override for authentication behavior tests."""
return TestClient(unauth_app)


@pytest.fixture
def auth_headers():
"""Authentication headers."""
Expand Down
Loading