243 lines
8.3 KiB
Python
243 lines
8.3 KiB
Python
"""
|
|
Integration tests for the elicitation REST API endpoints.
|
|
|
|
Covers the ``GET`` and ``POST`` elicitation endpoints exercised as a
|
|
standalone REST flow (no REPL, no SSE client). The companion file
|
|
``test_sessions_elicitation_resolve_url.py`` covers the round-trip
|
|
resolve path (park a real Future, resolve via URL, assert the hook
|
|
returns the correct decision). This file fills the remaining gaps:
|
|
|
|
- GET returns the correct pending-request shape (fields, types).
|
|
- POST resolve for an already-resolved elicitation is idempotent (202).
|
|
- GET after resolution returns ``status: "resolved"``.
|
|
- GET/POST against a valid session with a nonexistent elicitation id.
|
|
|
|
Uses the shared ``client`` fixture (real stores + mock LLM) and the
|
|
``_park_permission_hook`` helper from the resolve-URL module to create
|
|
real parked elicitations without duplicating the hook machinery.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from tests.server.helpers import create_test_agent
|
|
|
|
# Re-use the hook-parking helpers from the sibling module so we don't
|
|
# duplicate the SSE-drain + PermissionRequest machinery.
|
|
from tests.server.integration.test_sessions_elicitation_resolve_url import (
|
|
_create_session,
|
|
_park_permission_hook,
|
|
)
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
# ── GET /sessions/{id}/elicitations/{eid} ────────────────
|
|
|
|
|
|
async def test_get_pending_elicitation_shape(client: httpx.AsyncClient) -> None:
|
|
"""
|
|
GET on a pending elicitation returns status "pending" with the
|
|
expected top-level keys and correct types.
|
|
"""
|
|
from omnigent.runtime import pending_elicitations
|
|
|
|
agent = await create_test_agent(client, "test-get-pending-shape")
|
|
session_id = await _create_session(client, agent["id"])
|
|
hook_task, elicitation_id = await _park_permission_hook(client, session_id)
|
|
|
|
try:
|
|
resp = await client.get(
|
|
f"/v1/sessions/{session_id}/elicitations/{elicitation_id}",
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
|
|
# Shape assertions: every expected key present with correct type.
|
|
assert data["status"] == "pending"
|
|
assert isinstance(data["message"], str) and data["message"]
|
|
assert isinstance(data["phase"], str)
|
|
assert isinstance(data["policy_name"], str)
|
|
assert isinstance(data["content_preview"], str)
|
|
|
|
# Clean up: resolve so the hook doesn't leak.
|
|
await client.post(
|
|
f"/v1/sessions/{session_id}/elicitations/{elicitation_id}/resolve",
|
|
json={"action": "decline"},
|
|
)
|
|
async with asyncio.timeout(5):
|
|
_ = await hook_task
|
|
finally:
|
|
if not hook_task.done():
|
|
hook_task.cancel()
|
|
await asyncio.gather(hook_task, return_exceptions=True)
|
|
pending_elicitations.reset_for_tests()
|
|
|
|
|
|
async def test_get_nonexistent_elicitation_on_valid_session(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
GET with a valid session but unknown elicitation id returns
|
|
``status: "resolved"`` (not 404), since the elicitation may have
|
|
been resolved before the page loaded.
|
|
"""
|
|
agent = await create_test_agent(client, "test-get-nonexistent-eid")
|
|
session_id = await _create_session(client, agent["id"])
|
|
|
|
resp = await client.get(
|
|
f"/v1/sessions/{session_id}/elicitations/elicit_does_not_exist",
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
assert data["status"] == "resolved"
|
|
# Resolved responses must NOT leak pending-only fields.
|
|
assert "message" not in data
|
|
assert "phase" not in data
|
|
assert "policy_name" not in data
|
|
|
|
|
|
async def test_get_nonexistent_session_returns_404(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
GET against a session that does not exist returns 404.
|
|
"""
|
|
resp = await client.get(
|
|
"/v1/sessions/conv_no_such_session/elicitations/elicit_irrelevant",
|
|
)
|
|
assert resp.status_code == 404, resp.text
|
|
|
|
|
|
async def test_get_after_resolution_returns_resolved(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
After resolving an elicitation, GET returns ``status: "resolved"``.
|
|
|
|
Parks a real elicitation, resolves it, then asserts the GET
|
|
endpoint no longer reports it as pending.
|
|
"""
|
|
from omnigent.runtime import pending_elicitations
|
|
|
|
agent = await create_test_agent(client, "test-get-after-resolve")
|
|
session_id = await _create_session(client, agent["id"])
|
|
hook_task, elicitation_id = await _park_permission_hook(client, session_id)
|
|
|
|
try:
|
|
# Resolve it.
|
|
verdict = await client.post(
|
|
f"/v1/sessions/{session_id}/elicitations/{elicitation_id}/resolve",
|
|
json={"action": "accept"},
|
|
)
|
|
assert verdict.status_code == 202, verdict.text
|
|
async with asyncio.timeout(5):
|
|
_ = await hook_task
|
|
|
|
# GET should now show resolved.
|
|
resp = await client.get(
|
|
f"/v1/sessions/{session_id}/elicitations/{elicitation_id}",
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
assert resp.json()["status"] == "resolved"
|
|
finally:
|
|
if not hook_task.done():
|
|
hook_task.cancel()
|
|
await asyncio.gather(hook_task, return_exceptions=True)
|
|
pending_elicitations.reset_for_tests()
|
|
|
|
|
|
# ── POST /sessions/{id}/elicitations/{eid}/resolve ───────
|
|
|
|
|
|
async def test_post_resolve_nonexistent_session_returns_404(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
POST resolve against a nonexistent session returns 404.
|
|
"""
|
|
resp = await client.post(
|
|
"/v1/sessions/conv_no_such_session/elicitations/elicit_irrelevant/resolve",
|
|
json={"action": "accept"},
|
|
)
|
|
assert resp.status_code == 404, resp.text
|
|
|
|
|
|
async def test_post_resolve_already_resolved_is_idempotent(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
Resolving an already-resolved elicitation returns 202 (no-op).
|
|
|
|
The ``_resolve_elicitation`` helper skips a done Future gracefully,
|
|
so a double-submit from the UI must not error.
|
|
"""
|
|
from omnigent.runtime import pending_elicitations
|
|
|
|
agent = await create_test_agent(client, "test-double-resolve")
|
|
session_id = await _create_session(client, agent["id"])
|
|
hook_task, elicitation_id = await _park_permission_hook(client, session_id)
|
|
|
|
try:
|
|
# First resolve — actually wakes the hook.
|
|
first = await client.post(
|
|
f"/v1/sessions/{session_id}/elicitations/{elicitation_id}/resolve",
|
|
json={"action": "accept"},
|
|
)
|
|
assert first.status_code == 202, first.text
|
|
async with asyncio.timeout(5):
|
|
_ = await hook_task
|
|
|
|
# Second resolve — the Future is already done; should still 202.
|
|
second = await client.post(
|
|
f"/v1/sessions/{session_id}/elicitations/{elicitation_id}/resolve",
|
|
json={"action": "decline"},
|
|
)
|
|
assert second.status_code == 202, second.text
|
|
assert second.json() == {"queued": False}
|
|
finally:
|
|
if not hook_task.done():
|
|
hook_task.cancel()
|
|
await asyncio.gather(hook_task, return_exceptions=True)
|
|
pending_elicitations.reset_for_tests()
|
|
|
|
|
|
async def test_post_resolve_nonexistent_elicitation_on_valid_session(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
POST resolve with a valid session but unknown elicitation id
|
|
returns 202 (idempotent no-op) — the elicitation may have already
|
|
been resolved by timeout or another tab.
|
|
"""
|
|
agent = await create_test_agent(client, "test-resolve-unknown-eid")
|
|
session_id = await _create_session(client, agent["id"])
|
|
|
|
resp = await client.post(
|
|
f"/v1/sessions/{session_id}/elicitations/elicit_does_not_exist/resolve",
|
|
json={"action": "accept"},
|
|
)
|
|
assert resp.status_code == 202, resp.text
|
|
assert resp.json() == {"queued": False}
|
|
|
|
|
|
async def test_post_resolve_invalid_action_returns_422(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
"""
|
|
A body with an invalid ``action`` value is rejected with 422
|
|
before any resolution logic runs.
|
|
"""
|
|
agent = await create_test_agent(client, "test-resolve-bad-action")
|
|
session_id = await _create_session(client, agent["id"])
|
|
|
|
resp = await client.post(
|
|
f"/v1/sessions/{session_id}/elicitations/elicit_whatever/resolve",
|
|
json={"action": "yolo"},
|
|
)
|
|
assert resp.status_code == 422, resp.text
|