Submatter/.agents/skills/tdd-red-green/SKILL.md
Randall Stillwell 3fc7d01585 feat(worker): OpenHands session bootstrap + 5 Agent Skills
Phase 1: Add 5 SKILL.md files to .agents/skills/
  - backlog-sync: plans/ markdown ↔ DB contract
  - convoy-lifecycle: status machine, gates, MCP tools, checkpoints
  - model-routing: role→model via OmniRoute profiles + registry
  - code-review-two-axis: Standards vs Spec parallel sub-agent review
  - tdd-red-green: TDD loop with seams

Phase 2: OmniRoute operational docs
  - docs/omniroute-role-mapping.md: profile→role matrix, convoy examples
  - docs/omniroute-setup.md: step-by-step operator guide

Phase 3: Worker OpenHands integration
  - apps/worker/src/session/load-skills.ts: loads .agents/skills/
  - apps/worker/src/session/openhands-session.ts: agent-server REST adapter
  - apps/worker/src/executor/index.ts: createOpenHandsExecutor() factory
  - apps/worker/src/env.ts: WORKER_SESSION_TYPE, AGENT_SERVER_URL, SESSION_API_KEY
  - apps/worker/src/handle-job.ts: routes to OpenHands when configured
  - apps/worker/src/session/minimal-session.ts: injects skills into system prompt
  - packages/shared/src/worker/index.ts: re-export WorkerExecutorOptions
2026-09-07 15:10:02 -05:00

10 KiB


name: tdd-red-green description: Test-Driven Development loop: write a failing test (Red), make it pass with minimal code (Green), then refactor (Refactor). Write tests before implementation, use seams for isolation, and avoid common TDD anti-patterns. Use when implementing features, fixing bugs, or adding tests to existing code.

TDD: Red-Green-Refactor

A practical test-driven development loop adapted for Echodo's codebase. Write the test first, make it pass with minimal code, then refactor. This skill teaches you the TDD cycle, how to write effective tests, and common pitfalls to avoid.

When to Activate

Use this skill when:

  • Implementing a new feature (add tests first)
  • Fixing a bug (write a test that reproduces it, then fix)
  • Adding coverage to existing code
  • Following Echodo's TDD convention in role-implementer briefs

The Core Loop

┌─────────────────────────────────────────────────────────────┐
│  1. Write failing test (Red)                                │
│     → Test describes expected behavior                      │
│     → Run test → fails (expected)                           │
├─────────────────────────────────────────────────────────────┤
│  2. Write minimal code to pass (Green)                      │
│     → Only enough to make test pass                         │
│     → No "preemptive" abstractions                          │
│     → Run test → passes                                     │
├─────────────────────────────────────────────────────────────┤
│  3. Refactor (Refactor)                                     │
│     → Clean up without changing behavior                    │
│     → Keep tests passing                                    │
│     → Run full test suite                                   │
└─────────────────────────────────────────────────────────────┘

Step 1: Write Failing Test (Red)

What Makes a Good Test?

  1. Describes behavior, not implementation — "should create user with valid email" not "should call User.create"
  2. Single assertion per test (or very few) — easier to diagnose failures
  3. Test name explains the scenario — describe('when email is invalid', () => { it('should return 400') })
  4. Arrange-Act-Assert structure — clear setup, action, and expectation

Test Location

Follow Echodo conventions:

  • Tests next to source: src/index.ts → src/index.test.ts
  • Test file suffix: .test.ts (Vitest)
  • Use existing test utilities in packages/shared/test/ if available

Example Red Test

// apps/web/server/routers/auth.test.ts
describe('POST /api/auth/register', () => {
  it('should create a user with valid email and password', async () => {
    // Arrange
    const payload = {
      email: 'test@example.com',
      password: 'securepassword123'
    };

    // Act
    const response = await fetch('/api/auth/register', {
      method: 'POST',
      body: JSON.stringify(payload)
    });

    // Assert
    expect(response.status).toBe(201);
    const body = await response.json();
    expect(body.user.email).toBe('test@example.com');
  });

  it('should return 400 when email is invalid', async () => {
    // Arrange
    const payload = {
      email: 'not-an-email',
      password: 'securepassword123'
    };

    // Act
    const response = await fetch('/api/auth/register', {
      method: 'POST',
      body: JSON.stringify(payload)
    });

    // Assert
    expect(response.status).toBe(400);
  });
});

Run test → fails (because route doesn't exist yet)

When Bug-Fixing: Start with a Failing Test

Instead of:

  1. Reading the bug
  2. Fixing the code
  3. Running full test suite

Do this:

  1. Write a test that reproduces the bug
  2. Run test → fails (proves bug exists)
  3. Fix code
  4. Run test → passes (proves bug fixed)
  5. Run full suite

Step 2: Write Minimal Code (Green)

Key Principles

  1. Minimal implementation — only enough to pass the test
  2. No premature abstraction — don't build "flexible" systems "just in case"
  3. YAGNI — You Aren't Gonna Need It
  4. Trust the test — if the test is right, the code should make it pass

Example Green Implementation

// Minimal route to make test pass
// apps/web/app/api/auth/register/route.ts
import { NextResponse } from 'next/server';
import { z } from 'zod';

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});

