Skip to content

Commit

Permalink
Merge pull request #270 from elie222/loading-onboarding-modal
Browse files Browse the repository at this point in the history
Multi condition supports. Allow smart categories in rules
  • Loading branch information
elie222 authored Dec 21, 2024
2 parents 4927b49 + 25490c4 commit 7819ab8
Show file tree
Hide file tree
Showing 74 changed files with 2,922 additions and 1,330 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Run Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=\"$(pnpm store path --silent)\"" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install

- name: Run tests
run: pnpm -F inbox-zero-ai test
env:
RUN_AI_TESTS: false
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/postgres"
NEXTAUTH_SECRET: "secret"
GOOGLE_CLIENT_ID: "client_id"
GOOGLE_CLIENT_SECRET: "client_secret"
GOOGLE_PUBSUB_TOPIC_NAME: "topic"
9 changes: 6 additions & 3 deletions apps/web/__tests__/ai-categorize-senders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
import { defaultCategory } from "@/utils/categories";
import { aiCategorizeSender } from "@/utils/ai/categorize-sender/ai-categorize-single-sender";

// pnpm test-ai ai-categorize-senders

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

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

// Test data setup
const testUser = {
email: "[email protected]",
aiProvider: null,
Expand Down Expand Up @@ -44,7 +47,7 @@ const testSenders = [
},
];

