Engineering

Cómo evité que Claude Code rompiera mi arquitectura con 18 tests en 0,4 segundos

Publicado el 21 may 2026·8 min de lectura
Cómo evité que Claude Code rompiera mi arquitectura con 18 tests en 0,4 segundos

Cómo evité que Claude Code rompiera mi arquitectura con 18 tests que se ejecutan en 0,4 segundos

Pasé las últimas semanas construyendo un boilerplate de producción para sistemas de AI Agent e IoT. FastAPI, asyncpg, LangGraph, MQTT, pgvector — un stack complejo con fronteras arquitectónicas muy específicas que no se pueden violar.

El problema: estaba usando Claude Code y Cursor para acelerar el desarrollo. Y son brillantes. Pero también son completamente agnósticos a la arquitectura que tienes en la cabeza.

Déjame mostrarte el tipo de cosas que pasan sin protección.

El Problema

Mi sistema tiene una regla crítica: el IngestionService nunca debe importar SQLModel ni SQLAlchemy. Es el hot path para ingesta de telemetría IoT — solo SQL crudo con asyncpg, cero overhead de ORM. Esta separación es intencional y está documentada en 20 páginas de ARCHITECTURE.md.

En una sesión típica, le pedí a Claude Code que "refactorizara el IngestionService para ser más consistente con el resto del código base".

Resultado generado:

# services/ingestion.py — generado por Claude Code
from sqlmodel import Session  # ← VIOLACIÓN CRÍTICA
from core.database import get_session

class IngestionService:
    async def ingest(self, payload, trace_id):
        async with get_session() as session:  # ← destruye el hot path
            session.add(SensorReading(**payload.dict()))
            await session.commit()

Técnicamente correcto. Arquitectónicamente catastrófico. La latencia de escritura salta de 0,8ms a 4–6ms. A 5.000 mensajes por segundo, esa es la diferencia entre un sistema que aguanta la carga y uno que colapsa.

Claude Code no sabe esto. No puede saberlo. La arquitectura vive en mi cabeza y en un documento de texto que puede o no haber leído antes de generar el código.

La Solución: Architecture Fitness Tests

La idea no es nueva — Martin Fowler escribe sobre "fitness functions" en Building Evolutionary Architectures. Pero la aplicación al desarrollo asistido por IA es muy concreta: si el modelo va a tener permiso total de refactor, necesitas tests que fallen inmediatamente cuando se cruza una frontera arquitectónica.

No tests en runtime. Tests estructurales estáticos — análisis AST del código Python, cero dependencias externas, cero servidor corriendo.

La suite completa corre en 0,4 segundos. Pre-commit, no post-deploy.

Un Ejemplo Concreto

El test más importante en mi sistema:

# tests/test_architecture_fitness.py

def test_ingestion_service_never_imports_sqlmodel(self):
    """
    El IngestionService es el Hot Path. SQLModel (SQLAlchemy) nunca
    debe aparecer aquí. Es la frontera más crítica del sistema.
    """
    if not INGESTION_SERVICE.exists():
        pytest.skip(f"IngestionService not yet created at {INGESTION_SERVICE}")

    violations = []
    forbidden = {"sqlmodel", "sqlalchemy", "SQLModel", "AsyncSession"}

    for imp in _get_imports(INGESTION_SERVICE):
        module = imp["module"]
        names = imp["names"]
        if any(module.startswith(f) for f in forbidden):
            violations.append(imp)
        if any(name in forbidden for name in names):
            violations.append(imp)

    assert not violations, _format_violation(
        violations,
        "IngestionService imported SQLModel/SQLAlchemy.\n"
        "  Fix: Use asyncpg raw SQL only in services/ingestion.py.\n"
        "  This is the Dual-Path contract. The hot path has zero ORM overhead."
    )

La función _get_imports usa el módulo ast de la stdlib de Python para parsear el archivo sin ejecutarlo:

def _get_imports(filepath: Path) -> list[dict]:
    tree = ast.parse(filepath.read_text(encoding="utf-8"))
    imports = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ImportFrom):
            module = node.module or ""
            imports.append({
                "module": module,
                "names": [alias.name for alias in node.names],
                "line": node.lineno,
                "file": str(filepath),
            })
    return imports

