feat(l1): l1_session_service resolve / escalate / escalate_without_walk
resolve: sets status=resolved, helpful, resolution_notes, resolved_at; flips FlowProposal.validated_by_outcome on helpful=True proposal walks; closes linked internal ticket. PSA close is a Phase 2 stub. escalate: marks session + internal ticket as escalated. PSA reassign deferred to Phase 2. escalate_without_walk: creates an immediately-escalated adhoc session with no walked_path, used by the BuildAbortedNoKB → Escalate path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,10 @@ from uuid import UUID
|
|||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.flow_proposal import FlowProposal
|
||||||
from app.models.l1_walk_session import L1WalkSession
|
from app.models.l1_walk_session import L1WalkSession
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.services import internal_ticket_service
|
||||||
|
|
||||||
|
|
||||||
def _resolve_acting_as(user: User) -> Optional[str]:
|
def _resolve_acting_as(user: User) -> Optional[str]:
|
||||||
@@ -149,3 +151,126 @@ async def update_notes(
|
|||||||
session.last_step_at = datetime.now(timezone.utc)
|
session.last_step_at = datetime.now(timezone.utc)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
session_id: UUID,
|
||||||
|
helpful: bool,
|
||||||
|
resolution_notes: str,
|
||||||
|
) -> L1WalkSession:
|
||||||
|
"""Close a session as resolved.
|
||||||
|
|
||||||
|
- Sets status='resolved', helpful, resolution_notes, resolved_at.
|
||||||
|
- On helpful=True AND session_kind='proposal': flips
|
||||||
|
flow_proposal.validated_by_outcome=True (one-bit aggregate signal).
|
||||||
|
- Closes the linked internal ticket (PSA close stubbed for Phase 2).
|
||||||
|
- Raises ValueError on missing or non-active session.
|
||||||
|
"""
|
||||||
|
session = await db.get(L1WalkSession, session_id)
|
||||||
|
if not session:
|
||||||
|
raise ValueError(f"L1WalkSession {session_id} not found")
|
||||||
|
if session.status != "active":
|
||||||
|
raise ValueError(f"Session not active (status={session.status})")
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
session.status = "resolved"
|
||||||
|
session.helpful = helpful
|
||||||
|
session.resolution_notes = resolution_notes
|
||||||
|
session.resolved_at = now
|
||||||
|
session.last_step_at = now
|
||||||
|
|
||||||
|
if helpful and session.session_kind == "proposal" and session.flow_proposal_id:
|
||||||
|
proposal = await db.get(FlowProposal, session.flow_proposal_id)
|
||||||
|
if proposal:
|
||||||
|
proposal.validated_by_outcome = True
|
||||||
|
|
||||||
|
if session.ticket_kind == "internal":
|
||||||
|
await internal_ticket_service.update_status(
|
||||||
|
db,
|
||||||
|
ticket_id=UUID(session.ticket_id),
|
||||||
|
status="resolved",
|
||||||
|
resolution_notes=resolution_notes,
|
||||||
|
)
|
||||||
|
# PSA close deferred to Phase 2 — no-op for now
|
||||||
|
|
||||||
|
await db.flush()
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
async def escalate(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
session_id: UUID,
|
||||||
|
reason: str,
|
||||||
|
reason_category: str,
|
||||||
|
) -> L1WalkSession:
|
||||||
|
"""Escalate an active session to engineering.
|
||||||
|
|
||||||
|
- Sets status='escalated', escalation_reason, escalation_reason_category, resolved_at.
|
||||||
|
- Marks the linked internal ticket as escalated (PSA reassign deferred to Phase 2).
|
||||||
|
- Raises ValueError on missing or non-active session.
|
||||||
|
"""
|
||||||
|
session = await db.get(L1WalkSession, session_id)
|
||||||
|
if not session:
|
||||||
|
raise ValueError(f"L1WalkSession {session_id} not found")
|
||||||
|
if session.status != "active":
|
||||||
|
raise ValueError(f"Session not active (status={session.status})")
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
session.status = "escalated"
|
||||||
|
session.escalation_reason = reason
|
||||||
|
session.escalation_reason_category = reason_category
|
||||||
|
session.resolved_at = now
|
||||||
|
session.last_step_at = now
|
||||||
|
|
||||||
|
if session.ticket_kind == "internal":
|
||||||
|
await internal_ticket_service.update_status(
|
||||||
|
db,
|
||||||
|
ticket_id=UUID(session.ticket_id),
|
||||||
|
status="escalated",
|
||||||
|
)
|
||||||
|
# PSA reassign deferred to Phase 2
|
||||||
|
|
||||||
|
await db.flush()
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
async def escalate_without_walk(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
account_id: UUID,
|
||||||
|
user: User,
|
||||||
|
ticket_id: str,
|
||||||
|
ticket_kind: str,
|
||||||
|
reason_category: str,
|
||||||
|
reason: Optional[str] = None,
|
||||||
|
) -> L1WalkSession:
|
||||||
|
"""Create an immediately-escalated session with no walked_path.
|
||||||
|
|
||||||
|
Used from the BuildAbortedNoKB screen (no KB content available to walk a
|
||||||
|
tree). Captures the call as an audit record + escalates the ticket without
|
||||||
|
requiring a walker session in between.
|
||||||
|
"""
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
session = L1WalkSession(
|
||||||
|
account_id=account_id,
|
||||||
|
created_by_user_id=user.id,
|
||||||
|
acting_as=_resolve_acting_as(user),
|
||||||
|
ticket_id=ticket_id,
|
||||||
|
ticket_kind=ticket_kind,
|
||||||
|
session_kind="adhoc",
|
||||||
|
status="escalated",
|
||||||
|
escalation_reason=reason,
|
||||||
|
escalation_reason_category=reason_category,
|
||||||
|
resolved_at=now,
|
||||||
|
last_step_at=now,
|
||||||
|
)
|
||||||
|
db.add(session)
|
||||||
|
if ticket_kind == "internal":
|
||||||
|
await internal_ticket_service.update_status(
|
||||||
|
db,
|
||||||
|
ticket_id=UUID(ticket_id),
|
||||||
|
status="escalated",
|
||||||
|
)
|
||||||
|
await db.flush()
|
||||||
|
return session
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Tests for l1_session_service start_* functions (T12) and record_step/update_notes (T13)."""
|
"""Tests for l1_session_service start_* functions (T12), record_step/update_notes (T13), resolve/escalate (T14)."""
|
||||||
import uuid
|
import uuid
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
@@ -15,7 +15,11 @@ from app.services.l1_session_service import (
|
|||||||
_resolve_acting_as,
|
_resolve_acting_as,
|
||||||
record_step,
|
record_step,
|
||||||
update_notes,
|
update_notes,
|
||||||
|
resolve,
|
||||||
|
escalate,
|
||||||
|
escalate_without_walk,
|
||||||
)
|
)
|
||||||
|
from app.services import internal_ticket_service
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -419,3 +423,353 @@ async def test_update_notes_size_cap(test_db: AsyncSession):
|
|||||||
notes = [{"timestamp": "2026-05-28T10:00:00Z", "content": big_content}]
|
notes = [{"timestamp": "2026-05-28T10:00:00Z", "content": big_content}]
|
||||||
with pytest.raises(ValueError, match="256KB"):
|
with pytest.raises(ValueError, match="256KB"):
|
||||||
await update_notes(test_db, session_id=session.id, notes=notes)
|
await update_notes(test_db, session_id=session.id, notes=notes)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# T14: resolve, escalate, escalate_without_walk tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _make_internal_ticket(db: AsyncSession, *, account_id: uuid.UUID, user_id: uuid.UUID) -> object:
|
||||||
|
"""Create an internal ticket and return it."""
|
||||||
|
return await internal_ticket_service.create_ticket(
|
||||||
|
db,
|
||||||
|
account_id=account_id,
|
||||||
|
created_by_user_id=user_id,
|
||||||
|
problem_statement="Customer cannot log in",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_proposal_helpful_flips_validated_by_outcome(test_db: AsyncSession):
|
||||||
|
"""resolve(helpful=True) on a proposal session sets validated_by_outcome=True."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
ai_session = await _make_ai_session(test_db, user_id=l1.id, account_id=account.id)
|
||||||
|
proposal = await _make_proposal(test_db, account_id=account.id, source_session_id=ai_session.id)
|
||||||
|
|
||||||
|
session = await start_proposal_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
flow_proposal_id=proposal.id,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
resolved = await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
helpful=True,
|
||||||
|
resolution_notes="Issue fixed by proposal walk",
|
||||||
|
)
|
||||||
|
assert resolved.status == "resolved"
|
||||||
|
assert resolved.helpful is True
|
||||||
|
assert resolved.resolution_notes == "Issue fixed by proposal walk"
|
||||||
|
assert resolved.resolved_at is not None
|
||||||
|
|
||||||
|
# Proposal should now be validated
|
||||||
|
await test_db.refresh(proposal)
|
||||||
|
assert proposal.validated_by_outcome is True
|
||||||
|
|
||||||
|
# Internal ticket should be closed
|
||||||
|
await test_db.refresh(ticket)
|
||||||
|
assert ticket.status == "resolved"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_proposal_not_helpful_leaves_validated_by_outcome_false(test_db: AsyncSession):
|
||||||
|
"""resolve(helpful=False) on a proposal session does NOT flip validated_by_outcome."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
ai_session = await _make_ai_session(test_db, user_id=l1.id, account_id=account.id)
|
||||||
|
proposal = await _make_proposal(test_db, account_id=account.id, source_session_id=ai_session.id)
|
||||||
|
|
||||||
|
session = await start_proposal_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
flow_proposal_id=proposal.id,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
resolved = await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
helpful=False,
|
||||||
|
resolution_notes="Proposal did not help",
|
||||||
|
)
|
||||||
|
assert resolved.helpful is False
|
||||||
|
await test_db.refresh(proposal)
|
||||||
|
assert proposal.validated_by_outcome is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_flow_session_closes_ticket_no_proposal_update(test_db: AsyncSession):
|
||||||
|
"""resolve on a flow session closes the ticket and does not touch proposals."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
|
||||||
|
|
||||||
|
session = await start_flow_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
flow_id=tree.id,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
resolved = await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
helpful=True,
|
||||||
|
resolution_notes="Flow resolved the issue",
|
||||||
|
)
|
||||||
|
assert resolved.status == "resolved"
|
||||||
|
assert resolved.session_kind == "flow"
|
||||||
|
assert resolved.flow_proposal_id is None
|
||||||
|
|
||||||
|
await test_db.refresh(ticket)
|
||||||
|
assert ticket.status == "resolved"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_adhoc_session_closes_ticket(test_db: AsyncSession):
|
||||||
|
"""resolve on an adhoc session closes the ticket with no proposal interaction."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
|
||||||
|
session = await start_adhoc_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
resolved = await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
helpful=True,
|
||||||
|
resolution_notes="Adhoc resolved",
|
||||||
|
)
|
||||||
|
assert resolved.status == "resolved"
|
||||||
|
await test_db.refresh(ticket)
|
||||||
|
assert ticket.status == "resolved"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_psa_session_no_ticket_update(test_db: AsyncSession):
|
||||||
|
"""resolve on a PSA-backed session does not attempt to update an internal ticket."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
|
||||||
|
|
||||||
|
session = await start_flow_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
flow_id=tree.id,
|
||||||
|
ticket_id="psa-external-id-123",
|
||||||
|
ticket_kind="psa",
|
||||||
|
)
|
||||||
|
resolved = await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
helpful=True,
|
||||||
|
resolution_notes="PSA ticket resolved externally",
|
||||||
|
)
|
||||||
|
assert resolved.status == "resolved"
|
||||||
|
assert resolved.ticket_kind == "psa"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_raises_on_missing_session(test_db: AsyncSession):
|
||||||
|
"""resolve raises ValueError when session does not exist."""
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=uuid.uuid4(),
|
||||||
|
helpful=True,
|
||||||
|
resolution_notes="N/A",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_raises_on_inactive_session(test_db: AsyncSession):
|
||||||
|
"""resolve raises ValueError when session is not active."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
|
||||||
|
session = await start_adhoc_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
session.status = "escalated"
|
||||||
|
await test_db.flush()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="not active"):
|
||||||
|
await resolve(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
helpful=False,
|
||||||
|
resolution_notes="N/A",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_marks_session_and_ticket_as_escalated(test_db: AsyncSession):
|
||||||
|
"""escalate sets session status=escalated and closes the internal ticket as escalated."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
tree = await _make_tree(test_db, account_id=account.id, author_id=l1.id)
|
||||||
|
|
||||||
|
session = await start_flow_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
flow_id=tree.id,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
escalated = await escalate(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
reason="Customer reported intermittent failure not covered by flow",
|
||||||
|
reason_category="out_of_scope",
|
||||||
|
)
|
||||||
|
assert escalated.status == "escalated"
|
||||||
|
assert escalated.escalation_reason == "Customer reported intermittent failure not covered by flow"
|
||||||
|
assert escalated.escalation_reason_category == "out_of_scope"
|
||||||
|
assert escalated.resolved_at is not None
|
||||||
|
assert escalated.last_step_at is not None
|
||||||
|
|
||||||
|
await test_db.refresh(ticket)
|
||||||
|
assert ticket.status == "escalated"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_raises_on_missing_session(test_db: AsyncSession):
|
||||||
|
"""escalate raises ValueError when session does not exist."""
|
||||||
|
with pytest.raises(ValueError, match="not found"):
|
||||||
|
await escalate(
|
||||||
|
test_db,
|
||||||
|
session_id=uuid.uuid4(),
|
||||||
|
reason="Some reason",
|
||||||
|
reason_category="unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_raises_on_inactive_session(test_db: AsyncSession):
|
||||||
|
"""escalate raises ValueError when session is already inactive."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
|
||||||
|
session = await start_adhoc_session(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
)
|
||||||
|
session.status = "resolved"
|
||||||
|
await test_db.flush()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="not active"):
|
||||||
|
await escalate(
|
||||||
|
test_db,
|
||||||
|
session_id=session.id,
|
||||||
|
reason="Too late",
|
||||||
|
reason_category="other",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_without_walk_creates_escalated_adhoc_session(test_db: AsyncSession):
|
||||||
|
"""escalate_without_walk creates an immediately-escalated session with empty walked_path."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
|
||||||
|
session = await escalate_without_walk(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
reason_category="no_kb_content",
|
||||||
|
reason="No KB article matched this issue",
|
||||||
|
)
|
||||||
|
assert session.status == "escalated"
|
||||||
|
assert session.session_kind == "adhoc"
|
||||||
|
assert session.walked_path == []
|
||||||
|
assert session.escalation_reason == "No KB article matched this issue"
|
||||||
|
assert session.escalation_reason_category == "no_kb_content"
|
||||||
|
assert session.resolved_at is not None
|
||||||
|
assert session.last_step_at is not None
|
||||||
|
assert session.account_id == account.id
|
||||||
|
assert session.created_by_user_id == l1.id
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_without_walk_escalates_internal_ticket(test_db: AsyncSession):
|
||||||
|
"""escalate_without_walk marks the internal ticket as escalated."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
|
||||||
|
await escalate_without_walk(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
reason_category="no_kb_content",
|
||||||
|
)
|
||||||
|
await test_db.refresh(ticket)
|
||||||
|
assert ticket.status == "escalated"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_without_walk_psa_does_not_touch_internal_ticket(test_db: AsyncSession):
|
||||||
|
"""escalate_without_walk with ticket_kind='psa' does not update internal tickets."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
|
||||||
|
session = await escalate_without_walk(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id="psa-ticket-999",
|
||||||
|
ticket_kind="psa",
|
||||||
|
reason_category="no_kb_content",
|
||||||
|
)
|
||||||
|
assert session.status == "escalated"
|
||||||
|
assert session.ticket_kind == "psa"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_escalate_without_walk_reason_is_optional(test_db: AsyncSession):
|
||||||
|
"""escalate_without_walk works without a reason string."""
|
||||||
|
account = await _make_account(test_db)
|
||||||
|
l1 = await _make_user(test_db, account_id=account.id)
|
||||||
|
ticket = await _make_internal_ticket(test_db, account_id=account.id, user_id=l1.id)
|
||||||
|
|
||||||
|
session = await escalate_without_walk(
|
||||||
|
test_db,
|
||||||
|
account_id=account.id,
|
||||||
|
user=l1,
|
||||||
|
ticket_id=str(ticket.id),
|
||||||
|
ticket_kind="internal",
|
||||||
|
reason_category="no_kb_content",
|
||||||
|
)
|
||||||
|
assert session.escalation_reason is None
|
||||||
|
assert session.escalation_reason_category == "no_kb_content"
|
||||||
|
|||||||
Reference in New Issue
Block a user