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>
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { SlaService } from '@/modules/orchestration/sla/service/sla.service';
|
|
import { SLARun } from '@prisma/client';
|
|
|
|
function run(overrides: Partial<SLARun>): SLARun {
|
|
return {
|
|
id: 'run-1',
|
|
ticketId: 'ticket-1',
|
|
policyId: 'policy-1',
|
|
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,
|
|
firstResponseBreachedAt: null,
|
|
completedAt: null,
|
|
...overrides,
|
|
} as SLARun;
|
|
}
|
|
|
|
describe('SlaService pause/resume', () => {
|
|
it('pause records pausedAt and flips status to paused', async () => {
|
|
const found = run({});
|
|
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(updateWithVersion).toHaveBeenCalledWith('run-1', 1, expect.objectContaining({ status: 'paused' }));
|
|
});
|
|
|
|
it('resume shifts both due dates forward by exactly the paused wall-clock duration', async () => {
|
|
const pausedAt = new Date(Date.now() - 30 * 60 * 1000); // paused 30 minutes ago
|
|
const paused = run({
|
|
status: 'paused',
|
|
pausedAt,
|
|
firstResponseDueAt: new Date('2026-01-05T12:00:00.000Z'),
|
|
resolutionDueAt: new Date('2026-01-05T17:00:00.000Z'),
|
|
});
|
|
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(updateWithVersion).toHaveBeenCalledTimes(1);
|
|
const [, , patch] = updateWithVersion.mock.calls[0] as [string, number, Record<string, unknown>];
|
|
expect(patch.status).toBe('running');
|
|
expect(patch.pausedAt).toBeNull();
|
|
|
|
const shiftedResolution = (patch.resolutionDueAt as Date).getTime();
|
|
const expectedShiftMin =
|
|
new Date('2026-01-05T17:00:00.000Z').getTime() + (before - pausedAt.getTime());
|
|
const expectedShiftMax =
|
|
new Date('2026-01-05T17:00:00.000Z').getTime() + (after - pausedAt.getTime());
|
|
expect(shiftedResolution).toBeGreaterThanOrEqual(expectedShiftMin);
|
|
expect(shiftedResolution).toBeLessThanOrEqual(expectedShiftMax);
|
|
});
|
|
|
|
it('a second pause/resume cycle composes correctly (never resets to the original due date)', async () => {
|
|
const afterFirstResume = run({
|
|
status: 'running',
|
|
pausedAt: null,
|
|
resolutionDueAt: new Date('2026-01-05T18:00:00.000Z'), // already shifted by +1h once
|
|
});
|
|
|
|
const secondPausedAt = new Date(Date.now() - 10 * 60 * 1000);
|
|
const pausedAgain = {
|
|
...afterFirstResume,
|
|
status: 'paused',
|
|
pausedAt: secondPausedAt,
|
|
} as SLARun;
|
|
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] = 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());
|
|
});
|
|
|
|
it('never resumes a run that is not currently paused', async () => {
|
|
const runningRun = run({ status: 'running', pausedAt: null });
|
|
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(updateWithVersion).not.toHaveBeenCalled();
|
|
});
|
|
});
|