"""AP0.7 — Actor directory, workspace data layer, tenant hardening tests.""" from __future__ import annotations from auth import AUTH_HEADER from services.actors import create_actor from tests.factories import provision_user_in_tenant from tests.test_initiatives_actions import ( _auth, _create_action, _create_initiative, _login, ) def test_list_actors_tenant_scoped(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) agent = create_actor(tenant_id=user["tenant_id"], actor_type="agent", name="Bot Alpha") res = client.get("/api/actors", headers=_auth(token)) assert res.status_code == 200 body = res.json() ids = {a["id"] for a in body} assert user["actor_id"] in ids assert agent["id"] in ids types = {a["actor_type"] for a in body} assert "human" in types assert "agent" in types for actor in body: assert "user_id" not in actor def test_list_actors_filter_by_type(client): user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) create_actor(tenant_id=user["tenant_id"], actor_type="agent", name="Filter Agent") res = client.get("/api/actors?actor_type=agent", headers=_auth(token)) assert res.status_code == 200 assert all(a["actor_type"] == "agent" for a in res.json()) def test_list_actors_excludes_inactive_by_default(client): user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) inactive = create_actor( tenant_id=user["tenant_id"], actor_type="external_system", name="Legacy" ) conn = __import__("db").get_connection() try: with conn.cursor() as cur: cur.execute("UPDATE actors SET is_active = FALSE WHERE id = %s", (inactive["id"],)) conn.commit() finally: conn.close() res = client.get("/api/actors", headers=_auth(token)) assert inactive["id"] not in {a["id"] for a in res.json()} res_active = client.get("/api/actors?include_inactive=true", headers=_auth(token)) assert inactive["id"] in {a["id"] for a in res_active.json()} def test_get_actor_detail(client): user = provision_user_in_tenant() token = _login(client, user) agent = create_actor(tenant_id=user["tenant_id"], actor_type="working_group", name="Team A") res = client.get(f"/api/actors/{agent['id']}", headers=_auth(token)) assert res.status_code == 200 assert res.json()["name"] == "Team A" assert res.json()["actor_type"] == "working_group" def test_cross_tenant_actor_directory_isolation(client): user_a = provision_user_in_tenant() user_b = provision_user_in_tenant() token_a = _login(client, user_a) token_b = _login(client, user_b) agent_a = create_actor(tenant_id=user_a["tenant_id"], actor_type="agent", name="Tenant A Agent") list_b = client.get("/api/actors", headers=_auth(token_b)).json() assert agent_a["id"] not in {a["id"] for a in list_b} assert user_a["actor_id"] not in {a["id"] for a in list_b} detail = client.get(f"/api/actors/{agent_a['id']}", headers=_auth(token_b)) assert detail.status_code == 404 def test_member_has_actor_and_workspace_capabilities(client): member = provision_user_in_tenant(tenant_role="member", portal_role="user") token = _login(client, member) ctx = client.get("/api/me/context", headers=_auth(token)).json() assert "kairo.actor.read" in ctx["capabilities"] assert "kairo.workspace.read" in ctx["capabilities"] def test_workspace_summary(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] open_action = _create_action( client, token, initiative_id, assigned_actor_ids=[user["actor_id"]], ).json() blocked = _create_action( client, token, initiative_id, title="Blocked", assigned_actor_ids=[user["actor_id"]], ).json() client.patch( f"/api/actions/{blocked['id']}", json={"status": "blocked"}, headers=_auth(token), ) done = _create_action( client, token, initiative_id, title="Done", assigned_actor_ids=[user["actor_id"]], ).json() client.patch( f"/api/actions/{done['id']}", json={"status": "done"}, headers=_auth(token), ) res = client.get("/api/workspace/summary", headers=_auth(token)) assert res.status_code == 200 summary = res.json() assert summary["open_actions_count"] >= 1 assert summary["blocked_actions_count"] >= 1 assert summary["active_initiatives_count"] >= 1 assert summary["done_actions_recent_count"] >= 1 assert open_action["id"] # used def test_workspace_open_and_blocked_actions(client): user = provision_user_in_tenant(tenant_role="member") token = _login(client, user) initiative_id = _create_initiative(client, token).json()["id"] open_id = _create_action( client, token, initiative_id, title="Open", assigned_actor_ids=[user["actor_id"]], ).json()["id"] blocked_id = _create_action( client, token, initiative_id, title="Blocked", assigned_actor_ids=[user["actor_id"]], ).json()["id"] client.patch( f"/api/actions/{blocked_id}", json={"status": "blocked"}, headers=_auth(token), ) open_res = client.get("/api/workspace/actions/open", headers=_auth(token)) blocked_res = client.get("/api/workspace/actions/blocked", headers=_auth(token)) assert open_res.status_code == 200 assert blocked_res.status_code == 200 assert open_id in {a["id"] for a in open_res.json()} assert blocked_id in {a["id"] for a in blocked_res.json()} assert blocked_id not in {a["id"] for a in open_res.json()} def test_workspace_active_initiatives(client): user = provision_user_in_tenant() token = _login(client, user) initiative_id = _create_initiative(client, token, title="Active One").json()["id"] res = client.get("/api/workspace/initiatives/active?limit=5", headers=_auth(token)) assert res.status_code == 200 ids = {i["id"] for i in res.json()} assert initiative_id in ids def test_workspace_actor_workload(client): user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) agent = create_actor(tenant_id=user["tenant_id"], actor_type="agent", name="Workload Bot") initiative_id = _create_initiative(client, token).json()["id"] _create_action( client, token, initiative_id, assigned_actor_ids=[agent["id"]], ) res = client.get("/api/workspace/actors/workload", headers=_auth(token)) assert res.status_code == 200 workload = {row["actor_id"]: row for row in res.json()} assert agent["id"] in workload assert workload[agent["id"]]["open_actions"] >= 1 def test_cross_tenant_workspace_summary_isolation(client): user_a = provision_user_in_tenant() user_b = provision_user_in_tenant() token_a = _login(client, user_a) token_b = _login(client, user_b) initiative_id = _create_initiative(client, token_a).json()["id"] _create_action( client, token_a, initiative_id, assigned_actor_ids=[user_a["actor_id"]], ) summary_b = client.get("/api/workspace/summary", headers=_auth(token_b)).json() assert summary_b["open_actions_count"] == 0 open_b = client.get("/api/workspace/actions/open", headers=_auth(token_b)).json() assert open_b == [] def test_assign_action_to_tenant_actor(client): user = provision_user_in_tenant(tenant_role="admin") token = _login(client, user) agent = create_actor(tenant_id=user["tenant_id"], actor_type="agent", name="Assignee") initiative_id = _create_initiative(client, token).json()["id"] action_id = _create_action(client, token, initiative_id).json()["id"] res = client.put( f"/api/actions/{action_id}/assignments", json={"actor_ids": [agent["id"]]}, headers=_auth(token), ) assert res.status_code == 200 assert res.json()["assigned_actor_ids"] == [agent["id"]] def test_unauthenticated_workspace_rejected(client): assert client.get("/api/workspace/summary").status_code == 401 assert client.get("/api/actors").status_code == 401