Files
stack/apps/api/src/brain/brain.service.test.ts
Jason Woltje 1bd21b33d7 feat(#22): implement brain query API
- 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
2026-01-29 19:40:30 -06:00

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();
});
});
});