import { describe, it, expect, beforeEach } from 'vitest'; import { computeLabelMutation, resolveLabelId, moveThreadToFolder, folderDisplayName, __clearLabelCache } from '../services/gmailLabels.js'; const FULL_IDS = { TRIAGE: 'L_TRIAGE', ESCALATED: 'L_ESC', RESOLVED: 'L_RES', FOR_JAKE: 'L_FJ', DASHBOARD_ERRORS: 'L_DE', PARTNERSHIP_OFFERS: 'L_PO', SPAM: 'SPAM' }; const FULL_LABELS = [ { name: 'Triage', id: 'L_TRIAGE' }, { name: 'Escalated', id: 'L_ESC' }, { name: 'Resolved', id: 'L_RES' }, { name: 'For Jake', id: 'L_FJ' }, { name: 'Dashboard Errors', id: 'L_DE' }, { name: 'Partnership Offers', id: 'L_PO' } ]; describe('computeLabelMutation', () => { it('adds the target, removes every other managed label plus INBOX/UNREAD', () => { const { addLabelIds, removeLabelIds } = computeLabelMutation('FOR_JAKE', FULL_IDS); expect(addLabelIds).toEqual(['L_FJ']); expect(removeLabelIds).toContain('INBOX'); expect(removeLabelIds).toContain('UNREAD'); expect(removeLabelIds).toContain('SPAM'); expect(removeLabelIds).toContain('L_TRIAGE'); expect(removeLabelIds).not.toContain('L_FJ'); // target is never removed }); it('moving to SPAM adds SPAM and removes all user labels but not SPAM itself', () => { const { addLabelIds, removeLabelIds } = computeLabelMutation('SPAM', FULL_IDS); expect(addLabelIds).toEqual(['SPAM']); expect(removeLabelIds).not.toContain('SPAM'); expect(removeLabelIds).toContain('L_TRIAGE'); expect(removeLabelIds).toContain('INBOX'); }); it('throws when the target id is missing', () => { expect(() => computeLabelMutation('FOR_JAKE', { TRIAGE: 'x' })).toThrow(); }); }); describe('folderDisplayName', () => { it('returns null for the system SPAM folder', () => { expect(folderDisplayName('SPAM')).toBeNull(); }); it('returns the configured/default name for a user folder', () => { expect(folderDisplayName('FOR_JAKE')).toBe('For Jake'); }); it('throws on an unknown key', () => { expect(() => folderDisplayName('NOPE')).toThrow(); }); }); describe('resolveLabelId', () => { beforeEach(() => __clearLabelCache()); it('short-circuits SPAM to the system id without any API call', async () => { let called = false; const gmail = { users: { labels: { list: async () => { called = true; return { data: {} }; } } } }; expect(await resolveLabelId(gmail, 'SPAM')).toBe('SPAM'); expect(called).toBe(false); }); it('returns an existing label id matched by name', async () => { const gmail = { users: { labels: { list: async () => ({ data: { labels: [{ name: 'For Jake', id: 'L_EXISTING' }] } }), create: async () => { throw new Error('should not create'); } } } }; expect(await resolveLabelId(gmail, 'FOR_JAKE')).toBe('L_EXISTING'); }); it('creates a missing user label under its configured name and caches it', async () => { let createdName = null; const gmail = { users: { labels: { list: async () => ({ data: { labels: [] } }), create: async ({ requestBody }) => { createdName = requestBody.name; return { data: { id: 'L_NEW' } }; } } } }; expect(await resolveLabelId(gmail, 'FOR_JAKE')).toBe('L_NEW'); expect(createdName).toBe('For Jake'); }); }); describe('moveThreadToFolder', () => { beforeEach(() => __clearLabelCache()); it('resolves labels then issues one threads.modify with exclusive sets', async () => { let modifyArgs = null; const gmail = { users: { labels: { list: async () => ({ data: { labels: FULL_LABELS } }), create: async () => { throw new Error('no create expected'); } }, threads: { modify: async (args) => { modifyArgs = args; return { data: {} }; } } } }; await moveThreadToFolder('thread123', 'ESCALATED', gmail); expect(modifyArgs.id).toBe('thread123'); expect(modifyArgs.requestBody.addLabelIds).toEqual(['L_ESC']); expect(modifyArgs.requestBody.removeLabelIds).toContain('L_TRIAGE'); expect(modifyArgs.requestBody.removeLabelIds).toContain('INBOX'); expect(modifyArgs.requestBody.removeLabelIds).not.toContain('L_ESC'); }); it('clears the cache and retries once on an invalid-label error', async () => { let modifyCalls = 0; let listCalls = 0; const gmail = { users: { labels: { list: async () => { listCalls++; return { data: { labels: FULL_LABELS } }; }, create: async () => ({ data: { id: 'X' } }) }, threads: { modify: async () => { modifyCalls++; if (modifyCalls === 1) { const e = new Error('invalid label'); e.code = 400; throw e; } return { data: {} }; } } } }; await moveThreadToFolder('t1', 'TRIAGE', gmail); expect(modifyCalls).toBe(2); expect(listCalls).toBe(2); // cache was cleared and labels re-listed }); it('rejects an unknown folder key before touching the API', async () => { const gmail = { users: { labels: { list: async () => ({ data: { labels: [] } }) }, threads: { modify: async () => ({}) } } }; await expect(moveThreadToFolder('t', 'BOGUS', gmail)).rejects.toThrow(); }); });