fix(015-reporting-dashboards): count humanEscalated by Assignment existence, not status

ManagementRepository/ProductReportRepository.countEverEscalatedToHuman checked
a list of terminal statuses that ticket-state-machine.ts's own transition
table shows are reachable from BOTH the AI-resolved path and the
human-escalation path once they converge (RESOLUTION_PENDING_CUSTOMER,
RESOLVED, CLOSED, REOPENED). Every AI-resolved ticket was being double-counted
as human-escalated too — confirmed live against real seeded dev data
(humanEscalated: 34 out of totalCases: 34, an impossible 100%).

Fixed by keying off assignments: { some: {} } instead, since
orchestrationService.handleHumanEscalation is the only code path that ever
creates an Assignment row. Updated management-dashboard.test.ts's own
human-resolved fixture to create a real Assignment row, since it previously
relied on the now-fixed buggy status-based signal without one.

Found via manual verification against a real running dev server while
building supporthub-web's 002-reporting-dashboards-ui, not by any existing
automated test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
saqib mir
2026-09-09 16:31:13 +05:30
co-authored by Claude Sonnet 5
parent d65683641a
commit 7106753ed3
3 changed files with 30 additions and 27 deletions
@@ -16,23 +16,26 @@ export class ManagementRepository {
});
}
/** Ever reached HUMAN_ESCALATION — the state machine (003-ticketing) makes this a one-way
* gate, so a ticket currently past it (IN_PROGRESS, WAITING_FOR_CUSTOMER, etc.) still counts. */
/**
* Ever escalated to a human. NOT a current-status check: 003-ticketing's own state machine
* lets both the AI path (AI_RESOLVED) and the human path (HUMAN_ESCALATION) converge on the
* same shared terminal statuses (RESOLUTION_PENDING_CUSTOMER, RESOLVED, CLOSED, REOPENED are
* all reachable from AI_RESOLVED directly, per ticket-state-machine.ts's own transition
* table) — a status-list check over those shared statuses would count every AI-resolved
* ticket as "human escalated" too (caught via manual verification against real seeded data,
* not by any test fixture, since every existing test's fixtures happened to keep the two
* paths' terminal statuses apart).
*
* The unambiguous, direct signal instead: 007-orchestration-assignment's own
* `orchestrationService.handleHumanEscalation` is the *only* code path that ever creates an
* `Assignment` row (research.md's own module map) — a ticket has one if and only if it was
* actually escalated to a human at some point, regardless of its current status.
*/
async countEverEscalatedToHuman(range: DateRange): Promise<number> {
return this.prisma.ticket.count({
where: {
createdAt: { gte: range.from, lte: range.to },
status: {
in: [
'HUMAN_ESCALATION',
'IN_PROGRESS',
'WAITING_FOR_CUSTOMER',
'RESOLUTION_PENDING_CUSTOMER',
'RESOLVED',
'CLOSED',
'REOPENED',
],
},
assignments: { some: {} },
},
});
}
@@ -40,24 +40,15 @@ export class ProductReportRepository {
});
}
/** Same "ever reached HUMAN_ESCALATION" one-way-gate logic as
* ManagementRepository.countEverEscalatedToHuman, scoped to one product. */
/** Same fixed "has at least one Assignment row" signal as
* ManagementRepository.countEverEscalatedToHuman (see its own comment for why a current-status
* check is wrong), scoped to one product. */
async countEverEscalatedToHuman(productId: string, range: DateRange): Promise<number> {
return this.prisma.ticket.count({
where: {
productId,
createdAt: { gte: range.from, lte: range.to },
status: {
in: [
'HUMAN_ESCALATION',
'IN_PROGRESS',
'WAITING_FOR_CUSTOMER',
'RESOLUTION_PENDING_CUSTOMER',
'RESOLVED',
'CLOSED',
'REOPENED',
],
},
assignments: { some: {} },
},
});
}
@@ -98,8 +98,17 @@ describe('Management dashboard (User Story 1)', () => {
await resolutionRepository.create({ ticketId: aiTicketId, outcome: 'fixed', resolvedBy: 'ai' });
await driveDirectly(aiTicketId, ['RESOLVED']);
// Human-resolved ticket, with a first agent response recorded.
// Human-resolved ticket, with a first agent response recorded and a real Assignment row —
// "ever escalated to a human" is keyed off Assignment existence (see
// ManagementRepository.countEverEscalatedToHuman's own comment on why a ticket's current
// status can't distinguish the AI path from the human path once both converge on the same
// shared terminal statuses).
const team = await prismaClient.team.create({ data: { name: `Mgmt Report Team ${Date.now()}` } });
const agent = await prismaClient.agent.create({ data: { teamId: team.id, name: 'Mgmt Report Agent' } });
const humanTicketId = await createTicket();
await prismaClient.assignment.create({
data: { ticketId: humanTicketId, agentId: agent.id, strategy: 'MANUAL', isCurrent: true },
});
await messagesService.post(humanTicketId, 'agent-1', 'AGENT_MESSAGE', 'Looking into this.');
await driveDirectly(humanTicketId, [
'HUMAN_ESCALATION',