/** * Phase 5b — escalate / de-escalate event recording tests. * * Follows the same injectable-parameter pattern as claimEvents.test.js: * _TicketModel — controls the DB layer (updateOne) * _recordAction — captures recording calls without any module mocking * * No vi.mock needed; all dependencies injected directly. * * Covers: * (a) escalate real — modifiedCount 1 → one 'escalate' event with new tier * (b) deescalate real — modifiedCount 1 → one 'deescalate' event with new tier * (c) no-op write — modifiedCount 0 → no event for either direction */ import { describe, it, expect, vi } from 'vitest'; import { runEscalation, runDeescalation } from '../handlers/commands/escalation.js'; // --------------------------------------------------------------------------- // Shared factories // --------------------------------------------------------------------------- function makeTicket(overrides = {}) { return { gmailThreadId: 'discord-test-001', escalationTier: 0, escalated: false, claimerId: 'claimer-001', claimedBy: 'ClaimerName', priority: 'normal', game: 'TestGame', senderEmail: 'user@example.com', creatorId: 'creator-001', ticketNumber: 42, welcomeMessageId: null, ...overrides }; } function makeInteraction(userId = 'staff-001') { return { user: { id: userId, username: 'staffuser', tag: 'staffuser#0001', toString: () => `<@${userId}>` }, member: { displayName: 'Staff Member' }, guild: { id: 'guild-001', members: { fetch: vi.fn().mockRejectedValue(new Error('no member in test env')) } }, channel: { id: 'chan-001', name: 'ticket-chan-001', isThread: vi.fn().mockReturnValue(true), send: vi.fn().mockResolvedValue({ id: 'sent-msg-001' }) }, editReply: vi.fn().mockResolvedValue(undefined), client: { channels: { fetch: vi.fn().mockResolvedValue(null) } } }; } // --------------------------------------------------------------------------- // (a) Real escalate — modifiedCount 1 // --------------------------------------------------------------------------- describe('runEscalation — real escalate emits one event', () => { it('emits exactly one "escalate" event with the correct staffId', async () => { const ticket = makeTicket({ escalationTier: 0 }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await runEscalation(interaction, ticket, 1, { updateOne: mockUpdateOne }, mockRecord); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type] = mockRecord.mock.calls[0]; expect(staffId).toBe('staff-001'); expect(type).toBe('escalate'); }); it('passes the ticket with the new tier', async () => { const ticket = makeTicket({ escalationTier: 0 }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await runEscalation(interaction, ticket, 1, { updateOne: mockUpdateOne }, mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.ticket.escalationTier).toBe(1); expect(payload.guildId).toBe('guild-001'); }); it('uses conditional filter escalationTier $ne nextTier', async () => { const ticket = makeTicket({ escalationTier: 0 }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const interaction = makeInteraction('staff-001'); await runEscalation(interaction, ticket, 1, { updateOne: mockUpdateOne }, vi.fn()); const [filter] = mockUpdateOne.mock.calls[0]; expect(filter).toMatchObject({ gmailThreadId: 'discord-test-001', escalationTier: { $ne: 1 } }); }); }); // --------------------------------------------------------------------------- // (b) Real deescalate — modifiedCount 1 // --------------------------------------------------------------------------- describe('runDeescalation — real deescalate emits one event', () => { it('emits exactly one "deescalate" event with the correct staffId', async () => { const ticket = makeTicket({ escalationTier: 1, escalated: true }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await runDeescalation(interaction, ticket, { updateOne: mockUpdateOne }, mockRecord); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type] = mockRecord.mock.calls[0]; expect(staffId).toBe('staff-001'); expect(type).toBe('deescalate'); }); it('passes the ticket with the new (lower) tier', async () => { const ticket = makeTicket({ escalationTier: 2, escalated: true }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await runDeescalation(interaction, ticket, { updateOne: mockUpdateOne }, mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.ticket.escalationTier).toBe(1); expect(payload.guildId).toBe('guild-001'); }); it('uses conditional filter escalationTier $ne newTier', async () => { const ticket = makeTicket({ escalationTier: 1, escalated: true }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const interaction = makeInteraction('staff-001'); await runDeescalation(interaction, ticket, { updateOne: mockUpdateOne }, vi.fn()); const [filter] = mockUpdateOne.mock.calls[0]; expect(filter).toMatchObject({ gmailThreadId: 'discord-test-001', escalationTier: { $ne: 0 } }); }); }); // --------------------------------------------------------------------------- // (c) No-op write — modifiedCount 0 → no event for either direction // --------------------------------------------------------------------------- describe('no-op tier write emits no event', () => { it('escalate: emits no event when modifiedCount is 0', async () => { const ticket = makeTicket({ escalationTier: 1 }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 0 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await runEscalation(interaction, ticket, 1, { updateOne: mockUpdateOne }, mockRecord); expect(mockRecord).not.toHaveBeenCalled(); }); it('deescalate: emits no event when modifiedCount is 0', async () => { const ticket = makeTicket({ escalationTier: 1, escalated: true }); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 0 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await runDeescalation(interaction, ticket, { updateOne: mockUpdateOne }, mockRecord); expect(mockRecord).not.toHaveBeenCalled(); }); });