fix(l1): add missing next-node + escalations routes; reconcile Phase-1 intake tests

An earlier anchor-edit silently failed, so POST /sessions/{id}/next-node and
GET /escalations were never added (they 404'd). Add both, anchored on the real
/escalate-without-walk route.

Phase-1 test_l1_endpoints tests used POST /intake to create adhoc setup sessions,
but Phase 2A intake now dispatches via match_or_build (build/matched/suggest/
out_of_scope — never adhoc). Add a _create_adhoc_session service helper and route
the step/notes/resolve/escalate/cross-account setup through it; rewrite
test_intake_adhoc as test_intake_build_creates_ai_build_session (mocked outcome).

All green: test_l1_endpoints + test_l1_api_ai_build = 25 passed; full Phase 2A
backend service/unit/model suite = 56 passed; notification suite = 18 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 19:58:22 -04:00
parent c3d50069cc
commit 04d2cfb9a5
2 changed files with 123 additions and 48 deletions

View File

@@ -82,27 +82,72 @@ async def _make_l1_user(
return {"user_data": {"id": str(user.id), "account_id": str(account_id)}, "headers": None}
async def _create_adhoc_session(db: AsyncSession, info: dict, *, problem: str = "setup") -> str:
"""Create an adhoc walk session (backed by a real internal ticket) via the service.
Phase 2A: POST /l1/intake dispatches through match_or_build and no longer
yields an adhoc session directly, so step/notes/resolve/escalate/cross-account
tests build their setup session here instead of through intake. The test
client shares this same DB session (conftest override_get_db), so the
committed session is visible to the API immediately.
"""
from sqlalchemy import select as sa_select
from app.services import internal_ticket_service, l1_session_service
account_id = info["account_id"]
user_id = uuid.UUID(info["user_data"]["id"])
user = (await db.execute(sa_select(User).where(User.id == user_id))).scalar_one()
ticket = await internal_ticket_service.create_ticket(
db,
account_id=account_id,
created_by_user_id=user_id,
problem_statement=problem,
customer_name=None,
customer_contact=None,
)
session = await l1_session_service.start_adhoc_session(
db,
account_id=account_id,
user=user,
ticket_id=str(ticket.id),
ticket_kind="internal",
)
await db.commit()
return str(session.id)
# ---------------------------------------------------------------------------
# 1. Intake without flow_id → 200 + session_kind='adhoc'
# 1. Intake (Phase 2A): build outcome → 200 + session_kind='ai_build'
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_intake_adhoc(client: AsyncClient, test_db: AsyncSession):
"""POST /l1/intake without flow_id creates adhoc session."""
async def test_intake_build_creates_ai_build_session(client: AsyncClient, test_db: AsyncSession):
"""POST /l1/intake with a 'build' outcome creates an ai_build session.
Phase 2A: intake dispatches via match_or_build; 'adhoc' is no longer a direct
intake outcome (it is offered from the out_of_scope prompt on the frontend).
"""
from unittest.mock import AsyncMock, patch
info = await _make_l1_user(client, test_db, email="l1intake@example.com")
headers = info["headers"]
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Printer won't turn on", "customer_name": "Alice"},
headers=headers,
)
with patch(
"app.api.endpoints.l1.match_or_build.match_or_build",
new=AsyncMock(return_value={"outcome": "build", "session_kind": "ai_build",
"category": "printer"}),
):
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Printer won't turn on", "customer_name": "Alice"},
headers=headers,
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["session_kind"] == "adhoc"
assert body["outcome"] == "build"
assert body["session_kind"] == "ai_build"
assert body["ticket_kind"] == "internal"
assert "session_id" in body
assert "ticket_id" in body
assert body["session_id"]
assert body["ticket_id"]
# ---------------------------------------------------------------------------
@@ -156,14 +201,7 @@ async def test_step_on_adhoc_returns_400(client: AsyncClient, test_db: AsyncSess
info = await _make_l1_user(client, test_db, email="l1step@example.com")
headers = info["headers"]
# Create adhoc session via intake
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Adhoc issue"},
headers=headers,
)
assert resp.status_code == 200, resp.text
session_id = resp.json()["session_id"]
session_id = await _create_adhoc_session(test_db, info, problem="Adhoc issue")
resp = await client.post(
f"/api/v1/l1/sessions/{session_id}/step",
@@ -184,13 +222,7 @@ async def test_notes_on_adhoc_session(client: AsyncClient, test_db: AsyncSession
info = await _make_l1_user(client, test_db, email="l1notes@example.com")
headers = info["headers"]
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Notes test"},
headers=headers,
)
assert resp.status_code == 200, resp.text
session_id = resp.json()["session_id"]
session_id = await _create_adhoc_session(test_db, info, problem="Notes test")
notes_payload = [{"text": "Customer called about printer", "ts": "2026-05-28T10:00:00Z"}]
resp = await client.post(
@@ -213,13 +245,7 @@ async def test_resolve_session(client: AsyncClient, test_db: AsyncSession):
info = await _make_l1_user(client, test_db, email="l1resolve@example.com")
headers = info["headers"]
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Resolve test"},
headers=headers,
)
assert resp.status_code == 200, resp.text
session_id = resp.json()["session_id"]
session_id = await _create_adhoc_session(test_db, info, problem="Resolve test")
resp = await client.post(
f"/api/v1/l1/sessions/{session_id}/resolve",
@@ -245,13 +271,7 @@ async def test_escalate_session(client: AsyncClient, test_db: AsyncSession):
info = await _make_l1_user(client, test_db, email="l1escalate@example.com")
headers = info["headers"]
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Escalation test"},
headers=headers,
)
assert resp.status_code == 200, resp.text
session_id = resp.json()["session_id"]
session_id = await _create_adhoc_session(test_db, info, problem="Escalation test")
resp = await client.post(
f"/api/v1/l1/sessions/{session_id}/escalate",
@@ -344,15 +364,8 @@ async def test_get_session_cross_account_returns_404(client: AsyncClient, test_d
"""GET /l1/sessions/{id} from a different account → 404."""
# Account A: creates a session
info_a = await _make_l1_user(client, test_db, email="l1tenanta@example.com")
headers_a = info_a["headers"]
resp = await client.post(
"/api/v1/l1/intake",
json={"problem_statement": "Account A issue"},
headers=headers_a,
)
assert resp.status_code == 200, resp.text
session_id = resp.json()["session_id"]
session_id = await _create_adhoc_session(test_db, info_a, problem="Account A issue")
# Account B: different user in a different account
info_b = await _make_l1_user(client, test_db, email="l1tenantb@example.com")