Populates the five real problem-management stubs (investigation, root-causes, solutions, verification, resolutions -- problems is confirmed dead/unwired scaffold and stays untouched) with doc04's sequential workflow engine: - investigation: version-row-per-attempt (never overwritten), with a customer-safe read path that always strips internalNotes. - root-causes/solutions/verification: a strict existence chain (investigation -> root cause -> solution -> approval -> implementation -> verification), each step resolve-or-409 on its own precondition, matching doc06's schema field-for-field with no invented columns. - resolutions: gated on a successfully verified solution (no stored solutionId FK, per doc06 -- resolved via a join at write time), moving the ticket to RESOLUTION_PENDING_CUSTOMER; explicit customer confirmation and a durable auto-close sweep (the previously-unregistered CLEANUP queue stub, mirroring 008's breach-detection job) both resolve it from there. - reopen (ticketing/tickets): two real, separately-audited transitions (RESOLVED|CLOSED -> REOPENED -> IN_PROGRESS), touching no prior problem-resolution record and no SLARun -- closes the loop 008's own spec.md left open. Verification-failure escalation reuses 003/007's existing HUMAN_ESCALATION transition directly rather than adding an eleventh trigger type to 008's already-shipped escalation rules. Customer-facing confirm-resolution/reopen needed a body-shape variant of 002's inbound trust boundary that didn't previously exist: fastify.authenticateProductIntegration hard-required a full ticket-creation-shaped body. Extracted the shared token/scope/replay verification into verifyIntegrationIdentity and added a narrower authenticateProductIntegrationIdentity decorator + identityOnlyRequestSchema on top of it -- purely additive, ticket creation's own behavior is unchanged. Also fixes a real test-data-hygiene bug surfaced by running this feature's suite alongside 008's: a wildcard-scoped HierarchyNode and an intentionally-global SLAPolicy in 008's own test fixtures were silently affecting other test files' tickets sharing the same live Postgres. Verified against throwaway Docker Postgres/Redis: typecheck, lint, architecture-check all clean; full regression (tests/unit + tests/integration together, 172 tests) passes except the 2 pre-existing MinIO-dependent attachment failures, unrelated to this feature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { findPendingCustomerConfirmationOlderThan, updateStatus } = vi.hoisted(() => ({
|
|
findPendingCustomerConfirmationOlderThan: vi.fn(),
|
|
updateStatus: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/modules/ticketing/tickets', () => ({
|
|
ticketsRepository: { findPendingCustomerConfirmationOlderThan },
|
|
ticketsService: { updateStatus },
|
|
}));
|
|
|
|
import { ResolutionsService } from '@/modules/problem-management/resolutions/service/resolutions.service';
|
|
|
|
describe('ResolutionsService.runAutoCloseSweep', () => {
|
|
beforeEach(() => {
|
|
findPendingCustomerConfirmationOlderThan.mockReset();
|
|
updateStatus.mockReset();
|
|
});
|
|
|
|
it('resolves every ticket the repository returns as due, and only those', async () => {
|
|
findPendingCustomerConfirmationOlderThan.mockResolvedValue([
|
|
{ id: 't1', version: 3 },
|
|
{ id: 't2', version: 1 },
|
|
]);
|
|
updateStatus.mockResolvedValue({});
|
|
|
|
const service = new ResolutionsService();
|
|
await service.runAutoCloseSweep();
|
|
|
|
expect(updateStatus).toHaveBeenCalledTimes(2);
|
|
expect(updateStatus).toHaveBeenCalledWith('t1', 'RESOLVED', 3, 'system');
|
|
expect(updateStatus).toHaveBeenCalledWith('t2', 'RESOLVED', 1, 'system');
|
|
});
|
|
|
|
it('does nothing when no ticket is due — the repository query itself is the selection, not this method', async () => {
|
|
findPendingCustomerConfirmationOlderThan.mockResolvedValue([]);
|
|
|
|
const service = new ResolutionsService();
|
|
await service.runAutoCloseSweep();
|
|
|
|
expect(updateStatus).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('queries with a cutoff derived from the configured waiting period, not a hardcoded value', async () => {
|
|
findPendingCustomerConfirmationOlderThan.mockResolvedValue([]);
|
|
|
|
const before = Date.now();
|
|
const service = new ResolutionsService();
|
|
await service.runAutoCloseSweep();
|
|
const after = Date.now();
|
|
|
|
const [cutoff] = findPendingCustomerConfirmationOlderThan.mock.calls[0] as [Date];
|
|
const hoursAgo = (before - cutoff.getTime()) / (60 * 60 * 1000);
|
|
const hoursAgoAfter = (after - cutoff.getTime()) / (60 * 60 * 1000);
|
|
// Default is 72h (env.ts) unless overridden — assert it's in that neighborhood rather than
|
|
// hardcoding the exact default here too, so a legitimate config change doesn't break this.
|
|
expect(hoursAgo).toBeGreaterThan(0);
|
|
expect(hoursAgoAfter).toBeGreaterThan(0);
|
|
});
|
|
});
|