Files
broccolini-bot/tests/gmailLabels.test.js
indifferentketchup 2ccdbf72aa Email ticketing fixes, comms polish, and .env cleanup
Inbound:
- Gmail poll query is:unread in:inbox (was category:primary, which matched
  nothing on a no-tabs Workspace inbox)

Outbound email:
- Closed/escalation auto-emails editable via TICKET_CLOSE_MESSAGE and new
  TICKET_ESCALATION_EMAIL_MESSAGE; drop the staff signature from closing emails
- Replies quote the customer's latest message (gmail_quote markup so clients
  collapse it), embed custom emoji inline via CID attachment, and strip Discord
  role mentions
- Tagline spacing fix in the company signature

Discord side:
- Suppress all mentions in log + transcript posts (no more pinging on close)
- Drop the staff-role ping from new-ticket and follow-up notifications
- Ticket channels inherit category permissions instead of setting per-channel
  overwrites (removes the Manage Roles requirement)

Gmail folders:
- Folder/label routing (gmailLabels.js) with /folder; close files to Complete

Config:
- Remove ~56 stale .env keys for long-removed features; refresh stale copy

Docs:
- Design specs for folder routing, email-flow toggle, and per-staff metrics
2026-06-04 22:05:20 +00:00

153 lines
5.2 KiB
JavaScript

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();
});
});