import { describe, expect, it, vi } from "vitest" import type { Doc, Id } from "../convex/_generated/dataModel" import { getTicketsHistoryStatsHandler, listTicketsHistoryHandler } from "../convex/machines" const MACHINE_ID = "machine_1" as Id<"machines"> const TENANT_ID = "tenant-1" function buildMachine(overrides: Partial> = {}): Doc<"machines"> { const machine: Record = { _id: MACHINE_ID, tenantId: TENANT_ID, hostname: "desktop-01", macAddresses: [], serialNumbers: [], fingerprint: "fp", isActive: true, lastHeartbeatAt: Date.now(), createdAt: Date.now() - 10_000, updatedAt: Date.now() - 5_000, linkedUserIds: [], remoteAccess: null, } return { ...(machine as Doc<"machines">), ...overrides } } function buildTicket(overrides: Partial> = {}): Doc<"tickets"> { const base: Record = { _id: "ticket_base" as Id<"tickets">, tenantId: TENANT_ID, reference: 42600, subject: "Generic ticket", summary: "", status: "PENDING", priority: "MEDIUM", channel: "EMAIL", queueId: undefined, requesterId: "user_req" as Id<"users">, requesterSnapshot: { name: "Alice", email: "alice@example.com", avatarUrl: undefined, teams: [] }, assigneeId: "user_assignee" as Id<"users">, assigneeSnapshot: { name: "Bob", email: "bob@example.com", avatarUrl: undefined, teams: [] }, companyId: undefined, companySnapshot: undefined, machineId: MACHINE_ID, machineSnapshot: undefined, working: false, dueAt: undefined, firstResponseAt: undefined, resolvedAt: undefined, closedAt: undefined, updatedAt: Date.now(), createdAt: Date.now() - 2000, tags: [], customFields: [], totalWorkedMs: 0, internalWorkedMs: 0, externalWorkedMs: 0, activeSessionId: undefined, } return { ...(base as Doc<"tickets">), ...overrides } } type PaginateHandler = (options: { cursor: string | null; numItems: number }) => Promise<{ page: Doc<"tickets">[] isDone: boolean continueCursor: string }> function createCtx({ machine = buildMachine(), queues = new Map>(), paginate, }: { machine?: Doc<"machines"> queues?: Map> paginate: PaginateHandler }) { const createFilterBuilder = () => { const builder: Record typeof builder> = {} builder.eq = () => builder builder.gte = () => builder builder.lte = () => builder builder.or = () => builder return builder } return { db: { get: vi.fn(async (id: Id<"machines"> | Id<"queues">) => { if (machine && id === machine._id) return machine const queue = queues.get(String(id)) if (queue) return queue return null }), query: vi.fn(() => { const chain = { filter: vi.fn((cb?: (builder: ReturnType) => unknown) => { cb?.(createFilterBuilder()) return chain }), paginate: vi.fn((options: { cursor: string | null; numItems: number }) => paginate(options)), } return { withIndex: vi.fn((_indexName: string, cb?: (builder: ReturnType) => unknown) => { cb?.(createFilterBuilder()) return { order: vi.fn(() => chain), } }), } }), }, } as unknown as Parameters[0] } describe("convex.machines.listTicketsHistory", () => { it("maps tickets metadata and resolves queue names", async () => { const machine = buildMachine() const ticket = buildTicket({ _id: "ticket_1" as Id<"tickets">, subject: "Printer offline", priority: "HIGH", status: "PENDING", queueId: "queue_1" as Id<"queues">, updatedAt: 170000, createdAt: 160000, }) const paginate = vi.fn(async () => ({ page: [ticket], isDone: false, continueCursor: "cursor-next", })) const queues = new Map>([ ["queue_1", { _id: "queue_1" as Id<"queues">, name: "Atendimento", tenantId: TENANT_ID } as Doc<"queues">], ]) const ctx = createCtx({ machine, queues, paginate }) const result = await listTicketsHistoryHandler(ctx, { machineId: machine._id, paginationOpts: { numItems: 25, cursor: null }, }) expect(paginate).toHaveBeenCalledWith({ numItems: 25, cursor: null }) expect(result.page).toHaveLength(1) expect(result.page[0]).toMatchObject({ id: "ticket_1", subject: "Printer offline", priority: "HIGH", status: "PENDING", queue: "Atendimento", }) expect(result.continueCursor).toBe("cursor-next") }) it("applies search filtering over paginated results", async () => { const machine = buildMachine() const ticketMatches = buildTicket({ _id: "ticket_match" as Id<"tickets">, reference: 44321, subject: "Notebook com tela quebrada", requesterSnapshot: { name: "Carla", email: "carla@example.com", avatarUrl: undefined, teams: [] }, }) const ticketIgnored = buildTicket({ _id: "ticket_other" as Id<"tickets">, subject: "Troca de teclado", requesterSnapshot: { name: "Roberto", email: "roberto@example.com", avatarUrl: undefined, teams: [] }, }) const paginate = vi.fn(async () => ({ page: [ticketMatches, ticketIgnored], isDone: true, continueCursor: "", })) const ctx = createCtx({ machine, paginate }) const result = await listTicketsHistoryHandler(ctx, { machineId: machine._id, search: "notebook", paginationOpts: { numItems: 50, cursor: null }, }) expect(result.page).toHaveLength(1) expect(result.page[0].id).toBe("ticket_match") expect(result.isDone).toBe(true) }) }) describe("convex.machines.getTicketsHistoryStats", () => { it("aggregates totals across multiple pages respecting open status", async () => { const machine = buildMachine() const firstPageTicket = buildTicket({ _id: "ticket_open" as Id<"tickets">, status: "AWAITING_ATTENDANCE", }) const secondPageTicket = buildTicket({ _id: "ticket_resolved" as Id<"tickets">, status: "RESOLVED", }) const paginate = vi.fn(async ({ cursor }: { cursor: string | null }) => { if (!cursor) { return { page: [firstPageTicket], isDone: false, continueCursor: "cursor-1" } } return { page: [secondPageTicket], isDone: true, continueCursor: "" } }) const ctx = createCtx({ machine, paginate }) const stats = await getTicketsHistoryStatsHandler( ctx as unknown as Parameters[0], { machineId: machine._id, } ) expect(stats).toEqual({ total: 2, openCount: 1, resolvedCount: 1 }) expect(paginate).toHaveBeenCalledTimes(2) }) it("filters results by search term when aggregating", async () => { const machine = buildMachine() const matchingTicket = buildTicket({ _id: "ticket_search" as Id<"tickets">, subject: "Notebook com lentidão", }) const nonMatchingTicket = buildTicket({ _id: "ticket_ignored" as Id<"tickets">, subject: "Impressora parada", }) const paginate = vi.fn(async ({ cursor }: { cursor: string | null }) => { if (!cursor) { return { page: [matchingTicket, nonMatchingTicket], isDone: true, continueCursor: "" } } return { page: [], isDone: true, continueCursor: "" } }) const ctx = createCtx({ machine, paginate }) const stats = await getTicketsHistoryStatsHandler( ctx as unknown as Parameters[0], { machineId: machine._id, search: "notebook", } ) expect(stats).toEqual({ total: 1, openCount: 1, resolvedCount: 0 }) }) })