Files
stack/docs/2-development/1-workflow/2-testing.md
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

6.5 KiB

Testing Requirements

Test-driven development standards and practices for Mosaic Stack.

Testing Philosophy

  • Test-Driven Development (TDD) — Write tests before implementation
  • Minimum 85% coverage for all new code
  • All tests must pass before PR approval
  • No untested code in production

Test Types

Unit Tests

Test individual functions and methods in isolation.

Location: *.spec.ts next to source file

Example:

// apps/api/src/auth/auth.service.spec.ts
describe("AuthService", () => {
  it("should create a session for valid user", async () => {
    const result = await authService.createSession(mockUser);
    expect(result.session.token).toBeDefined();
  });
});

Integration Tests

Test interactions between components.

Location: *.integration.spec.ts in module directory

Example:

// apps/api/src/auth/auth.integration.spec.ts
describe("Auth Integration", () => {
  it("should complete full login flow", async () => {
    const login = await request(app.getHttpServer())
      .post("/auth/sign-in")
      .send({ email, password });
    expect(login.status).toBe(200);
  });
});

E2E Tests

Test complete user flows across the entire stack.

Location: apps/web/tests/e2e/ (when implemented)

Framework: Playwright

Running Tests

# All tests
pnpm test

# Watch mode (re-run on changes)
pnpm test:watch

# Coverage report
pnpm test:coverage

# Specific file
pnpm test apps/api/src/auth/auth.service.spec.ts

# API tests only
pnpm test:api

# E2E tests (when implemented)
pnpm test:e2e

Writing Tests

Structure

import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

describe("ComponentName", () => {
  beforeEach(() => {
    // Setup
  });

  afterEach(() => {
    // Cleanup
  });

  describe("methodName", () => {
    it("should handle normal case", () => {
      // Arrange
      const input = "test";

      // Act
      const result = component.method(input);

      // Assert
      expect(result).toBe("expected");
    });

    it("should handle error case", () => {
      expect(() => component.method(null)).toThrow();
    });
  });
});

Mocking

// Mock service dependency
const mockPrismaService = {
  user: {
    findUnique: vi.fn(),
    create: vi.fn(),
  },
};

// Mock module
vi.mock("./some-module", () => ({
  someFunction: vi.fn(() => "mocked"),
}));

Testing Async Code

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 reject with error", async () => {
  await expect(failingFunction()).rejects.toThrow("Error");
});

Coverage Requirements

Minimum Coverage

  • Statements: 85%
  • Branches: 80%
  • Functions: 85%
  • Lines: 85%

View Coverage Report

pnpm test:coverage

# Opens HTML report
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

Always document exemptions in PR description.

Best Practices

1. Test Behavior, Not Implementation

Bad:

it("should call getUserById", () => {
  service.login(email, password);
  expect(mockService.getUserById).toHaveBeenCalled();
});

Good:

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);
});

2. Use Descriptive Test Names

Bad:

it('works', () => { ... });
it('test 1', () => { ... });

Good:

it('should return 401 for invalid credentials', () => { ... });
it('should create session with 24h expiration', () => { ... });

3. Arrange-Act-Assert Pattern

it("should calculate total correctly", () => {
  // Arrange - Set up test data
  const items = [{ price: 10 }, { price: 20 }];

  // Act - Execute the code being tested
  const total = calculateTotal(items);

  // Assert - Verify the result
  expect(total).toBe(30);
});

4. Test Edge Cases

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 null", () => {
    expect(validateEmail(null)).toBe(false);
  });

  it("should reject invalid format", () => {
    expect(validateEmail("notanemail")).toBe(false);
  });
});

5. Keep Tests Independent

// ❌ Bad - Tests depend on order
let userId;
it("should create user", () => {
  userId = createUser();
});
it("should get user", () => {
  getUser(userId); // Fails if previous test fails
});

// ✅ Good - Each test is independent
it("should create user", () => {
  const userId = createUser();
  expect(userId).toBeDefined();
});
it("should get user", () => {
  const userId = createUser(); // Create fresh data
  const user = getUser(userId);
  expect(user).toBeDefined();
});

CI/CD Integration

Tests run automatically on:

  • Every push to feature branch
  • Every pull request
  • Before merge to develop

Pipeline must pass before merging.

Debugging Tests

Run Single Test

it.only("should test specific case", () => {
  // Only this test runs
});

Skip Test

it.skip("should test something", () => {
  // This test is skipped
});

Verbose Output

pnpm test --reporter=verbose

Debug in VS Code

Add to .vscode/launch.json:

{
  "type": "node",
  "request": "launch",
  "name": "Vitest",
  "runtimeExecutable": "pnpm",
  "runtimeArgs": ["test", "--no-coverage"],
  "console": "integratedTerminal"
}

Test Database

Use separate test database:

# .env.test
TEST_DATABASE_URL=postgresql://mosaic:mosaic@localhost:5432/mosaic_test

# Reset test DB before each run
pnpm test:db:reset

Next Steps