split-inbox-templates.test.tsx12.4 KBView on GitHub import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import {
SplitInboxTabs,
SUPERHUMAN_TEMPLATES,
} from '@/modules/threads/components/SplitInboxTabs';
const mockCreateAiLabelMutateAsync = jest.fn();
const mockBackfillAiLabelsMutateAsync = jest.fn();
const mockAddInbox = jest.fn();
let mockAllUserLabels: Array<{ id: string; name: string }> = [];
jest.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({
children,
onSelect,
}: {
children: React.ReactNode;
onSelect?: () => void;
}) => <button onClick={onSelect}>{children}</button>,
DropdownMenuSeparator: () => null,
}));
jest.mock('@/components/ui/dialog', () => ({
Dialog: ({ open, children }: { open: boolean; children: React.ReactNode }) =>
open ? <div>{children}</div> : null,
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
jest.mock('@/components/ui/button', () => ({
Button: ({
children,
onClick,
disabled,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}) => (
<button onClick={onClick} disabled={disabled}>
{children}
</button>
),
}));
jest.mock('@/components/ui/input', () => ({
Input: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
}));
jest.mock('@/components/ui/switch', () => ({
Switch: ({
checked,
onCheckedChange,
}: {
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
}) => (
<input
type="checkbox"
checked={checked}
onChange={(event) => onCheckedChange?.(event.target.checked)}
/>
),
}));
jest.mock('@/components/ui/checkbox', () => ({
Checkbox: ({
checked,
onCheckedChange,
}: {
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
}) => (
<input
type="checkbox"
checked={checked}
onChange={(event) => onCheckedChange?.(event.target.checked)}
/>
),
}));
jest.mock('@tanstack/react-query', () => ({
useMutation: (options: { mutationKey?: string[] }) => {
const key=[redacted];
if (key === 'createAiLabel') {
return { mutateAsync: mockCreateAiLabelMutateAsync };
}
if (key === 'backfillAiLabels') {
return { mutateAsync: mockBackfillAiLabelsMutateAsync };
}
return { mutateAsync: jest.fn() };
},
useQueryClient: () => ({ invalidateQueries: jest.fn() }),
}));
jest.mock('@/providers/query-provider', () => ({
useTRPC: () => ({
labels: {
list: {
queryKey: () => ['labels.list'],
},
create: {
mutationOptions: (options: Record<string, unknown> = {}) => ({
mutationKey: ['createUserLabel'],
...options,
}),
},
delete: {
mutationOptions: (options: Record<string, unknown> = {}) => ({
mutationKey: ['deleteUserLabel'],
...options,
}),
},
},
mail: {
createAiLabel: {
mutationOptions: (options: Record<string, unknown> = {}) => ({
mutationKey: ['createAiLabel'],
...options,
}),
},
backfillAiLabels: {
mutationOptions: (options: Record<string, unknown> = {}) => ({
mutationKey: ['backfillAiLabels'],
...options,
}),
},
deleteAiLabel: {
mutationOptions: (options: Record<string, unknown> = {}) => ({
mutationKey: ['deleteAiLabel'],
...options,
}),
},
},
}),
}));
jest.mock('@/modules/threads/hooks/use-inboxes', () => ({
getInboxSlug: (inbox: { id: string; name: string }) => {
if (inbox.id === 'default') return 'inbox';
if (inbox.id === 'important') return 'important';
if (inbox.id === 'other') return 'other';
return inbox.name.toLowerCase().replace(/\s+/g, '-');
},
findInboxBySlug: (inboxes: Array<{ id: string }>, slug: string) =>
inboxes.find((inbox) => (slug === 'inbox' ? inbox.id === 'default' : inbox.id === slug)),
getRowInboxOrder: (inboxes: Array<{ id: string }>) => inboxes,
useInboxes: () => ({
inboxes: [{ id: 'default', name: 'Inbox', position: 0, system: true, rule: { kind: 'all' } }],
activeInbox: { id: 'default', name: 'Inbox', position: 0, system: true, rule: { kind: 'all' } },
activeInboxId: 'default',
inboxLayout: 'inbox',
importantSignal: 'category_personal',
setActiveInboxId: jest.fn(),
setInboxLayout: jest.fn(),
setImportantSignal: jest.fn(),
addInbox: mockAddInbox,
updateInbox: jest.fn(),
renameInbox: jest.fn(),
removeInbox: jest.fn(),
reorderInboxes: jest.fn(),
isLoading: false,
inboxesLoading: false,
}),
}));
jest.mock('@/modules/labels/hooks/use-labels-search', () => ({
__esModule: true,
default: () => ({ setLabels: jest.fn(), labels: [] }),
}));
jest.mock('@/hooks/use-inbox-counts', () => ({
useInboxCounts: () => ({
byId: { default: { count: 0, isExact: true } },
done: { count: 0, isExact: true },
}),
}));
jest.mock('@/modules/aop/hooks/use-aops', () => ({
useAOPs: () => ({ data: { aops: [] } }),
}));
jest.mock('@/modules/labels/hooks/use-labels', () => ({
useLabels: () => ({ allUserLabels: mockAllUserLabels, systemLabels: [] }),
}));
jest.mock('@/modules/store', () => ({
useCedarStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
isThreadOpen: false,
isConversationOpen: false,
// The tab strip reads which inboxes the ⇧U filter is narrowing, and calls setUnreadOnly
// to clear one from its badge.
unreadOnlyByFolder: {},
setUnreadOnly: () => {},
}),
}));
jest.mock('sonner', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
info: jest.fn(),
},
}));
describe('split inbox template coverage and behavior', () => {
beforeEach(() => {
mockAllUserLabels = [];
mockCreateAiLabelMutateAsync.mockReset().mockResolvedValue({});
mockBackfillAiLabelsMutateAsync
.mockReset()
.mockResolvedValue({ attempted: 50, succeeded: 50, failed: 0 });
mockAddInbox.mockReset().mockResolvedValue('inbox-marketing');
});
/**
* The Calendar split shipped matching only two sender addresses plus
* `filename:ics`, and both halves leaked:
* - `filename:ics` needs the invite's text/calendar part to be a NAMED
* attachment, which Google Calendar invites often are not;
* - the sender terms miss RSVP notices and the human/auto replies on invite
* threads, which come from the ATTENDEE's own address.
* Real reported case: a rep still saw `Invitation:` and `Re: Declined:` mail in
* Important after enabling the split. These assertions pin the subject terms
* that fix it, and — just as importantly — that they stay narrow enough to
* leave product mail in the inbox.
*/
describe('Calendar template', () => {
const calendar = SUPERHUMAN_TEMPLATES.find((t) => t.name === 'Calendar');
/** The mirror interprets `subject:"x"` as a case-insensitive substring (ILIKE %x%). */
const subjectTerms = (query: string) =>
Array.from(query.matchAll(/subject:"([^"]+)"/g)).map((m) => m[1]!.toLowerCase());
const matches = (subject: string) =>
subjectTerms(calendar!.query!).some((term) => subject.toLowerCase().includes(term));
it('carves calendar mail out of Important rather than mirroring into it', () => {
// Without this the split is a copy, not a partition, and the clutter stays.
expect(calendar!.alsoShowInImportantOrOther ?? false).toBe(false);
});
it('still matches genuinely attached .ics files', () => {
expect(calendar!.query).toContain('filename:ics');
});
it.each([
'Invitation: Debrief - Nicolas Baranowski @ Thu Aug 27, 2026 11:45am - 12pm (EDT)',
'Re: Updated invitation: LivTech / Ode Sync @ Wed Aug 12, 2026 12pm - 12:30pm (EDT)',
'Automatic reply: Updated invitation with note: Fractional <> Waystar DS Knowledge Transfer',
'Out of office until 7/31 Re: Invitation: Project Review: LivTech AI Roadmapping',
'Re: Declined: LivTech / Ode Weekly Sync',
'Accepted: Varun / Abrey @ Thu Mar 26, 2026 9am - 10am (EDT)',
'Re: Canceled event: Salma / Varun @ Weekly from 9:30am to 10am on Wednesday (PDT)',
'Team Happy Hour @ Tue, Jul 21, 2026 5:30pm \u2013 7:30pm (GMT-04)',
])('catches calendar mail: %s', (subject) => {
expect(matches(subject)).toBe(true);
});
it.each([
'Isabelle invited you to work together in Slack',
'Your invitation to the Claude Console',
'Reminder: gracie has invited you to join the project LivTech',
'You\u2019re invited to Vornado',
])('leaves product mail in the inbox: %s', (subject) => {
// This is why every term keeps its trailing colon — a bare `invitation`
// would pull all of these out of the user's inbox.
expect(matches(subject)).toBe(false);
});
});
it('includes every documented Superhuman template', () => {
const templateNames = new Set(SUPERHUMAN_TEMPLATES.map((template) => template.name));
const expectedNames = [
'Important + Other',
'Calendar',
'VIP',
'Starred',
'Unread',
'Reminders',
'Shared',
'Team',
'Notes',
'Travel',
'Purchases',
'Finance',
'Notifications',
'News',
'Social',
'Marketing',
'Pitches',
'Documents',
'Google',
'Office',
'Notion',
'Coda',
'Confluence',
'Loom',
'Figma',
'GitHub',
'Linear',
'Jira',
'Aha!',
'Asana',
'Trello',
'ClickUp',
'Monday',
'Signature',
'DocuSign',
'SignNow',
'Dropbox Sign',
'Signeasy',
'PandaDoc',
'Salesforce',
'Zoho',
'HubSpot',
'Pipedrive',
'Gong',
'Chorus',
'DocSend',
'Workday Recruiting',
'Greenhouse',
'Lever',
'Ashby',
'SmartRecruiters',
'Workable',
'Breezy HR',
'Zoom',
'Meet',
'Otter',
'Fireflies.ai',
];
for (const expectedName of expectedNames) {
expect(templateNames.has(expectedName)).toBe(true);
}
});
it('configures Cedar AI label metadata for all AI-based templates', () => {
const aiTemplates = SUPERHUMAN_TEMPLATES.filter((template) =>
['Travel', 'Purchases', 'Finance', 'Notifications', 'News', 'Social', 'Marketing', 'Pitches'].includes(template.name),
);
expect(aiTemplates.length).toBe(8);
for (const template of aiTemplates) {
expect(template.aiLabels && template.aiLabels.length > 0).toBe(true);
for (const aiLabel of template.aiLabels ?? []) {
expect(aiLabel.slug.length).toBeGreaterThan(0);
expect(aiLabel.displayName.length).toBeGreaterThan(0);
expect(aiLabel.description.length).toBeGreaterThan(0);
}
}
});
it('creates missing AI labels and triggers 50-thread backfill when applying AI templates', async () => {
render(
<MemoryRouter initialEntries={['/mail/inbox']}>
<SplitInboxTabs />
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Edit inboxes'));
fireEvent.click(screen.getByText('Marketing').closest('button')!);
await waitFor(() => expect(mockCreateAiLabelMutateAsync).toHaveBeenCalledTimes(1));
expect(mockCreateAiLabelMutateAsync).toHaveBeenCalledWith(
expect.objectContaining({
displayName: 'Marketing',
}),
);
await waitFor(() => expect(mockAddInbox).toHaveBeenCalledTimes(1));
const addInboxArgs = mockAddInbox.mock.calls[0]?.[0] as { query: string };
expect(addInboxArgs.query).toContain('label:[superhuman]/ai/marketing');
expect(addInboxArgs.query).toContain('label:"Cedar/AI/marketing"');
await waitFor(() => expect(mockBackfillAiLabelsMutateAsync).toHaveBeenCalledTimes(1));
expect(mockBackfillAiLabelsMutateAsync).toHaveBeenCalledWith({
maxThreads: 50,
folder: 'INBOX',
reexecute: true,
bypassAgentExecutionEnabledCheck: true,
});
});
});