Files
stack/apps/api/src/agent-tasks/agent-tasks.service.spec.ts
Jason Woltje 12abdfe81d feat(#93): implement agent spawn via federation
Implements FED-010: Agent Spawn via Federation feature that enables
spawning and managing Claude agents on remote federated Mosaic Stack
instances via COMMAND message type.

Features:
- Federation agent command types (spawn, status, kill)
- FederationAgentService for handling agent operations
- Integration with orchestrator's agent spawner/lifecycle services
- API endpoints for spawning, querying status, and killing agents
- Full command routing through federation COMMAND infrastructure
- Comprehensive test coverage (12/12 tests passing)

Architecture:
- Hub → Spoke: Spawn agents on remote instances
- Command flow: FederationController → FederationAgentService →
  CommandService → Remote Orchestrator
- Response handling: Remote orchestrator returns agent status/results
- Security: Connection validation, signature verification

Files created:
- apps/api/src/federation/types/federation-agent.types.ts
- apps/api/src/federation/federation-agent.service.ts
- apps/api/src/federation/federation-agent.service.spec.ts

Files modified:
- apps/api/src/federation/command.service.ts (agent command routing)
- apps/api/src/federation/federation.controller.ts (agent endpoints)
- apps/api/src/federation/federation.module.ts (service registration)
- apps/orchestrator/src/api/agents/agents.controller.ts (status endpoint)
- apps/orchestrator/src/api/agents/agents.module.ts (lifecycle integration)

Testing:
- 12/12 tests passing for FederationAgentService
- All command service tests passing
- TypeScript compilation successful
- Linting passed

Refs #93

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 14:37:06 -06:00

348 lines
9.8 KiB
TypeScript

import { Test, TestingModule } from "@nestjs/testing";
import { AgentTasksService } from "./agent-tasks.service";
import { PrismaService } from "../prisma/prisma.service";
import { AgentTaskStatus, AgentTaskPriority } from "@prisma/client";
import { NotFoundException } from "@nestjs/common";
import { describe, it, expect, beforeEach, vi } from "vitest";
describe("AgentTasksService", () => {
let service: AgentTasksService;
let prisma: PrismaService;
const mockPrismaService = {
agentTask: {
create: vi.fn(),
findMany: vi.fn(),
findUnique: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
count: vi.fn(),
},
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AgentTasksService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
],
}).compile();
service = module.get<AgentTasksService>(AgentTasksService);
prisma = module.get<PrismaService>(PrismaService);
// Reset mocks
vi.clearAllMocks();
});
describe("create", () => {
it("should create a new agent task with default values", async () => {
const workspaceId = "workspace-1";
const userId = "user-1";
const createDto = {
title: "Test Task",
description: "Test Description",
agentType: "test-agent",
};
const mockTask = {
id: "task-1",
workspaceId,
title: "Test Task",
description: "Test Description",
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.MEDIUM,
agentType: "test-agent",
agentConfig: {},
result: null,
error: null,
createdById: userId,
createdAt: new Date(),
updatedAt: new Date(),
startedAt: null,
completedAt: null,
createdBy: {
id: userId,
name: "Test User",
email: "test@example.com",
},
};
mockPrismaService.agentTask.create.mockResolvedValue(mockTask);
const result = await service.create(workspaceId, userId, createDto);
expect(mockPrismaService.agentTask.create).toHaveBeenCalledWith({
data: expect.objectContaining({
title: "Test Task",
description: "Test Description",
agentType: "test-agent",
workspaceId,
createdById: userId,
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.MEDIUM,
agentConfig: {},
}),
include: {
createdBy: {
select: { id: true, name: true, email: true },
},
},
});
expect(result).toEqual(mockTask);
});
it("should set startedAt when status is RUNNING", async () => {
const workspaceId = "workspace-1";
const userId = "user-1";
const createDto = {
title: "Running Task",
agentType: "test-agent",
status: AgentTaskStatus.RUNNING,
};
mockPrismaService.agentTask.create.mockResolvedValue({
id: "task-1",
startedAt: expect.any(Date),
});
await service.create(workspaceId, userId, createDto);
expect(mockPrismaService.agentTask.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
startedAt: expect.any(Date),
}),
})
);
});
it("should set completedAt when status is COMPLETED", async () => {
const workspaceId = "workspace-1";
const userId = "user-1";
const createDto = {
title: "Completed Task",
agentType: "test-agent",
status: AgentTaskStatus.COMPLETED,
};
mockPrismaService.agentTask.create.mockResolvedValue({
id: "task-1",
completedAt: expect.any(Date),
});
await service.create(workspaceId, userId, createDto);
expect(mockPrismaService.agentTask.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
startedAt: expect.any(Date),
completedAt: expect.any(Date),
}),
})
);
});
});
describe("findAll", () => {
it("should return paginated agent tasks", async () => {
const workspaceId = "workspace-1";
const query = { workspaceId, page: 1, limit: 10 };
const mockTasks = [
{ id: "task-1", title: "Task 1" },
{ id: "task-2", title: "Task 2" },
];
mockPrismaService.agentTask.findMany.mockResolvedValue(mockTasks);
mockPrismaService.agentTask.count.mockResolvedValue(2);
const result = await service.findAll(query);
expect(result).toEqual({
data: mockTasks,
meta: {
total: 2,
page: 1,
limit: 10,
totalPages: 1,
},
});
expect(mockPrismaService.agentTask.findMany).toHaveBeenCalledWith({
where: { workspaceId },
include: {
createdBy: {
select: { id: true, name: true, email: true },
},
},
orderBy: {
createdAt: "desc",
},
skip: 0,
take: 10,
});
});
it("should apply filters correctly", async () => {
const workspaceId = "workspace-1";
const query = {
workspaceId,
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.HIGH,
agentType: "test-agent",
};
mockPrismaService.agentTask.findMany.mockResolvedValue([]);
mockPrismaService.agentTask.count.mockResolvedValue(0);
await service.findAll(query);
expect(mockPrismaService.agentTask.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: {
workspaceId,
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.HIGH,
agentType: "test-agent",
},
})
);
});
});
describe("findOne", () => {
it("should return a single agent task", async () => {
const id = "task-1";
const workspaceId = "workspace-1";
const mockTask = { id, title: "Task 1", workspaceId };
mockPrismaService.agentTask.findUnique.mockResolvedValue(mockTask);
const result = await service.findOne(id, workspaceId);
expect(result).toEqual(mockTask);
expect(mockPrismaService.agentTask.findUnique).toHaveBeenCalledWith({
where: { id, workspaceId },
include: {
createdBy: {
select: { id: true, name: true, email: true },
},
},
});
});
it("should throw NotFoundException when task not found", async () => {
const id = "non-existent";
const workspaceId = "workspace-1";
mockPrismaService.agentTask.findUnique.mockResolvedValue(null);
await expect(service.findOne(id, workspaceId)).rejects.toThrow(NotFoundException);
});
});
describe("update", () => {
it("should update an agent task", async () => {
const id = "task-1";
const workspaceId = "workspace-1";
const updateDto = { title: "Updated Task" };
const existingTask = {
id,
workspaceId,
status: AgentTaskStatus.PENDING,
startedAt: null,
};
const updatedTask = { ...existingTask, ...updateDto };
mockPrismaService.agentTask.findUnique.mockResolvedValue(existingTask);
mockPrismaService.agentTask.update.mockResolvedValue(updatedTask);
const result = await service.update(id, workspaceId, updateDto);
expect(result).toEqual(updatedTask);
expect(mockPrismaService.agentTask.update).toHaveBeenCalledWith({
where: { id, workspaceId },
data: updateDto,
include: {
createdBy: {
select: { id: true, name: true, email: true },
},
},
});
});
it("should set startedAt when status changes to RUNNING", async () => {
const id = "task-1";
const workspaceId = "workspace-1";
const updateDto = { status: AgentTaskStatus.RUNNING };
const existingTask = {
id,
workspaceId,
status: AgentTaskStatus.PENDING,
startedAt: null,
};
mockPrismaService.agentTask.findUnique.mockResolvedValue(existingTask);
mockPrismaService.agentTask.update.mockResolvedValue({
...existingTask,
...updateDto,
});
await service.update(id, workspaceId, updateDto);
expect(mockPrismaService.agentTask.update).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
startedAt: expect.any(Date),
}),
})
);
});
it("should throw NotFoundException when task not found", async () => {
const id = "non-existent";
const workspaceId = "workspace-1";
const updateDto = { title: "Updated Task" };
mockPrismaService.agentTask.findUnique.mockResolvedValue(null);
await expect(service.update(id, workspaceId, updateDto)).rejects.toThrow(NotFoundException);
});
});
describe("remove", () => {
it("should delete an agent task", async () => {
const id = "task-1";
const workspaceId = "workspace-1";
const mockTask = { id, workspaceId, title: "Task 1" };
mockPrismaService.agentTask.findUnique.mockResolvedValue(mockTask);
mockPrismaService.agentTask.delete.mockResolvedValue(mockTask);
const result = await service.remove(id, workspaceId);
expect(result).toEqual({ message: "Agent task deleted successfully" });
expect(mockPrismaService.agentTask.delete).toHaveBeenCalledWith({
where: { id, workspaceId },
});
});
it("should throw NotFoundException when task not found", async () => {
const id = "non-existent";
const workspaceId = "workspace-1";
mockPrismaService.agentTask.findUnique.mockResolvedValue(null);
await expect(service.remove(id, workspaceId)).rejects.toThrow(NotFoundException);
});
});
});