尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Asistente de finanzas personales para Argentina. Le contás tus gastos e ingresos como se los contarías a alguien («gasté 85.000 en el súper con débito», «pagué 15 dólares de Spotify con la tarjeta») y los registra, te dice en qué se te va la plata y a cuánto está el dólar. Es un chat con un modelo de lenguaje (OpenAI) que decide qué herramientas usar sobre tus datos guardados en Supabase y sobre las cotizaciones de dolarapi.com. Demo: https://administrador-de-finanzas-mspadoni.vercel.app (se entra con una cuenta de Google). Video demo (recorrido por la app): https://youtu.be/8iwNAZyjZMU檢視完整 README ↗
Asistente de finanzas personales para Argentina. Le contás tus gastos e ingresos como se los contarías a alguien («gasté 85.000 en el súper con débito», «pagué 15 dólares de Spotify con la tarjeta») y los registra, te dice en qué se te va la plata y a cuánto está el dólar. Es un chat con un modelo de lenguaje (OpenAI) que decide qué herramientas usar sobre tus datos guardados en Supabase y sobre las cotizaciones de dolarapi.com.
Demo: https://administrador-de-finanzas-mspadoni.vercel.app (se entra con una cuenta de Google).
Video demo (recorrido por la app): https://youtu.be/8iwNAZyjZMU
Ver todas las capturas, explicadas una por una →
Problema que resuelve. Mucha gente quiere saber en qué se le va la plata, pero abandona las planillas y las apps de gastos porque cargar cada cosa es tedioso: elegir categoría, fecha, medio de pago, y en Argentina además convertir los dólares a mano con la cotización que corresponda (oficial, blue, MEP o tarjeta, que pueden ser muy distintas).
Público objetivo. Personas en Argentina que manejan su plata en pesos y en dólares y quieren ordenar sus gastos sin volverse contadores: no saben (ni quieren saber) de planillas, pero sí quieren poder responder «¿cómo vengo este mes?».
Propuesta de valor. Cargar un gasto es escribir una frase. El asistente entiende qué pasó, pregunta solo lo que falta (nunca inventa el medio de pago ni una categoría dudosa), convierte los dólares con la cotización del día citando la fuente, y resume el mes con números que salen de la base de datos, no de la imaginación del modelo.
Una tool es una función del servidor que el modelo puede pedir que se ejecute. El modelo no ejecuta nada por su cuenta:
recibe la lista de tools con su descripción y qué datos esperan, y en cada respuesta decide si necesita alguna (y cuál,
con qué datos). El servidor valida esos datos, ejecuta la función y le devuelve el resultado al modelo, que con eso
arma la respuesta. Acá el modelo decide solo (toolChoice: "auto"), con hasta 8 pasos por respuesta (por ejemplo,
consultar movimientos y después borrar uno).
| Tool | Qué hace | Por qué existe |
|---|---|---|
registrar_movimiento | Guarda un gasto o ingreso: monto, moneda, categoría, medio de pago, descripción y fecha. | Es la razón de ser del bot: convertir una frase en un registro ordenado. Si es en dólares busca la cotización del día y guarda también el monto en pesos, para que todo se pueda sumar en una misma moneda. |
consultar_movimientos | Lista los movimientos de un período (con filtros por tipo y categoría) y sus totales. | Para «¿qué gasté esta semana?» y para encontrar el movimiento exacto (su id) antes de borrarlo: el modelo nunca borra «a ciegas». |
estadisticas | Balance, gastos por categoría, promedio diario y variación contra el período anterior. | Responde la pregunta que más importa («¿cómo vengo?»). Las cuentas las hace código propio, no el modelo: un LLM puede equivocarse sumando o calculando porcentajes, y acá un error de cuentas es justo lo que no puede pasar. |
borrar_movimiento | Borra un movimiento por su id. | Para corregir errores («ese gasto lo cargué dos veces»). Solo se usa después de que la persona confirmó cuál borrar. |
cotizacion_dolar | La cotización (compra y venta) del dólar oficial, blue, MEP y tarjeta, con la hora de actualización. | En Argentina «el dólar» son varios y cambian todos los días: la persona necesita el dato real, con su fuente, y el modelo no puede saberlo de memoria. |
convertir | Pasa un monto de pesos a dólares o al revés con la cotización actual (a valor de compra y de venta). | Para «¿cuánto son 200 dólares blue?» sin que el modelo haga la cuenta: la hace el código con la cotización real. |
Un total suelto no dice mucho: «gastaste $ 167.030 este mes» no te dice si vas bien o mal. Comparado con el mes pasado
sí («vas 15 % arriba»): te avisa a tiempo que algo cambió y, mirando las categorías, en qué. Por eso estadisticas
trae siempre, además del período pedido, el anterior del mismo largo (el mes pasado, la semana pasada, o los días
inmediatamente anteriores de un rango), y si no hay datos para comparar lo dice en vez de inventar.
Ojo: la comparación es en pesos nominales. Con inflación, parte de una suba puede ser solo que los precios aumentaron; ajustar por inflación está en las mejoras futuras.
1. Registrar un gasto conversando
Como persona que quiere llevar sus gastos
Quiero contarle al asistente lo que gasté con mis palabras
Para no tener que completar un formulario por cada compra
Criterios de aceptación:
- [x] «Gasté 85.000 en el súper con débito» queda registrado como gasto de supermercado, con débito, con fecha de hoy.
- [x] Si falta el medio de pago o la categoría es ambigua («gasté 12.000 en hot dogs»), pregunta antes de registrar.
- [x] Cada registro se ve como una tarjeta con lo guardado y el panel «Este mes» se actualiza sin recargar la página.
- [x] Un monto inválido (cero, negativo, con más de dos decimales o más grande de lo que entra en la base) no se guarda
y el asistente explica por qué.
- [x] Las fechas relativas («ayer», «el lunes») se entienden; una fecha futura solo si tiene sentido (un cheque diferido).
- [x] El asistente nunca dice «registrado» si la herramienta no lo guardó.
2. Registrar gastos en dólares
Como persona que paga servicios en dólares
Quiero registrar un gasto en dólares sin buscar la cotización
Para saber cuánto me costó en pesos
Criterios de aceptación:
- [x] «Pagué 15 dólares de Spotify con la tarjeta» se guarda en US$ 15 y en pesos con el dólar tarjeta de ese día.
- [x] La respuesta dice qué cotización usó y cita la fuente (dolarapi.com).
- [x] Si dolarapi.com no responde, no se registra con una cotización inventada: el asistente avisa que no pudo.
3. Entender en qué se va la plata
Como persona que llega justa a fin de mes
Quiero preguntar «¿en qué gasto más?» o «¿cómo vengo?»
Para decidir dónde recortar
Criterios de aceptación:
- [x] Responde con ingresos, gastos, balance, porcentaje por categoría y promedio diario del período.
- [x] Compara con el período anterior cuando hay datos, y lo dice cuando no los hay.
- [x] Los números salen de la base (la tool `estadisticas`), nunca del modelo.
- [x] «Este mes» está siempre a mano (al costado en la compu, en un cajón en el celular).
4. Consultar el dólar
Como persona que ahorra o cobra en dólares
Quiero preguntar a cuánto está el dólar y convertir montos
Para tomar decisiones con la cotización del día
Criterios de aceptación:
- [x] «¿A cuánto está el blue?» muestra compra, venta, hora de actualización y fuente.
- [x] «¿Cuánto son 200 dólares MEP?» convierte con la cotización actual (compra y venta).
- [x] Si el servicio falla, tarda o limita los pedidos, el asistente lo explica en una línea en vez de inventar.
5. Retomar una conversación
Como persona que usa la app desde el celular y la compu
Quiero que mis conversaciones y movimientos queden guardados
Para seguir donde dejé, desde cualquier dispositivo
Criterios de aceptación:
- [x] Al recargar o volver a entrar, la conversación sigue ahí, con sus tarjetas.
- [x] Cada conversación tiene un título y se puede borrar (con confirmación y unos segundos para deshacerlo).
- [x] Cada persona ve solo lo suyo, aunque conozca el id de una conversación ajena.
| Requisito | Cómo |
|---|---|
| Historial persistente | Conversaciones y mensajes guardados en Supabase (Postgres), por persona. Al reabrir una conversación se ve igual que cuando se dejó, con sus tarjetas. |
| Input con validación | No se puede mandar un mensaje vacío ni de más de 6000 caracteres: el navegador lo avisa y desactiva el botón, y el servidor lo vuelve a validar con Zod (nunca se confía solo en el navegador). |
| Estados de carga, error y éxito | Carga de la página, «Abriendo la conversación…», la respuesta apareciendo en vivo, tarjetas de éxito, «Actualizando…» en el panel, y errores con un mensaje útil y «Reintentar» cuando tiene sentido. |
| Panel de debug (bonus) | Por respuesta: qué tools usó el modelo y en qué orden, con sus datos y resultados, el modelo, los pasos, los tokens de entrada y salida y la demora. |
| Interpretación de intención | El modelo entiende si la persona quiere registrar algo, consultar, saber del dólar o algo fuera de tema, y en base a eso elige la tool (o ninguna). |
| Generación de respuesta | Respuestas contextuales en español rioplatense, que tienen en cuenta lo que se habló antes en la conversación. |
| Estructuración de datos | Para llamar a una tool el modelo arma un JSON con un formato fijo (por ejemplo { tipo, monto, moneda, categoria, medioDePago, … }), que se valida con un esquema de Zod antes de ejecutar nada (ver validaciones). |
| Tool calling nativo y few-shot (bonus) | Las 6 tools usan el tool calling nativo de OpenAI a través del AI SDK. El system prompt tiene 7 ejemplos de cómo actuar (cómo está armado el prompt). |
| API externa con manejo de errores | dolarapi.com: timeout de 5 s, un reintento ante error del servidor (5xx), timeout o límite de pedidos (429, respetando Retry-After), caché de 5 minutos y validación con Zod de la respuesta. |
| Mínimo 2 tools | 6: dos que consumen la API externa, tres de persistencia y una de cálculo. |
| Persistencia | Conversaciones, mensajes, movimientos y el uso del asistente, en Supabase. |
| Anti-«wrapper de ChatGPT» |
| Feature | Cómo | Por qué |
|---|---|---|
| Streaming | La respuesta llega palabra por palabra (streamText en el servidor, useChat en el navegador) y las tarjetas de las tools aparecen apenas se ejecutan. | Una respuesta con tools puede tardar varios segundos. Ver que ya está escribiendo (y qué está haciendo) se siente mucho más rápido que esperar en blanco, y muestra que el pedido no se perdió. |
| Testing | 544 tests con Vitest (unitarios y de integración, con la base local en Docker) y 18 de punta a punta con Playwright, en compu y celular. Ver Tests. | Con un LLM en el medio es fácil romper algo sin darse cuenta. Los tests prueban sobre todo lo que puede fallar: datos inválidos, errores de las APIs, permisos entre personas. |
| Rate limiting | Por persona: 10 mensajes por minuto y 150 por día. Los cuenta una función de Postgres (consumir_cuota) en un solo paso y con un candado por persona. | Cada mensaje gasta crédito de OpenAI. Sin límite, una persona (o un script con su sesión) podría agotarlo. Contarlo en la base hace que no se pueda esquivar con pedidos en paralelo. |
| Observabilidad | Log estructurado en JSON (una línea por evento: cada respuesta del modelo con sus tokens, tools y demora, y cada error), sin datos de la persona. Además, el panel de debug. | En producción no se puede «poner un console.log y mirar». Con un log en JSON se puede filtrar en Vercel por evento y entender qué pasó, sin exponer montos ni descripciones. |
| Accesibilidad | Tamaño de letra ajustable con una barra (85 % a 130 %, también con el teclado), que se recuerda. Etiquetas ARIA, todo usable con teclado y con foco visible, cajones que no dejan escapar el foco, anuncios para lectores de pantalla, contraste WCAG AA en tema claro y oscuro. Revisado con axe en los tests E2E. | Una app de finanzas tiene que poder usarla cualquiera, incluso con lector de pantalla o solo con teclado. El chequeo automático evita que una mejora visual rompa la accesibilidad. |
El proyecto está organizado en capas: cada carpeta tiene una responsabilidad y solo puede usar las capas de abajo.
Esas reglas no son solo una convención: las hace cumplir ESLint, así que si alguien las rompe, npm run lint y el build
de Vercel fallan con el motivo. El detalle completo está en docs/ARQUITECTURA.md; el vocabulario
del dominio (qué es un movimiento, una categoría, un período), en CONTEXT.md.
Cada capa tiene un trabajo. Siguiendo el ejemplo de alguien que escribe «Gasté 85.000 en el súper con débito»:
| Capa | Carpeta | Qué hace, en simple | En el ejemplo |
|---|---|---|---|
| Frontend | src/frontend/ | Lo que se ve: la pantalla del chat, las tarjetas, los paneles. | El campo donde escribís y el botón de enviar. |
| Rutas | src/app/ | La puerta de entrada al servidor: recibe el pedido, se fija que haya una sesión iniciada y se lo pasa a un controller. | Recibe el mensaje en /api/chat. |
| Controllers | src/backend/controllers/ | Revisan que el pedido esté bien armado y deciden qué hay que hacer con él. Hay uno por tipo de pedido. | ChatController revisa el mensaje, descuenta la cuota y llama al asistente. |
| Asistente | src/backend/asistente/ | Habla con OpenAI: le manda las instrucciones, la conversación y la lista de herramientas. Solo el agente y el titulador usan el modelo. | Le pregunta al modelo qué hacer, y el modelo decide registrar un gasto. |
| Tools | src/backend/tools/ | Las herramientas que el modelo puede usar. Cada una revisa los datos que armó el modelo y le pasa el pedido a un servicio. | registrar_movimiento recibe { gasto, 85000, supermercado, debito }. |
| Servicios | src/backend/servicios/ | Hacen el trabajo de verdad (registrar, calcular estadísticas, buscar el dólar). Los usan varias partes de la app. | MovimientosServicio registra el gasto. El panel «Este mes» usa el mismo servicio. |
| Dominio | src/backend/models/dominio/ | Las reglas del negocio, sin base de datos ni internet: qué es un gasto válido, cómo se suma la plata, qué es «este mes». | Controla que el monto sea positivo y que «supermercado» sea una categoría de gasto. |
| Repositorios | src/backend/models/repositorios/ | Los únicos que leen y escriben en la base de datos (Supabase). | Guarda el gasto en la tabla de movimientos. |
| Infraestructura | src/backend/lib/ | Los clientes de los servicios de afuera: OpenAI, dolarapi.com, y también el log y las claves. El modelo de OpenAI se crea solo acá (lib/openai.ts), una vez, y se reusa. | Si el gasto fuera en dólares, acá se pide la cotización a dolarapi.com. |
Dos reglas que ordenan todo (y que ESLint hace cumplir):
MovimientosServicio y nunca pueden mostrar números distintos./: la pantalla de inicio (con «Ingresar con Google» si no hay sesión)./conversacion/<id>: una conversación.POST /api/chat: un mensaje al asistente.GET /auth/callback: la vuelta del login de Google.Qué pasa, en orden, cuando la persona escribe «Pagué 15 dólares de Spotify con la tarjeta» y aprieta Enter:
En el navegador. El campo valida que el mensaje no esté vacío ni pase los 6000 caracteres. useChat (del AI SDK)
manda a POST /api/chat solo el mensaje nuevo y el id de la conversación; el historial no viaja, lo lee el servidor.
La ruta (src/app/api/chat/route.ts) verifica que haya sesión (si no, responde 401
«Tu sesión expiró») y le pasa el pedido a ChatController.
ChatController prepara todo antes de llamar al modelo, y corta apenas algo falla, sin guardar nada:
El agente llama al modelo (agente.ts) con el system prompt, el historial
(la última respuesta completa; las anteriores con los resultados de sus tools resumidos, para ahorrar tokens) y la
lista de tools.
El modelo decide qué hacer. Acá entiende que es un gasto en dólares con tarjeta y pide ejecutar
registrar_movimiento con { tipo: "gasto", monto: 15, moneda: "USD", categoria: "suscripciones", medioDePago: "credito", tipoDeDolar: "tarjeta", … }.
La tool valida ese JSON con su esquema y se lo pasa a MovimientosServicio.registrar, que:
useChat en React) y tipos derivados de las tools reales (si una tool cambia,
la pantalla deja de compilar hasta adaptarla). LangChain sumaba una capa de abstracción que este caso no necesita.lib/openai.ts): la primera vez que
se pide, y después se reusa. Lo usan solo el agente (las respuestas) y el titulador (los títulos); los controllers y
servicios no saben de OpenAI. Así, cambiar de proveedor o de modelo se hace en un solo archivo, y en los tests se les
pasa un modelo falso.Un movimiento pasa por tres controles antes de quedar guardado. Son redundantes a propósito: cada uno protege de algo distinto.
El esquema de la tool (validacionTools.ts) controla la forma de lo
que arma el modelo: que estén los campos, que el monto sea un número positivo, que la moneda sea ARS o USD, que
la categoría y el medio de pago sean de la lista, que la fecha sea AAAA-MM-DD. Si el modelo arma un JSON roto, la
tool ni se ejecuta. Además, las descripciones de cada campo son las instrucciones que lee el modelo («el medio de pago
nunca lo asumas»).
Las reglas del dominio (validacionDominio.ts) controlan que
el movimiento tenga sentido, antes de buscar la cotización y de guardar:
Si algo no cumple, el servicio devuelve { ok: false, motivo: "datos_invalidos", detalle: "La categoría no corresponde al tipo de movimiento." } y el modelo corrige el dato o se lo explica a la persona. Estas reglas están en
el dominio (y no solo en la tool) porque son del negocio, no del modelo: valen igual sin importar quién pida guardar.
Los check de la base (en la migración)
repiten las reglas importantes: monto positivo, moneda y medio de pago válidos, categoría según el tipo, y que un
movimiento en dólares tenga su cotización. Es la última red: aunque un bug saltee las capas anteriores, la base no
acepta un dato imposible.
{ ok: false, motivo, detalle }, el modelo se lo explica a la
persona en una línea y, si fue un dato inválido, lo corrige y reintenta una vez. Si falla algo inesperado (la base, un
bug), queda en el log y la tool devuelve error_interno: la conversación no se rompe.tiempo (no respondió en 5 s), limite
(429, demasiados pedidos), servicio (error del servidor o de la red) y respuesta_invalida (respondió algo que no
pasa la validación). Ante timeout, 5xx o 429 se reintenta una vez (con un 429, esperando lo que pide Retry-After si
es razonable). Nunca se registra un gasto en dólares con una cotización inventada.asistente_no_disponible), «saturado» (429 por rate limit,
con «Reintentar») y «demorado» (más de 45 s). Cada uno se muestra con un mensaje para la persona, sin detalles técnicos.limite_por_minuto, pedido_invalido,
conversacion_no_encontrada…) definido en erroresShared.ts, y un único archivo los
traduce a HTTP (respuestaDeError.ts).numeric(14,2).service_role) nunca se usa en la app: cada
consulta va con la sesión de la persona.Probando con OpenAI de verdad, el asistente llegó a decir «Listo: gasto de $ 40.000 en transporte» sin llamar a
registrar_movimiento: el gasto no se guardó. La causa: para ahorrar tokens, a las respuestas viejas se les sacaban
las tools, y el modelo veía en su historial confirmaciones sin ninguna llamada detrás, así que aprendía a confirmar sin
registrar. Ahora las respuestas viejas conservan la llamada a cada tool (con el resultado resumido) y el prompt solo deja
confirmar si la tool se llamó en esa misma respuesta. Tiene tests de regresión, y se volvió a verificar con el modelo
real (ver las capturas).
Está en src/backend/asistente/systemPrompt.ts, en este orden:
ok: false.Además, cada tool tiene su descripción y un esquema con una descripción por campo: son instrucciones que el modelo lee justo cuando está por llamarla. El historial que recibe son los últimos 20 mensajes: la última respuesta completa y, de las anteriores, el texto y las llamadas a tools con los resultados resumidos.
El proyecto se desarrolló con Claude Code (Anthropic) como asistente de programación, siguiendo reglas escritas para que el resultado fuera revisable y no código generado sin control:
CLAUDE.md y REGLAS-SKILLS.md fijan las reglas del proyecto:
Requisitos:
npm install
cp .env.example .env.local # en Windows (cmd): copy .env.example .env.local
npm run dev
La app queda en http://localhost:3000. .env.example explica cada variable. Las obligatorias son:
OPENAI_API_KEY;SUPABASE_URL y SUPABASE_PUBLISHABLE_KEY;FIRMA_DE_MENSAJES: cualquier texto aleatorio de 32 caracteres o más.Crear el proyecto y aplicar las migraciones de src/backend/supabase/migrations/ (esquema inicial, cuota de uso y
firma de mensajes):
npx supabase link --project-ref <id-del-proyecto> --workdir src/backend
npx supabase db push --workdir src/backend
O pegarlas en orden en el SQL Editor de Supabase.
Copiar la Project URL y la publishable key a .env.local.
En Authentication → URL Configuration, poner:
http://localhost:3000;http://localhost:3000/auth/callback;Google OAuth:
https://<id-del-proyecto>.supabase.co/auth/v1/callback.Los tests de integración y los E2E no usan tu proyecto de Supabase: usan una copia local, que el Supabase CLI levanta en Docker con las mismas migraciones y políticas RLS. Así se puede probar todo sin tocar datos reales, y cada test arranca con la base limpia.
Instalar Docker Desktop y abrirlo (tiene que quedar corriendo).
Levantar la base. La primera vez descarga las imágenes de Supabase y tarda unos minutos; después, segundos:
npm run db:start
Al terminar muestra las direcciones y las claves de la copia local. Se ve parecido a esto (los valores de este ejemplo son inventados; en tu máquina van a ser otros):
Started supabase local development setup.
API URL: http://127.0.0.1:54321
DB URL: postgresql://postgres:[email protected]:54322/postgres
Studio URL: http://127.0.0.1:54323
Publishable key: sb_publishable_EJEMPLO_inventado_123
Secret key: sb_secret_EJEMPLO_inventado_456
No hace falta copiar nada: los tests leen esas claves solos con supabase status. Studio
(http://127.0.0.1:54323 en el ejemplo) es el panel de Supabase de la copia local: sirve para mirar las tablas y los
datos que dejan los tests.
Correr los tests (ver Tests).
Al terminar, apagarla con npm run db:stop.
Si npm run db:start falla con un error de conexión, casi siempre es que Docker Desktop no está abierto. Si los tests
fallan con fetch failed después de suspender la máquina, reiniciar la base (npm run db:stop y npm run db:start)
lo resuelve.
La copia local no tiene configurado el login con Google: es para los tests. Para usar la app con npm run dev se usa
tu proyecto de Supabase (ver arriba).
Para correr la app:
| Script | Qué hace |
|---|---|
npm install | Instala las dependencias. Una sola vez, al bajar el proyecto. |
npm run dev | Levanta la app en http://localhost:3000. Se recarga sola al guardar un archivo. |
npm run build | Compila la app para producción (es lo que corre Vercel). Si hay un error de tipos o de lint, falla. |
npm start | Sirve la app ya compilada con build, como en producción. |
Para los tests:
| Script | Qué hace |
|---|---|
npm run db:start | Levanta la base de datos local en Docker (ver arriba). Hace falta para npm test y los E2E. |
npm test | Corre todos los tests de Vitest: los unitarios y los de integración. |
npm run test:unitarios | Solo los tests unitarios (no necesitan Docker ni internet). Tardan segundos. |
npm run test:integracion | Solo los de integración: contra la base local y contra un servidor falso de dolarapi. |
npm run test:e2e | Los tests de punta a punta con Playwright, en un navegador real (compu y celular). |
npm run db:stop | Apaga la base local al terminar. |
Ningún test llama a OpenAI ni a dolarapi.com de verdad: se usan un modelo falso del AI SDK y servidores falsos, que además permiten provocar a demanda los errores (un 429, un timeout, una respuesta rota). Tampoco usan una base remota: lo que guarda datos se prueba contra la copia local de Supabase en Docker, con las mismas migraciones y políticas RLS que producción. Son deterministas: no dependen de internet, del reloj ni del orden en que corren.
| Suite | Dónde | Qué prueba |
|---|---|---|
| Unitarios | src/backend/tests/unitarios/, src/frontend/tests/ | Dominio, servicios, tools, asistente y componentes de React. Sin Docker ni internet. |
| Integración Supabase | src/backend/tests/integracion/supabase/ | Repositorios, RLS (que nadie vea lo de otro), auth, cuota, firma y tools contra la base. |
| Integración HTTP local | src/backend/tests/integracion/http-local/ | El cliente del dólar contra un servidor HTTP local (errores, timeouts, 429). |
| De punta a punta (E2E) | e2e/ | Los flujos críticos en un navegador real, en compu y celular, con chequeo de accesibilidad (axe). |
npm run db:start # una vez por sesión (Docker Desktop abierto)
npm test # unitarios + las dos integraciones
npm run test:e2e # Playwright
npm run db:stop
Vercel despliega cada push a main. En Vercel van las mismas variables de .env.example; OPENAI_API_KEY y
FIRMA_DE_MENSAJES como secretas. Las migraciones nuevas se aplican en la Supabase de producción antes de desplegar el
código que las usa.
https://administrador-de-finanzas-mspadoni.vercel.app
Video demo: https://youtu.be/8iwNAZyjZMU
| El modelo elige las tools según el contexto; sus datos se validan con esquemas; cita la fuente de las cotizaciones; usa el historial; y la lógica de negocio (validar movimientos, convertir, calcular estadísticas, límites de uso) es código propio que corre antes y después del modelo. Además el modelo nunca ve un historial inventado (ver seguridad). |
Deploy en Vercel y .env.example | Link al principio; todas las variables documentadas en .env.example. |
| Compartido | src/shared/ | Lo que usan tanto el servidor como el navegador: el formato de los mensajes, los códigos de error y las URLs. | El formato del mensaje que viaja del navegador al servidor. |
Si algo falla (un dato inválido, dolarapi caído), no se guarda nada y la tool le devuelve al modelo
{ ok: false, motivo, detalle }.
El modelo recibe el resultado y escribe la respuesta («Registrado: US$ 15… Fuente: dolarapi.com»). Si necesita otra tool, vuelve al paso 5 (hasta 8 pasos).
La persona ve la respuesta mientras se arma, sin esperar a que termine todo (esto es el streaming): apenas la tool guarda el gasto aparece la tarjeta «Gasto registrado», y después el texto del asistente va apareciendo de a poco, palabra por palabra, como si lo estuviera escribiendo.
Al terminar, el servidor firma la respuesta y la guarda. En el navegador: