Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/web/app/api/ai/digest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ export const POST = withError(

logger.with({ emailAccountId, messageId: message.id });

const emailAccount = await getEmailAccountWithAi({ emailAccountId });
const emailAccount = await getEmailAccountWithAi({
emailAccountId,
});
if (!emailAccount) {
throw new Error("Email account not found");
}
Expand All @@ -182,7 +184,7 @@ export const POST = withError(
},
});

if (!summary) {
if (!summary?.content) {
logger.info("Skipping digest item because it is not worth summarizing");
return new NextResponse("OK", { status: 200 });
}
Expand Down
12 changes: 12 additions & 0 deletions apps/web/app/api/resend/digest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,17 @@ async function sendEmail({
return acc;
}, {} as Digest);

if (Object.keys(executedRulesByRule).length === 0) {
logger.info(
"No executed rules found, skipping digest email",
loggerOptions,
);
return {
success: true,
message: "No executed rules found, skipping digest email",
};
}

const token = await createUnsubscribeToken({ emailAccountId });

logger.info("Sending digest email", loggerOptions);
Expand All @@ -226,6 +237,7 @@ async function sendEmail({
date: new Date(),
ruleNames: Object.fromEntries(ruleNameMap),
...executedRulesByRule,
emailAccountId,
},
});

Expand Down
262 changes: 165 additions & 97 deletions apps/web/utils/ai/digest/summarize-email-for-digest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,61 @@ import { schema as DigestEmailSummarySchema } from "@/utils/ai/digest/summarize-
import type { EmailAccountWithAI } from "@/utils/llms/types";
import type { EmailForLLM } from "@/utils/types";

// Type for the email account with name property as expected by the function
type EmailAccountForDigest = EmailAccountWithAI & {
name: string | null;
};

// Run with: pnpm test-ai TEST

vi.mock("server-only", () => ({}));

const isAiTest = process.env.RUN_AI_TESTS === "true";

// Helper functions for common test data
function getEmailAccount(overrides = {}): EmailAccountForDigest {
return {
id: "email-account-id",
userId: "user1",
email: "user@test.com",
about: "Software engineer working on email automation",
name: "Test User",
account: {
provider: "gmail",
},
user: {
aiModel: "gpt-4",
aiProvider: "openai",
aiApiKey: process.env.OPENAI_API_KEY || null,
},
...overrides,
};
}

function getTestEmail(overrides = {}): EmailForLLM {
return {
id: "email-id",
from: "sender@example.com",
to: "user@test.com",
subject: "Test Email",
content: "This is a test email content",
...overrides,
};
}

