fix(016-load-concurrency-testing): US3 — finish escalation idempotency plumbing
Completes the escalation-event repository/service changes from the prior
commit: normalizes the exactOptionalPropertyTypes mismatch in the
findFirst fallback lookup, and updates every existing unit test
(sla-pause-resume, sla-breach-detection, sla-compliance-metric,
escalation-rule-match) to the new SlaRunRepository.updateWithVersion and
EscalationEventRepository.create({event, wasNewlyCreated}) signatures.
Full typecheck/lint/architecture-check clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
794e16f349
commit
93d6fe8b94
@@ -50,7 +50,10 @@ export class EscalationEventRepository {
|
||||
if (!isDuplicateRuleEscalationConflict(error)) throw error;
|
||||
|
||||
const existing = await this.prisma.escalationEvent.findFirst({
|
||||
where: { ticketId: data.ticketId, ruleId: data.ruleId },
|
||||
where: {
|
||||
ticketId: data.ticketId,
|
||||
...(data.ruleId !== undefined ? { ruleId: data.ruleId } : {}),
|
||||
},
|
||||
});
|
||||
if (!existing) throw error; // conflict raced with a delete — surface the original error.
|
||||
return { event: existing, wasNewlyCreated: false };
|
||||
|
||||
@@ -7,6 +7,7 @@ function fakeRun(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
id: 'run-1',
|
||||
ticketId: 'ticket-1',
|
||||
status: 'running',
|
||||
version: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -20,7 +21,7 @@ describe('SLA compliance metric (014-full-observability data-model.md #6)', () =
|
||||
const incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
||||
const runs = {
|
||||
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'running' })),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
updateWithVersion: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
||||
} as never;
|
||||
const service = new SlaService(undefined, runs);
|
||||
|
||||
@@ -33,7 +34,7 @@ describe('SLA compliance metric (014-full-observability data-model.md #6)', () =
|
||||
const incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
||||
const runs = {
|
||||
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'breached' })),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
updateWithVersion: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
||||
} as never;
|
||||
const service = new SlaService(undefined, runs);
|
||||
|
||||
@@ -46,13 +47,13 @@ describe('SLA compliance metric (014-full-observability data-model.md #6)', () =
|
||||
const incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
||||
const runs = {
|
||||
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
||||
update: vi.fn().mockResolvedValue(undefined),
|
||||
updateWithVersion: vi.fn().mockResolvedValue(undefined),
|
||||
} as never;
|
||||
const service = new SlaService(undefined, runs);
|
||||
|
||||
await service.complete('ticket-1');
|
||||
|
||||
expect(incSpy).not.toHaveBeenCalled();
|
||||
expect((runs as unknown as { update: ReturnType<typeof vi.fn> }).update).not.toHaveBeenCalled();
|
||||
expect((runs as unknown as { updateWithVersion: ReturnType<typeof vi.fn> }).updateWithVersion).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,9 @@ describe('EscalationService.handleBreach', () => {
|
||||
findApplicable: vi.fn().mockResolvedValue({ id: 'policy-1', productId: 'prod-1' }),
|
||||
} as never;
|
||||
const rules = { findActiveRules: vi.fn().mockResolvedValue([rule]) } as never;
|
||||
const events = { create: vi.fn().mockResolvedValue({ id: 'event-1' }) } as never;
|
||||
const events = {
|
||||
create: vi.fn().mockResolvedValue({ event: { id: 'event-1' }, wasNewlyCreated: true }),
|
||||
} as never;
|
||||
const assignmentEngine = {
|
||||
assignToSpecificNode: vi.fn().mockResolvedValue(undefined),
|
||||
} as never;
|
||||
|
||||
@@ -10,6 +10,7 @@ function run(overrides: Partial<SLARun>): SLARun {
|
||||
firstResponseDueAt: null,
|
||||
resolutionDueAt: null,
|
||||
status: 'running',
|
||||
version: 1,
|
||||
pausedAt: null,
|
||||
resumedAt: null,
|
||||
breachedAt: null,
|
||||
@@ -22,11 +23,11 @@ function run(overrides: Partial<SLARun>): SLARun {
|
||||
describe('SlaService.runBreachDetectionSweep', () => {
|
||||
it('marks every running run past its resolution due date as breached and fires escalation', async () => {
|
||||
const overdue = run({ id: 'r1', ticketId: 't1' });
|
||||
const update = vi.fn().mockResolvedValue(overdue);
|
||||
const updateWithVersion = vi.fn().mockResolvedValue(overdue);
|
||||
const runsRepo = {
|
||||
findRunningPastResolutionDueAt: vi.fn().mockResolvedValue([overdue]),
|
||||
findRunningPastFirstResponseDueAt: vi.fn().mockResolvedValue([]),
|
||||
update,
|
||||
updateWithVersion,
|
||||
} as never;
|
||||
const handleBreach = vi.fn().mockResolvedValue(undefined);
|
||||
const escalation = { handleBreach } as never;
|
||||
@@ -34,7 +35,7 @@ describe('SlaService.runBreachDetectionSweep', () => {
|
||||
const service = new SlaService(undefined, runsRepo, undefined, undefined, escalation);
|
||||
await service.runBreachDetectionSweep();
|
||||
|
||||
expect(update).toHaveBeenCalledWith('r1', expect.objectContaining({ status: 'breached' }));
|
||||
expect(updateWithVersion).toHaveBeenCalledWith('r1', 1, expect.objectContaining({ status: 'breached' }));
|
||||
expect(handleBreach).toHaveBeenCalledWith('t1', 'resolution_breach');
|
||||
});
|
||||
|
||||
@@ -42,7 +43,7 @@ describe('SlaService.runBreachDetectionSweep', () => {
|
||||
const runsRepo = {
|
||||
findRunningPastResolutionDueAt: vi.fn().mockResolvedValue([]),
|
||||
findRunningPastFirstResponseDueAt: vi.fn().mockResolvedValue([]),
|
||||
update: vi.fn(),
|
||||
updateWithVersion: vi.fn(),
|
||||
} as never;
|
||||
const handleBreach = vi.fn();
|
||||
const service = new SlaService(undefined, runsRepo, undefined, undefined, {
|
||||
|
||||
@@ -10,6 +10,7 @@ function run(overrides: Partial<SLARun>): SLARun {
|
||||
firstResponseDueAt: new Date('2026-01-05T12:00:00.000Z'),
|
||||
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
||||
status: 'running',
|
||||
version: 1,
|
||||
pausedAt: null,
|
||||
resumedAt: null,
|
||||
breachedAt: null,
|
||||
@@ -22,13 +23,13 @@ function run(overrides: Partial<SLARun>): SLARun {
|
||||
describe('SlaService pause/resume', () => {
|
||||
it('pause records pausedAt and flips status to paused', async () => {
|
||||
const found = run({});
|
||||
const update = vi.fn().mockResolvedValue(found);
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(found), update } as never;
|
||||
const updateWithVersion = vi.fn().mockResolvedValue(found);
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(found), updateWithVersion } as never;
|
||||
const service = new SlaService(undefined, runsRepo);
|
||||
|
||||
await service.pause('ticket-1');
|
||||
|
||||
expect(update).toHaveBeenCalledWith('run-1', expect.objectContaining({ status: 'paused' }));
|
||||
expect(updateWithVersion).toHaveBeenCalledWith('run-1', 1, expect.objectContaining({ status: 'paused' }));
|
||||
});
|
||||
|
||||
it('resume shifts both due dates forward by exactly the paused wall-clock duration', async () => {
|
||||
@@ -39,16 +40,16 @@ describe('SlaService pause/resume', () => {
|
||||
firstResponseDueAt: new Date('2026-01-05T12:00:00.000Z'),
|
||||
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
||||
});
|
||||
const update = vi.fn().mockResolvedValue(paused);
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(paused), update } as never;
|
||||
const updateWithVersion = vi.fn().mockResolvedValue(paused);
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(paused), updateWithVersion } as never;
|
||||
const service = new SlaService(undefined, runsRepo);
|
||||
|
||||
const before = Date.now();
|
||||
await service.resume('ticket-1');
|
||||
const after = Date.now();
|
||||
|
||||
expect(update).toHaveBeenCalledTimes(1);
|
||||
const [, patch] = update.mock.calls[0] as [string, Record<string, unknown>];
|
||||
expect(updateWithVersion).toHaveBeenCalledTimes(1);
|
||||
const [, , patch] = updateWithVersion.mock.calls[0] as [string, number, Record<string, unknown>];
|
||||
expect(patch.status).toBe('running');
|
||||
expect(patch.pausedAt).toBeNull();
|
||||
|
||||
@@ -74,13 +75,13 @@ describe('SlaService pause/resume', () => {
|
||||
status: 'paused',
|
||||
pausedAt: secondPausedAt,
|
||||
} as SLARun;
|
||||
const update = vi.fn().mockResolvedValue(pausedAgain);
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(pausedAgain), update } as never;
|
||||
const updateWithVersion = vi.fn().mockResolvedValue(pausedAgain);
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(pausedAgain), updateWithVersion } as never;
|
||||
const service = new SlaService(undefined, runsRepo);
|
||||
|
||||
await service.resume('ticket-1');
|
||||
|
||||
const [, patch] = update.mock.calls[0] as [string, Record<string, unknown>];
|
||||
const [, , patch] = updateWithVersion.mock.calls[0] as [string, number, Record<string, unknown>];
|
||||
const shifted = (patch.resolutionDueAt as Date).getTime();
|
||||
// Must be shifted from the ALREADY-shifted 18:00 baseline, not the original 17:00 baseline.
|
||||
expect(shifted).toBeGreaterThan(new Date('2026-01-05T18:00:00.000Z').getTime());
|
||||
@@ -88,11 +89,11 @@ describe('SlaService pause/resume', () => {
|
||||
|
||||
it('never resumes a run that is not currently paused', async () => {
|
||||
const runningRun = run({ status: 'running', pausedAt: null });
|
||||
const update = vi.fn();
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(runningRun), update } as never;
|
||||
const updateWithVersion = vi.fn();
|
||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(runningRun), updateWithVersion } as never;
|
||||
const service = new SlaService(undefined, runsRepo);
|
||||
|
||||
await service.resume('ticket-1');
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
expect(updateWithVersion).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user