- 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
280 lines
7.9 KiB
TypeScript
280 lines
7.9 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|