- Create brain module with service, controller, and DTOs - POST /api/brain/query - Structured queries for tasks, events, projects - GET /api/brain/context - Get current workspace context for agents - GET /api/brain/search - Search across all entities - Support filters: status, priority, date ranges, assignee, etc. - 41 tests covering service (27) and controller (14) - Integrated with AuthGuard, WorkspaceGuard, PermissionGuard
508 lines
14 KiB
TypeScript
508 lines
14 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import { BrainService } from "./brain.service";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { TaskStatus, TaskPriority, ProjectStatus, EntityType } from "@prisma/client";
|
|
|
|
describe("BrainService", () => {
|
|
let service: BrainService;
|
|
let mockPrisma: {
|
|
task: {
|
|
findMany: ReturnType<typeof vi.fn>;
|
|
count: ReturnType<typeof vi.fn>;
|
|
};
|
|
event: {
|
|
findMany: ReturnType<typeof vi.fn>;
|
|
count: ReturnType<typeof vi.fn>;
|
|
};
|
|
project: {
|
|
findMany: ReturnType<typeof vi.fn>;
|
|
count: ReturnType<typeof vi.fn>;
|
|
};
|
|
workspace: {
|
|
findUniqueOrThrow: ReturnType<typeof vi.fn>;
|
|
};
|
|
};
|
|
|
|
const mockWorkspaceId = "123e4567-e89b-12d3-a456-426614174000";
|
|
|
|
const mockTasks = [
|
|
{
|
|
id: "task-1",
|
|
title: "Test Task 1",
|
|
description: "Description 1",
|
|
status: TaskStatus.IN_PROGRESS,
|
|
priority: TaskPriority.HIGH,
|
|
dueDate: new Date("2025-02-01"),
|
|
assignee: { id: "user-1", name: "John Doe", email: "john@example.com" },
|
|
project: { id: "project-1", name: "Project 1", color: "#ff0000" },
|
|
},
|
|
{
|
|
id: "task-2",
|
|
title: "Test Task 2",
|
|
description: null,
|
|
status: TaskStatus.NOT_STARTED,
|
|
priority: TaskPriority.MEDIUM,
|
|
dueDate: null,
|
|
assignee: null,
|
|
project: null,
|
|
},
|
|
];
|
|
|
|
const mockEvents = [
|
|
{
|
|
id: "event-1",
|
|
title: "Test Event 1",
|
|
description: "Event description",
|
|
startTime: new Date("2025-02-01T10:00:00Z"),
|
|
endTime: new Date("2025-02-01T11:00:00Z"),
|
|
allDay: false,
|
|
location: "Conference Room A",
|
|
project: { id: "project-1", name: "Project 1", color: "#ff0000" },
|
|
},
|
|
];
|
|
|
|
const mockProjects = [
|
|
{
|
|
id: "project-1",
|
|
name: "Project 1",
|
|
description: "Project description",
|
|
status: ProjectStatus.ACTIVE,
|
|
startDate: new Date("2025-01-01"),
|
|
endDate: new Date("2025-06-30"),
|
|
color: "#ff0000",
|
|
_count: { tasks: 5, events: 3 },
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockPrisma = {
|
|
task: {
|
|
findMany: vi.fn().mockResolvedValue(mockTasks),
|
|
count: vi.fn().mockResolvedValue(10),
|
|
},
|
|
event: {
|
|
findMany: vi.fn().mockResolvedValue(mockEvents),
|
|
count: vi.fn().mockResolvedValue(5),
|
|
},
|
|
project: {
|
|
findMany: vi.fn().mockResolvedValue(mockProjects),
|
|
count: vi.fn().mockResolvedValue(3),
|
|
},
|
|
workspace: {
|
|
findUniqueOrThrow: vi.fn().mockResolvedValue({
|
|
id: mockWorkspaceId,
|
|
name: "Test Workspace",
|
|
}),
|
|
},
|
|
};
|
|
|
|
service = new BrainService(mockPrisma as unknown as PrismaService);
|
|
});
|
|
|
|
describe("query", () => {
|
|
it("should query all entity types by default", async () => {
|
|
const result = await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
});
|
|
|
|
expect(result.tasks).toHaveLength(2);
|
|
expect(result.events).toHaveLength(1);
|
|
expect(result.projects).toHaveLength(1);
|
|
expect(result.meta.totalTasks).toBe(2);
|
|
expect(result.meta.totalEvents).toBe(1);
|
|
expect(result.meta.totalProjects).toBe(1);
|
|
});
|
|
|
|
it("should query only specified entity types", async () => {
|
|
const result = await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
entities: [EntityType.TASK],
|
|
});
|
|
|
|
expect(result.tasks).toHaveLength(2);
|
|
expect(result.events).toHaveLength(0);
|
|
expect(result.projects).toHaveLength(0);
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalled();
|
|
expect(mockPrisma.event.findMany).not.toHaveBeenCalled();
|
|
expect(mockPrisma.project.findMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should apply task filters", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
tasks: {
|
|
status: TaskStatus.IN_PROGRESS,
|
|
priority: TaskPriority.HIGH,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
workspaceId: mockWorkspaceId,
|
|
status: TaskStatus.IN_PROGRESS,
|
|
priority: TaskPriority.HIGH,
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply task statuses filter (array)", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
tasks: {
|
|
statuses: [TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS],
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
status: { in: [TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS] },
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply overdue filter", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
tasks: {
|
|
overdue: true,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
dueDate: expect.objectContaining({ lt: expect.any(Date) }),
|
|
status: { in: [TaskStatus.NOT_STARTED, TaskStatus.IN_PROGRESS] },
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply unassigned filter", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
tasks: {
|
|
unassigned: true,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
assigneeId: null,
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply due date range filter", async () => {
|
|
const dueDateFrom = new Date("2025-01-01");
|
|
const dueDateTo = new Date("2025-01-31");
|
|
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
tasks: {
|
|
dueDateFrom,
|
|
dueDateTo,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
dueDate: { gte: dueDateFrom, lte: dueDateTo },
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply event filters", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
events: {
|
|
allDay: true,
|
|
upcoming: true,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.event.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
allDay: true,
|
|
startTime: { gte: expect.any(Date) },
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply event date range filter", async () => {
|
|
const startFrom = new Date("2025-02-01");
|
|
const startTo = new Date("2025-02-28");
|
|
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
events: {
|
|
startFrom,
|
|
startTo,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.event.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
startTime: { gte: startFrom, lte: startTo },
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply project filters", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
projects: {
|
|
status: ProjectStatus.ACTIVE,
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.project.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
status: ProjectStatus.ACTIVE,
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply project statuses filter (array)", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
projects: {
|
|
statuses: [ProjectStatus.PLANNING, ProjectStatus.ACTIVE],
|
|
},
|
|
});
|
|
|
|
expect(mockPrisma.project.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
status: { in: [ProjectStatus.PLANNING, ProjectStatus.ACTIVE] },
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply search term across tasks", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
search: "test",
|
|
entities: [EntityType.TASK],
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
OR: [
|
|
{ title: { contains: "test", mode: "insensitive" } },
|
|
{ description: { contains: "test", mode: "insensitive" } },
|
|
],
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply search term across events", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
search: "conference",
|
|
entities: [EntityType.EVENT],
|
|
});
|
|
|
|
expect(mockPrisma.event.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
OR: [
|
|
{ title: { contains: "conference", mode: "insensitive" } },
|
|
{ description: { contains: "conference", mode: "insensitive" } },
|
|
{ location: { contains: "conference", mode: "insensitive" } },
|
|
],
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should apply search term across projects", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
search: "project",
|
|
entities: [EntityType.PROJECT],
|
|
});
|
|
|
|
expect(mockPrisma.project.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
OR: [
|
|
{ name: { contains: "project", mode: "insensitive" } },
|
|
{ description: { contains: "project", mode: "insensitive" } },
|
|
],
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should respect limit parameter", async () => {
|
|
await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
limit: 5,
|
|
});
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
take: 5,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should include query and filters in meta", async () => {
|
|
const result = await service.query({
|
|
workspaceId: mockWorkspaceId,
|
|
query: "What tasks are due?",
|
|
tasks: { status: TaskStatus.IN_PROGRESS },
|
|
});
|
|
|
|
expect(result.meta.query).toBe("What tasks are due?");
|
|
expect(result.meta.filters.tasks).toEqual({ status: TaskStatus.IN_PROGRESS });
|
|
});
|
|
});
|
|
|
|
describe("getContext", () => {
|
|
it("should return context with summary", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
});
|
|
|
|
expect(result.timestamp).toBeInstanceOf(Date);
|
|
expect(result.workspace.id).toBe(mockWorkspaceId);
|
|
expect(result.workspace.name).toBe("Test Workspace");
|
|
expect(result.summary).toEqual({
|
|
activeTasks: 10,
|
|
overdueTasks: 10,
|
|
upcomingEvents: 5,
|
|
activeProjects: 3,
|
|
});
|
|
});
|
|
|
|
it("should include tasks when requested", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
includeTasks: true,
|
|
});
|
|
|
|
expect(result.tasks).toBeDefined();
|
|
expect(result.tasks).toHaveLength(2);
|
|
expect(result.tasks![0].isOverdue).toBeDefined();
|
|
});
|
|
|
|
it("should include events when requested", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
includeEvents: true,
|
|
});
|
|
|
|
expect(result.events).toBeDefined();
|
|
expect(result.events).toHaveLength(1);
|
|
});
|
|
|
|
it("should include projects when requested", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
includeProjects: true,
|
|
});
|
|
|
|
expect(result.projects).toBeDefined();
|
|
expect(result.projects).toHaveLength(1);
|
|
expect(result.projects![0].taskCount).toBeDefined();
|
|
});
|
|
|
|
it("should use custom eventDays", async () => {
|
|
await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
eventDays: 14,
|
|
});
|
|
|
|
expect(mockPrisma.event.count).toHaveBeenCalled();
|
|
expect(mockPrisma.event.findMany).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should not include tasks when explicitly disabled", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
includeTasks: false,
|
|
includeEvents: true,
|
|
includeProjects: true,
|
|
});
|
|
|
|
expect(result.tasks).toBeUndefined();
|
|
expect(result.events).toBeDefined();
|
|
expect(result.projects).toBeDefined();
|
|
});
|
|
|
|
it("should not include events when explicitly disabled", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
includeTasks: true,
|
|
includeEvents: false,
|
|
includeProjects: true,
|
|
});
|
|
|
|
expect(result.tasks).toBeDefined();
|
|
expect(result.events).toBeUndefined();
|
|
expect(result.projects).toBeDefined();
|
|
});
|
|
|
|
it("should not include projects when explicitly disabled", async () => {
|
|
const result = await service.getContext({
|
|
workspaceId: mockWorkspaceId,
|
|
includeTasks: true,
|
|
includeEvents: true,
|
|
includeProjects: false,
|
|
});
|
|
|
|
expect(result.tasks).toBeDefined();
|
|
expect(result.events).toBeDefined();
|
|
expect(result.projects).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("search", () => {
|
|
it("should search across all entities", async () => {
|
|
const result = await service.search(mockWorkspaceId, "test");
|
|
|
|
expect(result.tasks).toHaveLength(2);
|
|
expect(result.events).toHaveLength(1);
|
|
expect(result.projects).toHaveLength(1);
|
|
expect(result.meta.query).toBe("test");
|
|
});
|
|
|
|
it("should respect limit parameter", async () => {
|
|
await service.search(mockWorkspaceId, "test", 5);
|
|
|
|
expect(mockPrisma.task.findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
take: 5,
|
|
})
|
|
);
|
|
});
|
|
|
|
it("should handle empty search term", async () => {
|
|
const result = await service.search(mockWorkspaceId, "");
|
|
|
|
expect(result.tasks).toBeDefined();
|
|
expect(result.events).toBeDefined();
|
|
expect(result.projects).toBeDefined();
|
|
});
|
|
});
|
|
});
|