Files
stack/apps/api/src/agent-tasks/agent-tasks.controller.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

237 lines
6.3 KiB
TypeScript

import { Test, TestingModule } from "@nestjs/testing";
import { AgentTasksController } from "./agent-tasks.controller";
import { AgentTasksService } from "./agent-tasks.service";
import { AgentTaskStatus, AgentTaskPriority } from "@prisma/client";
import { AuthGuard } from "../auth/guards/auth.guard";
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
import { ExecutionContext } from "@nestjs/common";
import { describe, it, expect, beforeEach, vi } from "vitest";
describe("AgentTasksController", () => {
let controller: AgentTasksController;
let service: AgentTasksService;
const mockAgentTasksService = {
create: vi.fn(),
findAll: vi.fn(),
findOne: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
};
const mockAuthGuard = {
canActivate: vi.fn(() => true),
};
const mockWorkspaceGuard = {
canActivate: vi.fn(() => true),
};
const mockPermissionGuard = {
canActivate: vi.fn(() => true),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AgentTasksController],
providers: [
{
provide: AgentTasksService,
useValue: mockAgentTasksService,
},
],
})
.overrideGuard(AuthGuard)
.useValue(mockAuthGuard)
.overrideGuard(WorkspaceGuard)
.useValue(mockWorkspaceGuard)
.overrideGuard(PermissionGuard)
.useValue(mockPermissionGuard)
.compile();
controller = module.get<AgentTasksController>(AgentTasksController);
service = module.get<AgentTasksService>(AgentTasksService);
// Reset mocks
vi.clearAllMocks();
});
describe("create", () => {
it("should create a new agent task", async () => {
const workspaceId = "workspace-1";
const user = { id: "user-1", email: "test@example.com" };
const createDto = {
title: "Test Task",
description: "Test Description",
agentType: "test-agent",
};
const mockTask = {
id: "task-1",
...createDto,
workspaceId,
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.MEDIUM,
agentConfig: {},
result: null,
error: null,
createdById: user.id,
createdAt: new Date(),
updatedAt: new Date(),
startedAt: null,
completedAt: null,
};
mockAgentTasksService.create.mockResolvedValue(mockTask);
const result = await controller.create(createDto, workspaceId, user);
expect(mockAgentTasksService.create).toHaveBeenCalledWith(workspaceId, user.id, createDto);
expect(result).toEqual(mockTask);
});
});
describe("findAll", () => {
it("should return paginated agent tasks", async () => {
const workspaceId = "workspace-1";
const query = {
page: 1,
limit: 10,
};
const mockResponse = {
data: [
{ id: "task-1", title: "Task 1" },
{ id: "task-2", title: "Task 2" },
],
meta: {
total: 2,
page: 1,
limit: 10,
totalPages: 1,
},
};
mockAgentTasksService.findAll.mockResolvedValue(mockResponse);
const result = await controller.findAll(query, workspaceId);
expect(mockAgentTasksService.findAll).toHaveBeenCalledWith({
...query,
workspaceId,
});
expect(result).toEqual(mockResponse);
});
it("should apply filters when provided", async () => {
const workspaceId = "workspace-1";
const query = {
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.HIGH,
agentType: "test-agent",
};
const mockResponse = {
data: [],
meta: {
total: 0,
page: 1,
limit: 50,
totalPages: 0,
},
};
mockAgentTasksService.findAll.mockResolvedValue(mockResponse);
const result = await controller.findAll(query, workspaceId);
expect(mockAgentTasksService.findAll).toHaveBeenCalledWith({
...query,
workspaceId,
});
expect(result).toEqual(mockResponse);
});
});
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,
status: AgentTaskStatus.PENDING,
priority: AgentTaskPriority.MEDIUM,
agentType: "test-agent",
agentConfig: {},
result: null,
error: null,
createdById: "user-1",
createdAt: new Date(),
updatedAt: new Date(),
startedAt: null,
completedAt: null,
};
mockAgentTasksService.findOne.mockResolvedValue(mockTask);
const result = await controller.findOne(id, workspaceId);
expect(mockAgentTasksService.findOne).toHaveBeenCalledWith(id, workspaceId);
expect(result).toEqual(mockTask);
});
});
describe("update", () => {
it("should update an agent task", async () => {
const id = "task-1";
const workspaceId = "workspace-1";
const updateDto = {
title: "Updated Task",
status: AgentTaskStatus.RUNNING,
};
const mockTask = {
id,
...updateDto,
workspaceId,
priority: AgentTaskPriority.MEDIUM,
agentType: "test-agent",
agentConfig: {},
result: null,
error: null,
createdById: "user-1",
createdAt: new Date(),
updatedAt: new Date(),
startedAt: new Date(),
completedAt: null,
};
mockAgentTasksService.update.mockResolvedValue(mockTask);
const result = await controller.update(id, updateDto, workspaceId);
expect(mockAgentTasksService.update).toHaveBeenCalledWith(id, workspaceId, updateDto);
expect(result).toEqual(mockTask);
});
});
describe("remove", () => {
it("should delete an agent task", async () => {
const id = "task-1";
const workspaceId = "workspace-1";
const mockResponse = { message: "Agent task deleted successfully" };
mockAgentTasksService.remove.mockResolvedValue(mockResponse);
const result = await controller.remove(id, workspaceId);
expect(mockAgentTasksService.remove).toHaveBeenCalledWith(id, workspaceId);
expect(result).toEqual(mockResponse);
});
});
});