describe("AI Sender Categorization", () => {
describe.skipIf(!isAiTest)("AI Sender Categorization", () => {
describe("Bulk Categorization", () => {
it("should categorize senders with snippets using AI", async () => {
const result = await aiCategorizeSenders({
Expand Down Expand Up @@ -125,7 +128,7 @@ describe("AI Sender Categorization", () => {
expect(result?.category).toBe(expectedCategory);
}
}
}, 15_000);
}, 30_000);

it("should handle unknown sender appropriately", async () => {
const unknownSender = testSenders.find(
Expand Down
17 changes: 13 additions & 4 deletions apps/web/__tests__/ai-choose-args.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { describe, expect, test, vi } from "vitest";
import { getActionItemsWithAiArgs } from "@/utils/ai/choose-rule/ai-choose-args";
import { type Action, ActionType, RuleType } from "@prisma/client";
import { type Action, ActionType, LogicalOperator } from "@prisma/client";
import type { RuleWithActions } from "@/utils/types";

// pnpm test-ai ai-choose-args

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

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

describe("getActionItemsWithAiArgs", () => {
describe.skipIf(!isAiTest)("getActionItemsWithAiArgs", () => {
test("should return actions unchanged when no AI args needed", async () => {
const actions = [getAction({})];
const rule = getRule("Test rule", actions);
Expand Down Expand Up @@ -167,7 +172,10 @@ function getAction(action: Partial<Action> = {}): Action {
};
}

function getRule(instructions: string, actions: Action[] = []) {
function getRule(
instructions: string,
actions: Action[] = [],
): RuleWithActions {
return {
instructions,
name: "Test Rule",
Expand All @@ -183,9 +191,10 @@ function getRule(instructions: string, actions: Action[] = []) {
subject: null,
body: null,
to: null,
type: RuleType.AI,
enabled: true,
categoryFilterType: null,
conditionalOperator: LogicalOperator.AND,
type: null,
};
}

Expand Down
165 changes: 76 additions & 89 deletions apps/web/__tests__/ai-choose-rule.test.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,101 @@
import { expect, test, vi } from "vitest";
import { chooseRule } from "@/utils/ai/choose-rule/choose";
import { type Action, ActionType, RuleType } from "@prisma/client";
import { describe, expect, test, vi } from "vitest";
import { aiChooseRule } from "@/utils/ai/choose-rule/ai-choose-rule";
import { type Action, ActionType, LogicalOperator } from "@prisma/client";

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

test("Should return no rule when no rules passed", async () => {
const result = await chooseRule({
rules: [],
email: getEmail(),
user: getUser(),
});

expect(result).toEqual({ reason: "No rules" });
});
// pnpm test-ai ai-choose-rule

test("Should return correct rule when only one rule passed", async () => {
const rule = getRule(
"Match emails that have the word 'test' in the subject line",
);
const isAiTest = process.env.RUN_AI_TESTS === "true";

const result = await chooseRule({
email: getEmail({ subject: "test" }),
rules: [rule],
user: getUser(),
});
vi.mock("server-only", () => ({}));

expect(result).toEqual({ rule, reason: expect.any(String), actionItems: [] });
});
describe.skipIf(!isAiTest)("aiChooseRule", () => {
test("Should return no rule when no rules passed", async () => {
const result = await aiChooseRule({
rules: [],
email: getEmail(),
user: getUser(),
});

test("Should return correct rule when multiple rules passed", async () => {
const rule1 = getRule(
"Match emails that have the word 'test' in the subject line",
);
const rule2 = getRule(
"Match emails that have the word 'remember' in the subject line",
);

const result = await chooseRule({
rules: [rule1, rule2],
email: getEmail({ subject: "remember that call" }),
user: getUser(),
expect(result).toEqual({ reason: "No rules" });
});

expect(result).toEqual({
rule: rule2,
reason: expect.any(String),
actionItems: [],
test("Should return correct rule when only one rule passed", async () => {
const rule = getRule(
"Match emails that have the word 'test' in the subject line",
);

const result = await aiChooseRule({
email: getEmail({ subject: "test" }),
rules: [rule],
user: getUser(),
});

expect(result).toEqual({
rule,
reason: expect.any(String),
});
});
});

test("Should generate action arguments", async () => {
const rule1 = getRule(
"Match emails that have the word 'question' in the subject line",
);
const rule2 = getRule("Match emails asking for a joke", [
{
id: "id",
createdAt: new Date(),
updatedAt: new Date(),
type: ActionType.REPLY,
ruleId: "ruleId",
label: null,
subject: null,
content: "{{Write a joke}}",
to: null,
cc: null,
bcc: null,

labelPrompt: null,
subjectPrompt: null,
contentPrompt: null,
toPrompt: null,
ccPrompt: null,
bccPrompt: null,
},
]);

const result = await chooseRule({
rules: [rule1, rule2],
email: getEmail({ subject: "Joke", content: "Tell me a joke about sheep" }),
user: getUser(),
test("Should return correct rule when multiple rules passed", async () => {
const rule1 = getRule(
"Match emails that have the word 'test' in the subject line",
);
const rule2 = getRule(
"Match emails that have the word 'remember' in the subject line",
);

const result = await aiChooseRule({
rules: [rule1, rule2],
email: getEmail({ subject: "remember that call" }),
user: getUser(),
});

expect(result).toEqual({
rule: rule2,
reason: expect.any(String),
});
});

console.debug("Generated content:\n", result.actionItems?.[0].content);

expect(result).toEqual({
rule: rule2,
reason: expect.any(String),
actionItems: [
test("Should generate action arguments", async () => {
const rule1 = getRule(
"Match emails that have the word 'question' in the subject line",
);
const rule2 = getRule("Match emails asking for a joke", [
{
bcc: null,
cc: null,
content: expect.any(String),
id: "id",
createdAt: new Date(),
updatedAt: new Date(),
type: ActionType.REPLY,
ruleId: "ruleId",
label: null,
subject: null,
content: "{{Write a joke}}",
to: null,
type: "REPLY",
cc: null,
bcc: null,

labelPrompt: null,
subjectPrompt: null,
contentPrompt: null,
toPrompt: null,
ccPrompt: null,
bccPrompt: null,
createdAt: expect.any(Date),
updatedAt: expect.any(Date),
id: "id",
ruleId: "ruleId",
},
],
]);

const result = await aiChooseRule({
rules: [rule1, rule2],
email: getEmail({
subject: "Joke",
content: "Tell me a joke about sheep",
}),
user: getUser(),
});

expect(result).toEqual({
rule: rule2,
reason: expect.any(String),
});
});
});

Expand All @@ -129,9 +116,9 @@ function getRule(instructions: string, actions: Action[] = []) {
subject: null,
body: null,
to: null,
type: RuleType.AI,
enabled: true,
categoryFilterType: null,
conditionalOperator: LogicalOperator.AND,
};
}

Expand Down
6 changes: 5 additions & 1 deletion apps/web/__tests__/ai-create-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { aiGenerateGroupItems } from "@/utils/ai/group/create-group";
import { queryBatchMessages } from "@/utils/gmail/message";
import type { ParsedMessage } from "@/utils/types";

// pnpm test-ai ai-create-group

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

vi.mock("server-only", () => ({}));
vi.mock("@/utils/gmail/message", () => ({
queryBatchMessages: vi.fn(),
}));

describe("aiGenerateGroupItems", () => {
describe.skipIf(!isAiTest)("aiGenerateGroupItems", () => {
it("should generate group items based on user prompt", async () => {
const user = {
email: "[email protected]",
Expand Down
6 changes: 5 additions & 1 deletion apps/web/__tests__/ai-diff-rules.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { describe, it, expect, vi } from "vitest";
import { aiDiffRules } from "@/utils/ai/rule/diff-rules";

// pnpm test-ai ai-diff-rules

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

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

describe("aiDiffRules", () => {
describe.skipIf(!isAiTest)("aiDiffRules", () => {
it("should correctly identify added, edited, and removed rules", async () => {
const user = {
email: "[email protected]",
Expand Down
6 changes: 5 additions & 1 deletion apps/web/__tests__/ai-example-matches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { queryBatchMessages } from "@/utils/gmail/message";
import type { ParsedMessage } from "@/utils/types";
import { findExampleMatchesSchema } from "@/utils/ai/example-matches/find-example-matches";

// pnpm test-ai ai-find-example-matches

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

vi.mock("server-only", () => ({}));
vi.mock("@/utils/gmail/message", () => ({
queryBatchMessages: vi.fn(),
}));

describe("aiFindExampleMatches", () => {
describe.skipIf(!isAiTest)("aiFindExampleMatches", () => {
it("should find example matches based on user prompt", async () => {
const user = {
email: "[email protected]",
Expand Down
6 changes: 4 additions & 2 deletions apps/web/__tests__/ai-find-snippets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { describe, expect, test, vi } from "vitest";
import { aiFindSnippets } from "@/utils/ai/snippets/find-snippets";
import type { EmailForLLM } from "@/utils/ai/choose-rule/stringify-email";

// pnpm test ai-find-snippets
// pnpm test-ai ai-find-snippets

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

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

describe("aiFindSnippets", () => {
describe.skipIf(!isAiTest)("aiFindSnippets", () => {
test("should find snippets in similar emails", async () => {
const emails = [
getEmail({
Expand Down
Loading

1 comment on commit 7819ab8

@vercel
Copy link

@vercel vercel bot commented on 7819ab8 Dec 21, 2024

Choose a reason for hiding this comment

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

Please sign in to comment.