Gmail folder auto-advance + /forward command
- Reply-cycle auto-advance: staff reply files the thread to "Awaiting Reply", a customer response files it to "Needs Response" (new GMAIL_LABEL_AWAITING_REPLY / GMAIL_LABEL_NEEDS_RESPONSE labels + autoAdvanceFolder, which only moves threads still in the auto-cycle and leaves hand-filed folders alone) - /forward: forward a ticket's email to another address (handlers/commands/forward.js + forward composition in services/gmail.js) - Tests for the auto-advance cycle; label fixtures updated for the new labels
This commit is contained in:
@@ -4,6 +4,9 @@ import {
|
||||
resolveLabelId,
|
||||
moveThreadToFolder,
|
||||
folderDisplayName,
|
||||
getManagedFolderKey,
|
||||
autoAdvanceFolder,
|
||||
MANAGED_USER_KEYS,
|
||||
__clearLabelCache
|
||||
} from '../services/gmailLabels.js';
|
||||
|
||||
@@ -19,8 +22,11 @@ const FULL_IDS = {
|
||||
|
||||
const FULL_LABELS = [
|
||||
{ name: 'Triage', id: 'L_TRIAGE' },
|
||||
{ name: 'Awaiting Reply', id: 'L_AR' },
|
||||
{ name: 'Needs Response', id: 'L_NR' },
|
||||
{ name: 'Escalated', id: 'L_ESC' },
|
||||
{ name: 'Resolved', id: 'L_RES' },
|
||||
{ name: 'Complete', id: 'L_RES' },
|
||||
{ name: 'For Jake', id: 'L_FJ' },
|
||||
{ name: 'Dashboard Errors', id: 'L_DE' },
|
||||
{ name: 'Partnership Offers', id: 'L_PO' }
|
||||
@@ -150,3 +156,76 @@ describe('moveThreadToFolder', () => {
|
||||
await expect(moveThreadToFolder('t', 'BOGUS', gmail)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// Derive label name↔id from the live config so these tests don't depend on the
|
||||
// actual GMAIL_LABEL_* names in .env (e.g. RESOLVED may be customized to "Complete").
|
||||
const idForKey = key => `LID_${key}`;
|
||||
const CYCLE_LABELS = MANAGED_USER_KEYS.map(k => ({ name: folderDisplayName(k), id: idForKey(k) }));
|
||||
|
||||
// Mock Gmail whose thread carries the given label ids; records threads.modify.
|
||||
function makeGmail({ threadLabelIds = [], onModify } = {}) {
|
||||
return {
|
||||
users: {
|
||||
labels: {
|
||||
list: async () => ({ data: { labels: CYCLE_LABELS } }),
|
||||
create: async () => { throw new Error('no create expected'); }
|
||||
},
|
||||
threads: {
|
||||
get: async () => ({ data: { messages: [{ labelIds: threadLabelIds }] } }),
|
||||
modify: async (args) => { if (onModify) onModify(args); return { data: {} }; }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe('getManagedFolderKey', () => {
|
||||
beforeEach(() => __clearLabelCache());
|
||||
|
||||
it('maps a thread label id to its managed folder key', async () => {
|
||||
expect(await getManagedFolderKey('t', makeGmail({ threadLabelIds: [idForKey('FOR_JAKE'), 'INBOX'] }))).toBe('FOR_JAKE');
|
||||
});
|
||||
|
||||
it('detects the system SPAM label', async () => {
|
||||
expect(await getManagedFolderKey('t', makeGmail({ threadLabelIds: ['SPAM'] }))).toBe('SPAM');
|
||||
});
|
||||
|
||||
it('returns null when no managed label is present', async () => {
|
||||
expect(await getManagedFolderKey('t', makeGmail({ threadLabelIds: ['INBOX', 'UNREAD'] }))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoAdvanceFolder', () => {
|
||||
beforeEach(() => __clearLabelCache());
|
||||
|
||||
it('advances an auto-cycle thread (Awaiting Reply → Needs Response)', async () => {
|
||||
let modifyArgs = null;
|
||||
const gmail = makeGmail({ threadLabelIds: [idForKey('AWAITING_REPLY')], onModify: a => { modifyArgs = a; } });
|
||||
expect(await autoAdvanceFolder('t', 'NEEDS_RESPONSE', gmail)).toBe(true);
|
||||
expect(modifyArgs.requestBody.addLabelIds).toEqual([idForKey('NEEDS_RESPONSE')]);
|
||||
});
|
||||
|
||||
it('advances a thread with no managed label', async () => {
|
||||
let called = false;
|
||||
const gmail = makeGmail({ threadLabelIds: ['INBOX'], onModify: () => { called = true; } });
|
||||
expect(await autoAdvanceFolder('t', 'AWAITING_REPLY', gmail)).toBe(true);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves a manually-filed thread (For Jake) untouched', async () => {
|
||||
let called = false;
|
||||
const gmail = makeGmail({ threadLabelIds: [idForKey('FOR_JAKE')], onModify: () => { called = true; } });
|
||||
expect(await autoAdvanceFolder('t', 'NEEDS_RESPONSE', gmail)).toBe(false);
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves a SPAM-filed thread untouched', async () => {
|
||||
let called = false;
|
||||
const gmail = makeGmail({ threadLabelIds: ['SPAM'], onModify: () => { called = true; } });
|
||||
expect(await autoAdvanceFolder('t', 'NEEDS_RESPONSE', gmail)).toBe(false);
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an unknown target key before touching the API', async () => {
|
||||
await expect(autoAdvanceFolder('t', 'BOGUS', makeGmail())).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user