From 601c41c566fab6e69ba4bb1d9dae57a0f4b1462d Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Thu, 18 Dec 2025 00:14:07 +0200 Subject: [PATCH 1/4] dont use stale label ids on outlook. e2e test --- .../microsoft-thread-category-removal.test.ts | 260 ++++++++++++++++++ apps/web/utils/reply-tracker/label-helpers.ts | 12 + version.txt | 2 +- 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts diff --git a/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts b/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts new file mode 100644 index 0000000000..e07a02d29c --- /dev/null +++ b/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts @@ -0,0 +1,260 @@ +/** + * E2E tests for Microsoft Outlook thread category removal + * + * These tests verify that conversation status labels (To Reply, Awaiting Reply, FYI, Actioned) + * are mutually exclusive within a thread - when applying a new label, existing conflicting + * labels should be removed from ALL messages in the thread. + * + * Usage: + * pnpm test-e2e microsoft-thread-category-removal + */ + +import { describe, test, expect, beforeAll, afterAll, vi } from "vitest"; +import prisma from "@/utils/prisma"; +import { createEmailProvider } from "@/utils/email/provider"; +import type { EmailProvider } from "@/utils/email/types"; +import type { ParsedMessage } from "@/utils/types"; +import { getRuleLabel } from "@/utils/rule/consts"; +import { SystemType } from "@/generated/prisma/enums"; +import { removeConflictingThreadStatusLabels } from "@/utils/reply-tracker/label-helpers"; +import { createScopedLogger } from "@/utils/logger"; + +const RUN_E2E_TESTS = process.env.RUN_E2E_TESTS; +const TEST_OUTLOOK_EMAIL = process.env.TEST_OUTLOOK_EMAIL; + +vi.mock("server-only", () => ({})); + +describe.skipIf(!RUN_E2E_TESTS)( + "Microsoft Outlook Thread Category Removal E2E Tests", + () => { + let provider: EmailProvider; + let emailAccountId: string; + let testThreadId: string; + let testMessages: ParsedMessage[]; + const createdTestLabels: string[] = []; + const logger = createScopedLogger("e2e-test"); + + beforeAll(async () => { + if (!TEST_OUTLOOK_EMAIL) { + throw new Error("TEST_OUTLOOK_EMAIL env var is required"); + } + + const emailAccount = await prisma.emailAccount.findFirst({ + where: { + email: TEST_OUTLOOK_EMAIL, + account: { provider: "microsoft" }, + }, + include: { account: true }, + }); + + if (!emailAccount) { + throw new Error(`No Outlook account found for ${TEST_OUTLOOK_EMAIL}`); + } + + emailAccountId = emailAccount.id; + provider = await createEmailProvider({ + emailAccountId: emailAccount.id, + provider: "microsoft", + }); + + // Find a suitable test thread with 2+ messages + const { threadId, messages } = await findThreadWithMultipleMessages( + provider, + 2, + ); + testThreadId = threadId; + testMessages = messages; + }, 60_000); + + afterAll(async () => { + // Clean up test labels + for (const labelName of createdTestLabels) { + try { + const label = await provider.getLabelByName(labelName); + if (label) { + await provider.removeThreadLabel(testThreadId, label.id); + await provider.deleteLabel(label.id); + } + } catch { + // Ignore cleanup errors + } + } + }); + + // ============================================ + // TEST 1: Provider Level - removeThreadLabels() + // ============================================ + describe("Provider Level: removeThreadLabels()", () => { + test("should remove categories from ALL messages in a thread", async () => { + expect( + testMessages.length, + "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", + ).toBeGreaterThanOrEqual(2); + + // Create test category + const testCategoryName = `E2E-ThreadRemoval-${Date.now()}`; + createdTestLabels.push(testCategoryName); + const category = await provider.createLabel(testCategoryName); + + // Apply category to ALL messages in the thread + for (const msg of testMessages) { + await provider.labelMessage({ + messageId: msg.id, + labelId: category.id, + labelName: category.name, + }); + } + + // Verify all messages have the category + for (const msg of testMessages) { + const message = await provider.getMessage(msg.id); + expect(message.labelIds).toContain(category.name); + } + + // Remove the category from the thread using removeThreadLabels + await provider.removeThreadLabels(testThreadId, [category.id]); + + // Verify ALL messages no longer have the category + for (const msg of testMessages) { + const message = await provider.getMessage(msg.id); + expect(message.labelIds).not.toContain(category.name); + } + }, 60_000); + + test("should remove multiple categories from all messages in a thread", async () => { + expect( + testMessages.length, + "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", + ).toBeGreaterThanOrEqual(2); + + // Create multiple test categories + const category1Name = `E2E-Multi1-${Date.now()}`; + const category2Name = `E2E-Multi2-${Date.now()}`; + createdTestLabels.push(category1Name, category2Name); + + const category1 = await provider.createLabel(category1Name); + const category2 = await provider.createLabel(category2Name); + + // Apply both categories to all messages + for (const msg of testMessages) { + await provider.labelMessage({ + messageId: msg.id, + labelId: category1.id, + labelName: category1.name, + }); + await provider.labelMessage({ + messageId: msg.id, + labelId: category2.id, + labelName: category2.name, + }); + } + + // Verify all messages have both categories + for (const msg of testMessages) { + const message = await provider.getMessage(msg.id); + expect(message.labelIds).toContain(category1.name); + expect(message.labelIds).toContain(category2.name); + } + + // Remove both categories from the thread + await provider.removeThreadLabels(testThreadId, [ + category1.id, + category2.id, + ]); + + // Verify ALL messages have neither category + for (const msg of testMessages) { + const message = await provider.getMessage(msg.id); + expect(message.labelIds).not.toContain(category1.name); + expect(message.labelIds).not.toContain(category2.name); + } + }, 60_000); + }); + + // ============================================ + // TEST 2: Label Helpers Level - removeConflictingThreadStatusLabels() + // ============================================ + describe("Label Helpers Level: removeConflictingThreadStatusLabels()", () => { + test("should remove conflicting conversation status categories when applying a new status", async () => { + expect( + testMessages.length, + "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", + ).toBeGreaterThanOrEqual(2); + + // Create conversation status labels + const toReplyLabelName = getRuleLabel(SystemType.TO_REPLY); + const awaitingReplyLabelName = getRuleLabel(SystemType.AWAITING_REPLY); + createdTestLabels.push(toReplyLabelName, awaitingReplyLabelName); + + const toReplyLabel = await provider.createLabel(toReplyLabelName); + const awaitingReplyLabel = await provider.createLabel( + awaitingReplyLabelName, + ); + + // Apply "To Reply" to first message + await provider.labelMessage({ + messageId: testMessages[0].id, + labelId: toReplyLabel.id, + labelName: toReplyLabel.name, + }); + + // Apply "Awaiting Reply" to second message + await provider.labelMessage({ + messageId: testMessages[1].id, + labelId: awaitingReplyLabel.id, + labelName: awaitingReplyLabel.name, + }); + + // Verify labels are applied + const msg1Before = await provider.getMessage(testMessages[0].id); + expect(msg1Before.labelIds).toContain(toReplyLabel.name); + + const msg2Before = await provider.getMessage(testMessages[1].id); + expect(msg2Before.labelIds).toContain(awaitingReplyLabel.name); + + // Call removeConflictingThreadStatusLabels with FYI status + // This should remove TO_REPLY and AWAITING_REPLY labels from the thread + await removeConflictingThreadStatusLabels({ + emailAccountId, + threadId: testThreadId, + systemType: SystemType.FYI, + provider, + logger, + }); + + // Verify ALL conflicting labels are removed from ALL messages + for (const msg of testMessages) { + const message = await provider.getMessage(msg.id); + expect(message.labelIds).not.toContain(toReplyLabel.name); + expect(message.labelIds).not.toContain(awaitingReplyLabel.name); + } + }, 60_000); + }); + }, +); + +/** + * Finds a thread with at least minMessages messages from the inbox. + * Looks through recent inbox messages and finds one with multiple messages in thread. + */ +async function findThreadWithMultipleMessages( + provider: EmailProvider, + minMessages = 2, +): Promise<{ threadId: string; messages: ParsedMessage[] }> { + const inboxMessages = await provider.getInboxMessages(50); + + // Group by threadId and find one with enough messages + const threadIds = [...new Set(inboxMessages.map((m) => m.threadId))]; + + for (const threadId of threadIds) { + const messages = await provider.getThreadMessages(threadId); + if (messages.length >= minMessages) { + return { threadId, messages }; + } + } + + throw new Error( + `TEST PREREQUISITE NOT MET: No thread found with ${minMessages}+ messages. ` + + "Send an email to the test account and reply to it to create a multi-message thread.", + ); +} diff --git a/apps/web/utils/reply-tracker/label-helpers.ts b/apps/web/utils/reply-tracker/label-helpers.ts index ff4b44169a..c7af241d76 100644 --- a/apps/web/utils/reply-tracker/label-helpers.ts +++ b/apps/web/utils/reply-tracker/label-helpers.ts @@ -40,11 +40,23 @@ export async function removeConflictingThreadStatusLabels({ ]); const removeLabelIds: string[] = []; + const providerLabelIds = new Set(providerLabels.map((l) => l.id)); for (const type of CONVERSATION_STATUS_TYPES) { if (type === systemType) continue; let label = dbLabels[type as ConversationStatus]; + + // If DB has a label ID, verify it still exists in the provider + // If not, fall back to looking up by name (label may have been recreated) + if (label.labelId && !providerLabelIds.has(label.labelId)) { + logger.warn("DB label ID not found in provider, looking up by name", { + type, + staleId: label.labelId, + }); + label = { labelId: null, label: null }; + } + if (!label.labelId && !label.label) { const l = providerLabels.find((l) => l.name === getRuleLabel(type)); if (!l?.id) { diff --git a/version.txt b/version.txt index eae4b4133c..7db52267db 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.24.0 +v2.24.1 From eaee2ab0fdf8bc88540ee4e63d495231c185be2c Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Thu, 18 Dec 2025 00:21:35 +0200 Subject: [PATCH 2/4] gmail e2e conversation test --- .../gmail-thread-label-removal.test.ts | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts diff --git a/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts b/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts new file mode 100644 index 0000000000..f8cc3e90ad --- /dev/null +++ b/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts @@ -0,0 +1,229 @@ +/** + * E2E tests for Gmail thread label removal + * + * These tests verify that conversation status labels (To Reply, Awaiting Reply, FYI, Actioned) + * are mutually exclusive within a thread - when applying a new label, existing conflicting + * labels should be removed from ALL messages in the thread. + * + * Usage: + * pnpm test-e2e gmail-thread-label-removal + */ + +import { describe, test, expect, beforeAll, afterAll, vi } from "vitest"; +import prisma from "@/utils/prisma"; +import { createEmailProvider } from "@/utils/email/provider"; +import type { EmailProvider } from "@/utils/email/types"; +import type { ParsedMessage } from "@/utils/types"; +import { getRuleLabel } from "@/utils/rule/consts"; +import { SystemType } from "@/generated/prisma/enums"; +import { removeConflictingThreadStatusLabels } from "@/utils/reply-tracker/label-helpers"; +import { createScopedLogger } from "@/utils/logger"; + +const RUN_E2E_TESTS = process.env.RUN_E2E_TESTS; +const TEST_GMAIL_EMAIL = process.env.TEST_GMAIL_EMAIL; + +vi.mock("server-only", () => ({})); + +describe.skipIf(!RUN_E2E_TESTS)("Gmail Thread Label Removal E2E Tests", () => { + let provider: EmailProvider; + let emailAccountId: string; + let testThreadId: string; + let testMessages: ParsedMessage[]; + const createdTestLabels: string[] = []; + const logger = createScopedLogger("e2e-test"); + + beforeAll(async () => { + if (!TEST_GMAIL_EMAIL) { + throw new Error("TEST_GMAIL_EMAIL env var is required"); + } + + const emailAccount = await prisma.emailAccount.findFirst({ + where: { + email: TEST_GMAIL_EMAIL, + account: { provider: "google" }, + }, + include: { account: true }, + }); + + if (!emailAccount) { + throw new Error(`No Gmail account found for ${TEST_GMAIL_EMAIL}`); + } + + emailAccountId = emailAccount.id; + provider = await createEmailProvider({ + emailAccountId: emailAccount.id, + provider: "google", + }); + + // Find a suitable test thread with 2+ messages + const { threadId, messages } = await findThreadWithMultipleMessages( + provider, + 2, + ); + testThreadId = threadId; + testMessages = messages; + }, 60_000); + + afterAll(async () => { + // Clean up test labels + for (const labelName of createdTestLabels) { + try { + const label = await provider.getLabelByName(labelName); + if (label) { + await provider.removeThreadLabel(testThreadId, label.id); + await provider.deleteLabel(label.id); + } + } catch { + // Ignore cleanup errors + } + } + }); + + // ============================================ + // TEST 1: Provider Level - removeThreadLabels() + // ============================================ + describe("Provider Level: removeThreadLabels()", () => { + test("should remove labels from thread", async () => { + expect( + testMessages.length, + "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", + ).toBeGreaterThanOrEqual(2); + + // Create test label + const testLabelName = `E2E-ThreadRemoval-${Date.now()}`; + createdTestLabels.push(testLabelName); + const label = await provider.createLabel(testLabelName); + + // Apply label to the thread (Gmail applies to all messages in thread) + await provider.labelMessage({ + messageId: testMessages[0].id, + labelId: label.id, + labelName: label.name, + }); + + // Verify the thread has the label + const msgBefore = await provider.getMessage(testMessages[0].id); + expect(msgBefore.labelIds).toContain(label.id); + + // Remove the label from the thread using removeThreadLabels + await provider.removeThreadLabels(testThreadId, [label.id]); + + // Verify the thread no longer has the label + const msgAfter = await provider.getMessage(testMessages[0].id); + expect(msgAfter.labelIds).not.toContain(label.id); + }, 60_000); + + test("should remove multiple labels from thread", async () => { + expect( + testMessages.length, + "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", + ).toBeGreaterThanOrEqual(2); + + // Create multiple test labels + const label1Name = `E2E-Multi1-${Date.now()}`; + const label2Name = `E2E-Multi2-${Date.now()}`; + createdTestLabels.push(label1Name, label2Name); + + const label1 = await provider.createLabel(label1Name); + const label2 = await provider.createLabel(label2Name); + + // Apply both labels to the thread + await provider.labelMessage({ + messageId: testMessages[0].id, + labelId: label1.id, + labelName: label1.name, + }); + await provider.labelMessage({ + messageId: testMessages[0].id, + labelId: label2.id, + labelName: label2.name, + }); + + // Verify thread has both labels + const msgBefore = await provider.getMessage(testMessages[0].id); + expect(msgBefore.labelIds).toContain(label1.id); + expect(msgBefore.labelIds).toContain(label2.id); + + // Remove both labels from the thread + await provider.removeThreadLabels(testThreadId, [label1.id, label2.id]); + + // Verify thread has neither label + const msgAfter = await provider.getMessage(testMessages[0].id); + expect(msgAfter.labelIds).not.toContain(label1.id); + expect(msgAfter.labelIds).not.toContain(label2.id); + }, 60_000); + }); + + // ============================================ + // TEST 2: Label Helpers Level - removeConflictingThreadStatusLabels() + // ============================================ + describe("Label Helpers Level: removeConflictingThreadStatusLabels()", () => { + test("should remove conflicting conversation status labels when applying a new status", async () => { + expect( + testMessages.length, + "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", + ).toBeGreaterThanOrEqual(2); + + // Create conversation status labels + const toReplyLabelName = getRuleLabel(SystemType.TO_REPLY); + const awaitingReplyLabelName = getRuleLabel(SystemType.AWAITING_REPLY); + createdTestLabels.push(toReplyLabelName, awaitingReplyLabelName); + + const toReplyLabel = await provider.createLabel(toReplyLabelName); + const awaitingReplyLabel = await provider.createLabel( + awaitingReplyLabelName, + ); + + // Apply "To Reply" label to thread + await provider.labelMessage({ + messageId: testMessages[0].id, + labelId: toReplyLabel.id, + labelName: toReplyLabel.name, + }); + + // Verify label is applied + const msgBefore = await provider.getMessage(testMessages[0].id); + expect(msgBefore.labelIds).toContain(toReplyLabel.id); + + // Call removeConflictingThreadStatusLabels with FYI status + // This should remove TO_REPLY and AWAITING_REPLY labels from the thread + await removeConflictingThreadStatusLabels({ + emailAccountId, + threadId: testThreadId, + systemType: SystemType.FYI, + provider, + logger, + }); + + // Verify conflicting label is removed + const msgAfter = await provider.getMessage(testMessages[0].id); + expect(msgAfter.labelIds).not.toContain(toReplyLabel.id); + }, 60_000); + }); +}); + +/** + * Finds a thread with at least minMessages messages from the inbox. + * Looks through recent inbox messages and finds one with multiple messages in thread. + */ +async function findThreadWithMultipleMessages( + provider: EmailProvider, + minMessages = 2, +): Promise<{ threadId: string; messages: ParsedMessage[] }> { + const inboxMessages = await provider.getInboxMessages(50); + + // Group by threadId and find one with enough messages + const threadIds = [...new Set(inboxMessages.map((m) => m.threadId))]; + + for (const threadId of threadIds) { + const messages = await provider.getThreadMessages(threadId); + if (messages.length >= minMessages) { + return { threadId, messages }; + } + } + + throw new Error( + `TEST PREREQUISITE NOT MET: No thread found with ${minMessages}+ messages. ` + + "Send an email to the test account and reply to it to create a multi-message thread.", + ); +} From bcb9a24f549c63a1409986c6aa3bdf483cce916b Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Thu, 18 Dec 2025 00:30:42 +0200 Subject: [PATCH 3/4] disable drafts/report debug pages --- apps/web/app/(app)/[emailAccountId]/debug/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx index 22a39db1ca..6aa38f4787 100644 --- a/apps/web/app/(app)/[emailAccountId]/debug/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/debug/page.tsx @@ -17,17 +17,17 @@ export default async function DebugPage(props: { - + */} - + */} ); From 3427cef9e2fbbfd7dd8600502b29fac39f222652 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:03:39 +0200 Subject: [PATCH 4/4] fixes --- .../gmail-thread-label-removal.test.ts | 40 ++++++------------- apps/web/__tests__/e2e/labeling/helpers.ts | 28 +++++++++++++ .../microsoft-thread-category-removal.test.ts | 27 +------------ 3 files changed, 41 insertions(+), 54 deletions(-) create mode 100644 apps/web/__tests__/e2e/labeling/helpers.ts diff --git a/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts b/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts index f8cc3e90ad..8ab6c16f11 100644 --- a/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts +++ b/apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts @@ -18,6 +18,7 @@ import { getRuleLabel } from "@/utils/rule/consts"; import { SystemType } from "@/generated/prisma/enums"; import { removeConflictingThreadStatusLabels } from "@/utils/reply-tracker/label-helpers"; import { createScopedLogger } from "@/utils/logger"; +import { findThreadWithMultipleMessages } from "./helpers"; const RUN_E2E_TESTS = process.env.RUN_E2E_TESTS; const TEST_GMAIL_EMAIL = process.env.TEST_GMAIL_EMAIL; @@ -181,9 +182,17 @@ describe.skipIf(!RUN_E2E_TESTS)("Gmail Thread Label Removal E2E Tests", () => { labelName: toReplyLabel.name, }); - // Verify label is applied + // Apply "Awaiting Reply" label to thread + await provider.labelMessage({ + messageId: testMessages[0].id, + labelId: awaitingReplyLabel.id, + labelName: awaitingReplyLabel.name, + }); + + // Verify labels are applied const msgBefore = await provider.getMessage(testMessages[0].id); expect(msgBefore.labelIds).toContain(toReplyLabel.id); + expect(msgBefore.labelIds).toContain(awaitingReplyLabel.id); // Call removeConflictingThreadStatusLabels with FYI status // This should remove TO_REPLY and AWAITING_REPLY labels from the thread @@ -195,35 +204,10 @@ describe.skipIf(!RUN_E2E_TESTS)("Gmail Thread Label Removal E2E Tests", () => { logger, }); - // Verify conflicting label is removed + // Verify conflicting labels are removed const msgAfter = await provider.getMessage(testMessages[0].id); expect(msgAfter.labelIds).not.toContain(toReplyLabel.id); + expect(msgAfter.labelIds).not.toContain(awaitingReplyLabel.id); }, 60_000); }); }); - -/** - * Finds a thread with at least minMessages messages from the inbox. - * Looks through recent inbox messages and finds one with multiple messages in thread. - */ -async function findThreadWithMultipleMessages( - provider: EmailProvider, - minMessages = 2, -): Promise<{ threadId: string; messages: ParsedMessage[] }> { - const inboxMessages = await provider.getInboxMessages(50); - - // Group by threadId and find one with enough messages - const threadIds = [...new Set(inboxMessages.map((m) => m.threadId))]; - - for (const threadId of threadIds) { - const messages = await provider.getThreadMessages(threadId); - if (messages.length >= minMessages) { - return { threadId, messages }; - } - } - - throw new Error( - `TEST PREREQUISITE NOT MET: No thread found with ${minMessages}+ messages. ` + - "Send an email to the test account and reply to it to create a multi-message thread.", - ); -} diff --git a/apps/web/__tests__/e2e/labeling/helpers.ts b/apps/web/__tests__/e2e/labeling/helpers.ts new file mode 100644 index 0000000000..e994158fd9 --- /dev/null +++ b/apps/web/__tests__/e2e/labeling/helpers.ts @@ -0,0 +1,28 @@ +import type { EmailProvider } from "@/utils/email/types"; +import type { ParsedMessage } from "@/utils/types"; + +/** + * Finds a thread with at least minMessages messages from the inbox. + * Looks through recent inbox messages and finds one with multiple messages in thread. + */ +export async function findThreadWithMultipleMessages( + provider: EmailProvider, + minMessages = 2, +): Promise<{ threadId: string; messages: ParsedMessage[] }> { + const inboxMessages = await provider.getInboxMessages(50); + + // Group by threadId and find one with enough messages + const threadIds = [...new Set(inboxMessages.map((m) => m.threadId))]; + + for (const threadId of threadIds) { + const messages = await provider.getThreadMessages(threadId); + if (messages.length >= minMessages) { + return { threadId, messages }; + } + } + + throw new Error( + `TEST PREREQUISITE NOT MET: No thread found with ${minMessages}+ messages. ` + + "Send an email to the test account and reply to it to create a multi-message thread.", + ); +} diff --git a/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts b/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts index e07a02d29c..fa15da5c63 100644 --- a/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts +++ b/apps/web/__tests__/e2e/labeling/microsoft-thread-category-removal.test.ts @@ -18,6 +18,7 @@ import { getRuleLabel } from "@/utils/rule/consts"; import { SystemType } from "@/generated/prisma/enums"; import { removeConflictingThreadStatusLabels } from "@/utils/reply-tracker/label-helpers"; import { createScopedLogger } from "@/utils/logger"; +import { findThreadWithMultipleMessages } from "./helpers"; const RUN_E2E_TESTS = process.env.RUN_E2E_TESTS; const TEST_OUTLOOK_EMAIL = process.env.TEST_OUTLOOK_EMAIL; @@ -232,29 +233,3 @@ describe.skipIf(!RUN_E2E_TESTS)( }); }, ); - -/** - * Finds a thread with at least minMessages messages from the inbox. - * Looks through recent inbox messages and finds one with multiple messages in thread. - */ -async function findThreadWithMultipleMessages( - provider: EmailProvider, - minMessages = 2, -): Promise<{ threadId: string; messages: ParsedMessage[] }> { - const inboxMessages = await provider.getInboxMessages(50); - - // Group by threadId and find one with enough messages - const threadIds = [...new Set(inboxMessages.map((m) => m.threadId))]; - - for (const threadId of threadIds) { - const messages = await provider.getThreadMessages(threadId); - if (messages.length >= minMessages) { - return { threadId, messages }; - } - } - - throw new Error( - `TEST PREREQUISITE NOT MET: No thread found with ${minMessages}+ messages. ` + - "Send an email to the test account and reply to it to create a multi-message thread.", - ); -}