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: {
- */}
Rule History
-
+ {/*
Report
-
+ */}
);
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.",
- );
-}