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:
Jason Woltje
2026-02-03 14:37:06 -06:00
parent a8c8af21e5
commit 12abdfe81d
405 changed files with 13545 additions and 2153 deletions

View File

@@ -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

View File

@@ -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",