Files
support_backend/tests/unit/problem-management/auto-close-sweep.test.ts
T
saqib mirandClaude Sonnet 5 16daf8d32d feat: implement problem resolution (009)
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>
2026-09-03 15:09:29 +05:30

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);
});
});