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
This commit is contained in:
279
apps/api/src/brain/brain.controller.test.ts
Normal file
279
apps/api/src/brain/brain.controller.test.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { BrainController } from "./brain.controller";
|
||||
import { BrainService, BrainQueryResult, BrainContext } from "./brain.service";
|
||||
import { TaskStatus, TaskPriority, ProjectStatus, EntityType } from "@prisma/client";
|
||||
|
||||
describe("BrainController", () => {
|
||||
let controller: BrainController;
|
||||
let mockService: {
|
||||
query: ReturnType<typeof vi.fn>;
|
||||
getContext: ReturnType<typeof vi.fn>;
|
||||
search: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
const mockWorkspaceId = "123e4567-e89b-12d3-a456-426614174000";
|
||||
|
||||
const mockQueryResult: BrainQueryResult = {
|
||||
tasks: [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Test Task",
|
||||
description: null,
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.HIGH,
|
||||
dueDate: null,
|
||||
assignee: null,
|
||||
project: null,
|
||||
},
|
||||
],
|
||||
events: [
|
||||
{
|
||||
id: "event-1",
|
||||
title: "Test Event",
|
||||
description: null,
|
||||
startTime: new Date("2025-02-01T10:00:00Z"),
|
||||
endTime: new Date("2025-02-01T11:00:00Z"),
|
||||
allDay: false,
|
||||
location: null,
|
||||
project: null,
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
id: "project-1",
|
||||
name: "Test Project",
|
||||
description: null,
|
||||
status: ProjectStatus.ACTIVE,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
color: null,
|
||||
_count: { tasks: 5, events: 2 },
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
totalTasks: 1,
|
||||
totalEvents: 1,
|
||||
totalProjects: 1,
|
||||
filters: {},
|
||||
},
|
||||
};
|
||||
|
||||
const mockContext: BrainContext = {
|
||||
timestamp: new Date(),
|
||||
workspace: { id: mockWorkspaceId, name: "Test Workspace" },
|
||||
summary: {
|
||||
activeTasks: 10,
|
||||
overdueTasks: 2,
|
||||
upcomingEvents: 5,
|
||||
activeProjects: 3,
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: "task-1",
|
||||
title: "Test Task",
|
||||
status: TaskStatus.IN_PROGRESS,
|
||||
priority: TaskPriority.HIGH,
|
||||
dueDate: null,
|
||||
isOverdue: false,
|
||||
},
|
||||
],
|
||||
events: [
|
||||
{
|
||||
id: "event-1",
|
||||
title: "Test Event",
|
||||
startTime: new Date("2025-02-01T10:00:00Z"),
|
||||
endTime: new Date("2025-02-01T11:00:00Z"),
|
||||
allDay: false,
|
||||
location: null,
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
id: "project-1",
|
||||
name: "Test Project",
|
||||
status: ProjectStatus.ACTIVE,
|
||||
taskCount: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockService = {
|
||||
query: vi.fn().mockResolvedValue(mockQueryResult),
|
||||
getContext: vi.fn().mockResolvedValue(mockContext),
|
||||
search: vi.fn().mockResolvedValue(mockQueryResult),
|
||||
};
|
||||
|
||||
controller = new BrainController(mockService as unknown as BrainService);
|
||||
});
|
||||
|
||||
describe("query", () => {
|
||||
it("should call service.query with merged workspaceId", async () => {
|
||||
const queryDto = {
|
||||
workspaceId: "different-id",
|
||||
query: "What tasks are due?",
|
||||
};
|
||||
|
||||
const result = await controller.query(queryDto, mockWorkspaceId);
|
||||
|
||||
expect(mockService.query).toHaveBeenCalledWith({
|
||||
...queryDto,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(result).toEqual(mockQueryResult);
|
||||
});
|
||||
|
||||
it("should handle query with filters", async () => {
|
||||
const queryDto = {
|
||||
workspaceId: mockWorkspaceId,
|
||||
entities: [EntityType.TASK, EntityType.EVENT],
|
||||
tasks: { status: TaskStatus.IN_PROGRESS },
|
||||
events: { upcoming: true },
|
||||
};
|
||||
|
||||
await controller.query(queryDto, mockWorkspaceId);
|
||||
|
||||
expect(mockService.query).toHaveBeenCalledWith({
|
||||
...queryDto,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle query with search term", async () => {
|
||||
const queryDto = {
|
||||
workspaceId: mockWorkspaceId,
|
||||
search: "important",
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
await controller.query(queryDto, mockWorkspaceId);
|
||||
|
||||
expect(mockService.query).toHaveBeenCalledWith({
|
||||
...queryDto,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return query result structure", async () => {
|
||||
const result = await controller.query(
|
||||
{ workspaceId: mockWorkspaceId },
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("tasks");
|
||||
expect(result).toHaveProperty("events");
|
||||
expect(result).toHaveProperty("projects");
|
||||
expect(result).toHaveProperty("meta");
|
||||
expect(result.tasks).toHaveLength(1);
|
||||
expect(result.events).toHaveLength(1);
|
||||
expect(result.projects).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContext", () => {
|
||||
it("should call service.getContext with merged workspaceId", async () => {
|
||||
const contextDto = {
|
||||
workspaceId: "different-id",
|
||||
includeTasks: true,
|
||||
};
|
||||
|
||||
const result = await controller.getContext(contextDto, mockWorkspaceId);
|
||||
|
||||
expect(mockService.getContext).toHaveBeenCalledWith({
|
||||
...contextDto,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
expect(result).toEqual(mockContext);
|
||||
});
|
||||
|
||||
it("should handle context with all options", async () => {
|
||||
const contextDto = {
|
||||
workspaceId: mockWorkspaceId,
|
||||
includeTasks: true,
|
||||
includeEvents: true,
|
||||
includeProjects: true,
|
||||
eventDays: 14,
|
||||
};
|
||||
|
||||
await controller.getContext(contextDto, mockWorkspaceId);
|
||||
|
||||
expect(mockService.getContext).toHaveBeenCalledWith({
|
||||
...contextDto,
|
||||
workspaceId: mockWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return context structure", async () => {
|
||||
const result = await controller.getContext(
|
||||
{ workspaceId: mockWorkspaceId },
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("timestamp");
|
||||
expect(result).toHaveProperty("workspace");
|
||||
expect(result).toHaveProperty("summary");
|
||||
expect(result.summary).toHaveProperty("activeTasks");
|
||||
expect(result.summary).toHaveProperty("overdueTasks");
|
||||
expect(result.summary).toHaveProperty("upcomingEvents");
|
||||
expect(result.summary).toHaveProperty("activeProjects");
|
||||
});
|
||||
|
||||
it("should include detailed lists when requested", async () => {
|
||||
const result = await controller.getContext(
|
||||
{
|
||||
workspaceId: mockWorkspaceId,
|
||||
includeTasks: true,
|
||||
includeEvents: true,
|
||||
includeProjects: true,
|
||||
},
|
||||
mockWorkspaceId
|
||||
);
|
||||
|
||||
expect(result.tasks).toBeDefined();
|
||||
expect(result.events).toBeDefined();
|
||||
expect(result.projects).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it("should call service.search with parameters", async () => {
|
||||
const result = await controller.search("test query", "10", mockWorkspaceId);
|
||||
|
||||
expect(mockService.search).toHaveBeenCalledWith(mockWorkspaceId, "test query", 10);
|
||||
expect(result).toEqual(mockQueryResult);
|
||||
});
|
||||
|
||||
it("should use default limit when not provided", async () => {
|
||||
await controller.search("test", undefined as unknown as string, mockWorkspaceId);
|
||||
|
||||
expect(mockService.search).toHaveBeenCalledWith(mockWorkspaceId, "test", 20);
|
||||
});
|
||||
|
||||
it("should cap limit at 100", async () => {
|
||||
await controller.search("test", "500", mockWorkspaceId);
|
||||
|
||||
expect(mockService.search).toHaveBeenCalledWith(mockWorkspaceId, "test", 100);
|
||||
});
|
||||
|
||||
it("should handle empty search term", async () => {
|
||||
await controller.search(undefined as unknown as string, "10", mockWorkspaceId);
|
||||
|
||||
expect(mockService.search).toHaveBeenCalledWith(mockWorkspaceId, "", 10);
|
||||
});
|
||||
|
||||
it("should handle invalid limit", async () => {
|
||||
await controller.search("test", "invalid", mockWorkspaceId);
|
||||
|
||||
expect(mockService.search).toHaveBeenCalledWith(mockWorkspaceId, "test", 20);
|
||||
});
|
||||
|
||||
it("should return search result structure", async () => {
|
||||
const result = await controller.search("test", "10", mockWorkspaceId);
|
||||
|
||||
expect(result).toHaveProperty("tasks");
|
||||
expect(result).toHaveProperty("events");
|
||||
expect(result).toHaveProperty("projects");
|
||||
expect(result).toHaveProperty("meta");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user