Cero imports externos. Cero servidor. Cero base de datos. Solo Python.

Las 8 Fronteras que Testeo

Mi sistema tiene un contrato arquitectónico de 7 capas. Los tests cubren las violaciones más comunes que la IA genera:

1. Aislamiento de Capas — Import Guards

  • Los workers MQTT nunca importan LangGraph
  • El IngestionService nunca importa SQLModel
  • Los agentes nunca escriben directo via ORM
  • Los routers nunca llaman al IngestionService directamente

2. Integridad del Hot Path

  • El IngestionService usa solo strings SQL crudos, nunca query builders
  • Las tablas del hot path nunca se definen como modelos SQLModel

3. Contratos de Eventos

  • Las publicaciones a Redis Stream usan el envelope canónico DomainEvent
  • Los workers MQTT validan con Pydantic antes de publicar

4. Async Enforcement

  • Sin clientes HTTP síncronos (requests) en routers
  • Sin time.sleep() en el IngestionService
  • Los handlers de workers son async def

5. Contrato de Trace ID

  • IngestionService.ingest() acepta trace_id como parámetro
  • Todos los eventos de dominio incluyen trace_id

6. Convención del Keyspace de Redis

  • Sin claves Redis sin prefijo de namespace
  • checkpoint:{id}, stream:{name}, cache:{type}:{id}

7. Integridad de Migraciones

  • Cero sentencias DDL en código de aplicación

8. Topología de Dependencias Completa

  • Valida la matriz completa de imports prohibidos en una sola pasada

El Resultado en Producción

Cuando le doy a Claude Code permiso total para refactorizar cualquier archivo, el ciclo es:

Claude Code genera código
        ↓
pytest tests/test_architecture_fitness.py -v
        ↓ (0,4 segundos)
Rojo → Claude Code corrige
        ↓
Verde → commit

El modelo nunca puede violar fronteras arquitectónicas sin que me entere inmediatamente. No porque sea adversarial — sino porque optimiza para consistencia local, no para contratos globales.

Después de añadir estos tests, todas las violaciones arquitectónicas desaparecieron de las code reviews. Claude Code empezó a generar código compliant automáticamente porque el archivo de contexto CLAUDE.md describe explícitamente lo que los tests verifican.

CLAUDE.md — El Segundo Mecanismo

Los tests son enforcement. CLAUDE.md es prevención.

Es un archivo en la raíz del repo que Cursor y Claude Code leen antes de generar código. Contiene los contratos en lenguaje explícito con ejemplos ✅ correctos vs ❌ incorrectos:

## Hard Boundaries (Enforced by tests/test_architecture_fitness.py)

### 1. The Dual-Path Contract

services/ingestion.py → asyncpg ONLY
  ✅ await pool.acquire() → await conn.execute("INSERT INTO ...", ...)
  ❌ from sqlmodel import Session
  ❌ session.add() / session.commit()
  ❌ time.sleep() → use await asyncio.sleep()

La combinación de tests que fallan + documentación explícita crea un ambiente donde la IA puede refactorizar libremente con garantías estructurales.

Números Finales

Suite completa:        18 tests
Tiempo de ejecución:   0,42 segundos
Dependencias:          0 (Python stdlib + pytest)
Violaciones detectadas
antes de los tests:    12+
Violaciones después:   0

Conclusión

El desarrollo asistido por IA es genuinamente transformador para la productividad. Pero crea una nueva categoría de riesgo: deriva arquitectónica silenciosa. El modelo optimiza para lo que ve, no para lo que la arquitectura global requiere.

Los Architecture Fitness Tests son la respuesta. No son difíciles de escribir — el módulo ast de Python hace todo el trabajo pesado. Y el retorno es inmediato: libertad total para usar IA en tareas de refactor sin ansiedad por lo que podría haberse roto.

Si estás construyendo un sistema distribuido con desarrollo asistido por IA, estos tests son lo primero que deberías escribir — no lo último.


El boilerplate completo (FastAPI + asyncpg + LangGraph + MQTT + pgvector + Prometheus + Grafana) con los 18 tests, el CLAUDE.md y un ARCHITECTURE.md de 20 secciones está disponible en murinelo.gumroad.com/l/pdmfvr