diff --git a/tests/server/routes/test_default_policies.py b/tests/server/routes/test_default_policies.py index 6cee67c355..9f2a19a5fb 100644 --- a/tests/server/routes/test_default_policies.py +++ b/tests/server/routes/test_default_policies.py @@ -9,6 +9,7 @@ from collections.abc import AsyncIterator from pathlib import Path +from unittest.mock import patch import httpx import pytest @@ -301,3 +302,38 @@ async def test_list_no_config_policies_when_caps_empty( data = resp.json()["data"] config_entries = [p for p in data if p.get("source") == "config"] assert config_entries == [] + + +# ── Telemetry ───────────────────────────────────────────────────────── + + +async def test_create_default_policy_emits_telemetry( + policy_client: httpx.AsyncClient, +) -> None: + """``POST /v1/policies`` emits a ``PolicyRegisteredEvent`` with scope='admin'.""" + from omnigent.telemetry.events import PolicyRegisteredEvent + + with patch("omnigent.server.routes.default_policies._tel_emit") as mock_emit: + resp = await policy_client.post("/v1/policies", json=_policy_payload()) + assert resp.status_code == 200 + + mock_emit.assert_called_once() + event = mock_emit.call_args[0][0] + assert isinstance(event, PolicyRegisteredEvent) + assert event.scope == "admin" + assert event.session_id is None + assert event.handler == _REGISTERED_HANDLER + assert event.policy_type == "python" + + +async def test_create_default_policy_no_telemetry_on_error( + policy_client: httpx.AsyncClient, +) -> None: + """``POST /v1/policies`` does not emit telemetry when the request fails.""" + with patch("omnigent.server.routes.default_policies._tel_emit") as mock_emit: + # Duplicate name → 409 before the emit block is reached. + await policy_client.post("/v1/policies", json=_policy_payload(name="dup")) + mock_emit.reset_mock() + resp = await policy_client.post("/v1/policies", json=_policy_payload(name="dup")) + assert resp.status_code == 409 + mock_emit.assert_not_called() diff --git a/tests/server/routes/test_session_policies_crud.py b/tests/server/routes/test_session_policies_crud.py index 83db457d33..f0b570f0b4 100644 --- a/tests/server/routes/test_session_policies_crud.py +++ b/tests/server/routes/test_session_policies_crud.py @@ -10,6 +10,7 @@ from collections.abc import AsyncIterator from pathlib import Path +from unittest.mock import patch import httpx import pytest @@ -255,3 +256,43 @@ async def test_delete_session_policy(client: httpx.AsyncClient, session_id: str) # Verify it's gone from the session policies get_resp = await client.get(f"/v1/sessions/{session_id}/policies/{pid}") assert get_resp.status_code == 404 + + +# ── Telemetry ───────────────────────────────────────────────────────── + + +async def test_create_session_policy_emits_telemetry( + client: httpx.AsyncClient, session_id: str +) -> None: + """``POST /v1/sessions/{id}/policies`` emits a ``PolicyRegisteredEvent`` with scope='session'.""" # noqa: E501 + from omnigent.telemetry.events import PolicyRegisteredEvent + + with patch("omnigent.server.routes.session_policies._tel_emit") as mock_emit: + resp = await client.post( + f"/v1/sessions/{session_id}/policies", + json=_policy_payload(), + ) + assert resp.status_code == 200 + + mock_emit.assert_called_once() + event = mock_emit.call_args[0][0] + assert isinstance(event, PolicyRegisteredEvent) + assert event.scope == "session" + assert event.session_id == session_id + assert event.handler == "https://example.com/policies/eval" + assert event.policy_type == "url" + + +async def test_create_session_policy_no_telemetry_on_error( + client: httpx.AsyncClient, session_id: str +) -> None: + """``POST /v1/sessions/{id}/policies`` does not emit telemetry when the request fails.""" + with patch("omnigent.server.routes.session_policies._tel_emit") as mock_emit: + # Duplicate name → 409 before the emit block is reached. + await client.post(f"/v1/sessions/{session_id}/policies", json=_policy_payload(name="dup")) + mock_emit.reset_mock() + resp = await client.post( + f"/v1/sessions/{session_id}/policies", json=_policy_payload(name="dup") + ) + assert resp.status_code == 409 + mock_emit.assert_not_called() diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py index 150d97d16b..99e519f444 100644 --- a/tests/test_telemetry.py +++ b/tests/test_telemetry.py @@ -491,6 +491,77 @@ def test_host_registry_get_host_installation_id_registered() -> None: assert registry.get_host_installation_id("host_abc") == "inst-xyz" +# ── PolicyRegisteredEvent ──────────────────────────────────────────────────── + + +def test_build_record_policy_registered_event_admin_scope() -> None: + """``_build_record`` serialises admin-scope ``PolicyRegisteredEvent`` correctly. + + ``installation_id``, ``session_id``, and ``anon_user_id`` are promoted to + top-level ``data`` fields; ``handler``, ``policy_type``, and ``scope`` go + into ``params``. Admin policies have ``session_id=None`` which becomes + an empty string in the wire format. + """ + import omnigent.telemetry.client as _mod + from omnigent.telemetry.events import PolicyRegisteredEvent + + event = PolicyRegisteredEvent( + installation_id="inst-abc", + handler="omnigent.policies.builtins.safety.ask_on_os_tools", + policy_type="python", + scope="admin", + session_id=None, + anon_user_id="deadbeef01234567", + ) + record = _mod._build_record(event) + data = record["data"] + + assert data["event_name"] == "PolicyRegisteredEvent" + assert data["installation_id"] == "inst-abc" + assert data["anon_user_id"] == "deadbeef01234567" + # session_id=None → empty string sentinel in wire format + assert data["session_id"] == "" + # Event-specific fields live in params, not at top level. + params = json.loads(data["params"]) if data["params"] else {} + assert params["handler"] == "omnigent.policies.builtins.safety.ask_on_os_tools" + assert params["policy_type"] == "python" + assert params["scope"] == "admin" + # Promoted fields must not leak into params. + assert "installation_id" not in params + assert "session_id" not in params + assert "anon_user_id" not in params + + +def test_build_record_policy_registered_event_session_scope() -> None: + """``_build_record`` serialises session-scope ``PolicyRegisteredEvent`` correctly. + + ``session_id`` is a real string for session-scoped policies and must appear + as the top-level ``session_id`` field, not inside ``params``. + """ + import omnigent.telemetry.client as _mod + from omnigent.telemetry.events import PolicyRegisteredEvent + + event = PolicyRegisteredEvent( + installation_id="inst-xyz", + handler="https://example.com/policies/eval", + policy_type="url", + scope="session", + session_id="sess_abc123", + anon_user_id=None, + ) + record = _mod._build_record(event) + data = record["data"] + + assert data["event_name"] == "PolicyRegisteredEvent" + assert data["session_id"] == "sess_abc123" + assert data["anon_user_id"] is None + params = json.loads(data["params"]) if data["params"] else {} + assert params["scope"] == "session" + assert params["handler"] == "https://example.com/policies/eval" + assert params["policy_type"] == "url" + assert "session_id" not in params + + def test_build_record_promotes_host_installation_id() -> None: """``_build_record`` lifts ``host_installation_id`` to top-level data.""" import omnigent.telemetry.client as _mod