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

1063 lines
41 KiB
Python

"""End-to-end tests for all /api/v1/agents/* endpoints.
Exercises every route in the merged agents router against a real FastAPI
TestClient with authentication enabled. The operations layer is patched
only where it reaches into session-trace infrastructure that isn't available
in a lightweight test context — everything else (registry, models, DB) runs
for real.
Endpoints under test:
GET /connections — list agent connections
GET /connections/{agent_id} — agent connection detail
POST /register — register agent connection
POST /unregister — unregister agent connection
GET /list — list agents from DB
POST /create — create agent
GET /{agent_id} — get agent from DB
DELETE /{agent_id} — delete agent
"""
import os
import uuid
import pytest
from unittest.mock import MagicMock, patch
with patch("dotenv.load_dotenv"):
os.environ["REQUIRE_AUTHENTICATION"] = "true"
os.environ["ENABLE_BACKEND_ACCESS_CONTROL"] = "false"
os.environ["HASH_API_KEY"] = "false"
os.environ["COGNEE_AGENT_MODE"] = "true"
from fastapi.testclient import TestClient
from cognee.modules.agents import agent_mode
from cognee.modules.agents.registry import AGENT_CONFIG_NAME, clear_registered_agent_connections
RUN_ID = uuid.uuid4().hex[:8]
OWNER_EMAIL = f"e2e-agents-{RUN_ID}@example.com"
OWNER_PASSWORD = "e2epass987!"
AGENT_NAME = f"e2e-agent-{RUN_ID}"
class TestAgentsE2E:
"""Full lifecycle test for every agents endpoint."""
@pytest.fixture(scope="class")
def client(self):
from cognee.api.client import app
with TestClient(app) as c:
yield c
@pytest.fixture(scope="class")
def owner(self, client):
reg = client.post(
"/api/v1/auth/register",
json={"email": OWNER_EMAIL, "password": OWNER_PASSWORD},
)
assert reg.status_code in (200, 201), reg.text
owner_id = reg.json()["id"]
login = client.post(
"/api/v1/auth/login",
data={"username": OWNER_EMAIL, "password": OWNER_PASSWORD},
)
assert login.status_code == 200, login.text
return {"id": owner_id, "token": login.json()["access_token"]}
@pytest.fixture(scope="class")
def headers(self, owner):
return {"Authorization": f"Bearer {owner['token']}"}
# ------------------------------------------------------------------ #
# Agent connection endpoints
# ------------------------------------------------------------------ #
def test_list_connections_empty(self, client, headers, _patch_operations):
clear_registered_agent_connections()
resp = client.get("/api/v1/agents/connections", headers=headers)
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 0
assert body["agents"] == []
assert body["has_more"] is False
def test_register_connection(self, client, headers, _patch_operations):
clear_registered_agent_connections()
resp = client.post(
"/api/v1/agents/register",
headers=headers,
json={
"agent_session_name": "support_bot",
"type": "api",
"memory_mode": "hybrid",
"session_id": "sess-001",
"dataset_ids": [],
"dataset_names": ["company_brain"],
"source": "api",
"origin_function": "handle_ticket",
"metadata": {"env": "test"},
},
)
assert resp.status_code == 201, resp.text
connection = resp.json()
assert connection["agent_session_name"] == "support_bot"
assert connection["type"] == "api"
assert connection["memory_mode"] == "hybrid"
assert connection["session_id"] == "sess-001"
assert connection["source"] == "api"
assert connection["origin_function"] == "handle_ticket"
assert connection["status"] == "active"
assert connection["metadata"]["env"] == "test"
assert len(connection["datasets"]) == 1
assert connection["datasets"][0]["name"] == "company_brain"
assert connection["datasets"][0]["type"] == "company_brain"
assert connection["datasets"][0]["role"] == "read_write"
def test_register_connection_defaults(self, client, headers, _patch_operations):
clear_registered_agent_connections()
resp = client.post(
"/api/v1/agents/register",
headers=headers,
json={"agent_session_name": "minimal_agent"},
)
assert resp.status_code == 201, resp.text
connection = resp.json()
assert connection["agent_session_name"] == "minimal_agent"
assert connection["type"] == "api"
assert connection["memory_mode"] == "unknown"
assert connection["source"] == "api"
assert connection["datasets"] == []
def test_list_connections_shows_registered(self, client, headers, _patch_operations):
clear_registered_agent_connections()
reg = client.post(
"/api/v1/agents/register",
headers=headers,
json={
"agent_session_name": "list_test_agent",
"type": "sdk",
"memory_mode": "cognee",
},
)
assert reg.status_code == 201
agent_id = reg.json()["id"]
resp = client.get("/api/v1/agents/connections", headers=headers)
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 1
assert body["agents"][0]["id"] == agent_id
assert body["agents"][0]["agent_session_name"] == "list_test_agent"
assert body["limit"] == 50
assert body["offset"] == 0
def test_list_connections_pagination(self, client, headers, _patch_operations):
clear_registered_agent_connections()
for i in range(3):
client.post(
"/api/v1/agents/register",
headers=headers,
json={"agent_session_name": f"paginated_{i}"},
)
resp = client.get("/api/v1/agents/connections?limit=2&offset=0", headers=headers)
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 3
assert len(body["agents"]) == 2
assert body["has_more"] is True
assert body["limit"] == 2
assert body["offset"] == 0
resp2 = client.get("/api/v1/agents/connections?limit=2&offset=2", headers=headers)
body2 = resp2.json()
assert len(body2["agents"]) == 1
assert body2["has_more"] is False
def test_list_connections_status_filter(self, client, headers, _patch_operations):
clear_registered_agent_connections()
client.post(
"/api/v1/agents/register",
headers=headers,
json={"agent_session_name": "active_agent"},
)
resp = client.get("/api/v1/agents/connections?status=active", headers=headers)
assert resp.status_code == 200
assert resp.json()["total"] == 1
resp2 = client.get("/api/v1/agents/connections?status=inactive", headers=headers)
assert resp2.status_code == 200
assert resp2.json()["total"] == 0
def test_get_connection_detail(self, client, headers, owner, _patch_operations):
clear_registered_agent_connections()
reg = client.post(
"/api/v1/agents/register",
headers=headers,
json={
"agent_session_name": "detail_agent",
"type": "mcp",
"memory_mode": "session",
"session_id": "detail-sess",
},
)
assert reg.status_code == 201
resp = client.get(
f"/api/v1/agents/connections/{owner['id']}?agent_session_name=detail_agent",
headers=headers,
)
assert resp.status_code == 200
body = resp.json()
assert body["agent"]["agent_session_name"] == "detail_agent"
assert body["agent"]["type"] == "mcp"
assert "memory_sources" in body
assert "recent_sessions" in body
assert "recent_traces" in body
assert "recent_qas" in body
def test_get_connection_detail_not_found(self, client, headers, _patch_operations):
clear_registered_agent_connections()
fake_id = str(uuid.uuid4())
resp = client.get(f"/api/v1/agents/connections/{fake_id}", headers=headers)
assert resp.status_code == 404
assert resp.json()["detail"] == "connection not found"
def test_register_multiple_connections_and_list(self, client, headers, _patch_operations):
clear_registered_agent_connections()
names = ["agent_alpha", "agent_beta", "agent_gamma"]
ids = []
for name in names:
r = client.post(
"/api/v1/agents/register",
headers=headers,
json={"agent_session_name": name, "type": "sdk"},
)
assert r.status_code == 201
ids.append(r.json()["id"])
resp = client.get("/api/v1/agents/connections", headers=headers)
body = resp.json()
assert body["total"] == 3
returned_ids = {a["id"] for a in body["agents"]}
for agent_id in ids:
assert agent_id in returned_ids
def test_register_connection_with_dataset_ids(self, client, headers, _patch_operations):
clear_registered_agent_connections()
dataset_id = str(uuid.uuid4())
resp = client.post(
"/api/v1/agents/register",
headers=headers,
json={
"agent_session_name": "dataset_agent",
"dataset_ids": [dataset_id],
},
)
assert resp.status_code == 201
connection = resp.json()
assert len(connection["datasets"]) == 1
assert connection["datasets"][0]["id"] == dataset_id
assert connection["datasets"][0]["role"] == "read_write"
def test_register_connection_idempotent_update(self, client, headers, _patch_operations):
clear_registered_agent_connections()
payload = {
"agent_session_name": "idempotent_agent",
"type": "api",
"metadata": {"version": "1"},
}
r1 = client.post("/api/v1/agents/register", headers=headers, json=payload)
assert r1.status_code == 201
first_id = r1.json()["id"]
payload["metadata"] = {"version": "2"}
r2 = client.post("/api/v1/agents/register", headers=headers, json=payload)
assert r2.status_code == 201
assert r2.json()["id"] == first_id
assert r2.json()["metadata"]["version"] == "2"
resp = client.get("/api/v1/agents/connections", headers=headers)
assert resp.json()["total"] == 1
# ------------------------------------------------------------------ #
# Sub-user agent CRUD endpoints
# ------------------------------------------------------------------ #
def test_create_sub_user_agent(self, client, headers):
resp = client.post(
f"/api/v1/agents/create?name={AGENT_NAME}",
headers=headers,
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert "agentId" in body
assert "agentEmail" in body
assert "agentApiKey" in body
assert "@cognee.agent" in body["agentEmail"]
def test_create_duplicate_sub_user_returns_409(self, client, headers):
resp = client.post(
f"/api/v1/agents/create?name={AGENT_NAME}",
headers=headers,
)
assert resp.status_code == 409
def test_list_agents(self, client, headers):
resp = client.get("/api/v1/agents/list", headers=headers)
assert resp.status_code == 200
agents = resp.json()
assert isinstance(agents, list)
def test_sub_user_api_key_authenticates(self, client, headers):
resp = client.post(
f"/api/v1/agents/create?name=apikey-test-{RUN_ID}",
headers=headers,
)
assert resp.status_code == 200
api_key = resp.json()["agentApiKey"]
me_resp = client.get(
"/api/v1/auth/me",
headers={"X-Api-Key": api_key},
)
assert me_resp.status_code == 200
def test_delete_sub_user_agent(self, client, headers):
create_resp = client.post(
f"/api/v1/agents/create?name=to-delete-{RUN_ID}",
headers=headers,
)
assert create_resp.status_code == 200
agent_id = create_resp.json()["agentId"]
del_resp = client.delete(
f"/api/v1/agents/{agent_id}",
headers=headers,
)
assert del_resp.status_code == 200
def test_delete_nonexistent_agent_fails(self, client, headers):
fake_id = str(uuid.uuid4())
resp = client.delete(
f"/api/v1/agents/{fake_id}",
headers=headers,
)
assert resp.status_code in (403, 404, 500)
# ------------------------------------------------------------------ #
# Unregister endpoint
# ------------------------------------------------------------------ #
def test_unregister(self, client, headers, _patch_operations):
clear_registered_agent_connections()
agent_mode._active_count = 0
agent_mode._active_connection_ids.clear()
client.post(
"/api/v1/agents/register", headers=headers, json={"agent_session_name": "unreg-a"}
)
client.post(
"/api/v1/agents/register", headers=headers, json={"agent_session_name": "unreg-b"}
)
assert agent_mode._active_count == 2
resp = client.post(
"/api/v1/agents/unregister", json={"agent_session_name": "unreg-a"}, headers=headers
)
assert resp.status_code == 200
assert resp.json()["activeAgents"] == 1
resp2 = client.post(
"/api/v1/agents/unregister", json={"agent_session_name": "unreg-b"}, headers=headers
)
assert resp2.json()["activeAgents"] == 0
def test_unregister_floor_at_zero(self, client, headers):
agent_mode._active_count = 0
agent_mode._active_connection_ids.clear()
resp = client.post(
"/api/v1/agents/unregister", json={"agent_session_name": "nonexistent"}, headers=headers
)
assert resp.status_code == 200
assert resp.json()["activeAgents"] == 0
# ------------------------------------------------------------------ #
# Auth: unauthenticated requests rejected
# ------------------------------------------------------------------ #
def test_unauthenticated_rejected(self, client):
client.post("/api/v1/auth/logout")
saved_cookies = dict(client.cookies)
client.cookies.clear()
try:
endpoints = [
("GET", "/api/v1/agents/connections", None),
("POST", "/api/v1/agents/register", {"agent_session_name": "x"}),
("GET", f"/api/v1/agents/connections/{uuid.uuid4()}", None),
("POST", "/api/v1/agents/create?name=nope", None),
("GET", "/api/v1/agents/list", None),
("DELETE", f"/api/v1/agents/{uuid.uuid4()}", None),
("POST", "/api/v1/agents/unregister", {"agent_session_name": "x"}),
]
for method, path, body in endpoints:
resp = client.request(method, path, json=body)
assert resp.status_code == 401, f"{method} {path} returned {resp.status_code}"
finally:
client.cookies.update(saved_cookies)
# ------------------------------------------------------------------ #
# Fixtures
# ------------------------------------------------------------------ #
@pytest.fixture(autouse=True)
def _reset_agent_mode(self):
saved_count = agent_mode._active_count
saved_ids = set(agent_mode._active_connection_ids)
yield
agent_mode._active_count = saved_count
agent_mode._active_connection_ids.clear()
agent_mode._active_connection_ids.update(saved_ids)
@pytest.fixture(scope="class")
def _patch_operations(self, owner):
async def readable_datasets_for(_user):
return []
async def visible_user_ids(_user):
return [uuid.UUID(owner["id"])]
async def persisted_agent_connections(_user_id, active_only=True):
return []
with (
patch(
"cognee.modules.agents.operations._readable_datasets_for",
readable_datasets_for,
),
patch(
"cognee.modules.agents.operations._visible_user_ids",
visible_user_ids,
),
patch(
"cognee.modules.agents.operations.list_persisted_agent_connections",
persisted_agent_connections,
),
):
yield
PERSIST_RUN_ID = uuid.uuid4().hex[:8]
PERSIST_OWNER_EMAIL = f"persist-agents-{PERSIST_RUN_ID}@example.com"
PERSIST_OWNER_PASSWORD = "persistpass123!"
PERSIST_AGENT_NAME = f"persist-agent-{PERSIST_RUN_ID}"
class TestAgentPersistence:
"""Verify agent registration persists to principal_configuration
without overwriting existing data."""
@pytest.fixture(scope="class")
def client(self):
from cognee.api.client import app
with TestClient(app) as c:
yield c
@pytest.fixture(scope="class")
def owner(self, client):
reg = client.post(
"/api/v1/auth/register",
json={"email": PERSIST_OWNER_EMAIL, "password": PERSIST_OWNER_PASSWORD},
)
assert reg.status_code in (200, 201), reg.text
owner_id = reg.json()["id"]
login = client.post(
"/api/v1/auth/login",
data={"username": PERSIST_OWNER_EMAIL, "password": PERSIST_OWNER_PASSWORD},
)
assert login.status_code == 200, login.text
return {"id": owner_id, "token": login.json()["access_token"]}
@pytest.fixture(scope="class")
def headers(self, owner):
return {"Authorization": f"Bearer {owner['token']}"}
def _seed_configuration(self, client, headers):
resp = client.post(
"/api/v1/configuration/store_user_configuration",
json={"name": AGENT_CONFIG_NAME, "config": {"custom_setting": "keep_me", "agents": {}}},
headers=headers,
)
assert resp.status_code == 200, resp.text
def _read_agent_config(self, client, headers):
resp = client.get(
"/api/v1/configuration/get_user_configuration/",
headers=headers,
)
assert resp.status_code == 200, resp.text
for config in resp.json():
if config.get("name") == AGENT_CONFIG_NAME:
return config["configuration"]
return None
def test_register_persists_without_overwriting(self, client, headers, owner):
self._seed_configuration(client, headers)
clear_registered_agent_connections()
resp = client.post(
"/api/v1/agents/register",
headers=headers,
json={
"agent_session_name": PERSIST_AGENT_NAME,
"type": "api",
"memory_mode": "hybrid",
"source": "api",
},
)
assert resp.status_code == 201, resp.text
connection = resp.json()
agent_id = connection["id"]
agent_config = self._read_agent_config(client, headers)
assert agent_config is not None, "agent_configuration not found in principal_configuration"
assert agent_config["custom_setting"] == "keep_me", "pre-existing config was overwritten"
assert agent_id in agent_config["agents"], "registered agent not persisted"
persisted = agent_config["agents"][agent_id]
assert persisted["agent_session_name"] == PERSIST_AGENT_NAME
assert persisted["type"] == "api"
assert persisted["memory_mode"] == "hybrid"
def test_list_returns_persisted_agent(self, client, headers, owner):
resp = client.get("/api/v1/agents/connections", headers=headers)
assert resp.status_code == 200
body = resp.json()
agent_names = {a["agent_session_name"] for a in body["agents"]}
assert PERSIST_AGENT_NAME in agent_names
def test_parent_sees_child_agent_connection(self, client, headers, owner):
child_name = f"child-agent-{PERSIST_RUN_ID}"
create_resp = client.post(
f"/api/v1/agents/create?name={child_name}",
headers=headers,
)
assert create_resp.status_code == 200, create_resp.text
child_api_key = create_resp.json()["agentApiKey"]
child_headers = {"X-Api-Key": child_api_key}
child_connection_name = f"child-bot-{PERSIST_RUN_ID}"
reg_resp = client.post(
"/api/v1/agents/register",
headers=child_headers,
json={
"agent_session_name": child_connection_name,
"type": "sdk",
"memory_mode": "cognee",
"source": "api",
},
)
assert reg_resp.status_code == 201, reg_resp.text
child_connection_id = reg_resp.json()["id"]
clear_registered_agent_connections()
resp = client.get("/api/v1/agents/connections", headers=headers)
assert resp.status_code == 200
body = resp.json()
agent_ids = {a["id"] for a in body["agents"]}
assert child_connection_id in agent_ids, (
"parent user should see child agent's persisted connection"
)
LIFECYCLE_RUN_ID = uuid.uuid4().hex[:8]
LIFECYCLE_OWNER_EMAIL = f"lifecycle-{LIFECYCLE_RUN_ID}@example.com"
LIFECYCLE_OWNER_PASSWORD = "lifecycle123!"
class TestAgentFullLifecycle:
"""Full lifecycle: create agents, register them, verify connections,
unregister, confirm cleanup, and test watchdog shutdown."""
@pytest.fixture(scope="class")
def client(self):
from cognee.api.client import app
with TestClient(app) as c:
yield c
@pytest.fixture(scope="class")
def owner(self, client):
reg = client.post(
"/api/v1/auth/register",
json={"email": LIFECYCLE_OWNER_EMAIL, "password": LIFECYCLE_OWNER_PASSWORD},
)
assert reg.status_code in (200, 201), reg.text
owner_id = reg.json()["id"]
login = client.post(
"/api/v1/auth/login",
data={"username": LIFECYCLE_OWNER_EMAIL, "password": LIFECYCLE_OWNER_PASSWORD},
)
assert login.status_code == 200, login.text
return {"id": owner_id, "token": login.json()["access_token"]}
@pytest.fixture(scope="class")
def headers(self, owner):
return {"Authorization": f"Bearer {owner['token']}"}
@pytest.fixture(autouse=True)
def _reset(self):
agent_mode._active_count = 0
agent_mode._active_connection_ids.clear()
agent_mode._watchdog_started = False
clear_registered_agent_connections()
yield
def test_full_lifecycle(self, client, headers, owner):
agent_a_name = f"agent-a-{LIFECYCLE_RUN_ID}"
agent_b_name = f"agent-b-{LIFECYCLE_RUN_ID}"
# -- Step 1: Create two agents in the DB --
resp_a = client.post(
f"/api/v1/agents/create?name={agent_a_name}",
headers=headers,
)
assert resp_a.status_code == 200, resp_a.text
agent_a = resp_a.json()
agent_a_key = agent_a["agentApiKey"]
resp_b = client.post(
f"/api/v1/agents/create?name={agent_b_name}",
headers=headers,
)
assert resp_b.status_code == 200, resp_b.text
agent_b = resp_b.json()
agent_b_key = agent_b["agentApiKey"]
# -- Step 2: Verify both appear in GET /list (DB agents) --
list_resp = client.get("/api/v1/agents/list", headers=headers)
assert list_resp.status_code == 200
db_agents = list_resp.json()
db_agent_ids = {a["agentId"] for a in db_agents}
assert agent_a["agentId"] in db_agent_ids
assert agent_b["agentId"] in db_agent_ids
# -- Step 3: Verify GET /{agent_id} returns each agent --
detail_a = client.get(f"/api/v1/agents/{agent_a['agentId']}", headers=headers)
assert detail_a.status_code == 200
assert detail_a.json()["agentId"] == agent_a["agentId"]
detail_b = client.get(f"/api/v1/agents/{agent_b['agentId']}", headers=headers)
assert detail_b.status_code == 200
assert detail_b.json()["agentId"] == agent_b["agentId"]
# -- Step 4: Register both agents (each authenticates with its API key) --
reg_a = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_a_key},
json={"agent_session_name": agent_a_name, "type": "api", "memory_mode": "cognee"},
)
assert reg_a.status_code == 201, reg_a.text
connection_a_id = reg_a.json()["id"]
assert agent_mode._active_count == 1
reg_b = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_b_key},
json={"agent_session_name": agent_b_name, "type": "sdk", "memory_mode": "hybrid"},
)
assert reg_b.status_code == 201, reg_b.text
connection_b_id = reg_b.json()["id"]
assert agent_mode._active_count == 2
# -- Step 5: Re-registering same agent doesn't increment counter --
reg_a_again = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_a_key},
json={"agent_session_name": agent_a_name, "type": "api", "memory_mode": "cognee"},
)
assert reg_a_again.status_code == 201
assert agent_mode._active_count == 2, "re-register should not increment"
# -- Step 6: Verify GET /connections shows both --
conn_resp = client.get("/api/v1/agents/connections", headers=headers)
assert conn_resp.status_code == 200
conn_ids = {a["id"] for a in conn_resp.json()["agents"]}
assert connection_a_id in conn_ids
assert connection_b_id in conn_ids
# -- Step 7: Verify GET /connections/{agent_id} returns detail --
detail_conn = client.get(
f"/api/v1/agents/connections/{agent_a['agentId']}?agent_session_name={agent_a_name}",
headers=headers,
)
assert detail_conn.status_code == 200
assert detail_conn.json()["agent"]["agent_session_name"] == agent_a_name
# -- Step 8: Unregister agent A's connection --
unreg_a = client.post(
"/api/v1/agents/unregister",
headers={"X-Api-Key": agent_a_key},
json={"agent_session_name": agent_a_name},
)
assert unreg_a.status_code == 200
assert unreg_a.json()["activeAgents"] == 1
# -- Step 9: Active connections should show B but not A --
conn_after = client.get("/api/v1/agents/connections", headers=headers)
assert conn_after.status_code == 200
conn_ids_after = {a["id"] for a in conn_after.json()["agents"]}
assert connection_a_id not in conn_ids_after, "unregistered agent should not appear"
assert connection_b_id in conn_ids_after, "still-registered agent should appear"
# -- Step 9b: active_only=false should still show A as inactive --
conn_all = client.get("/api/v1/agents/connections?active_only=false", headers=headers)
assert conn_all.status_code == 200
all_agents = conn_all.json()["agents"]
all_ids = {a["id"] for a in all_agents}
assert connection_a_id in all_ids, "inactive agent should appear with active_only=false"
agent_a_conn = next(a for a in all_agents if a["id"] == connection_a_id)
assert agent_a_conn["status"] == "inactive"
# -- Step 10: DB list still shows both (unregister doesn't delete the user) --
list_after = client.get("/api/v1/agents/list", headers=headers)
db_ids_after = {a["agentId"] for a in list_after.json()}
assert agent_a["agentId"] in db_ids_after
assert agent_b["agentId"] in db_ids_after
# -- Step 11: Unregister agent B's connection --
unreg_b = client.post(
"/api/v1/agents/unregister",
headers={"X-Api-Key": agent_b_key},
json={"agent_session_name": agent_b_name},
)
assert unreg_b.status_code == 200
assert unreg_b.json()["activeAgents"] == 0
# -- Step 12: Active connections should be empty --
conn_empty = client.get("/api/v1/agents/connections", headers=headers)
assert conn_empty.json()["agents"] == []
# -- Step 12b: But all connections still exist as inactive in DB --
conn_all_final = client.get("/api/v1/agents/connections?active_only=false", headers=headers)
inactive_statuses = {a["status"] for a in conn_all_final.json()["agents"]}
assert inactive_statuses == {"inactive"}
# -- Step 13: Watchdog should trigger shutdown with 0 agents --
with patch.object(agent_mode, "_shutdown_server") as mock_shutdown:
agent_mode._watchdog()
mock_shutdown.assert_called_once()
# -- Step 14: Agent A can re-register after unregistering --
re_reg = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_a_key},
json={"agent_session_name": agent_a_name, "type": "api"},
)
assert re_reg.status_code == 201
assert agent_mode._active_count == 1, "re-register after unregister should increment"
# -- Step 15: Watchdog should NOT shutdown with active agent --
with patch.object(agent_mode, "_shutdown_server") as mock_shutdown:
agent_mode._watchdog()
mock_shutdown.assert_not_called()
# -- Step 16: Delete agent B from DB --
del_resp = client.delete(
f"/api/v1/agents/{agent_b['agentId']}",
headers=headers,
)
assert del_resp.status_code == 200
# -- Step 17: DB list no longer shows B --
list_final = client.get("/api/v1/agents/list", headers=headers)
final_ids = {a["agentId"] for a in list_final.json()}
assert agent_b["agentId"] not in final_ids
assert agent_a["agentId"] in final_ids
# -- Step 18: Re-register agent A with different values (same name/type) --
re_reg_updated = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_a_key},
json={
"agent_session_name": agent_a_name,
"type": "api",
"memory_mode": "session",
"metadata": {"version": "2"},
},
)
assert re_reg_updated.status_code == 201
updated_conn = re_reg_updated.json()
assert updated_conn["memory_mode"] == "session", "new values should replace old"
assert updated_conn["metadata"]["version"] == "2"
assert agent_mode._active_count == 1, "same user should not increment again"
# -- Step 19: Only one connection for agent A in the registry --
conn_after_update = client.get("/api/v1/agents/connections", headers=headers)
agent_a_conns = [
a for a in conn_after_update.json()["agents"] if a["id"] == updated_conn["id"]
]
assert len(agent_a_conns) == 1
assert agent_a_conns[0]["memory_mode"] == "session"
# -- Step 20: Re-register with different name creates a second connection --
re_reg_new_name = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_a_key},
json={
"agent_session_name": f"{agent_a_name}-v2",
"type": "api",
"memory_mode": "hybrid",
},
)
assert re_reg_new_name.status_code == 201
new_conn_id = re_reg_new_name.json()["id"]
assert new_conn_id != updated_conn["id"], "different name should produce different ID"
conn_both = client.get("/api/v1/agents/connections", headers=headers)
both_ids = {a["id"] for a in conn_both.json()["agents"]}
assert updated_conn["id"] in both_ids, "old connection should still exist"
assert new_conn_id in both_ids, "new connection should also exist"
assert agent_mode._active_count == 2, "two connections from same user count separately"
# -- Step 21: Unregister one of agent A's connections, other stays active --
client.post(
"/api/v1/agents/unregister",
headers={"X-Api-Key": agent_a_key},
json={"agent_session_name": agent_a_name},
)
conn_after_partial = client.get(
"/api/v1/agents/connections?active_only=false", headers=headers
)
a_conn = next(
(a for a in conn_after_partial.json()["agents"] if a["id"] == updated_conn["id"]),
None,
)
assert a_conn is not None and a_conn["status"] == "inactive"
v2_conn = next(
(a for a in conn_after_partial.json()["agents"] if a["id"] == new_conn_id),
None,
)
assert v2_conn is not None and v2_conn["status"] == "active", (
"other connection should still be active"
)
# -- Step 22: Re-register agent A, verify DB status flips back to active --
re_reg_after_deactivate = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_a_key},
json={"agent_session_name": agent_a_name, "type": "api", "memory_mode": "cognee"},
)
assert re_reg_after_deactivate.status_code == 201
reactivated_id = re_reg_after_deactivate.json()["id"]
conn_reactivated = client.get(
"/api/v1/agents/connections?active_only=false", headers=headers
)
reactivated = next(
a for a in conn_reactivated.json()["agents"] if a["id"] == reactivated_id
)
assert reactivated["status"] == "active", "re-registered connection should be active in DB"
# -- Step 23: Create agent C, register it, then delete it from DB --
agent_c_name = f"agent-c-{LIFECYCLE_RUN_ID}"
resp_c = client.post(
f"/api/v1/agents/create?name={agent_c_name}",
headers=headers,
)
assert resp_c.status_code == 200
agent_c = resp_c.json()
agent_c_key = agent_c["agentApiKey"]
reg_c = client.post(
"/api/v1/agents/register",
headers={"X-Api-Key": agent_c_key},
json={"agent_session_name": agent_c_name, "type": "api"},
)
assert reg_c.status_code == 201
connection_c_id = reg_c.json()["id"]
# Verify agent C shows in connections before delete
conn_before_delete = client.get("/api/v1/agents/connections", headers=headers)
assert connection_c_id in {a["id"] for a in conn_before_delete.json()["agents"]}
# Delete agent C from DB
del_c = client.delete(
f"/api/v1/agents/{agent_c['agentId']}",
headers=headers,
)
assert del_c.status_code == 200
# Verify agent C is gone from DB list
list_after_del = client.get("/api/v1/agents/list", headers=headers)
assert agent_c["agentId"] not in {a["agentId"] for a in list_after_del.json()}
# Verify agent C connections are cleaned up (not in active or inactive)
conn_after_delete = client.get(
"/api/v1/agents/connections?active_only=false", headers=headers
)
assert connection_c_id not in {a["id"] for a in conn_after_delete.json()["agents"]}, (
"deleted agent's connections should be cleaned up"
)
ISOLATION_RUN_ID = uuid.uuid4().hex[:8]
ISOLATION_USER_A_EMAIL = f"isolation-a-{ISOLATION_RUN_ID}@example.com"
ISOLATION_USER_B_EMAIL = f"isolation-b-{ISOLATION_RUN_ID}@example.com"
ISOLATION_PASSWORD = "isolation123!"
class TestMultiTenantIsolation:
"""Verify that users cannot see each other's agents or connections."""
@pytest.fixture(scope="class")
def client(self):
from cognee.api.client import app
with TestClient(app) as c:
yield c
@pytest.fixture(scope="class")
def user_a(self, client):
reg = client.post(
"/api/v1/auth/register",
json={"email": ISOLATION_USER_A_EMAIL, "password": ISOLATION_PASSWORD},
)
assert reg.status_code in (200, 201), reg.text
login = client.post(
"/api/v1/auth/login",
data={"username": ISOLATION_USER_A_EMAIL, "password": ISOLATION_PASSWORD},
)
assert login.status_code == 200
return {"id": reg.json()["id"], "token": login.json()["access_token"]}
@pytest.fixture(scope="class")
def user_b(self, client):
reg = client.post(
"/api/v1/auth/register",
json={"email": ISOLATION_USER_B_EMAIL, "password": ISOLATION_PASSWORD},
)
assert reg.status_code in (200, 201), reg.text
login = client.post(
"/api/v1/auth/login",
data={"username": ISOLATION_USER_B_EMAIL, "password": ISOLATION_PASSWORD},
)
assert login.status_code == 200
return {"id": reg.json()["id"], "token": login.json()["access_token"]}
@pytest.fixture(autouse=True)
def _reset(self):
agent_mode._active_count = 0
agent_mode._active_connection_ids.clear()
agent_mode._watchdog_started = False
clear_registered_agent_connections()
yield
def test_users_cannot_see_each_others_agents(self, client, user_a, user_b):
headers_a = {"Authorization": f"Bearer {user_a['token']}"}
headers_b = {"Authorization": f"Bearer {user_b['token']}"}
# User A creates an agent
resp_a = client.post(
f"/api/v1/agents/create?name=agent-of-a-{ISOLATION_RUN_ID}",
headers=headers_a,
)
assert resp_a.status_code == 200
agent_a_id = resp_a.json()["agentId"]
# User B creates an agent
resp_b = client.post(
f"/api/v1/agents/create?name=agent-of-b-{ISOLATION_RUN_ID}",
headers=headers_b,
)
assert resp_b.status_code == 200
agent_b_id = resp_b.json()["agentId"]
# User A lists agents — should see only their own
list_a = client.get("/api/v1/agents/list", headers=headers_a)
a_agent_ids = {a["agentId"] for a in list_a.json()}
assert agent_a_id in a_agent_ids
assert agent_b_id not in a_agent_ids, "user A should not see user B's agent"
# User B lists agents — should see only their own
list_b = client.get("/api/v1/agents/list", headers=headers_b)
b_agent_ids = {a["agentId"] for a in list_b.json()}
assert agent_b_id in b_agent_ids
assert agent_a_id not in b_agent_ids, "user B should not see user A's agent"
def test_users_cannot_see_each_others_connections(self, client, user_a, user_b):
headers_a = {"Authorization": f"Bearer {user_a['token']}"}
headers_b = {"Authorization": f"Bearer {user_b['token']}"}
# User A registers a connection
reg_a = client.post(
"/api/v1/agents/register",
headers=headers_a,
json={"agent_session_name": f"conn-a-{ISOLATION_RUN_ID}", "type": "api"},
)
assert reg_a.status_code == 201
conn_a_id = reg_a.json()["id"]
# User B registers a connection
reg_b = client.post(
"/api/v1/agents/register",
headers=headers_b,
json={"agent_session_name": f"conn-b-{ISOLATION_RUN_ID}", "type": "sdk"},
)
assert reg_b.status_code == 201
conn_b_id = reg_b.json()["id"]
# User A lists connections — should see only their own
conns_a = client.get("/api/v1/agents/connections", headers=headers_a)
a_conn_ids = {a["id"] for a in conns_a.json()["agents"]}
assert conn_a_id in a_conn_ids
assert conn_b_id not in a_conn_ids, "user A should not see user B's connection"
# User B lists connections — should see only their own
conns_b = client.get("/api/v1/agents/connections", headers=headers_b)
b_conn_ids = {a["id"] for a in conns_b.json()["agents"]}
assert conn_b_id in b_conn_ids
assert conn_a_id not in b_conn_ids, "user B should not see user A's connection"
def test_user_cannot_delete_others_agent(self, client, user_a, user_b):
headers_a = {"Authorization": f"Bearer {user_a['token']}"}
headers_b = {"Authorization": f"Bearer {user_b['token']}"}
resp = client.post(
f"/api/v1/agents/create?name=protected-{ISOLATION_RUN_ID}",
headers=headers_a,
)
assert resp.status_code == 200
agent_id = resp.json()["agentId"]
del_resp = client.delete(
f"/api/v1/agents/{agent_id}",
headers=headers_b,
)
assert del_resp.status_code == 403, "user B should not be able to delete user A's agent"
def test_user_cannot_get_others_agent(self, client, user_a, user_b):
headers_a = {"Authorization": f"Bearer {user_a['token']}"}
headers_b = {"Authorization": f"Bearer {user_b['token']}"}
resp = client.post(
f"/api/v1/agents/create?name=private-{ISOLATION_RUN_ID}",
headers=headers_a,
)
assert resp.status_code == 200
agent_id = resp.json()["agentId"]
get_resp = client.get(
f"/api/v1/agents/{agent_id}",
headers=headers_b,
)
assert get_resp.status_code == 403, "user B should not be able to get user A's agent"