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;
|
if (!isDuplicateRuleEscalationConflict(error)) throw error;
|
||||||
|
|
||||||
const existing = await this.prisma.escalationEvent.findFirst({
|
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.
|
if (!existing) throw error; // conflict raced with a delete — surface the original error.
|
||||||
return { event: existing, wasNewlyCreated: false };
|
return { event: existing, wasNewlyCreated: false };
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ function fakeRun(overrides: Partial<Record<string, unknown>> = {}) {
|
|||||||
id: 'run-1',
|
id: 'run-1',
|
||||||
ticketId: 'ticket-1',
|
ticketId: 'ticket-1',
|
||||||
status: 'running',
|
status: 'running',
|
||||||
|
version: 1,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,7 @@ describe('SLA compliance metric (014-full-observability data-model.md #6)', () =
|
|||||||
const incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
const incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
||||||
const runs = {
|
const runs = {
|
||||||
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'running' })),
|
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'running' })),
|
||||||
update: vi.fn().mockResolvedValue(undefined),
|
updateWithVersion: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
||||||
} as never;
|
} as never;
|
||||||
const service = new SlaService(undefined, runs);
|
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 incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
||||||
const runs = {
|
const runs = {
|
||||||
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'breached' })),
|
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'breached' })),
|
||||||
update: vi.fn().mockResolvedValue(undefined),
|
updateWithVersion: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
||||||
} as never;
|
} as never;
|
||||||
const service = new SlaService(undefined, runs);
|
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 incSpy = vi.spyOn(observability.slaRunOutcomesCounter, 'inc');
|
||||||
const runs = {
|
const runs = {
|
||||||
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
findByTicketId: vi.fn().mockResolvedValue(fakeRun({ status: 'completed' })),
|
||||||
update: vi.fn().mockResolvedValue(undefined),
|
updateWithVersion: vi.fn().mockResolvedValue(undefined),
|
||||||
} as never;
|
} as never;
|
||||||
const service = new SlaService(undefined, runs);
|
const service = new SlaService(undefined, runs);
|
||||||
|
|
||||||
await service.complete('ticket-1');
|
await service.complete('ticket-1');
|
||||||
|
|
||||||
expect(incSpy).not.toHaveBeenCalled();
|
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' }),
|
findApplicable: vi.fn().mockResolvedValue({ id: 'policy-1', productId: 'prod-1' }),
|
||||||
} as never;
|
} as never;
|
||||||
const rules = { findActiveRules: vi.fn().mockResolvedValue([rule]) } 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 = {
|
const assignmentEngine = {
|
||||||
assignToSpecificNode: vi.fn().mockResolvedValue(undefined),
|
assignToSpecificNode: vi.fn().mockResolvedValue(undefined),
|
||||||
} as never;
|
} as never;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ function run(overrides: Partial<SLARun>): SLARun {
|
|||||||
firstResponseDueAt: null,
|
firstResponseDueAt: null,
|
||||||
resolutionDueAt: null,
|
resolutionDueAt: null,
|
||||||
status: 'running',
|
status: 'running',
|
||||||
|
version: 1,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
resumedAt: null,
|
resumedAt: null,
|
||||||
breachedAt: null,
|
breachedAt: null,
|
||||||
@@ -22,11 +23,11 @@ function run(overrides: Partial<SLARun>): SLARun {
|
|||||||
describe('SlaService.runBreachDetectionSweep', () => {
|
describe('SlaService.runBreachDetectionSweep', () => {
|
||||||
it('marks every running run past its resolution due date as breached and fires escalation', async () => {
|
it('marks every running run past its resolution due date as breached and fires escalation', async () => {
|
||||||
const overdue = run({ id: 'r1', ticketId: 't1' });
|
const overdue = run({ id: 'r1', ticketId: 't1' });
|
||||||
const update = vi.fn().mockResolvedValue(overdue);
|
const updateWithVersion = vi.fn().mockResolvedValue(overdue);
|
||||||
const runsRepo = {
|
const runsRepo = {
|
||||||
findRunningPastResolutionDueAt: vi.fn().mockResolvedValue([overdue]),
|
findRunningPastResolutionDueAt: vi.fn().mockResolvedValue([overdue]),
|
||||||
findRunningPastFirstResponseDueAt: vi.fn().mockResolvedValue([]),
|
findRunningPastFirstResponseDueAt: vi.fn().mockResolvedValue([]),
|
||||||
update,
|
updateWithVersion,
|
||||||
} as never;
|
} as never;
|
||||||
const handleBreach = vi.fn().mockResolvedValue(undefined);
|
const handleBreach = vi.fn().mockResolvedValue(undefined);
|
||||||
const escalation = { handleBreach } as never;
|
const escalation = { handleBreach } as never;
|
||||||
@@ -34,7 +35,7 @@ describe('SlaService.runBreachDetectionSweep', () => {
|
|||||||
const service = new SlaService(undefined, runsRepo, undefined, undefined, escalation);
|
const service = new SlaService(undefined, runsRepo, undefined, undefined, escalation);
|
||||||
await service.runBreachDetectionSweep();
|
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');
|
expect(handleBreach).toHaveBeenCalledWith('t1', 'resolution_breach');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ describe('SlaService.runBreachDetectionSweep', () => {
|
|||||||
const runsRepo = {
|
const runsRepo = {
|
||||||
findRunningPastResolutionDueAt: vi.fn().mockResolvedValue([]),
|
findRunningPastResolutionDueAt: vi.fn().mockResolvedValue([]),
|
||||||
findRunningPastFirstResponseDueAt: vi.fn().mockResolvedValue([]),
|
findRunningPastFirstResponseDueAt: vi.fn().mockResolvedValue([]),
|
||||||
update: vi.fn(),
|
updateWithVersion: vi.fn(),
|
||||||
} as never;
|
} as never;
|
||||||
const handleBreach = vi.fn();
|
const handleBreach = vi.fn();
|
||||||
const service = new SlaService(undefined, runsRepo, undefined, undefined, {
|
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'),
|
firstResponseDueAt: new Date('2026-01-05T12:00:00.000Z'),
|
||||||
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
||||||
status: 'running',
|
status: 'running',
|
||||||
|
version: 1,
|
||||||
pausedAt: null,
|
pausedAt: null,
|
||||||
resumedAt: null,
|
resumedAt: null,
|
||||||
breachedAt: null,
|
breachedAt: null,
|
||||||
@@ -22,13 +23,13 @@ function run(overrides: Partial<SLARun>): SLARun {
|
|||||||
describe('SlaService pause/resume', () => {
|
describe('SlaService pause/resume', () => {
|
||||||
it('pause records pausedAt and flips status to paused', async () => {
|
it('pause records pausedAt and flips status to paused', async () => {
|
||||||
const found = run({});
|
const found = run({});
|
||||||
const update = vi.fn().mockResolvedValue(found);
|
const updateWithVersion = vi.fn().mockResolvedValue(found);
|
||||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(found), update } as never;
|
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(found), updateWithVersion } as never;
|
||||||
const service = new SlaService(undefined, runsRepo);
|
const service = new SlaService(undefined, runsRepo);
|
||||||
|
|
||||||
await service.pause('ticket-1');
|
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 () => {
|
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'),
|
firstResponseDueAt: new Date('2026-01-05T12:00:00.000Z'),
|
||||||
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
||||||
});
|
});
|
||||||
const update = vi.fn().mockResolvedValue(paused);
|
const updateWithVersion = vi.fn().mockResolvedValue(paused);
|
||||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(paused), update } as never;
|
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(paused), updateWithVersion } as never;
|
||||||
const service = new SlaService(undefined, runsRepo);
|
const service = new SlaService(undefined, runsRepo);
|
||||||
|
|
||||||
const before = Date.now();
|
const before = Date.now();
|
||||||
await service.resume('ticket-1');
|
await service.resume('ticket-1');
|
||||||
const after = Date.now();
|
const after = Date.now();
|
||||||
|
|
||||||
expect(update).toHaveBeenCalledTimes(1);
|
expect(updateWithVersion).toHaveBeenCalledTimes(1);
|
||||||
const [, patch] = update.mock.calls[0] as [string, Record<string, unknown>];
|
const [, , patch] = updateWithVersion.mock.calls[0] as [string, number, Record<string, unknown>];
|
||||||
expect(patch.status).toBe('running');
|
expect(patch.status).toBe('running');
|
||||||
expect(patch.pausedAt).toBeNull();
|
expect(patch.pausedAt).toBeNull();
|
||||||
|
|
||||||
@@ -74,13 +75,13 @@ describe('SlaService pause/resume', () => {
|
|||||||
status: 'paused',
|
status: 'paused',
|
||||||
pausedAt: secondPausedAt,
|
pausedAt: secondPausedAt,
|
||||||
} as SLARun;
|
} as SLARun;
|
||||||
const update = vi.fn().mockResolvedValue(pausedAgain);
|
const updateWithVersion = vi.fn().mockResolvedValue(pausedAgain);
|
||||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(pausedAgain), update } as never;
|
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(pausedAgain), updateWithVersion } as never;
|
||||||
const service = new SlaService(undefined, runsRepo);
|
const service = new SlaService(undefined, runsRepo);
|
||||||
|
|
||||||
await service.resume('ticket-1');
|
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();
|
const shifted = (patch.resolutionDueAt as Date).getTime();
|
||||||
// Must be shifted from the ALREADY-shifted 18:00 baseline, not the original 17:00 baseline.
|
// 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());
|
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 () => {
|
it('never resumes a run that is not currently paused', async () => {
|
||||||
const runningRun = run({ status: 'running', pausedAt: null });
|
const runningRun = run({ status: 'running', pausedAt: null });
|
||||||
const update = vi.fn();
|
const updateWithVersion = vi.fn();
|
||||||
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(runningRun), update } as never;
|
const runsRepo = { findByTicketId: vi.fn().mockResolvedValue(runningRun), updateWithVersion } as never;
|
||||||
const service = new SlaService(undefined, runsRepo);
|
const service = new SlaService(undefined, runsRepo);
|
||||||
|
|
||||||
await service.resume('ticket-1');
|
await service.resume('ticket-1');
|
||||||
expect(update).not.toHaveBeenCalled();
|
expect(updateWithVersion).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user