export async function POST(request: Request) {
  const body = await request.json();
  
  // Basic validation (the test checks this)
  const result = registerSchema.safeParse(body);
  if (!result.success) {
    return NextResponse.json({ error: 'Invalid input' }, { status: 400 });
  }

  // Minimal DB insert (simplified for example)
  const user = await createUser(result.data.email, result.data.password);
  
  return NextResponse.json({ user }, { status: 201 });
}

// Placeholder - in real code, call actual DB
async function createUser(email: string, password: string) {
  return { id: '1', email };
}

Run test → passes

Step 3: Refactor

When to Refactor

  • Code is duplicated
  • Names are unclear
  • Function does too much
  • Test is fragile or implementation-specific
  • You need to add a new test that requires a small change

While Refactoring

  1. Keep tests passing — run after every change
  2. Small steps — refactor one thing at a time
  3. No new functionality — only clean up existing code

Example Refactor

Before:

// auth.ts - duplicated validation
function validateEmail(email: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// Different file, same validation
function isValidEmailAddress(email: string) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

After (extract to shared):

// lib/validation.ts
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export function isValidEmail(email: string): boolean {
  return emailRegex.test(email);
}

Seams: Testing Without Dependencies

A seam is a place where you can alter behavior without changing the code being tested.

Types of Seams

Seam How to Use Example
Dependency Injection Pass dependencies as params function process(db) → process(mockDb)
Interface/Abstract class Swap implementations Database abstract → MockDatabase
Factory functions Return different instances createClient() → createMockClient()
Environment variables Control behavior if (process.env.NODE_ENV === 'test')
Configuration objects Pass config at runtime new Service({ retries: 3 })

Example: Seams in Echodo

// Without seam (hard to test)
async function createUser(email: string) {
  const db = new PostgresDatabase(); // hard-coded
  return db.users.insert({ email });
}

// With seam (testable)
async function createUser(email: string, db: Database = defaultDb) {
  return db.users.insert({ email });
}

// In test
const mockDb = { users: { insert: vi.fn() } };
await createUser('test@example.com', mockDb);
expect(mockDb.users.insert).toHaveBeenCalledWith({ email: 'test@example.com' });

Anti-Patterns to Avoid

1. Test After Implementation

❌ Write code → write tests
✅ Write failing test → write code → pass → refactor

2. Implementation-Specific Tests

❌ expect(service.createUser).toHaveBeenCalledWith(data)
✅ expect(response.status).toBe(201) (behavior, not implementation)

3. Brittle Tests

❌ Test checks exact string: expect(body).toBe('{"id":"1"}')
✅ Test checks structure: expect(body.user.id).toBeDefined()

4. Shared State Between Tests

❌ Global variable modified in one test, relied on by another
✅ Each test sets up its own state

5. Testing Too Much

❌ One test checks validation, DB insert, email send, response format
✅ Separate tests for each concern

6. No Assertions

❌ await service.process(data) — no expect
✅ expect(result).toEqual(expected)

7. Commented-Out Tests

❌ // skip this test for now
✅ Delete or fix the test

Echodo Testing Conventions

  • Framework: Vitest (see packages/*/vitest.config.ts)
  • Location: Same directory as source, .test.ts suffix
  • Mocking: Use vi.fn() from Vitest, mock via dependency injection
  • Database tests: Use test database or in-memory alternative
  • Run tests: pnpm test or pnpm --filter @tasks/<package> test

Example: Bug Fix Flow

Bug report: "Users can register with empty password"

Before (wrong approach)

  1. Read bug report
  2. Add validation to route
  3. Run full test suite
  4. Deploy

TDD Approach (right way)

  1. Write failing test:
it('should reject password shorter than 8 characters', async () => {
  const response = await register({ email: 'test@example.com', password: 'short' });
  expect(response.status).toBe(400);
});
  1. Run test → fails (shows bug exists)

  2. Fix code minimally:

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8)
});
  1. Run test → passes (proves fix works)

  2. Run full suite (no regressions)

  3. Refactor if needed

Integration with Other Skills

  • code-review-two-axis: Reviewer checks for TDD pattern in implementation
  • backlog-sync: When testing markdown import/export features
  • convoy-lifecycle: When testing convoy status transitions
  • model-routing: When testing model resolution logic

Running Tests in Echodo

# Run all tests
pnpm test

# Run specific package
pnpm --filter @tasks/web test
pnpm --filter @tasks/shared test

# Watch mode
pnpm --filter @tasks/web test:watch

# Coverage
pnpm test --coverage

References