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

@@ -284,6 +284,68 @@ async def post_escalate(
return _to_response(updated)
@router.post("/sessions/{session_id}/next-node", response_model=NextNodeResponse)
async def next_node(
session_id: UUID,
payload: NextNodeRequest,
db: Annotated[AsyncSession, Depends(get_db)],
user: Annotated[User, Depends(require_l1_or_coverage)],
):
"""Record the answer/ack on the current node, then generate the next node.
problem_text comes from the linked internal ticket; category from the hidden
meta entry seeded at intake (ai_tree_builder skips meta entries). node_text is
the rendered text of the node being answered (the client holds it) so the
walked path and the captured tree stay legible.
"""
session = await _get_session_or_404(db, session_id, user)
ticket = await internal_ticket_service.get_ticket(
db, ticket_id=UUID(session.ticket_id)
)
problem_text = ticket.problem_statement if ticket else ""
category = next(
(s.get("category") for s in (session.walked_path or [])
if s.get("node_type") == "meta"),
"unknown",
)
try:
node = await l1_session_service.advance_ai_build(
db,
session_id=session_id,
problem_text=problem_text,
category=category or "unknown",
node_id=payload.node_id,
node_text=payload.node_text,
answer=payload.answer,
note=payload.note,
)
except ValueError as exc:
raise HTTPException(
status_code=http_status.HTTP_409_CONFLICT, detail=str(exc)
)
await db.commit()
return NextNodeResponse(node=node, session_status=session.status)
@router.get("/escalations", response_model=list[WalkSessionResponse])
async def l1_escalations(
db: Annotated[AsyncSession, Depends(get_db)],
user: Annotated[User, Depends(require_engineer_or_admin)],
limit: int = 50,
):
"""Engineer-visible list of escalated L1 sessions (the handoff queue)."""
rows = await db.execute(
select(L1WalkSession)
.where(
L1WalkSession.account_id == user.account_id,
L1WalkSession.status == "escalated",
)
.order_by(L1WalkSession.last_step_at.desc())
.limit(limit)
)
return [_to_response(s) for s in rows.scalars()]
@router.post("/escalate-without-walk", response_model=WalkSessionResponse)
async def post_escalate_without_walk(
payload: EscalateWithoutWalkRequest,