6. Desarrollo / Implementación
STATUS: borrador (v1) Estimación: 8-9 páginas
6.1 Entorno de desarrollo
El desarrollo de Quetzy se realiza en local sobre Windows 11 con WSL2 disponible para tareas que requieren entorno Unix, aunque la mayoría del flujo (npm, git, Docker Desktop) funciona directamente en PowerShell o Git Bash. El editor principal es Visual Studio Code, complementado por Claude Code en terminal como asistente agéntico para investigación previa, generación de planes y ejecución de cambios atómicos.
El stack local replica fielmente el de producción: Node.js 20, npm como gestor de paquetes y Docker Desktop para los servicios auxiliares (Insforge BaaS y LiveKit cuando se requiere probar el flujo completo). La aplicación arranca con npm run dev (Next.js en modo Turbopack) sin necesidad de levantar contenedores, gracias al sistema de doble implementación de repositorios: en desarrollo se usa por defecto la implementación mock, que permite trabajar sin conexión a Insforge. La variable de entorno ERP_DATA_SOURCE=insforge activa la implementación productiva cuando se desea validar contra el backend real.
El control de versiones se gestiona con Git y GitHub. Cada feature parte de una rama nueva con la convención tipo/scope-descripcion (feat/erp-chat-attachments, fix/erp-livekit-audio-asymmetric…). Los commits siguen Conventional Commits y todas las ramas se mergean a main con squash and merge para mantener el historial lineal y trazable.
Como herramienta complementaria al desarrollo, se utiliza Claude Code como asistente conversacional con acceso al sistema de ficheros y herramientas (gh CLI, Insforge MCP, búsqueda en repo). El patrón de uso establecido —y que constituye una de las contribuciones metodológicas del proyecto— es el de prompts estructurados con investigación previa obligatoria, plan de commits atómicos numerados, restricciones explícitas y criterios de validación post-deploy. La IA no escribe a ciegas: primero investiga el repositorio, propone un plan que el humano aprueba, y solo entonces implementa.
6.2 Configuraciones importantes
Variables de entorno. El sistema usa un .env.local en desarrollo y un .env.production en el VPS, ambos fuera del control de versiones. Las variables críticas son:
INSFORGE_URL,INSFORGE_ANON_KEY,INSFORGE_JWT_SECRET: conexión y firma de tokens contra el BaaS.LIVEKIT_API_KEY,LIVEKIT_API_SECRET,LIVEKIT_URL: credenciales server-side y URL del servidor LiveKit.NEXT_PUBLIC_INSFORGE_URL,NEXT_PUBLIC_INSFORGE_ANON_KEY,NEXT_PUBLIC_LIVEKIT_URL: variantes públicas expuestas al cliente browser.ERP_DATA_SOURCE: selector entremockeinsforge(afecta al factory de repositorios).CRON_SECRET: secreto compartido para autenticar el endpoint/api/cron/notifications(ver 7.2).ENABLE_TEST_HARNESS: habilita endpoints/api/__test__/*solo en entornos de test e2e.
LiveKit configuración. La configuración del servidor LiveKit se inyecta como variable de entorno LIVEKIT_CONFIG con un YAML codificado en base64 dentro del docker-compose.prod.yml. Esta decisión vino de un bug detectado durante el desarrollo: la sintaxis ${VAR} dentro de livekit.yaml no se interpola en tiempo de ejecución, por lo que los secretos se inyectaban literales. La solución definitiva fue eliminar el archivo livekit.yaml del repo y pasar toda la configuración por variable de entorno, aprovechando que urfave/cli (la librería que usa LiveKit por debajo) resuelve cli.EnvVars automáticamente.
Caddyfile. Configura los tres dominios (erp.quetzy.eu, internal.quetzy.eu, livekit.quetzy.eu) más el redirect de quetzy.eu. El upstream de internal.quetzy.eu apunta a host.docker.internal:7130 para alcanzar la instancia de Insforge que corre fuera del Compose principal. TLS automático via Let’s Encrypt (ACME) sin intervención manual.
Docker Compose. Tres servicios definidos: erp (build local de la imagen quetzy-erp:latest), livekit (livekit/livekit-server:v1.11.0) y caddy (caddy:2-alpine). Red quetzy-net en modo bridge. Healthcheck en erp apuntando a /api/health.
6.3 Módulos implementados
El código del ERP se organiza siguiendo una estructura feature-first bajo src/features/, donde cada dominio funcional agrupa sus componentes, hooks, contextos y modelos. La capa de datos sigue el patrón Repository con doble implementación: las interfaces ErpRepository (src/lib/repositories/erp-repository.ts:40, 33 métodos) y ChatRepository (src/lib/repositories/chat-repository.ts:183, 10 métodos) definen el contrato, y un factory en src/lib/repositories/index.ts:5-8 selecciona la implementación según la variable ERP_DATA_SOURCE:
export const getErpRepository = (): ErpRepository =>
process.env.ERP_DATA_SOURCE === "insforge"
? insforgeErpRepository
: mockErpRepository;Esta decisión permite ejecutar toda la lógica de negocio y los 827 tests unitarios sin conexión al backend real, y es la base del RNF-06 (testabilidad). Todos los contratos de entrada/salida se validan con 16 schemas Zod bajo src/lib/contract/, compartidos entre cliente y servidor.
A continuación se describe la implementación de cada módulo con sus archivos, patrones y decisiones técnicas relevantes. Las decisiones se documentan en el código con el patrón Decision / Reason / Alternative descrito en 6.5. El capítulo 5 contiene los diagramas de clases, secuencia y ER; esta sección se centra en los detalles de implementación.
Autenticación y sesión
Archivos clave: src/app/(auth)/actions.ts, src/lib/auth/session.ts, src/lib/auth/insforge-auth-client.ts, src/middleware.ts.
La autenticación usa Insforge Auth con email y contraseña. El SDK se instancia como singleton server-side con isServerMode: true (insforge-auth-client.ts), que internamente envía client_type=mobile al backend. Esto significa que Insforge devuelve los tokens (accessToken + refreshToken) en el cuerpo JSON en lugar de establecer cookies automáticamente. La solución implementada se documenta en el código como “Plan B”:
// Decision: Plan B — manually persist session cookie.
// Reason: isServerMode uses mobile flow (client_type=mobile) which returns
// accessToken + refreshToken in the JSON body. The SDK does NOT set httpOnly
// cookies in server mode. We write the cookie ourselves.— src/app/(auth)/actions.ts:39-46
La cookie se configura en session.ts:26-32 con httpOnly: true, secure en producción, sameSite: "lax" y maxAge de 7 días. La validación de sesión usa jwtVerify de la librería jose contra el secreto JWT de Insforge (session.ts:57-60).
El middleware (src/middleware.ts) intercepta todas las peticiones y verifica la sesión, con excepciones explícitas para rutas públicas: /login, /api/health, /api/calls/webhook y /api/cron/notifications. La ruta del webhook de LiveKit se excluye porque las peticiones vienen del servidor LiveKit (sin cookie de usuario) y se autentican por firma JWT propia.
Shell del ERP
Archivos clave: src/app/(erp)/layout.tsx, src/features/erp/components/app-header.tsx, src/app/globals.css, 33 archivos .module.css.
El layout principal ((erp)/layout.tsx) actúa como barrera de autenticación server-side: llama a getSessionUser() y redirige a /login si no hay sesión válida. El AppHeader proporciona la navegación global (chat, tickets, clientes, proyectos, horas, contexto), el indicador de notificaciones y el botón de usuario.
El sistema visual se implementa con CSS Modules + CSS Custom Properties, sin Tailwind ni preprocesadores. El archivo src/app/globals.css define los tokens de diseño (colores, sombras, bordes) como variables CSS que los 33 módulos CSS consumen. Cada componente tiene su propio archivo .module.css colocado junto al .tsx, lo que garantiza encapsulación de estilos sin colisiones de nombres. El diseño visual se inspira en Linear (ver 4.3), con modo oscuro como única opción (color-scheme: dark en globals.css).
Los componentes compartidos del ERP (entity-form.tsx, entity-view.tsx, table-pagination.tsx, toast-provider.tsx) proporcionan las abstracciones de presentación comunes: formularios modales, vistas de detalle, paginación de tablas y notificaciones efímeras tipo toast.
Chat en tiempo real
Archivos clave: src/features/chat/ (18 componentes, 7 hooks, 2 contextos), src/lib/repositories/insforge/insforge-chat-repository.ts, src/lib/insforge-realtime-client.ts.
El módulo de chat es el más extenso del sistema. Implementa mensajería de texto con reply-to, mensajes de audio (grabación y reproducción inline), adjuntos (imágenes y PDF hasta 10 MB, con pegado Ctrl+V), reacciones con emoji toggle, typing indicators y embebido de tarjetas de proyecto.
La interfaz ChatRepository se separó deliberadamente de ErpRepository:
// Decision: Separate ChatRepository interface (not extending ErpRepository).
// Reason: Chat is a distinct workspace-scoped domain with different access
// patterns (realtime, no client_id). ErpRepository already has 30+ methods.
// Alternative: Extend ErpRepository — rejected to avoid bloating the interface.— src/lib/repositories/chat-repository.ts:1-4
Patrón de compensación manual en sendMessage. El SDK de Insforge no expone transacciones multi-tabla. La implementación de sendMessage (insforge-chat-repository.ts:202-305) ejecuta tres INSERTs secuenciales (mensaje, attachments, project_attachments) y, si alguno falla tras el primero, ejecuta compensación: DELETE del mensaje huérfano + storage.remove() de los archivos ya subidos (insforge-chat-repository.ts:239 y :277). Este patrón se detalla en el diagrama de secuencia de la Figura 5.2.
Enriquecimiento de mensajes en tiempo real. Los eventos WebSocket de Insforge entregan un payload parcial (sin sub-entidades como attachments o reactions). El hook use-chat-realtime.ts resuelve esto con un refetch en segundo plano: al recibir un evento INSERT_message, calcula un timestamp justBefore = created_at - 1ms y llama a fetchMessagesAfterRef.current(channelId, justBefore) (use-chat-realtime.ts:158). La función fetchMessagesAfter en chat-context.tsx:385-410 ejecuta la query con JOINs completos y aplica una lógica de merge-and-replace: reemplaza mensajes existentes con la versión enriquecida del servidor y añade los genuinamente nuevos, evitando duplicados y garantizando que attachments y reactions estén siempre presentes en el estado del cliente.
El cliente de realtime (insforge-realtime-client.ts) se instancia como singleton browser, obtiene un JWT dedicado vía /api/auth/realtime-token y gestiona la reconexión automática con un guard de refresco de token para evitar bucles de retry.
Tickets y máquina de estados
Archivos clave: src/features/tickets/model/ticket-state-machine.ts, src/features/tickets/components/ticket-detail.tsx, src/features/tickets/components/ticket-list.tsx, src/features/tickets/model/ticket-workspace.test.ts.
La máquina de estados es una de las piezas centrales del sistema. Define 14 estados (ver diagrama en Figura 5.10) y sus transiciones permitidas en la constante transitionPolicies (ticket-state-machine.ts:144). Cada transición lleva asociada una función guard que valida precondiciones sobre el snapshot del ticket:
requireComplexity(:75): el ticket debe tener complejidad asignada.requireClarification(:85): requiereclarified_inputyclient_summary.requirePrUrl(:105): requierepr_urlno nulo.requireDeployData(:113): requieredeploy_refydeployed_at.requireCloseEvidence(:133): requiere al menos uncontext_itemválido o unclose_override_note.
Las transiciones inválidas lanzan un TicketTransitionError (ticket-state-machine.ts:56) con el mensaje del guard que falló. La implementación es pura y funcional: no depende de ningún efecto secundario ni de la base de datos; recibe un snapshot del ticket y devuelve el nuevo estado o un error. Esto permite que los 15 tests de ticket-state-machine.test.ts validen toda la lógica sin mocks de infraestructura.
Los 4 gates humanos obligatorios del pipeline son: clasificación (received → classified), aprobación de diseño (pending_client_review → designed), validación de PR (pr_open → validated) y cierre con evidencia (learned → closed).
Llamadas WebRTC / LiveKit
Archivos clave: src/features/calls/lib/livekit-room.ts, src/features/calls/context/call-context.tsx, src/features/calls/components/ (4 componentes: call-banner, call-button, call-controls, screen-share-pane), src/app/api/calls/webhook/route.ts.
La integración con LiveKit resuelve dos problemas técnicos documentados con el patrón Decision/Reason/Alternative.
Suscripción a tracks remotos antes de connect(). Durante el desarrollo se detectó que los tracks de audio remoto se perdían intermitentemente. La causa era un race condition: el servidor LiveKit comienza la negociación SDP durante connect(), y si el listener de TrackPublished se registra después, los tracks llegan al peerConnection sin transceiver y se descartan. La solución registra el listener antes de conectar:
// Decision: register TrackPublished listener BEFORE connect()
// Reason: race condition — server starts negotiating remote tracks during
// connect(); without a handler ready, autoSubscribe-default tracks
// arrive at the peerConnection without a transceiver and are dropped.— src/features/calls/lib/livekit-room.ts:51-58
Además, un catch-up loop (livekit-room.ts:68-74) itera sobre room.remoteParticipants después de connect() para suscribirse a tracks ya publicados por peers que se unieron antes, cubriendo el caso de reconexión.
Verificación de webhooks. LiveKit envía webhooks a POST /api/calls/webhook para eventos como participant_joined y room_finished. La API route lee el cuerpo crudo del request y verifica la firma JWT con verifyWebhook (webhook/route.ts:24). El evento room_finished (webhook/route.ts:35-47) calcula la duración de la llamada, actualiza la tabla chat_call y publica un evento CALL_ENDED vía realtime para que los clientes browser eliminen el banner de llamada activa.
El CallContext (call-context.tsx) gestiona el estado local de la llamada (conectando, activa, desconectada), los controles de mute/unmute y la compartición de pantalla. Los 28 tests de call-context.test.ts cubren todos los estados y transiciones.
Gestión de clientes
Archivos clave: src/features/clients/components/ (client-list-screen.tsx, client-form-modal.tsx, client-screen.tsx), src/features/clients/hooks/ (use-client-list.ts, use-client-detail.ts).
Módulo CRUD que permite crear, editar y consultar clientes con sus metadatos (nombre, tier, presupuesto mensual de tickets, estado). La vista de detalle muestra los proyectos asociados al cliente (relación 1:N definida en data_model.sql). La validación de formularios usa los schemas Zod de src/lib/contract/client-detail-schema.ts y client-list-schema.ts, compartidos con las API routes del servidor.
Gestión de proyectos
Archivos clave: src/features/projects/components/ (project-list-screen.tsx, project-form-modal.tsx, project-screen.tsx), src/features/projects/hooks/ (use-project-list.ts, use-project-detail.ts).
Estructura idéntica al módulo de clientes. Cada proyecto pertenece a un cliente y puede tener metadatos de infraestructura (repo_url, vps_host). Al crear un proyecto, se genera automáticamente un canal de chat tipo project vinculado al project_id, lo que conecta la comunicación del equipo directamente con el contexto del proyecto.
Control horario
Archivos clave: src/features/hours/components/ (hours-list-screen.tsx, scope-picker-modal.tsx, timer-display.tsx), src/features/hours/context/timer-context.tsx, src/features/hours/model/ (format-elapsed.ts, hours-list-utils.ts).
El módulo de control horario implementa un timer con estados (iniciado, pausado, reanudado, detenido) asociable a cliente, proyecto y ticket mediante el ScopePickerModal. El TimerContext gestiona el estado del timer activo a nivel global del ERP, y TimerDisplay muestra el tiempo transcurrido con actualización en tiempo real. Las funciones de formato (format-elapsed.ts) y utilidades de listado (hours-list-utils.ts) tienen tests unitarios dedicados.
La tabla time_entry se creó directamente en la consola de Insforge durante el desarrollo y no aparece en data_model.sql. Esta discrepancia queda documentada como deuda técnica explícita (ver 3.6) y se corregirá en la fase de mantenimiento post-TFG.
Contexto técnico y capa de IA
Archivos clave: src/features/context/components/context-bucket.tsx, src/features/context/components/reject-reason-modal.tsx, src/lib/ai/ai-provider.ts, src/lib/ai/skill.ts, src/lib/ai/providers/gemini-provider.ts, src/lib/ai/providers/none-provider.ts, src/lib/ai/skills/context-extractor.ts.
El módulo de contexto implementa el CRUD de context_items con flujo de aprobación humana (propuesto → aprobado / rechazado), soporte para revalidación, marcado como obsoleto y enlace N:M a entidades del ERP vía entity_context_link. La vista ContextBucket muestra los items filtrables por estado con badges visuales y acciones de revisión; el RejectReasonModal solicita justificación al rechazar un item.
La capa de IA se construye sobre dos interfaces mínimas, deliberadamente desacopladas de frameworks agénticos como LangChain o LangGraph (decisión justificada en 4.3):
AIProvider(src/lib/ai/ai-provider.ts): interfaz con un métodocomplete(input: AICompletionInput): Promise<AICompletionResult>que abstrae el proveedor de LLM. Implementada porGeminiProvider(producción) yNoneProvider(entornos sin IA, lanza error explícito).Skill<TInput, TOutput>(src/lib/ai/skill.ts): interfaz genérica conbuildPrompt(input),parseOutput(raw)yvalidate(output). Separa la construcción del prompt, el parsing de la respuesta y la validación de negocio en tres fases distintas.
La única skill implementada en v1 es ContextExtractor (context-extractor.ts), que recibe raw_context (texto crudo de reuniones, emails, documentación) y extrae context_items estructurados con categoría, contenido y entidades relacionadas. Los items extraídos entran en estado proposed y requieren aprobación humana antes de ser consumibles por futuros prompts de IA.
Cada invocación de IA se registra en la tabla ai_call_log con skill utilizado, modelo, tokens consumidos y resultado, proporcionando observabilidad básica del gasto de IA.
Notificaciones y presencia
Archivos clave: src/features/notifications/components/notification-list-screen.tsx, src/features/notifications/hooks/use-notifications-list.ts, src/features/presence/components/online-users-popover.tsx, src/features/presence/hooks/use-presence.ts, src/features/presence/hooks/use-online-users.ts.
Las notificaciones internas se generan por el sistema (alertas de VPS, cambios de estado de tickets) y se persisten en la tabla notification. La vista muestra un contador de no leídas en el AppHeader y permite marcarlas como leídas individualmente o en bloque. El endpoint /api/cron/notifications permite la generación periódica de notificaciones y se autentica con el header x-cron-secret contra la variable CRON_SECRET.
La presencia se implementa con un heartbeat periódico: el hook use-presence.ts envía un UPSERT a la tabla erp_presence cada N segundos. La tabla tiene RLS habilitado para que cada usuario solo pueda modificar su propio registro. El OnlineUsersPopover consume use-online-users.ts para mostrar la lista de usuarios conectados.
La limpieza de presencia al cerrar sesión es un caso especial documentado en el código:
// Decision: best-effort presence cleanup before destroying session.
// Reason: getSessionUser reads the cookie that clearSessionCookie deletes,
// so it must run first. Failure must not block logout (D3 / D6).— src/app/(auth)/actions.ts:52-67
El signout primero intenta limpiar el registro de presencia del usuario y solo después destruye la cookie de sesión. Si la limpieza falla, el logout continúa igualmente para no bloquear al usuario.
6.4 Integraciones externas
El sistema integra con tres servicios externos, dos de ellos auto-alojados:
Insforge BaaS (auto-alojado en internal.quetzy.eu). Cubre cuatro responsabilidades en un único servicio:
- Auth: registro y login con email/password, emisión de JWT, sesión cookie httpOnly de 7 días.
- Database: PostgreSQL 15.15 embebido, expuesto a través del SDK con API tipo fluent query builder.
- Realtime: capa WebSocket que distribuye eventos publicados desde la BD (vía triggers SQL) o desde TypeScript (vía
db().rpc('publish_realtime_event')). - Storage: almacenamiento de archivos por bucket con URLs públicas, usado por el bucket
chat-attachments.
La integración usa el SDK oficial @insforge/sdk@1.2.5, que se instancia como singleton del lado servidor (getInsforgeClient()) y como cliente browser autenticado por sesión.
LiveKit Server (auto-alojado en livekit.quetzy.eu). Servidor SFU (Selective Forwarding Unit) para llamadas WebRTC. La integración tiene dos lados:
- Client-side con
livekit-client@2.18.9para conectar al room, publicar audio local, suscribirse a tracks remotos, gestionar screen share. - Server-side con
livekit-server-sdk@2.15.2para generar tokens JWT firmados que el cliente usa para autenticarse en el room.
LiveKit envía webhooks al ERP (POST /api/calls/webhook) cuando ocurren eventos relevantes (participant_joined, participant_left, track_published, room_finished). El ERP verifica la firma JWT del webhook con la LIVEKIT_API_KEY antes de procesarlo, y actualiza la tabla chat_call con duración, participantes y estado final de la llamada.
Modelos de lenguaje (Gemini) vía lib/ai/providers/gemini-provider.ts. La integración es la única que va contra un servicio cloud no auto-alojado. El proveedor implementa la interfaz AIProvider con un método complete(input) que recibe systemPrompt, userPrompt y parámetros de generación, y devuelve texto + metadatos de uso (tokens consumidos). Esta capa está deliberadamente desacoplada de cualquier framework agéntico (LangChain, LangGraph) por las razones expuestas en 4.3.
6.5 Gestión del código y control de versiones (Git)
El flujo Git de Quetzy está estandarizado al máximo para que sea operable indistintamente por humanos o por agentes IA sin perder coherencia.
Convención de ramas. Cada rama sigue el patrón tipo/scope-descripcion:
feat/: nueva funcionalidad.fix/: corrección de bug.chore/: tareas de mantenimiento (refactors, deps, configuración).docs/: documentación, incluyendo los capítulos de esta memoria (docs/tfg-cap-X).
El scope indica el área del sistema (erp, chat, infra, tfg…). La descripción se escribe en kebab-case y resume la intención. Ejemplos reales del repositorio: feat/erp-chat-attachments-storage, fix/erp-livekit-audio-asymmetric-track-subscribe, docs/tfg-cap-3.
Conventional Commits. Cada commit sigue el formato tipo(scope): descripción. Esto facilita la generación automática de changelogs y permite a los agentes IA inferir la intención del cambio sin necesidad de leer el diff completo.
Squash and merge a main. Todas las PRs se cierran con squash and merge, lo que mantiene el historial de main lineal y legible. El historial granular de cada PR queda accesible en GitHub aunque no en el log principal.
CI/CD obligatorio antes de mergear. GitHub Actions ejecuta cuatro jobs secuenciales en cada PR: lint (ESLint), test (Vitest, 827 tests), build (Next.js build) y deploy (SSH al VPS y ejecución del script deploy.sh). El job deploy solo se dispara tras merge a main y aplica un health check automático con rollback manual disponible.
Pre-push hook local (ECC). El equipo usa un hook personalizado que ejecuta lint y test antes de cada push. Esto reduce la fricción de descubrir fallos solo en CI, especialmente útil cuando un agente IA acaba de generar cambios.
Tickets transformados en planes de PRs. Cada ticket no se cierra con una descripción y una asignación; se cierra con un plan de Pull Requests concreto, sus dependencias y los criterios de aceptación. Esto convierte la planificación en un artefacto ejecutable que reduce la ambigüedad cuando interviene la IA.
Issues como feedback. Las incidencias detectadas en uso se abren como issues en GitHub directamente desde el flujo de trabajo. Durante el desarrollo del TFG se acumularon 15 issues abiertos pendientes de triage, que constituyen el primer backlog post-entrega.
Trazabilidad de decisiones técnicas. Las decisiones arquitectónicas relevantes se documentan dentro del propio código con bloques de comentario estructurado:
// Decision: register TrackPublished listener BEFORE connect()
// Reason: race condition — server starts negotiating remote tracks during
// connect(); without a handler ready, autoSubscribe-default tracks
// arrive at the peerConnection without a transceiver and are dropped.
// Alternative: rely on autoSubscribe default — fails when listeners are
// registered after connect().Este patrón Decision / Reason / Alternative aparece en múltiples puntos críticos del código y permite que cualquier persona (o agente IA) que retome el código entienda no solo qué hace, sino por qué se decidió hacerlo así.
6.6 Testing (unitarios + E2E)
La estrategia de testing de Quetzy se apoya en tres pilares: tests unitarios y de integración con Vitest, tests end-to-end con Playwright y una cobertura objetivo del 80 % en código de negocio. El sistema de doble implementación de repositorios (mock + producción) descrito en 6.3 es la pieza que hace posible testear la lógica de negocio completa sin conexión al backend real.
Estrategia de testing
El proyecto distingue tres niveles de testing:
-
Tests de modelo y lógica pura. Funciones sin efectos secundarios: la máquina de estados de tickets, utilidades de formato, agrupación de mensajes, validación de archivos, clasificación de adjuntos y shortcuts de teclado. Se testean directamente sin mocks.
-
Tests de integración con mock de infraestructura. La implementación mock del repositorio (
MockErpRepository,MockChatRepository) y el mock del SDK de Insforge permiten testear las capas de API routes, hooks y contextos React contra una implementación completa en memoria. Estos tests validan el contrato entre capas sin depender de red ni de base de datos. -
Tests end-to-end. Playwright ejecuta flujos completos contra una instancia del ERP con el test harness habilitado (
ENABLE_TEST_HARNESS=true), que expone endpoints auxiliares para setup y teardown de datos.
La capa de contratos Zod tiene sus propios tests dedicados: 10 archivos de test para los 16 schemas, validando que los schemas aceptan datos válidos y rechazan datos malformados. Esto garantiza que cualquier cambio en un contrato rompe los tests antes de llegar a producción.
Tests unitarios y de integración (Vitest)
Configuración. Vitest se configura en vitest.config.ts con entorno node, inclusión de src/**/*.test.ts y exclusión de e2e/**. El alias @ mapea a ./src para mantener imports consistentes con el código de producción.
Distribución de tests. El proyecto contiene 70 archivos de test con 827 tests en total. Los archivos con mayor concentración de tests reflejan las áreas de mayor complejidad:
| Archivo de test | Tests | Área |
|---|---|---|
insforge-erp-repository.test.ts | 106 | Repositorio ERP (Insforge) |
mock-erp-repository.test.ts | 71 | Repositorio ERP (mock) |
call-context.test.ts | 28 | Contexto de llamadas |
insforge-chat-repository.test.ts | 23 | Repositorio chat (Insforge) |
chat-routes.test.ts | 22 | API routes de chat |
time-entry-repository.test.ts | 16 | Repositorio de horas |
ticket-workspace.test.ts | 16 | Workspace de tickets |
ticket-state-machine.test.ts | 15 | Máquina de estados |
use-chat-realtime.test.ts | 13 | Hook de realtime |
middleware.test.ts | 12 | Middleware de auth |
Los tests de repositorio Insforge (insforge-erp-repository.test.ts con 106 tests y insforge-chat-repository.test.ts con 23 tests) mockean el SDK de Insforge a nivel de respuesta de query, validando que la capa de traducción entre el formato de la BD y los tipos del dominio funciona correctamente. Los tests del mock repository (mock-erp-repository.test.ts con 71 tests) validan la lógica de negocio pura (filtros, paginación, transiciones de estado, deduplicación de context_items) contra la implementación en memoria.
Tests end-to-end (Playwright)
Configuración. Playwright se configura en playwright.config.ts con ejecución secuencial (fullyParallel: false, 1 worker) sobre Chromium. Define dos proyectos:
chromium: ejecuta todos los specs exceptoauth.spec.ts, contra la instancia con test harness en puerto 3100.auth: ejecuta soloauth.spec.tscontra el servidor Next.js en puerto 3101, validando el flujo real de login/logout.
Los 5 archivos de specs cubren los flujos críticos:
| Spec | Flujo validado |
|---|---|
e2e/auth.spec.ts | Login con credenciales válidas e inválidas, redirección, logout |
e2e/crud.spec.ts | Creación, edición y consulta de entidades (clientes, proyectos) |
e2e/guards.spec.ts | Rechazos de transiciones inválidas en tickets (guards funcionales) |
e2e/context-bucket.spec.ts | Flujo de aprobación/rechazo de context_items |
e2e/happy-path.spec.ts | Flujo completo: crear cliente → proyecto → ticket → transicionar por pipeline |
Playwright captura screenshots solo en caso de fallo (screenshots: "only-on-failure") y retiene trazas de video en reintentos (video: "retain-on-failure", 2 retries en CI).
Cobertura y métricas
La cobertura se mide con @vitest/coverage-v8 sobre el código fuente (src/). Las métricas actuales del proyecto:
| Métrica | Valor |
|---|---|
| Statements | 82,22 % |
| Branches | 68,93 % |
| Functions | 84,16 % |
| Lines | 83,50 % |
| Archivos de test | 70 |
| Tests totales | 827 |
| Schemas Zod | 16 archivos |
| Schemas con tests | 10 archivos |
| Specs E2E | 5 archivos |
| Build (compilación) | 22,6 s |
| Páginas generadas | 34 |
La cobertura de statements (82,22 %) y funciones (84,16 %) supera el objetivo del 80 % establecido en el RNF-05. La cobertura de branches (68,93 %) queda por debajo del objetivo porque incluye ramas de error handling defensivo en las implementaciones de Insforge que no se ejercitan en los tests unitarios (se cubren parcialmente en los E2E). La cobertura de líneas (83,50 %) es consistente con la de statements.
El build de producción (Next.js standalone) compila en 22,6 segundos con verificación de tipos TypeScript en 11,8 segundos, generando 34 páginas estáticas y dinámicas. El pipeline CI/CD completo (lint + test + build + deploy) valida cada merge a main antes de que el código llegue a producción.