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:
co-authored by
Claude Sonnet 5
parent
d65683641a
commit
7106753ed3
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user