Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

548 lines
19 KiB
Python

"""Unit tests for the agents SDK namespace (``cognee.agents``) and the
``AgentsCommand`` CLI command.
These tests run WITHOUT a live LLM, network, or relational database. The
backend ``cognee.modules.agents.*`` functions and permission helpers are
monkeypatched at the names imported into the SDK module, so the SDK's own
glue logic (display-email stripping, permission ordering, error translation,
return-shape) is exercised against fakes.
If the SDK module is not yet importable (it is built alongside these tests)
the whole module is skipped rather than erroring at collection time.
"""
import argparse
import asyncio
from types import SimpleNamespace
from uuid import UUID, uuid4
import pytest
agents_sdk = pytest.importorskip(
"cognee.api.v1.agents.agents",
reason="agents SDK namespace not yet available",
)
agents = agents_sdk.agents
# --------------------------------------------------------------------------- #
# Fakes / helpers
# --------------------------------------------------------------------------- #
def _make_user(user_id=None):
"""A minimal stand-in for a cognee User object."""
return SimpleNamespace(id=user_id or uuid4(), tenant_id=None)
def _make_agent_info(parent_id, slug="support", api_key_label="support"):
"""Build an AgentInfo-like object whose email mirrors create_agent output."""
agent_id = uuid4()
email = f"{slug}+{parent_id}@cognee.agent"
agent_user = SimpleNamespace(id=agent_id, email=email, parent_user_id=parent_id)
return SimpleNamespace(user=agent_user, api_key_label=api_key_label)
def _patch(monkeypatch, name, value):
"""Patch ``name`` on the agents SDK module if it is referenced there."""
if hasattr(agents_sdk, name):
monkeypatch.setattr(agents_sdk, name, value)
# --------------------------------------------------------------------------- #
# create -> list -> get -> delete round trip
# --------------------------------------------------------------------------- #
@pytest.mark.asyncio
async def test_create_list_get_delete_round_trip(monkeypatch):
parent = _make_user()
store = {} # agent_id -> AgentInfo
async def fake_create_agent(name, parent_user):
slug = name.lower().replace(" ", "-")
info = _make_agent_info(parent_user.id, slug=slug, api_key_label=slug)
store[info.user.id] = info
return info.user, "agent-api-key-123"
async def fake_list_agents(owner_id):
return [i for i in store.values() if i.user.parent_user_id == owner_id]
async def fake_get_agent(agent_id, owner_id):
info = store.get(agent_id)
if info is None:
raise LookupError("Agent not found")
if info.user.parent_user_id != owner_id:
raise PermissionError("Not authorized to view this agent")
return info
async def fake_delete_agent(agent_id, owner_id):
info = store.get(agent_id)
if info is None:
raise LookupError("Agent not found")
if info.user.parent_user_id != owner_id:
raise PermissionError("Not authorized to delete this agent")
del store[agent_id]
_patch(monkeypatch, "create_agent", fake_create_agent)
_patch(monkeypatch, "list_agents", fake_list_agents)
_patch(monkeypatch, "get_agent", fake_get_agent)
_patch(monkeypatch, "delete_agent", fake_delete_agent)
# create (no datasets -> no permission helpers touched)
created = await agents.create("Support Bot", user=parent)
assert created["agent_api_key"] == "agent-api-key-123"
# display email strips the '+{parent_id}' segment
assert created["agent_email"] == "support-bot@cognee.agent"
assert "+" not in created["agent_email"]
agent_id = created["agent_id"]
assert agent_id == str(next(iter(store)))
# list shows the created agent
listed = await agents.list(user=parent)
assert len(listed) == 1
assert listed[0]["agent_id"] == agent_id
assert listed[0]["agent_email"] == "support-bot@cognee.agent"
assert listed[0]["api_key_label"] == "support-bot"
# get returns it
got = await agents.get(agent_id, user=parent)
assert got["agent_id"] == agent_id
assert got["agent_email"] == "support-bot@cognee.agent"
# delete removes it
result = await agents.delete(agent_id, user=parent)
assert result is None
assert await agents.list(user=parent) == []
@pytest.mark.asyncio
async def test_create_grants_agent_read_write_after_caller_check(monkeypatch):
parent = _make_user()
dataset_id = uuid4()
call_order = []
async def fake_create_agent(name, parent_user):
info = _make_agent_info(parent_user.id, slug="bot")
return info.user, "key"
async def fake_get_authorized_dataset(user, ds_id, permission="read"):
call_order.append(("authorize", user.id, ds_id, permission))
return SimpleNamespace(id=ds_id)
granted = []
async def fake_give_permission_on_dataset(user, ds_id, permission):
call_order.append(("grant", user.id, ds_id, permission))
granted.append((user.id, ds_id, permission))
_patch(monkeypatch, "create_agent", fake_create_agent)
_patch(monkeypatch, "get_authorized_dataset", fake_get_authorized_dataset)
_patch(monkeypatch, "give_permission_on_dataset", fake_give_permission_on_dataset)
await agents.create("Bot", datasets=[dataset_id], user=parent)
# caller authorization happens before any grant to the agent
assert call_order[0][0] == "authorize"
assert call_order[0][1] == parent.id
grant_perms = {(g[2]) for g in granted}
assert grant_perms == {"read", "write"}
# the agent (not the parent) receives the grants
assert all(g[0] != parent.id for g in granted)
@pytest.mark.asyncio
async def test_create_resolves_dataset_name(monkeypatch):
parent = _make_user()
resolved_id = uuid4()
async def fake_create_agent(name, parent_user):
info = _make_agent_info(parent_user.id, slug="bot")
return info.user, "key"
async def fake_get_datasets_by_name(name, owner_id):
if name == "known":
return [SimpleNamespace(id=resolved_id, name="known")]
return []
authorized = []
async def fake_get_authorized_dataset(user, ds_id, permission="read"):
authorized.append(ds_id)
return SimpleNamespace(id=ds_id)
async def fake_give_permission_on_dataset(user, ds_id, permission):
pass
_patch(monkeypatch, "create_agent", fake_create_agent)
_patch(monkeypatch, "get_datasets_by_name", fake_get_datasets_by_name)
_patch(monkeypatch, "get_authorized_dataset", fake_get_authorized_dataset)
_patch(monkeypatch, "give_permission_on_dataset", fake_give_permission_on_dataset)
await agents.create("Bot", datasets=["known"], user=parent)
assert resolved_id in authorized
# --------------------------------------------------------------------------- #
# Permissioning
# --------------------------------------------------------------------------- #
@pytest.mark.asyncio
async def test_create_unauthorized_dataset_raises(monkeypatch):
from cognee.modules.users.exceptions import PermissionDeniedError
parent = _make_user()
dataset_id = uuid4()
async def fake_create_agent(name, parent_user):
info = _make_agent_info(parent_user.id, slug="bot")
return info.user, "key"
async def fake_get_authorized_dataset(user, ds_id, permission="read"):
# Either raising or returning None signals "no access".
return None
grant_calls = []
async def fake_give_permission_on_dataset(user, ds_id, permission):
grant_calls.append((ds_id, permission))
_patch(monkeypatch, "create_agent", fake_create_agent)
_patch(monkeypatch, "get_authorized_dataset", fake_get_authorized_dataset)
_patch(monkeypatch, "give_permission_on_dataset", fake_give_permission_on_dataset)
with pytest.raises((PermissionDeniedError, ValueError)):
await agents.create("Bot", datasets=[dataset_id], user=parent)
# the agent must never be granted access to a dataset the caller can't read
assert grant_calls == []
@pytest.mark.asyncio
async def test_get_other_users_agent_is_denied(monkeypatch):
from cognee.modules.users.exceptions import PermissionDeniedError
owner = _make_user()
other_agent_id = uuid4()
async def fake_get_agent(agent_id, owner_id):
# backend raises PermissionError when parent_user_id != owner_id
raise PermissionError("Not authorized to view this agent")
_patch(monkeypatch, "get_agent", fake_get_agent)
with pytest.raises((PermissionDeniedError, PermissionError)):
await agents.get(other_agent_id, user=owner)
@pytest.mark.asyncio
async def test_delete_other_users_agent_is_denied(monkeypatch):
from cognee.modules.users.exceptions import PermissionDeniedError
owner = _make_user()
other_agent_id = uuid4()
async def fake_delete_agent(agent_id, owner_id):
raise PermissionError("Not authorized to delete this agent")
_patch(monkeypatch, "delete_agent", fake_delete_agent)
with pytest.raises((PermissionDeniedError, PermissionError)):
await agents.delete(other_agent_id, user=owner)
@pytest.mark.asyncio
async def test_get_missing_agent_raises_value_error(monkeypatch):
owner = _make_user()
missing_id = uuid4()
async def fake_get_agent(agent_id, owner_id):
raise LookupError("Agent not found")
_patch(monkeypatch, "get_agent", fake_get_agent)
with pytest.raises(ValueError):
await agents.get(missing_id, user=owner)
# --------------------------------------------------------------------------- #
# register -> list_connections -> unregister
# --------------------------------------------------------------------------- #
@pytest.mark.asyncio
async def test_register_list_connections_unregister(monkeypatch):
from cognee.modules.agents.models import (
AgentConnection,
AgentsListResponse,
)
user = _make_user()
session_name = "session-A"
captured_request = {}
async def fake_register_agent(u, request):
captured_request["request"] = request
return AgentConnection(
id=f"{u.id}:{request.agent_session_name}",
agent_session_name=request.agent_session_name,
type=request.type,
memory_mode=request.memory_mode,
user_id=u.id,
)
async def fake_list_agent_connections(**kwargs):
conn = AgentConnection(
id=f"{user.id}:{session_name}",
agent_session_name=session_name,
user_id=user.id,
status="active",
)
return AgentsListResponse(
agents=[conn],
memory_sources=[],
total=1,
limit=kwargs.get("limit", 50),
offset=kwargs.get("offset", 0),
has_more=False,
)
async def fake_unregister_agent(u, request):
# mimic active-count drop to zero after unregister
return 0
_patch(monkeypatch, "register_agent", fake_register_agent)
_patch(monkeypatch, "list_agent_connections", fake_list_agent_connections)
_patch(monkeypatch, "unregister_agent", fake_unregister_agent)
# register
registered = await agents.register(session_name, user=user, memory_mode="hybrid")
assert isinstance(registered, dict)
assert registered["agent_session_name"] == session_name
# the SDK should have forwarded the kwargs into the request model
assert captured_request["request"].memory_mode == "hybrid"
# list_connections shows the connection (range_key kwarg, not range)
listing = await agents.list_connections(user=user, range_key="30d")
assert isinstance(listing, dict)
assert listing["total"] == 1
assert listing["agents"][0]["agent_session_name"] == session_name
# unregister deactivates it -> active count int
count = await agents.unregister(session_name, user=user)
assert count == 0
@pytest.mark.asyncio
async def test_register_unauthorized_dataset_id_raises(monkeypatch):
from cognee.modules.users.exceptions import PermissionDeniedError
user = _make_user()
bad_dataset = str(uuid4())
register_called = []
async def fake_get_authorized_dataset(u, ds_id, permission="read"):
return None # no access
async def fake_register_agent(u, request):
register_called.append(request)
return SimpleNamespace(model_dump=lambda mode="json": {})
_patch(monkeypatch, "get_authorized_dataset", fake_get_authorized_dataset)
_patch(monkeypatch, "register_agent", fake_register_agent)
with pytest.raises((PermissionDeniedError, ValueError)):
await agents.register("sess", user=user, dataset_ids=[bad_dataset])
# register must NOT be reached if a dataset is not accessible
assert register_called == []
@pytest.mark.asyncio
async def test_list_connections_drops_unscoped_connections(monkeypatch):
"""Connections with no owning user AND no datasets are visible-to-all in the
backend; the SDK must defensively filter them out and adjust the totals."""
from cognee.modules.agents.models import AgentConnection, AgentsListResponse
user = _make_user()
async def fake_list_agent_connections(**kwargs):
scoped = AgentConnection(
id="scoped",
agent_session_name="mine",
user_id=user.id,
status="active",
)
# ownerless + datasetless -> backend marks visible to everyone
leaked = AgentConnection(
id="leaked",
agent_session_name="someone-elses",
user_id=None,
status="active",
)
return AgentsListResponse(
agents=[scoped, leaked],
memory_sources=[],
total=2,
limit=kwargs.get("limit", 50),
offset=kwargs.get("offset", 0),
has_more=False,
)
_patch(monkeypatch, "list_agent_connections", fake_list_agent_connections)
listing = await agents.list_connections(user=user)
session_names = [a["agent_session_name"] for a in listing["agents"]]
assert session_names == ["mine"]
assert "someone-elses" not in session_names
# total is decremented for the removed leaked connection
assert listing["total"] == 1
@pytest.mark.asyncio
async def test_get_connection_returns_none_when_missing(monkeypatch):
user = _make_user()
agent_id = uuid4()
async def fake_get_agent_connection_detail(**kwargs):
return None
_patch(monkeypatch, "get_agent_connection_detail", fake_get_agent_connection_detail)
result = await agents.get_connection(agent_id, user=user, agent_session_name="x")
assert result is None
# --------------------------------------------------------------------------- #
# CLI smoke test
# --------------------------------------------------------------------------- #
cli_module = pytest.importorskip(
"cognee.cli.commands.agents_command",
reason="AgentsCommand CLI not yet available",
)
def test_agents_command_parser_configures_actions():
command = cli_module.AgentsCommand()
assert command.command_string == "agents"
parser = argparse.ArgumentParser()
command.configure_parser(parser)
# parsing a "list" invocation succeeds and selects the list action
args = parser.parse_args(["list"])
assert getattr(args, "agents_action") == "list"
def test_agents_command_execute_list(monkeypatch):
command = cli_module.AgentsCommand()
parser = argparse.ArgumentParser()
command.configure_parser(parser)
args = parser.parse_args(["list"])
# avoid any DB work: resolve_cli_user and the SDK list call are mocked
fake_user = _make_user()
async def fake_resolve_cli_user(_user_id, strict=False):
return fake_user
monkeypatch.setattr("cognee.cli.user_resolution.resolve_cli_user", fake_resolve_cli_user)
listed_rows = [
{
"agent_id": str(uuid4()),
"agent_email": "bot@cognee.agent",
"api_key_label": "bot",
}
]
async def fake_list(user=None):
assert user is fake_user
return listed_rows
import cognee
# The top-level ``cognee.agents`` export may be wired in a separate edit.
# Ensure the CLI can resolve it regardless, then patch ``list`` on the
# SDK class object that the CLI calls through.
if not hasattr(cognee, "agents"):
monkeypatch.setattr(cognee, "agents", agents, raising=False)
monkeypatch.setattr(cognee.agents, "list", staticmethod(fake_list))
captured = []
monkeypatch.setattr("cognee.cli.echo.echo", lambda *a, **k: captured.append(a))
# execute() runs the list action without raising
command.execute(args)
assert any("bot@cognee.agent" in str(c) for c in captured)
def test_agents_command_execute_no_action_raises(monkeypatch):
from cognee.cli.exceptions import CliCommandException
command = cli_module.AgentsCommand()
parser = argparse.ArgumentParser()
command.configure_parser(parser)
args = parser.parse_args([]) # no subcommand
monkeypatch.setattr("cognee.cli.echo.error", lambda *a, **k: None)
with pytest.raises(CliCommandException):
command.execute(args)
# --------------------------------------------------------------------------- #
# Strict --user-id resolution (no silent fallback for agent commands)
# --------------------------------------------------------------------------- #
@pytest.mark.asyncio
async def test_resolve_cli_user_strict_unknown_uuid_raises(monkeypatch):
"""A valid-but-unknown --user-id must be a hard error in strict mode rather
than silently falling back to the default user."""
from cognee.cli import user_resolution
async def fake_get_user(uid):
raise Exception("user not found")
async def fake_get_default_user():
raise AssertionError("strict mode must not fall back to the default user")
monkeypatch.setattr("cognee.modules.users.methods.get_user", fake_get_user, raising=False)
monkeypatch.setattr(
"cognee.modules.users.methods.get_default_user", fake_get_default_user, raising=False
)
with pytest.raises(ValueError):
await user_resolution.resolve_cli_user(str(uuid4()), strict=True)
@pytest.mark.asyncio
async def test_resolve_cli_user_non_strict_falls_back(monkeypatch):
"""Default (non-strict) behaviour still warns and falls back, preserving the
existing contract for other commands (e.g. datasets)."""
from cognee.cli import user_resolution
sentinel = _make_user()
async def fake_get_user(uid):
raise Exception("user not found")
async def fake_get_default_user():
return sentinel
monkeypatch.setattr("cognee.modules.users.methods.get_user", fake_get_user, raising=False)
monkeypatch.setattr(
"cognee.modules.users.methods.get_default_user", fake_get_default_user, raising=False
)
monkeypatch.setattr("cognee.cli.echo.warning", lambda *a, **k: None)
resolved = await user_resolution.resolve_cli_user(str(uuid4()))
assert resolved is sentinel