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>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { WebSocketGateway } from './websocket.gateway';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { WebSocketGateway } from "./websocket.gateway";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { Server, Socket } from "socket.io";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
interface AuthenticatedSocket extends Socket {
|
||||
data: {
|
||||
@@ -12,7 +12,7 @@ interface AuthenticatedSocket extends Socket {
|
||||
};
|
||||
}
|
||||
|
||||
describe('WebSocketGateway', () => {
|
||||
describe("WebSocketGateway", () => {
|
||||
let gateway: WebSocketGateway;
|
||||
let authService: AuthService;
|
||||
let prismaService: PrismaService;
|
||||
@@ -53,7 +53,7 @@ describe('WebSocketGateway', () => {
|
||||
|
||||
// Mock authenticated client
|
||||
mockClient = {
|
||||
id: 'test-socket-id',
|
||||
id: "test-socket-id",
|
||||
join: vi.fn(),
|
||||
leave: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
@@ -61,7 +61,7 @@ describe('WebSocketGateway', () => {
|
||||
data: {},
|
||||
handshake: {
|
||||
auth: {
|
||||
token: 'valid-token',
|
||||
token: "valid-token",
|
||||
},
|
||||
},
|
||||
} as unknown as AuthenticatedSocket;
|
||||
@@ -76,36 +76,36 @@ describe('WebSocketGateway', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
it('should validate token and populate socket.data on successful authentication', async () => {
|
||||
describe("Authentication", () => {
|
||||
it("should validate token and populate socket.data on successful authentication", async () => {
|
||||
const mockSessionData = {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
session: { id: 'session-123' },
|
||||
user: { id: "user-123", email: "test@example.com" },
|
||||
session: { id: "session-123" },
|
||||
};
|
||||
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, 'findFirst').mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
role: 'MEMBER',
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, "findFirst").mockResolvedValue({
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
role: "MEMBER",
|
||||
} as never);
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(authService.verifySession).toHaveBeenCalledWith('valid-token');
|
||||
expect(mockClient.data.userId).toBe('user-123');
|
||||
expect(mockClient.data.workspaceId).toBe('workspace-456');
|
||||
expect(authService.verifySession).toHaveBeenCalledWith("valid-token");
|
||||
expect(mockClient.data.userId).toBe("user-123");
|
||||
expect(mockClient.data.workspaceId).toBe("workspace-456");
|
||||
});
|
||||
|
||||
it('should disconnect client with invalid token', async () => {
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(null);
|
||||
it("should disconnect client with invalid token", async () => {
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(null);
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(mockClient.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should disconnect client without token', async () => {
|
||||
it("should disconnect client without token", async () => {
|
||||
const clientNoToken = {
|
||||
...mockClient,
|
||||
handshake: { auth: {} },
|
||||
@@ -116,23 +116,23 @@ describe('WebSocketGateway', () => {
|
||||
expect(clientNoToken.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should disconnect client if token verification throws error', async () => {
|
||||
vi.spyOn(authService, 'verifySession').mockRejectedValue(new Error('Invalid token'));
|
||||
it("should disconnect client if token verification throws error", async () => {
|
||||
vi.spyOn(authService, "verifySession").mockRejectedValue(new Error("Invalid token"));
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(mockClient.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have connection timeout mechanism in place', () => {
|
||||
it("should have connection timeout mechanism in place", () => {
|
||||
// This test verifies that the gateway has a CONNECTION_TIMEOUT_MS constant
|
||||
// The actual timeout is tested indirectly through authentication failure tests
|
||||
expect((gateway as { CONNECTION_TIMEOUT_MS: number }).CONNECTION_TIMEOUT_MS).toBe(5000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting', () => {
|
||||
it('should reject connections exceeding rate limit', async () => {
|
||||
describe("Rate Limiting", () => {
|
||||
it("should reject connections exceeding rate limit", async () => {
|
||||
// Mock rate limiter to return false (limit exceeded)
|
||||
const rateLimitedClient = { ...mockClient } as AuthenticatedSocket;
|
||||
|
||||
@@ -146,109 +146,109 @@ describe('WebSocketGateway', () => {
|
||||
// expect(rateLimitedClient.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow connections within rate limit', async () => {
|
||||
it("should allow connections within rate limit", async () => {
|
||||
const mockSessionData = {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
session: { id: 'session-123' },
|
||||
user: { id: "user-123", email: "test@example.com" },
|
||||
session: { id: "session-123" },
|
||||
};
|
||||
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, 'findFirst').mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
role: 'MEMBER',
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, "findFirst").mockResolvedValue({
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
role: "MEMBER",
|
||||
} as never);
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(mockClient.disconnect).not.toHaveBeenCalled();
|
||||
expect(mockClient.data.userId).toBe('user-123');
|
||||
expect(mockClient.data.userId).toBe("user-123");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workspace Access Validation', () => {
|
||||
it('should verify user has access to workspace', async () => {
|
||||
describe("Workspace Access Validation", () => {
|
||||
it("should verify user has access to workspace", async () => {
|
||||
const mockSessionData = {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
session: { id: 'session-123' },
|
||||
user: { id: "user-123", email: "test@example.com" },
|
||||
session: { id: "session-123" },
|
||||
};
|
||||
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, 'findFirst').mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
role: 'MEMBER',
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, "findFirst").mockResolvedValue({
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
role: "MEMBER",
|
||||
} as never);
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(prismaService.workspaceMember.findFirst).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-123' },
|
||||
where: { userId: "user-123" },
|
||||
select: { workspaceId: true, userId: true, role: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('should disconnect client without workspace access', async () => {
|
||||
it("should disconnect client without workspace access", async () => {
|
||||
const mockSessionData = {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
session: { id: 'session-123' },
|
||||
user: { id: "user-123", email: "test@example.com" },
|
||||
session: { id: "session-123" },
|
||||
};
|
||||
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, 'findFirst').mockResolvedValue(null);
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, "findFirst").mockResolvedValue(null);
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(mockClient.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only allow joining workspace rooms user has access to', async () => {
|
||||
it("should only allow joining workspace rooms user has access to", async () => {
|
||||
const mockSessionData = {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
session: { id: 'session-123' },
|
||||
user: { id: "user-123", email: "test@example.com" },
|
||||
session: { id: "session-123" },
|
||||
};
|
||||
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, 'findFirst').mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
role: 'MEMBER',
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, "findFirst").mockResolvedValue({
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
role: "MEMBER",
|
||||
} as never);
|
||||
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
// Should join the workspace room they have access to
|
||||
expect(mockClient.join).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockClient.join).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleConnection', () => {
|
||||
describe("handleConnection", () => {
|
||||
beforeEach(() => {
|
||||
const mockSessionData = {
|
||||
user: { id: 'user-123', email: 'test@example.com' },
|
||||
session: { id: 'session-123' },
|
||||
user: { id: "user-123", email: "test@example.com" },
|
||||
session: { id: "session-123" },
|
||||
};
|
||||
|
||||
vi.spyOn(authService, 'verifySession').mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, 'findFirst').mockResolvedValue({
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
role: 'MEMBER',
|
||||
vi.spyOn(authService, "verifySession").mockResolvedValue(mockSessionData);
|
||||
vi.spyOn(prismaService.workspaceMember, "findFirst").mockResolvedValue({
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
role: "MEMBER",
|
||||
} as never);
|
||||
|
||||
mockClient.data = {
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
};
|
||||
});
|
||||
|
||||
it('should join client to workspace room on connection', async () => {
|
||||
it("should join client to workspace room on connection", async () => {
|
||||
await gateway.handleConnection(mockClient);
|
||||
|
||||
expect(mockClient.join).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockClient.join).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
});
|
||||
|
||||
it('should reject connection without authentication', async () => {
|
||||
it("should reject connection without authentication", async () => {
|
||||
const unauthClient = {
|
||||
...mockClient,
|
||||
data: {},
|
||||
@@ -261,23 +261,23 @@ describe('WebSocketGateway', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDisconnect', () => {
|
||||
it('should leave workspace room on disconnect', () => {
|
||||
describe("handleDisconnect", () => {
|
||||
it("should leave workspace room on disconnect", () => {
|
||||
// Populate data as if client was authenticated
|
||||
const authenticatedClient = {
|
||||
...mockClient,
|
||||
data: {
|
||||
userId: 'user-123',
|
||||
workspaceId: 'workspace-456',
|
||||
userId: "user-123",
|
||||
workspaceId: "workspace-456",
|
||||
},
|
||||
} as unknown as AuthenticatedSocket;
|
||||
|
||||
gateway.handleDisconnect(authenticatedClient);
|
||||
|
||||
expect(authenticatedClient.leave).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(authenticatedClient.leave).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
});
|
||||
|
||||
it('should not throw error when disconnecting unauthenticated client', () => {
|
||||
it("should not throw error when disconnecting unauthenticated client", () => {
|
||||
const unauthenticatedClient = {
|
||||
...mockClient,
|
||||
data: {},
|
||||
@@ -287,279 +287,279 @@ describe('WebSocketGateway', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitTaskCreated', () => {
|
||||
it('should emit task:created event to workspace room', () => {
|
||||
describe("emitTaskCreated", () => {
|
||||
it("should emit task:created event to workspace room", () => {
|
||||
const task = {
|
||||
id: 'task-1',
|
||||
title: 'Test Task',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "task-1",
|
||||
title: "Test Task",
|
||||
workspaceId: "workspace-456",
|
||||
};
|
||||
|
||||
gateway.emitTaskCreated('workspace-456', task);
|
||||
gateway.emitTaskCreated("workspace-456", task);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('task:created', task);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("task:created", task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitTaskUpdated', () => {
|
||||
it('should emit task:updated event to workspace room', () => {
|
||||
describe("emitTaskUpdated", () => {
|
||||
it("should emit task:updated event to workspace room", () => {
|
||||
const task = {
|
||||
id: 'task-1',
|
||||
title: 'Updated Task',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "task-1",
|
||||
title: "Updated Task",
|
||||
workspaceId: "workspace-456",
|
||||
};
|
||||
|
||||
gateway.emitTaskUpdated('workspace-456', task);
|
||||
gateway.emitTaskUpdated("workspace-456", task);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('task:updated', task);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("task:updated", task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitTaskDeleted', () => {
|
||||
it('should emit task:deleted event to workspace room', () => {
|
||||
const taskId = 'task-1';
|
||||
describe("emitTaskDeleted", () => {
|
||||
it("should emit task:deleted event to workspace room", () => {
|
||||
const taskId = "task-1";
|
||||
|
||||
gateway.emitTaskDeleted('workspace-456', taskId);
|
||||
gateway.emitTaskDeleted("workspace-456", taskId);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('task:deleted', { id: taskId });
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("task:deleted", { id: taskId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitEventCreated', () => {
|
||||
it('should emit event:created event to workspace room', () => {
|
||||
describe("emitEventCreated", () => {
|
||||
it("should emit event:created event to workspace room", () => {
|
||||
const event = {
|
||||
id: 'event-1',
|
||||
title: 'Test Event',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "event-1",
|
||||
title: "Test Event",
|
||||
workspaceId: "workspace-456",
|
||||
};
|
||||
|
||||
gateway.emitEventCreated('workspace-456', event);
|
||||
gateway.emitEventCreated("workspace-456", event);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('event:created', event);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("event:created", event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitEventUpdated', () => {
|
||||
it('should emit event:updated event to workspace room', () => {
|
||||
describe("emitEventUpdated", () => {
|
||||
it("should emit event:updated event to workspace room", () => {
|
||||
const event = {
|
||||
id: 'event-1',
|
||||
title: 'Updated Event',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "event-1",
|
||||
title: "Updated Event",
|
||||
workspaceId: "workspace-456",
|
||||
};
|
||||
|
||||
gateway.emitEventUpdated('workspace-456', event);
|
||||
gateway.emitEventUpdated("workspace-456", event);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('event:updated', event);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("event:updated", event);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitEventDeleted', () => {
|
||||
it('should emit event:deleted event to workspace room', () => {
|
||||
const eventId = 'event-1';
|
||||
describe("emitEventDeleted", () => {
|
||||
it("should emit event:deleted event to workspace room", () => {
|
||||
const eventId = "event-1";
|
||||
|
||||
gateway.emitEventDeleted('workspace-456', eventId);
|
||||
gateway.emitEventDeleted("workspace-456", eventId);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('event:deleted', { id: eventId });
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("event:deleted", { id: eventId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitProjectUpdated', () => {
|
||||
it('should emit project:updated event to workspace room', () => {
|
||||
describe("emitProjectUpdated", () => {
|
||||
it("should emit project:updated event to workspace room", () => {
|
||||
const project = {
|
||||
id: 'project-1',
|
||||
name: 'Updated Project',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "project-1",
|
||||
name: "Updated Project",
|
||||
workspaceId: "workspace-456",
|
||||
};
|
||||
|
||||
gateway.emitProjectUpdated('workspace-456', project);
|
||||
gateway.emitProjectUpdated("workspace-456", project);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('project:updated', project);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("project:updated", project);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Job Events', () => {
|
||||
describe('emitJobCreated', () => {
|
||||
it('should emit job:created event to workspace jobs room', () => {
|
||||
describe("Job Events", () => {
|
||||
describe("emitJobCreated", () => {
|
||||
it("should emit job:created event to workspace jobs room", () => {
|
||||
const job = {
|
||||
id: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
type: 'code-task',
|
||||
status: 'PENDING',
|
||||
id: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
type: "code-task",
|
||||
status: "PENDING",
|
||||
};
|
||||
|
||||
gateway.emitJobCreated('workspace-456', job);
|
||||
gateway.emitJobCreated("workspace-456", job);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456:jobs');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('job:created', job);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456:jobs");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("job:created", job);
|
||||
});
|
||||
|
||||
it('should emit job:created event to specific job room', () => {
|
||||
it("should emit job:created event to specific job room", () => {
|
||||
const job = {
|
||||
id: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
type: 'code-task',
|
||||
status: 'PENDING',
|
||||
id: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
type: "code-task",
|
||||
status: "PENDING",
|
||||
};
|
||||
|
||||
gateway.emitJobCreated('workspace-456', job);
|
||||
gateway.emitJobCreated("workspace-456", job);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('job:job-1');
|
||||
expect(mockServer.to).toHaveBeenCalledWith("job:job-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitJobStatusChanged', () => {
|
||||
it('should emit job:status event to workspace jobs room', () => {
|
||||
describe("emitJobStatusChanged", () => {
|
||||
it("should emit job:status event to workspace jobs room", () => {
|
||||
const data = {
|
||||
id: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
status: 'RUNNING',
|
||||
previousStatus: 'PENDING',
|
||||
id: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
status: "RUNNING",
|
||||
previousStatus: "PENDING",
|
||||
};
|
||||
|
||||
gateway.emitJobStatusChanged('workspace-456', 'job-1', data);
|
||||
gateway.emitJobStatusChanged("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456:jobs');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('job:status', data);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456:jobs");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("job:status", data);
|
||||
});
|
||||
|
||||
it('should emit job:status event to specific job room', () => {
|
||||
it("should emit job:status event to specific job room", () => {
|
||||
const data = {
|
||||
id: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
status: 'RUNNING',
|
||||
previousStatus: 'PENDING',
|
||||
id: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
status: "RUNNING",
|
||||
previousStatus: "PENDING",
|
||||
};
|
||||
|
||||
gateway.emitJobStatusChanged('workspace-456', 'job-1', data);
|
||||
gateway.emitJobStatusChanged("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('job:job-1');
|
||||
expect(mockServer.to).toHaveBeenCalledWith("job:job-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitJobProgress', () => {
|
||||
it('should emit job:progress event to workspace jobs room', () => {
|
||||
describe("emitJobProgress", () => {
|
||||
it("should emit job:progress event to workspace jobs room", () => {
|
||||
const data = {
|
||||
id: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
progressPercent: 45,
|
||||
message: 'Processing step 2 of 4',
|
||||
message: "Processing step 2 of 4",
|
||||
};
|
||||
|
||||
gateway.emitJobProgress('workspace-456', 'job-1', data);
|
||||
gateway.emitJobProgress("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456:jobs');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('job:progress', data);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456:jobs");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("job:progress", data);
|
||||
});
|
||||
|
||||
it('should emit job:progress event to specific job room', () => {
|
||||
it("should emit job:progress event to specific job room", () => {
|
||||
const data = {
|
||||
id: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
id: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
progressPercent: 45,
|
||||
message: 'Processing step 2 of 4',
|
||||
message: "Processing step 2 of 4",
|
||||
};
|
||||
|
||||
gateway.emitJobProgress('workspace-456', 'job-1', data);
|
||||
gateway.emitJobProgress("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('job:job-1');
|
||||
expect(mockServer.to).toHaveBeenCalledWith("job:job-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitStepStarted', () => {
|
||||
it('should emit step:started event to workspace jobs room', () => {
|
||||
describe("emitStepStarted", () => {
|
||||
it("should emit step:started event to workspace jobs room", () => {
|
||||
const data = {
|
||||
id: 'step-1',
|
||||
jobId: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
name: 'Build',
|
||||
id: "step-1",
|
||||
jobId: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
name: "Build",
|
||||
};
|
||||
|
||||
gateway.emitStepStarted('workspace-456', 'job-1', data);
|
||||
gateway.emitStepStarted("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456:jobs');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('step:started', data);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456:jobs");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("step:started", data);
|
||||
});
|
||||
|
||||
it('should emit step:started event to specific job room', () => {
|
||||
it("should emit step:started event to specific job room", () => {
|
||||
const data = {
|
||||
id: 'step-1',
|
||||
jobId: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
name: 'Build',
|
||||
id: "step-1",
|
||||
jobId: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
name: "Build",
|
||||
};
|
||||
|
||||
gateway.emitStepStarted('workspace-456', 'job-1', data);
|
||||
gateway.emitStepStarted("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('job:job-1');
|
||||
expect(mockServer.to).toHaveBeenCalledWith("job:job-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitStepCompleted', () => {
|
||||
it('should emit step:completed event to workspace jobs room', () => {
|
||||
describe("emitStepCompleted", () => {
|
||||
it("should emit step:completed event to workspace jobs room", () => {
|
||||
const data = {
|
||||
id: 'step-1',
|
||||
jobId: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
name: 'Build',
|
||||
id: "step-1",
|
||||
jobId: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
name: "Build",
|
||||
success: true,
|
||||
};
|
||||
|
||||
gateway.emitStepCompleted('workspace-456', 'job-1', data);
|
||||
gateway.emitStepCompleted("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456:jobs');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('step:completed', data);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456:jobs");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("step:completed", data);
|
||||
});
|
||||
|
||||
it('should emit step:completed event to specific job room', () => {
|
||||
it("should emit step:completed event to specific job room", () => {
|
||||
const data = {
|
||||
id: 'step-1',
|
||||
jobId: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
name: 'Build',
|
||||
id: "step-1",
|
||||
jobId: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
name: "Build",
|
||||
success: true,
|
||||
};
|
||||
|
||||
gateway.emitStepCompleted('workspace-456', 'job-1', data);
|
||||
gateway.emitStepCompleted("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('job:job-1');
|
||||
expect(mockServer.to).toHaveBeenCalledWith("job:job-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitStepOutput', () => {
|
||||
it('should emit step:output event to workspace jobs room', () => {
|
||||
describe("emitStepOutput", () => {
|
||||
it("should emit step:output event to workspace jobs room", () => {
|
||||
const data = {
|
||||
id: 'step-1',
|
||||
jobId: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
output: 'Build completed successfully',
|
||||
id: "step-1",
|
||||
jobId: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
output: "Build completed successfully",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
gateway.emitStepOutput('workspace-456', 'job-1', data);
|
||||
gateway.emitStepOutput("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('workspace:workspace-456:jobs');
|
||||
expect(mockServer.emit).toHaveBeenCalledWith('step:output', data);
|
||||
expect(mockServer.to).toHaveBeenCalledWith("workspace:workspace-456:jobs");
|
||||
expect(mockServer.emit).toHaveBeenCalledWith("step:output", data);
|
||||
});
|
||||
|
||||
it('should emit step:output event to specific job room', () => {
|
||||
it("should emit step:output event to specific job room", () => {
|
||||
const data = {
|
||||
id: 'step-1',
|
||||
jobId: 'job-1',
|
||||
workspaceId: 'workspace-456',
|
||||
output: 'Build completed successfully',
|
||||
id: "step-1",
|
||||
jobId: "job-1",
|
||||
workspaceId: "workspace-456",
|
||||
output: "Build completed successfully",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
gateway.emitStepOutput('workspace-456', 'job-1', data);
|
||||
gateway.emitStepOutput("workspace-456", "job-1", data);
|
||||
|
||||
expect(mockServer.to).toHaveBeenCalledWith('job:job-1');
|
||||
expect(mockServer.to).toHaveBeenCalledWith("job:job-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user