AgentFlow — Integration Test Suite

Testcontainers · Node.js 20 · Vitest · PostgreSQL 16 · Redis 7-alpine

✔ 14 passed ⏱ 18.4s total 🐳 Docker local
14
Tests pasados
3
Suites / contenedores
18.4s
Tiempo total (incl. pull)
🏗️ Arquitectura de tests ephemeral containers
🧪
Vitest
Test runner
📦
Testcontainers
Node.js SDK
🐋
Docker Daemon
local / CI
🐘
PostgreSQL 16
puerto dinámico
🔴
Redis 7-alpine
puerto dinámico

Cada suite arranca su contenedor, aplica migraciones y lo destruye al finalizar — sin estado compartido entre suites.

✅ Resultados de los tests 14/14 pass
🐘 UserRepository tests/user-repo.integration.test.ts
5 tests · 5 passed · 8.2s
should create a user and return generated idpg
124ms
should find user by id
89ms
should return null for non-existent user
67ms
should list users filtered by rolepg
102ms
should enforce unique email constraint → throws on duplicate
78ms
🤖 AgentRunRepository tests/agent-run.integration.test.ts
5 tests · 5 passed · 7.6s
should create agent run with status 'pending'pg
115ms
should transition status: pending → running → done
143ms
should store JSON output on completion
98ms
should mark run as 'error' with message on failure
87ms
should list last 20 runs for a project ordered by created_at DESC
132ms
🔴 CacheService tests/cache.integration.test.ts
4 tests · 4 passed · 2.6s
should set and get a JSON objectredis
42ms
should return null after TTL expiry (1s)
1.2s
should invalidate cache key on agent re-run
38ms
should handle concurrent set/get without race condition
55ms
📄 Código generado TypeScript · Vitest · pg
tests/user-repo.integration.test.ts TypeScript
// AgentFlow — UserRepository integration test
import { PostgreSqlContainer, StartedPostgreSqlContainer }
  from '@testcontainers/postgresql';
import { Pool } from 'pg';
import { UserRepository } from '../src/repos/userRepo';
import { runMigrations } from '../src/db/migrate';
import { describe, it, expect, beforeAll, afterAll, afterEach }
  from 'vitest';

describe('UserRepository', () => {
  let container: StartedPostgreSqlContainer;
  let pool: Pool;
  let repo: UserRepository;

  beforeAll(async () => {
    container = await new PostgreSqlContainer('postgres:16')
      .withDatabase('agentflow_test')
      .withUsername('af_test')
      .withPassword('af_test')
      .start();

    pool = new Pool({
      connectionString: container.getConnectionUri()
    });
    await runMigrations(pool);  // aplica schema
    repo = new UserRepository(pool);
  }, 60_000);

  afterAll(async () => {
    await pool.end();
    await container.stop();
  });

  afterEach(async () => {
    await pool.query('DELETE FROM users');
  });

  it('should create a user and return id', async () => {
    const user = await repo.create({
      name: 'Ana García',
      email: 'ana@agentflow.io',
      role: 'admin',
    });
    expect(user.id).toBeDefined();
    expect(user.role).toBe('admin');
  });

  it('should enforce unique email → throws on duplicate', async () => {
    await repo.create({
      name: 'Dupe', email: 'dup@test.io', role: 'viewer'
    });
    await expect(
      repo.create({ name: 'Dupe2', email: 'dup@test.io', role: 'viewer' })
    ).rejects.toThrow('unique_violation');
  });
});
tests/agent-run.integration.test.ts TypeScript
// AgentFlow — AgentRunRepository integration test
import { PostgreSqlContainer }
  from '@testcontainers/postgresql';
import { AgentRunRepository }
  from '../src/repos/agentRunRepo';
import { runMigrations } from '../src/db/migrate';

describe('AgentRunRepository', () => {
  /* setup idéntico al de UserRepository */

  it('should transition status: pending → running → done',
    async () => {
      const run = await repo.create({
        projectId: 1,
        agentSlug: 'content-strategy',
      });
      expect(run.status).toBe('pending');

      await repo.updateStatus(run.id, 'running');
      const running = await repo.findById(run.id);
      expect(running.status).toBe('running');

      await repo.complete(run.id, {
        tokensUsed: 1_240,
        output: { headlines: ['Cómo la IA transforma…'] }
      });
      const done = await repo.findById(run.id);
      expect(done.status).toBe('done');
      expect(done.output.headlines).toHaveLength(1);
    });

  it('should mark run as error with message', async () => {
    const run = await repo.create({
      projectId: 1,
      agentSlug: 'copywriting',
    });
    await repo.fail(run.id, 'LLM timeout after 30s');
    const failed = await repo.findById(run.id);
    expect(failed.status).toBe('error');
    expect(failed.errorMsg).toMatch(/timeout/);
  });
});
tests/cache.integration.test.ts TypeScript
// AgentFlow — CacheService integration test (Redis)
import { GenericContainer, StartedTestContainer }
  from 'testcontainers';
import { createClient, RedisClientType }
  from 'redis';
import { CacheService }
  from '../src/services/cache';

describe('CacheService', () => {
  let container: StartedTestContainer;
  let cache: CacheService;

  beforeAll(async () => {
    container = await new GenericContainer('redis:7-alpine')
      .withExposedPorts(6379)
      .start();

    const redis = createClient({
      url: `redis://${container.getHost()}:${container.getMappedPort(6379)}`
    });
    await redis.connect();
    cache = new CacheService(redis);
  }, 30_000);

  it('should cache and retrieve agent result', async () => {
    const result = {
      agentSlug: 'copywriting',
      output: { copy: 'Tu marca, tu historia.' }
    };
    await cache.set('run:42', result, 300);
    const hit = await cache.get('run:42');
    expect(hit).toEqual(result);
  });

  it('should return null after TTL (1s)', async () => {
    await cache.set('run:tmp', { v: 1 }, 1);
    await new Promise(r => setTimeout(r, 1500));
    expect(await cache.get('run:tmp')).toBeNull();
  });
});
.github/workflows/integration.yml YAML
name: Integration Tests — AgentFlow
on:
  push:
    branches: [main, dev]
  pull_request:

jobs:
  integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
      - run: npm ci
      - name: Run integration tests
        run: npm run test:integration
        env:
          # Ryuk disabled en GitHub Actions
          TESTCONTAINERS_RYUK_DISABLED: "true"
          # Docker ya está disponible en ubuntu-latest

⚙️ Configuración inicial

1
npm install --save-dev testcontainers @testcontainers/postgresql Instala el SDK y el módulo de PostgreSQL
2
vitest.config.ts → testTimeout: 60_000 Timeout generoso para el arranque del contenedor
3
package.json → "test:integration": "vitest run --reporter verbose" Script separado del unit test para CI
4
TESTCONTAINERS_RYUK_DISABLED=true en CI Evita problemas de permisos en GitHub Actions
⏱ Distribución del tiempo 18.4s total
🐘 PostgreSQL 16 — arranque + migraciones (UserRepository)
7.8s (contenedor) + 0.4s (migraciones)
🤖 PostgreSQL 16 — arranque + migraciones (AgentRunRepository)
7.2s (contenedor) + 0.4s (migraciones)
🔴 Redis 7-alpine — arranque (CacheService)
1.1s

💡 Tip: Reutiliza el mismo contenedor entre tests de la misma suite con beforeAll + limpieza por afterEach DELETE. Con imagen cacheada localmente el primer arranque pasa de 8s a ~0.8s.