sistema-de-chamados/tests/automations-engine.test.ts
rever-tecnologia 0a36ed049f
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 7s
Quality Checks / Lint, Test and Build (push) Successful in 4m2s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m56s
feat(tickets): adiciona menu dedicado para Visitas na sidebar
- Adiciona item "Visitas" no submenu de Tickets com icone MapPin
- Cria pagina /tickets/visits que filtra apenas tickets da fila Visitas
- Corrige teste de automacao para usar emailProps ao inves de html

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 16:20:09 -03:00

305 lines
8.9 KiB
TypeScript

import { describe, expect, it, vi } from "bun:test"
import { runTicketAutomationsForEvent } from "../convex/automations"
import type { AutomationConditionGroup, TicketForAutomation } from "../convex/automationsEngine"
import { evaluateAutomationConditions } from "../convex/automationsEngine"
function buildTicket(overrides: Partial<TicketForAutomation> = {}): TicketForAutomation {
const base: TicketForAutomation = {
tenantId: "tenant-1",
status: "AWAITING_ATTENDANCE",
priority: "MEDIUM",
channel: "EMAIL",
queueId: "queue-1" as never,
companyId: "company-1" as never,
categoryId: "cat-1" as never,
subcategoryId: "subcat-1" as never,
tags: ["vip", "windows"],
formTemplate: "DEFAULT",
chatEnabled: true,
}
return { ...base, ...overrides }
}
describe("automationsEngine.evaluateAutomationConditions", () => {
it("retorna true quando não há condições", () => {
const ticket = buildTicket()
expect(evaluateAutomationConditions(ticket, null)).toBe(true)
expect(evaluateAutomationConditions(ticket, undefined)).toBe(true)
})
it("aplica AND: todas devem bater", () => {
const ticket = buildTicket({ priority: "HIGH" })
const group: AutomationConditionGroup = {
op: "AND",
conditions: [
{ field: "priority", op: "eq", value: "HIGH" },
{ field: "status", op: "eq", value: "AWAITING_ATTENDANCE" },
],
}
expect(evaluateAutomationConditions(ticket, group)).toBe(true)
})
it("aplica OR: basta uma bater", () => {
const ticket = buildTicket({ priority: "LOW" })
const group: AutomationConditionGroup = {
op: "OR",
conditions: [
{ field: "priority", op: "eq", value: "HIGH" },
{ field: "priority", op: "eq", value: "LOW" },
],
}
expect(evaluateAutomationConditions(ticket, group)).toBe(true)
})
it("suporta comparadores eq/neq/in/not_in para campos simples", () => {
const ticket = buildTicket()
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "companyId", op: "eq", value: "company-1" }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "queueId", op: "neq", value: "queue-2" }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "priority", op: "in", value: ["LOW", "MEDIUM"] }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "status", op: "not_in", value: ["RESOLVED", "PAUSED"] }],
})
).toBe(true)
})
it("suporta chatEnabled (is_true/is_false/eq/neq)", () => {
const ticket = buildTicket({ chatEnabled: true })
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "chatEnabled", op: "is_true" }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "chatEnabled", op: "is_false" }],
})
).toBe(false)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "chatEnabled", op: "eq", value: true }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "chatEnabled", op: "neq", value: false }],
})
).toBe(true)
})
it("suporta tag (contains/not_contains/eq/neq)", () => {
const ticket = buildTicket({ tags: ["vip", "linux"] })
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "tag", op: "contains", value: "vip" }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "tag", op: "not_contains", value: "windows" }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "tag", op: "eq", value: "linux" }],
})
).toBe(true)
expect(
evaluateAutomationConditions(ticket, {
op: "AND",
conditions: [{ field: "tag", op: "neq", value: "vip" }],
})
).toBe(false)
})
})
describe("automations.runTicketAutomationsForEvent", () => {
it("agenda automação DELAYED via scheduler.runAfter", async () => {
const runAfter = vi.fn(async () => {})
const ticketId = "ticket_1" as never
const automationId = "auto_1" as never
const ticket = buildTicket({ tenantId: "tenant-1" }) as unknown as { _id: unknown; tenantId: string }
const automations = [
{
_id: automationId,
tenantId: "tenant-1",
name: "Auto",
enabled: true,
trigger: "TICKET_CREATED",
timing: "DELAYED",
delayMs: 60_000,
conditions: null,
actions: [{ type: "SET_CHAT_ENABLED", enabled: true }],
createdBy: "user_1",
createdAt: 0,
updatedAt: 0,
},
]
const take = vi.fn(async () => automations)
const filter = vi.fn(() => ({ take }))
const withIndex = vi.fn(() => ({ filter, take }))
const query = vi.fn(() => ({ withIndex }))
const get = vi.fn(async (id: unknown) => {
if (id === ticketId) return { ...ticket, _id: ticketId, tenantId: "tenant-1" }
return null
})
await runTicketAutomationsForEvent(
{
db: { get, query } as unknown,
scheduler: { runAfter } as unknown,
} as never,
{ tenantId: "tenant-1", ticketId, eventType: "TICKET_CREATED" }
)
expect(runAfter).toHaveBeenCalledTimes(1)
expect(runAfter).toHaveBeenCalledWith(
60_000,
expect.anything(),
expect.objectContaining({ automationId, ticketId, eventType: "TICKET_CREATED" })
)
})
it("agenda envio de e-mail quando ação SEND_EMAIL é aplicada", async () => {
const runAfter = vi.fn(async () => {})
const ticketId = "ticket_1" as never
const automationId = "auto_1" as never
const ticket = {
_id: ticketId,
tenantId: "tenant-1",
reference: 123,
subject: "Teste de automação",
status: "PENDING",
priority: "MEDIUM",
channel: "EMAIL",
requesterId: "user_req_1" as never,
requesterSnapshot: { name: "Renan", email: "cliente@empresa.com" },
createdAt: 0,
updatedAt: 0,
}
const automations = [
{
_id: automationId,
tenantId: "tenant-1",
name: "Auto e-mail",
enabled: true,
trigger: "TICKET_CREATED",
timing: "IMMEDIATE",
delayMs: undefined,
conditions: null,
actions: [
{
type: "SEND_EMAIL",
subject: "Atualização do chamado #{{ticket.reference}}",
message: "Olá {{requester.name}}, recebemos seu chamado: {{ticket.subject}}",
ctaTarget: "PORTAL",
ctaLabel: "Abrir chamado",
recipients: [{ type: "REQUESTER" }],
},
],
createdBy: "user_1",
createdAt: 0,
updatedAt: 0,
},
]
const takeAutomations = vi.fn(async () => automations)
const filterAutomations = vi.fn(() => ({ take: takeAutomations }))
const withIndexAutomations = vi.fn(() => ({ filter: filterAutomations, take: takeAutomations }))
const takeRuns = vi.fn(async () => [])
const orderRuns = vi.fn(() => ({ take: takeRuns }))
const withIndexRuns = vi.fn(() => ({ order: orderRuns, take: takeRuns }))
const query = vi.fn((table: string) => {
if (table === "ticketAutomations") {
return { withIndex: withIndexAutomations }
}
if (table === "ticketAutomationRuns") {
return { withIndex: withIndexRuns }
}
throw new Error(`unexpected table: ${table}`)
})
const get = vi.fn(async (id: unknown) => {
if (id === ticketId) return ticket
return null
})
const insert = vi.fn(async () => "run_1" as never)
const patch = vi.fn(async () => {})
const del = vi.fn(async () => {})
await runTicketAutomationsForEvent(
{
db: { get, query, insert, patch, delete: del } as unknown,
scheduler: { runAfter } as unknown,
} as never,
{ tenantId: "tenant-1", ticketId, eventType: "TICKET_CREATED" }
)
expect(runAfter).toHaveBeenCalledTimes(1)
expect(runAfter).toHaveBeenCalledWith(
1,
expect.anything(),
expect.objectContaining({
to: ["cliente@empresa.com"],
subject: "Atualização do chamado #123",
emailProps: expect.objectContaining({
title: "Atualização do chamado #123",
message: "Olá Renan, recebemos seu chamado: Teste de automação",
ticket: expect.objectContaining({
reference: 123,
subject: "Teste de automação",
status: "PENDING",
priority: "MEDIUM",
requesterName: "Renan",
}),
ctaLabel: "Abrir chamado",
}),
})
)
})
})