import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { buildApp } from '@/app'; import { prismaClient } from '@/infrastructure/database'; import { FastifyInstance } from 'fastify'; import { loginAs, authHeader } from '../helpers/auth'; import { encryptCredential, generateCredentialSecret, issueIntegrationToken, } from '@/modules/catalog/products'; /** Covers specs/007-orchestration-assignment/quickstart.md Scenario 2 (LEAST_LOADED, * SKILL_BASED, and the empty-eligible-set outcome) against a real Postgres/Redis. */ describe('Orchestration and assignment — strategies (User Story 2)', () => { let app: FastifyInstance; let authToken: string; let secret: string; let teamId: string; beforeAll(async () => { app = await buildApp(); authToken = await loginAs(app, 'ADMIN'); const team = await app.inject({ method: 'POST', url: '/admin/teams', headers: authHeader(authToken), payload: { name: `Strategy Team ${Date.now()}` }, }); teamId = team.json().data.id; }); afterAll(async () => { await app.close(); }); async function createProductAndEscalate(problem = 'Needs a human.') { const externalProductId = `TEST_STRAT_PROD_${Date.now()}_${Math.random().toString(36).slice(2)}`; const product = await prismaClient.product.create({ data: { externalProductId, name: 'Strategy Test Product', status: 'active' }, }); secret = generateCredentialSecret(); await prismaClient.productIntegration.create({ data: { productId: product.id, credentialRef: encryptCredential(secret), authMechanism: 'signed_token', allowedScope: { tenantIds: ['tenant-1'] }, status: 'active', rateLimitPerMinute: 1000, rateLimitPerUserPerMinute: 1000, }, }); const token = issueIntegrationToken(secret, { externalProductId, tenantId: 'tenant-1', userId: 'user-1', }); const created = await app.inject({ method: 'POST', url: '/v1/support/requests', headers: { authorization: `Bearer ${token}` }, payload: { productId: externalProductId, tenantId: 'tenant-1', userId: 'user-1', source: 'test', problem, }, }); return { externalProductId, productId: product.id, ticketId: created.json().data.ticketId }; } async function escalate(ticketId: string) { const ticket = await prismaClient.ticket.findUniqueOrThrow({ where: { id: ticketId } }); return app.inject({ method: 'PATCH', url: `/tickets/${ticketId}/status`, headers: authHeader(authToken), payload: { status: 'HUMAN_ESCALATION', expectedVersion: ticket.version }, }); } it('LEAST_LOADED picks the eligible agent with the lowest current workload', async () => { const skillTag = `least_loaded_skill_${Date.now()}`; const agentLow = await app.inject({ method: 'POST', url: `/admin/teams/${teamId}/agents`, headers: authHeader(authToken), payload: { name: 'Low Load Agent' }, }); const agentHigh = await app.inject({ method: 'POST', url: `/admin/teams/${teamId}/agents`, headers: authHeader(authToken), payload: { name: 'High Load Agent' }, }); for (const id of [agentLow.json().data.id, agentHigh.json().data.id]) { await app.inject({ method: 'PUT', url: `/admin/agents/${id}/skills/${skillTag}`, headers: authHeader(authToken), payload: { level: 1 }, }); } await app.inject({ method: 'PUT', url: `/admin/agents/${agentLow.json().data.id}/availability`, headers: authHeader(authToken), payload: { status: 'available', workingHours: {}, currentLoad: 1 }, }); await app.inject({ method: 'PUT', url: `/admin/agents/${agentHigh.json().data.id}/availability`, headers: authHeader(authToken), payload: { status: 'available', workingHours: {}, currentLoad: 9 }, }); const { externalProductId, ticketId } = await createProductAndEscalate(); await app.inject({ method: 'POST', url: '/admin/hierarchy-nodes', headers: authHeader(authToken), payload: { name: `LL Node ${Date.now()}`, order: 0, productScope: [externalProductId], skills: [skillTag], assignmentStrategy: 'LEAST_LOADED', }, }); await escalate(ticketId); const current = await app.inject({ method: 'GET', url: `/tickets/${ticketId}/assignment` }); expect(current.json().data.agentId).toBe(agentLow.json().data.id); }); it('SKILL_BASED prefers the eligible agent with the higher proficiency level', async () => { const skillTag = `skill_based_skill_${Date.now()}`; const agentExpert = await app.inject({ method: 'POST', url: `/admin/teams/${teamId}/agents`, headers: authHeader(authToken), payload: { name: 'Expert Agent' }, }); const agentNovice = await app.inject({ method: 'POST', url: `/admin/teams/${teamId}/agents`, headers: authHeader(authToken), payload: { name: 'Novice Agent' }, }); await app.inject({ method: 'PUT', url: `/admin/agents/${agentExpert.json().data.id}/skills/${skillTag}`, headers: authHeader(authToken), payload: { level: 9 }, }); await app.inject({ method: 'PUT', url: `/admin/agents/${agentNovice.json().data.id}/skills/${skillTag}`, headers: authHeader(authToken), payload: { level: 1 }, }); const { externalProductId, ticketId } = await createProductAndEscalate(); await app.inject({ method: 'POST', url: '/admin/hierarchy-nodes', headers: authHeader(authToken), payload: { name: `SB Node ${Date.now()}`, order: 0, productScope: [externalProductId], skills: [skillTag], assignmentStrategy: 'SKILL_BASED', }, }); await escalate(ticketId); const current = await app.inject({ method: 'GET', url: `/tickets/${ticketId}/assignment` }); expect(current.json().data.agentId).toBe(agentExpert.json().data.id); }); it('an empty eligible set assigns nobody, leaves the ticket escalated, and records the outcome', async () => { const skillTag = `nobody_has_this_skill_${Date.now()}`; const { externalProductId, ticketId } = await createProductAndEscalate(); await app.inject({ method: 'POST', url: '/admin/hierarchy-nodes', headers: authHeader(authToken), payload: { name: `Empty Node ${Date.now()}`, order: 0, productScope: [externalProductId], skills: [skillTag], assignmentStrategy: 'ROUND_ROBIN', }, }); await escalate(ticketId); const current = await app.inject({ method: 'GET', url: `/tickets/${ticketId}/assignment` }); expect(current.statusCode).toBe(404); const ticket = await prismaClient.ticket.findUniqueOrThrow({ where: { id: ticketId } }); expect(ticket.status).toBe('HUMAN_ESCALATION'); const history = await app.inject({ method: 'GET', url: `/tickets/${ticketId}/assignment-history`, }); const rows: { agentId: string | null; action: string }[] = history.json().data; expect(rows.some((r) => r.agentId === null && r.action === 'unassigned')).toBe(true); }); });