describe.runIf(isAiTest)("aiSummarizeEmailForDigest", () => {
beforeEach(() => {
vi.clearAllMocks();
});

test("summarizes email with structured data", async () => {
const emailAccount: EmailAccountWithAI = {
id: "email-account-id",
userId: "user1",
email: "user@test.com",
about: null,
user: {
aiModel: "gpt-4",
aiProvider: "openai",
aiApiKey: process.env.OPENAI_API_KEY || null,
},
};

const messageToSummarize: EmailForLLM = {
id: "email-id",
test("successfully summarizes email with order details", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "orders@example.com",
to: "user@test.com",
subject: "Order Confirmation #12345",
content:
"Thank you for your order! Order #12345 has been confirmed. Date: 2024-03-20. Items: 3. Total: $99.99",
};
});

const result = await aiSummarizeEmailForDigest({
ruleName: "order",
Expand All @@ -46,37 +69,22 @@ describe.runIf(isAiTest)("aiSummarizeEmailForDigest", () => {
console.debug("Generated content:\n", result);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove console.debug — violates repo guideline “Don't use console.”

These logs will fail linting in CI.

-    console.debug("Generated content:\n", result);
-    console.debug("Generated content:\n", result);
-    console.debug("Generated content:\n", result);
-    console.debug(`Generated content for ${category}:\n`, result);
-    console.debug("Generated content:\n", result);
-    console.debug("Generated content:\n", result);
-    console.debug("Generated content:\n", result);
-    console.debug("Generated content:\n", result);

Also applies to: 95-95, 119-119, 157-157, 181-181, 204-204, 226-226, 249-249

🤖 Prompt for AI Agents
apps/web/utils/ai/digest/summarize-email-for-digest.test.ts lines
69,95,119,157,181,204,226,249: remove the console.debug(...) calls (they violate
the "Don't use console." guideline and fail CI linting); either delete those
debug statements or replace them with the repo-approved logger API (or
Jest-friendly logging/mocks) and ensure any replacement respects lint rules,
then run tests/lint to confirm no console usage remains.


expect(result).toMatchObject({
type: "structured",
content: expect.arrayContaining([
expect.objectContaining({
label: expect.any(String),
value: expect.any(String),
}),
]),
content: expect.any(String),
});

// Verify the result matches the schema
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
}, 15_000);

test("summarizes email with unstructured content", async () => {
const emailAccount: EmailAccountWithAI = {
id: "email-account-id",
userId: "user1",
email: "user@test.com",
about: null,
user: {
aiModel: "gpt-4",
aiProvider: "openai",
aiApiKey: process.env.OPENAI_API_KEY || null,
},
};

const messageToSummarize: EmailForLLM = {
id: "email-id",
test("successfully summarizes email with meeting notes", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "team@example.com",
to: "user@test.com",
subject: "Weekly Team Meeting Notes",
content:
"Hi team, Here are the notes from our weekly meeting: 1. Project timeline updated - Phase 1 completion delayed by 1 week 2. New team member joining next week 3. Client presentation scheduled for Friday",
};
});

const result = await aiSummarizeEmailForDigest({
ruleName: "meeting",
Expand All @@ -87,31 +95,20 @@ describe.runIf(isAiTest)("aiSummarizeEmailForDigest", () => {
console.debug("Generated content:\n", result);

expect(result).toMatchObject({
type: "unstructured",
content: expect.any(String),
});

const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
}, 15_000);

test("handles empty email content", async () => {
const emailAccount: EmailAccountWithAI = {
id: "email-account-id",
userId: "user1",
email: "user@test.com",
about: null,
user: {
aiModel: "gpt-4",
aiProvider: "openai",
aiApiKey: process.env.OPENAI_API_KEY || null,
},
};

const messageToSummarize: EmailForLLM = {
id: "email-id",
test("handles empty email content gracefully", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "empty@example.com",
to: "user@test.com",
subject: "Empty Email",
content: "",
};
});

const result = await aiSummarizeEmailForDigest({
ruleName: "other",
Expand All @@ -121,25 +118,13 @@ describe.runIf(isAiTest)("aiSummarizeEmailForDigest", () => {

console.debug("Generated content:\n", result);

// Empty emails should return unstructured content
expect(result).toMatchObject({
type: "unstructured",
content: expect.any(String),
});
}, 15_000);

test("handles null message", async () => {
const emailAccount: EmailAccountWithAI = {
id: "email-account-id",
userId: "user1",
email: "user@test.com",
about: null,
user: {
aiModel: "gpt-4",
aiProvider: "openai",
aiApiKey: process.env.OPENAI_API_KEY || null,
},
};
test("handles null message gracefully", async () => {
const emailAccount = getEmailAccount();

const result = await aiSummarizeEmailForDigest({
ruleName: "other",
Expand All @@ -150,38 +135,121 @@ describe.runIf(isAiTest)("aiSummarizeEmailForDigest", () => {
expect(result).toBeNull();
}, 15_000);

test("ensures consistent output format", async () => {
const emailAccount: EmailAccountWithAI = {
id: "email-account-id",
userId: "user1",
email: "user@test.com",
about: null,
user: {
aiModel: "gpt-4",
aiProvider: "openai",
aiApiKey: process.env.OPENAI_API_KEY || null,
},
};

const messageToSummarize: EmailForLLM = {
id: "email-id",
from: "invoice@example.com",
to: "user@test.com",
subject: "Invoice #INV-2024-001",
test("handles different user configurations", async () => {
const emailAccount = getEmailAccount({
about: "Marketing manager focused on customer engagement",
name: "Marketing User",
});

const messageToSummarize = getTestEmail({
from: "newsletter@company.com",
subject: "Weekly Marketing Update",
content:
"Invoice #INV-2024-001\nAmount: $150.00\nDue Date: 2024-04-01\nStatus: Pending",
};
"This week's marketing metrics: Email open rate: 25%, Click-through rate: 3.2%, Conversion rate: 1.8%",
});

const result = await aiSummarizeEmailForDigest({
ruleName: "invoice",
ruleName: "newsletter",
emailAccount,
messageToSummarize,
});

console.debug("Generated content:\n", result);

// Verify the result matches the schema
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);

Comment on lines +138 to +163
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate schema in “different user configurations” test

Keep tests uniform and catch schema regressions early.

   expect(result).toMatchObject({
     content: expect.any(String),
   });
+  const validationResult = DigestEmailSummarySchema.safeParse(result);
+  expect(validationResult.success).toBe(true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("handles different user configurations", async () => {
const emailAccount = getEmailAccount({
about: "Marketing manager focused on customer engagement",
name: "Marketing User",
});
const messageToSummarize = getTestEmail({
from: "newsletter@company.com",
subject: "Weekly Marketing Update",
content:
"Invoice #INV-2024-001\nAmount: $150.00\nDue Date: 2024-04-01\nStatus: Pending",
};
"This week's marketing metrics: Email open rate: 25%, Click-through rate: 3.2%, Conversion rate: 1.8%",
});
const result = await aiSummarizeEmailForDigest({
ruleName: "invoice",
ruleName: "newsletter",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
// Verify the result matches the schema
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);
test("handles different user configurations", async () => {
const emailAccount = getEmailAccount({
about: "Marketing manager focused on customer engagement",
name: "Marketing User",
});
const messageToSummarize = getTestEmail({
from: "newsletter@company.com",
subject: "Weekly Marketing Update",
content:
"This week's marketing metrics: Email open rate: 25%, Click-through rate: 3.2%, Conversion rate: 1.8%",
});
const result = await aiSummarizeEmailForDigest({
ruleName: "newsletter",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
expect(result).toMatchObject({
content: expect.any(String),
});
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
}, 15_000);

test("handles various email categories correctly", async () => {
const emailAccount = getEmailAccount();
const categories = ["invoice", "receipt", "travel", "notification"];

for (const category of categories) {
const messageToSummarize = getTestEmail({
from: `${category}@example.com`,
subject: `Test ${category} email`,
content: `This is a test ${category} email with sample content`,
});

const result = await aiSummarizeEmailForDigest({
ruleName: category,
emailAccount,
messageToSummarize,
});

console.debug(`Generated content for ${category}:\n`, result);

expect(result).toMatchObject({
content: expect.any(String),
});
}
}, 30_000);

Comment on lines +164 to +188
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Validate schema inside the categories loop

Strengthens coverage across categories.

       expect(result).toMatchObject({
         content: expect.any(String),
       });
+      const validationResult = DigestEmailSummarySchema.safeParse(result);
+      expect(validationResult.success).toBe(true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("handles various email categories correctly", async () => {
const emailAccount = getEmailAccount();
const categories = ["invoice", "receipt", "travel", "notification"];
for (const category of categories) {
const messageToSummarize = getTestEmail({
from: `${category}@example.com`,
subject: `Test ${category} email`,
content: `This is a test ${category} email with sample content`,
});
const result = await aiSummarizeEmailForDigest({
ruleName: category,
emailAccount,
messageToSummarize,
});
console.debug(`Generated content for ${category}:\n`, result);
expect(result).toMatchObject({
content: expect.any(String),
});
}
}, 30_000);
test("handles various email categories correctly", async () => {
const emailAccount = getEmailAccount();
const categories = ["invoice", "receipt", "travel", "notification"];
for (const category of categories) {
const messageToSummarize = getTestEmail({
from: `${category}@example.com`,
subject: `Test ${category} email`,
content: `This is a test ${category} email with sample content`,
});
const result = await aiSummarizeEmailForDigest({
ruleName: category,
emailAccount,
messageToSummarize,
});
console.debug(`Generated content for ${category}:\n`, result);
expect(result).toMatchObject({
content: expect.any(String),
});
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
}
}, 30_000);
🤖 Prompt for AI Agents
In apps/web/utils/ai/digest/summarize-email-for-digest.test.ts around lines
164-188, add schema validation inside the categories loop after obtaining
result: call the project’s existing digest/schema validator (e.g.,
validateDigestSchema(result) or equivalent) or add explicit expects for required
fields (e.g., expect(result).toMatchObject({ content: expect.any(String), title:
expect.any(String) }) and any other required keys), ensuring the result conforms
to the digest schema for each category before finishing the iteration.

test("handles promotional emails appropriately", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "promotions@store.com",
subject: "50% OFF Everything! Limited Time Only!",
content:
"Don't miss our biggest sale of the year! Everything is 50% off for the next 24 hours only!",
});

const result = await aiSummarizeEmailForDigest({
ruleName: "marketing",
emailAccount,
messageToSummarize,
});

console.debug("Generated content:\n", result);

expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);

Comment on lines +189 to +210
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Align promotional-email expectation with prompt rule (“return null”)

System prompt instructs: spam/promotional → return "null". Adjust assertion accordingly.

-  expect(result).toMatchObject({
-    content: expect.any(String),
-  });
+  const validationResult = DigestEmailSummarySchema.safeParse(result);
+  expect(validationResult.success).toBe(true);
+  expect(result?.content?.toLowerCase()).toBe("null");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("handles promotional emails appropriately", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "promotions@store.com",
subject: "50% OFF Everything! Limited Time Only!",
content:
"Don't miss our biggest sale of the year! Everything is 50% off for the next 24 hours only!",
});
const result = await aiSummarizeEmailForDigest({
ruleName: "marketing",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);
test("handles promotional emails appropriately", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "promotions@store.com",
subject: "50% OFF Everything! Limited Time Only!",
content:
"Don't miss our biggest sale of the year! Everything is 50% off for the next 24 hours only!",
});
const result = await aiSummarizeEmailForDigest({
ruleName: "marketing",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
// Validate against our Zod schema and assert promotional rule returns "null"
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
expect(result?.content?.toLowerCase()).toBe("null");
}, 15_000);
🤖 Prompt for AI Agents
In apps/web/utils/ai/digest/summarize-email-for-digest.test.ts around lines 189
to 210, the test currently expects a string content for promotional emails, but
the system prompt specifies spam/promotional messages should return null; update
the assertion to expect result toBeNull or toMatchObject({ content: null })
consistent with how the function returns null (or adjust to
expect(result).toBeNull() if the function returns null directly), and remove or
change any debug/expectations that assume a string so the test aligns with the
prompt rule.

test("handles direct messages to user in second person", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "hr@company.com",
subject: "Your Annual Review is Due",
content:
"Hi Test User, Your annual performance review is due by Friday. Please complete the self-assessment form and schedule a meeting with your manager.",
});

const result = await aiSummarizeEmailForDigest({
ruleName: "hr",
emailAccount,
messageToSummarize,
});

console.debug("Generated content:\n", result);

expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);

Comment on lines +211 to +232
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Assert second-person phrasing for direct messages

Prompt requires second-person summaries for direct messages.

   expect(result).toMatchObject({
     content: expect.any(String),
   });
+  const validationResult = DigestEmailSummarySchema.safeParse(result);
+  expect(validationResult.success).toBe(true);
+  expect(result?.content).toMatch(/\bYou\b/i);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("handles direct messages to user in second person", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "hr@company.com",
subject: "Your Annual Review is Due",
content:
"Hi Test User, Your annual performance review is due by Friday. Please complete the self-assessment form and schedule a meeting with your manager.",
});
const result = await aiSummarizeEmailForDigest({
ruleName: "hr",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);
test("handles direct messages to user in second person", async () => {
const emailAccount = getEmailAccount();
const messageToSummarize = getTestEmail({
from: "hr@company.com",
subject: "Your Annual Review is Due",
content:
"Hi Test User, Your annual performance review is due by Friday. Please complete the self-assessment form and schedule a meeting with your manager.",
});
const result = await aiSummarizeEmailForDigest({
ruleName: "hr",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
expect(result).toMatchObject({
content: expect.any(String),
});
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);
expect(result?.content).toMatch(/\bYou\b/i);
}, 15_000);
🤖 Prompt for AI Agents
In apps/web/utils/ai/digest/summarize-email-for-digest.test.ts around lines
211-232, the test only asserts the summary is a string but the prompt requires
second-person phrasing for direct messages; update the test to assert the
generated summary addresses the user in second person by adding an expectation
such as expect(result.content).toMatch(/\byou\b/i) or
expect(result.content).toMatch(/\b(you|your|you'll|you’re|you've)\b/i) (and
optionally assert it does not use third-person pronouns), keeping the existing
flow and timeout.

test("handles edge case with very long email content", async () => {
const emailAccount = getEmailAccount();
const longContent = `${"This is a very long email content. ".repeat(100)}End of long content.`;

const messageToSummarize = getTestEmail({
from: "long@example.com",
subject: "Very Long Email",
content: longContent,
});

const result = await aiSummarizeEmailForDigest({
ruleName: "other",
emailAccount,
messageToSummarize,
});

console.debug("Generated content:\n", result);

expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);
});
Comment on lines +233 to 255
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add schema validation for long-content case

Ensures truncation/formatting changes don’t break the contract.

   expect(result).toMatchObject({
     content: expect.any(String),
   });
+  const validationResult = DigestEmailSummarySchema.safeParse(result);
+  expect(validationResult.success).toBe(true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("handles edge case with very long email content", async () => {
const emailAccount = getEmailAccount();
const longContent = `${"This is a very long email content. ".repeat(100)}End of long content.`;
const messageToSummarize = getTestEmail({
from: "long@example.com",
subject: "Very Long Email",
content: longContent,
});
const result = await aiSummarizeEmailForDigest({
ruleName: "other",
emailAccount,
messageToSummarize,
});
console.debug("Generated content:\n", result);
expect(result).toMatchObject({
content: expect.any(String),
});
}, 15_000);
});
expect(result).toMatchObject({
content: expect.any(String),
});
const validationResult = DigestEmailSummarySchema.safeParse(result);
expect(validationResult.success).toBe(true);

Loading
Loading