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>
155 lines
4.7 KiB
TypeScript
155 lines
4.7 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import { KnowledgeGraphController } from "./graph.controller";
|
|
import { GraphService } from "./services/graph.service";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
|
import { WorkspaceGuard } from "../common/guards/workspace.guard";
|
|
import { PermissionGuard } from "../common/guards/permission.guard";
|
|
|
|
describe("KnowledgeGraphController", () => {
|
|
let controller: KnowledgeGraphController;
|
|
let graphService: GraphService;
|
|
let prismaService: PrismaService;
|
|
|
|
const mockGraphService = {
|
|
getFullGraph: vi.fn(),
|
|
getGraphStats: vi.fn(),
|
|
getEntryGraph: vi.fn(),
|
|
getEntryGraphBySlug: vi.fn(),
|
|
};
|
|
|
|
const mockPrismaService = {
|
|
knowledgeEntry: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
controllers: [KnowledgeGraphController],
|
|
providers: [
|
|
{
|
|
provide: GraphService,
|
|
useValue: mockGraphService,
|
|
},
|
|
{
|
|
provide: PrismaService,
|
|
useValue: mockPrismaService,
|
|
},
|
|
],
|
|
})
|
|
.overrideGuard(AuthGuard)
|
|
.useValue({ canActivate: vi.fn(() => true) })
|
|
.overrideGuard(WorkspaceGuard)
|
|
.useValue({ canActivate: vi.fn(() => true) })
|
|
.overrideGuard(PermissionGuard)
|
|
.useValue({ canActivate: vi.fn(() => true) })
|
|
.compile();
|
|
|
|
controller = module.get<KnowledgeGraphController>(KnowledgeGraphController);
|
|
graphService = module.get<GraphService>(GraphService);
|
|
prismaService = module.get<PrismaService>(PrismaService);
|
|
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("should be defined", () => {
|
|
expect(controller).toBeDefined();
|
|
});
|
|
|
|
describe("getFullGraph", () => {
|
|
it("should return full graph without filters", async () => {
|
|
const mockGraph = {
|
|
nodes: [],
|
|
edges: [],
|
|
stats: { totalNodes: 0, totalEdges: 0, orphanCount: 0 },
|
|
};
|
|
mockGraphService.getFullGraph.mockResolvedValue(mockGraph);
|
|
|
|
const result = await controller.getFullGraph("workspace-1", {});
|
|
|
|
expect(graphService.getFullGraph).toHaveBeenCalledWith("workspace-1", {});
|
|
expect(result).toEqual(mockGraph);
|
|
});
|
|
|
|
it("should pass filters to service", async () => {
|
|
const mockGraph = {
|
|
nodes: [],
|
|
edges: [],
|
|
stats: { totalNodes: 0, totalEdges: 0, orphanCount: 0 },
|
|
};
|
|
mockGraphService.getFullGraph.mockResolvedValue(mockGraph);
|
|
|
|
const filters = {
|
|
tags: ["tag-1"],
|
|
status: "PUBLISHED",
|
|
limit: 100,
|
|
};
|
|
|
|
await controller.getFullGraph("workspace-1", filters);
|
|
|
|
expect(graphService.getFullGraph).toHaveBeenCalledWith("workspace-1", filters);
|
|
});
|
|
});
|
|
|
|
describe("getGraphStats", () => {
|
|
it("should return graph statistics", async () => {
|
|
const mockStats = {
|
|
totalEntries: 10,
|
|
totalLinks: 15,
|
|
orphanEntries: 2,
|
|
averageLinks: 1.5,
|
|
mostConnectedEntries: [],
|
|
tagDistribution: [],
|
|
};
|
|
mockGraphService.getGraphStats.mockResolvedValue(mockStats);
|
|
|
|
const result = await controller.getGraphStats("workspace-1");
|
|
|
|
expect(graphService.getGraphStats).toHaveBeenCalledWith("workspace-1");
|
|
expect(result).toEqual(mockStats);
|
|
});
|
|
});
|
|
|
|
describe("getEntryGraph", () => {
|
|
it("should return entry-centered graph", async () => {
|
|
const mockEntry = {
|
|
id: "entry-1",
|
|
slug: "test-entry",
|
|
title: "Test Entry",
|
|
};
|
|
|
|
const mockGraph = {
|
|
centerNode: mockEntry,
|
|
nodes: [mockEntry],
|
|
edges: [],
|
|
stats: { totalNodes: 1, totalEdges: 0, maxDepth: 1 },
|
|
};
|
|
|
|
mockGraphService.getEntryGraphBySlug.mockResolvedValue(mockGraph);
|
|
|
|
const result = await controller.getEntryGraph("workspace-1", "test-entry", { depth: 2 });
|
|
|
|
expect(graphService.getEntryGraphBySlug).toHaveBeenCalledWith("workspace-1", "test-entry", 2);
|
|
expect(result).toEqual(mockGraph);
|
|
});
|
|
|
|
it("should use default depth if not provided", async () => {
|
|
mockGraphService.getEntryGraphBySlug.mockResolvedValue({});
|
|
|
|
await controller.getEntryGraph("workspace-1", "test-entry", {});
|
|
|
|
expect(graphService.getEntryGraphBySlug).toHaveBeenCalledWith("workspace-1", "test-entry", 1);
|
|
});
|
|
|
|
it("should throw error if entry not found", async () => {
|
|
mockGraphService.getEntryGraphBySlug.mockRejectedValue(new Error("Entry not found"));
|
|
|
|
await expect(controller.getEntryGraph("workspace-1", "non-existent", {})).rejects.toThrow(
|
|
"Entry not found"
|
|
);
|
|
});
|
|
});
|
|
});
|