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:
@@ -7,11 +7,13 @@ Git workflow and branching conventions for Mosaic Stack.
|
||||
### Main Branches
|
||||
|
||||
**`main`** — Production-ready code only
|
||||
|
||||
- Never commit directly
|
||||
- Only merge from `develop` via release
|
||||
- Tagged with version numbers
|
||||
|
||||
**`develop`** — Active development (default branch)
|
||||
|
||||
- All features merge here first
|
||||
- Must always build and pass tests
|
||||
- Protected branch
|
||||
@@ -19,6 +21,7 @@ Git workflow and branching conventions for Mosaic Stack.
|
||||
### Supporting Branches
|
||||
|
||||
**`feature/*`** — New features
|
||||
|
||||
```bash
|
||||
# From: develop
|
||||
# Merge to: develop
|
||||
@@ -30,6 +33,7 @@ git checkout -b feature/6-frontend-auth
|
||||
```
|
||||
|
||||
**`fix/*`** — Bug fixes
|
||||
|
||||
```bash
|
||||
# From: develop (or main for hotfixes)
|
||||
# Merge to: develop (or both main and develop)
|
||||
@@ -40,6 +44,7 @@ git checkout -b fix/12-session-timeout
|
||||
```
|
||||
|
||||
**`refactor/*`** — Code improvements
|
||||
|
||||
```bash
|
||||
# From: develop
|
||||
# Merge to: develop
|
||||
@@ -49,6 +54,7 @@ git checkout -b refactor/auth-service-cleanup
|
||||
```
|
||||
|
||||
**`docs/*`** — Documentation updates
|
||||
|
||||
```bash
|
||||
# From: develop
|
||||
# Merge to: develop
|
||||
|
||||
@@ -18,10 +18,11 @@ Test individual functions and methods in isolation.
|
||||
**Location:** `*.spec.ts` next to source file
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// apps/api/src/auth/auth.service.spec.ts
|
||||
describe('AuthService', () => {
|
||||
it('should create a session for valid user', async () => {
|
||||
describe("AuthService", () => {
|
||||
it("should create a session for valid user", async () => {
|
||||
const result = await authService.createSession(mockUser);
|
||||
expect(result.session.token).toBeDefined();
|
||||
});
|
||||
@@ -35,12 +36,13 @@ Test interactions between components.
|
||||
**Location:** `*.integration.spec.ts` in module directory
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// apps/api/src/auth/auth.integration.spec.ts
|
||||
describe('Auth Integration', () => {
|
||||
it('should complete full login flow', async () => {
|
||||
describe("Auth Integration", () => {
|
||||
it("should complete full login flow", async () => {
|
||||
const login = await request(app.getHttpServer())
|
||||
.post('/auth/sign-in')
|
||||
.post("/auth/sign-in")
|
||||
.send({ email, password });
|
||||
expect(login.status).toBe(200);
|
||||
});
|
||||
@@ -82,9 +84,9 @@ pnpm test:e2e
|
||||
### Structure
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
describe('ComponentName', () => {
|
||||
describe("ComponentName", () => {
|
||||
beforeEach(() => {
|
||||
// Setup
|
||||
});
|
||||
@@ -93,19 +95,19 @@ describe('ComponentName', () => {
|
||||
// Cleanup
|
||||
});
|
||||
|
||||
describe('methodName', () => {
|
||||
it('should handle normal case', () => {
|
||||
describe("methodName", () => {
|
||||
it("should handle normal case", () => {
|
||||
// Arrange
|
||||
const input = 'test';
|
||||
const input = "test";
|
||||
|
||||
// Act
|
||||
const result = component.method(input);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe('expected');
|
||||
expect(result).toBe("expected");
|
||||
});
|
||||
|
||||
it('should handle error case', () => {
|
||||
it("should handle error case", () => {
|
||||
expect(() => component.method(null)).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -124,26 +126,26 @@ const mockPrismaService = {
|
||||
};
|
||||
|
||||
// Mock module
|
||||
vi.mock('./some-module', () => ({
|
||||
someFunction: vi.fn(() => 'mocked'),
|
||||
vi.mock("./some-module", () => ({
|
||||
someFunction: vi.fn(() => "mocked"),
|
||||
}));
|
||||
```
|
||||
|
||||
### Testing Async Code
|
||||
|
||||
```typescript
|
||||
it('should complete async operation', async () => {
|
||||
it("should complete async operation", async () => {
|
||||
const result = await asyncFunction();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
// Or with resolves/rejects
|
||||
it('should resolve with data', async () => {
|
||||
await expect(asyncFunction()).resolves.toBe('data');
|
||||
it("should resolve with data", async () => {
|
||||
await expect(asyncFunction()).resolves.toBe("data");
|
||||
});
|
||||
|
||||
it('should reject with error', async () => {
|
||||
await expect(failingFunction()).rejects.toThrow('Error');
|
||||
it("should reject with error", async () => {
|
||||
await expect(failingFunction()).rejects.toThrow("Error");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -168,6 +170,7 @@ open coverage/index.html
|
||||
### Exemptions
|
||||
|
||||
Some code types may have lower coverage requirements:
|
||||
|
||||
- **DTOs/Interfaces:** No coverage required (type checking sufficient)
|
||||
- **Constants:** No coverage required
|
||||
- **Database migrations:** Manual verification acceptable
|
||||
@@ -179,16 +182,18 @@ Always document exemptions in PR description.
|
||||
### 1. Test Behavior, Not Implementation
|
||||
|
||||
**❌ Bad:**
|
||||
|
||||
```typescript
|
||||
it('should call getUserById', () => {
|
||||
it("should call getUserById", () => {
|
||||
service.login(email, password);
|
||||
expect(mockService.getUserById).toHaveBeenCalled();
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Good:**
|
||||
|
||||
```typescript
|
||||
it('should return session for valid credentials', async () => {
|
||||
it("should return session for valid credentials", async () => {
|
||||
const result = await service.login(email, password);
|
||||
expect(result.session.token).toBeDefined();
|
||||
expect(result.user.email).toBe(email);
|
||||
@@ -198,12 +203,14 @@ it('should return session for valid credentials', async () => {
|
||||
### 2. Use Descriptive Test Names
|
||||
|
||||
**❌ Bad:**
|
||||
|
||||
```typescript
|
||||
it('works', () => { ... });
|
||||
it('test 1', () => { ... });
|
||||
```
|
||||
|
||||
**✅ Good:**
|
||||
|
||||
```typescript
|
||||
it('should return 401 for invalid credentials', () => { ... });
|
||||
it('should create session with 24h expiration', () => { ... });
|
||||
@@ -212,7 +219,7 @@ it('should create session with 24h expiration', () => { ... });
|
||||
### 3. Arrange-Act-Assert Pattern
|
||||
|
||||
```typescript
|
||||
it('should calculate total correctly', () => {
|
||||
it("should calculate total correctly", () => {
|
||||
// Arrange - Set up test data
|
||||
const items = [{ price: 10 }, { price: 20 }];
|
||||
|
||||
@@ -227,21 +234,21 @@ it('should calculate total correctly', () => {
|
||||
### 4. Test Edge Cases
|
||||
|
||||
```typescript
|
||||
describe('validateEmail', () => {
|
||||
it('should accept valid email', () => {
|
||||
expect(validateEmail('user@example.com')).toBe(true);
|
||||
describe("validateEmail", () => {
|
||||
it("should accept valid email", () => {
|
||||
expect(validateEmail("user@example.com")).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject empty string', () => {
|
||||
expect(validateEmail('')).toBe(false);
|
||||
it("should reject empty string", () => {
|
||||
expect(validateEmail("")).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject null', () => {
|
||||
it("should reject null", () => {
|
||||
expect(validateEmail(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject invalid format', () => {
|
||||
expect(validateEmail('notanemail')).toBe(false);
|
||||
it("should reject invalid format", () => {
|
||||
expect(validateEmail("notanemail")).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -251,19 +258,19 @@ describe('validateEmail', () => {
|
||||
```typescript
|
||||
// ❌ Bad - Tests depend on order
|
||||
let userId;
|
||||
it('should create user', () => {
|
||||
it("should create user", () => {
|
||||
userId = createUser();
|
||||
});
|
||||
it('should get user', () => {
|
||||
it("should get user", () => {
|
||||
getUser(userId); // Fails if previous test fails
|
||||
});
|
||||
|
||||
// ✅ Good - Each test is independent
|
||||
it('should create user', () => {
|
||||
it("should create user", () => {
|
||||
const userId = createUser();
|
||||
expect(userId).toBeDefined();
|
||||
});
|
||||
it('should get user', () => {
|
||||
it("should get user", () => {
|
||||
const userId = createUser(); // Create fresh data
|
||||
const user = getUser(userId);
|
||||
expect(user).toBeDefined();
|
||||
@@ -273,6 +280,7 @@ it('should get user', () => {
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically on:
|
||||
|
||||
- Every push to feature branch
|
||||
- Every pull request
|
||||
- Before merge to `develop`
|
||||
@@ -284,7 +292,7 @@ Tests run automatically on:
|
||||
### Run Single Test
|
||||
|
||||
```typescript
|
||||
it.only('should test specific case', () => {
|
||||
it.only("should test specific case", () => {
|
||||
// Only this test runs
|
||||
});
|
||||
```
|
||||
@@ -292,7 +300,7 @@ it.only('should test specific case', () => {
|
||||
### Skip Test
|
||||
|
||||
```typescript
|
||||
it.skip('should test something', () => {
|
||||
it.skip("should test something", () => {
|
||||
// This test is skipped
|
||||
});
|
||||
```
|
||||
@@ -306,6 +314,7 @@ pnpm test --reporter=verbose
|
||||
### Debug in VS Code
|
||||
|
||||
Add to `.vscode/launch.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "node",
|
||||
|
||||
Reference in New Issue
Block a user