/** * Phase 7 — gmail-poll email ticket persistence + reopen event recording. * * Uses the injectable-parameter pattern: persistEmailTicket accepts * _Ticket (model) and _recordAction as the 4th and 5th parameters. * No vi.mock needed; dependencies are injected directly. * * Covers: * (a) game persisted: findOneAndUpdate $set includes game from detectGame * (b) reopen event: staffId='system', resolverId = prior claimerId, guildId correct * (c) payload.ticket: the returned doc is passed verbatim for denormalization * (d) no reopen: wasReopened=false → _recordAction not called * (e) null claimerId: resolverId=null for unclaimed-ticket reopen */ import { describe, it, expect, vi } from 'vitest'; import { persistEmailTicket } from '../gmail-poll.js'; // --------------------------------------------------------------------------- // Shared factories // --------------------------------------------------------------------------- function makeFields(overrides = {}) { return { threadId: 'gmail-thread-001', discordThreadId: 'chan-001', senderEmail: 'user@example.com', subject: 'Help needed', createdAt: new Date('2026-01-01'), ticketNumber: 1, priority: 'normal', parentCategoryId: 'cat-001', game: 'TestGame', ...overrides }; } function makeReturnedDoc(overrides = {}) { return { gmailThreadId: 'gmail-thread-001', senderEmail: 'user@example.com', claimerId: 'claimer-001', escalationTier: 0, priority: 'normal', game: 'TestGame', ...overrides }; } function makeTicketModel(returnedDoc) { return { findOneAndUpdate: vi.fn().mockResolvedValue(returnedDoc) }; } // --------------------------------------------------------------------------- // (a) game included in the $set // --------------------------------------------------------------------------- describe('persistEmailTicket — game persisted in $set', () => { it('includes game in the findOneAndUpdate $set', async () => { const model = makeTicketModel(makeReturnedDoc()); await persistEmailTicket(makeFields({ game: 'Minecraft' }), 'guild-001', false, model, vi.fn()); const [, update] = model.findOneAndUpdate.mock.calls[0]; expect(update.$set.game).toBe('Minecraft'); }); it('passes null game when no game is detected', async () => { const model = makeTicketModel(makeReturnedDoc({ game: null })); await persistEmailTicket(makeFields({ game: null }), 'guild-001', false, model, vi.fn()); const [, update] = model.findOneAndUpdate.mock.calls[0]; expect(update.$set.game).toBeNull(); }); it('uses gmailThreadId as the findOneAndUpdate filter', async () => { const model = makeTicketModel(makeReturnedDoc()); await persistEmailTicket(makeFields({ threadId: 'thread-xyz' }), 'guild-001', false, model, vi.fn()); const [filter] = model.findOneAndUpdate.mock.calls[0]; expect(filter.gmailThreadId).toBe('thread-xyz'); }); it('sets status:"open" in the $set', async () => { const model = makeTicketModel(makeReturnedDoc()); await persistEmailTicket(makeFields(), 'guild-001', false, model, vi.fn()); const [, update] = model.findOneAndUpdate.mock.calls[0]; expect(update.$set.status).toBe('open'); }); }); // --------------------------------------------------------------------------- // (b) Reopen event: staffId='system', resolverId=prior claimerId, guildId correct // --------------------------------------------------------------------------- describe('persistEmailTicket — reopen event recording', () => { it('calls _recordAction once with staffId=system and type=reopen', async () => { const mockRecord = vi.fn(); const doc = makeReturnedDoc({ claimerId: 'prev-claimer' }); await persistEmailTicket(makeFields(), 'guild-001', true, makeTicketModel(doc), mockRecord); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type] = mockRecord.mock.calls[0]; expect(staffId).toBe('system'); expect(type).toBe('reopen'); }); it('sets resolverId = the claimerId from the returned doc', async () => { const mockRecord = vi.fn(); const doc = makeReturnedDoc({ claimerId: 'prev-claimer-123' }); await persistEmailTicket(makeFields(), 'guild-001', true, makeTicketModel(doc), mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.resolverId).toBe('prev-claimer-123'); }); it('includes guildId in the payload', async () => { const mockRecord = vi.fn(); const doc = makeReturnedDoc(); await persistEmailTicket(makeFields(), 'guild-999', true, makeTicketModel(doc), mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.guildId).toBe('guild-999'); }); }); // --------------------------------------------------------------------------- // (c) payload.ticket is the returned doc (so denormalization gets all fields) // --------------------------------------------------------------------------- describe('persistEmailTicket — returned doc passed as payload.ticket', () => { it('sets payload.ticket to the doc returned by findOneAndUpdate', async () => { const mockRecord = vi.fn(); const doc = makeReturnedDoc({ game: 'SomeGame', senderEmail: 'a@b.com' }); await persistEmailTicket(makeFields(), 'guild-001', true, makeTicketModel(doc), mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.ticket).toBe(doc); }); }); // --------------------------------------------------------------------------- // (d) No reopen event when wasReopened=false // --------------------------------------------------------------------------- describe('persistEmailTicket — no reopen on brand-new ticket', () => { it('does not call _recordAction when wasReopened=false', async () => { const mockRecord = vi.fn(); await persistEmailTicket(makeFields(), 'guild-001', false, makeTicketModel(makeReturnedDoc()), mockRecord); expect(mockRecord).not.toHaveBeenCalled(); }); }); // --------------------------------------------------------------------------- // (e) resolverId=null when prior claimerId is null (unclaimed ticket reopened) // --------------------------------------------------------------------------- describe('persistEmailTicket — reopen of unclaimed ticket', () => { it('sets resolverId=null when the prior claimerId is null', async () => { const mockRecord = vi.fn(); const doc = makeReturnedDoc({ claimerId: null }); await persistEmailTicket(makeFields(), 'guild-001', true, makeTicketModel(doc), mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.resolverId).toBeNull(); }); });