diff --git a/docs/plan_pruebas/README.md b/docs/plan_pruebas/README.md index 314ff2e..cc5bb40 100644 --- a/docs/plan_pruebas/README.md +++ b/docs/plan_pruebas/README.md @@ -1,45 +1,26 @@ -# Plan de pruebas IPSExtractor (LaTeX) +# Plan / Informe de pruebas IPSExtractor -Documento profesional de pruebas unitarias, integración, E2E, estrés y resiliencia. +## Entregable principal -## Requisitos +**Informe de Pruebas (resultados):** [`InformePruebas_IPSExtractor.pdf`](InformePruebas_IPSExtractor.pdf) +Fuentes: [`informe/`](informe/) -- [TeX Live](https://www.tug.org/texlive/) o MiKTeX -- `pdflatex` (incluido en MiKTeX/TeX Live) -- Opcional: `latexmk` (requiere Perl en MiKTeX) -- Paquetes: `booktabs`, `longtable`, `hyperref`, `fancyhdr`, `geometry`, `babel`, `listings`, `tikz`, `xcolor`, `enumitem`, `siunitx`, `titlesec` +Incluye métricas, gráficas, scorecard y análisis de viabilidad de **100.000 facturas/mes**. -## Compilación +## Plan histórico (referencia) -**Recomendado en Windows (MiKTeX sin Perl):** +El documento de plan de pruebas sigue en esta carpeta (`main.tex` → `PlanPruebas_IPSExtractor.pdf`) como referencia de metodología/IDs. El foco de QA es el **informe**. + +## Campañas + +- [`informes/2026-07-13-shared8g.md`](informes/2026-07-13-shared8g.md) +- Scripts k6: [`scripts/`](scripts/) + +## Compilar informe ```powershell -cd docs\plan_pruebas -pdflatex -interaction=nonstopmode -file-line-error main.tex -pdflatex -interaction=nonstopmode -file-line-error main.tex -Copy-Item main.pdf PlanPruebas_IPSExtractor.pdf -Force -``` - -**Con latexmk** (si Perl está instalado): - -```powershell -latexmk -pdf main.tex -``` - -Artefactos: `main.pdf` y `PlanPruebas_IPSExtractor.pdf`. - -## Scripts de estrés (k6) - -Ver [`scripts/README.md`](scripts/README.md). Requiere [k6](https://k6.io/) y el stack levantado en `http://127.0.0.1:8189`. - -## Estructura - -``` -docs/plan_pruebas/ - main.tex - preamble.tex - latexmkrc - sections/ # capítulos del plan - diagrams/ # TikZ - scripts/ # k6 de referencia +cd docs\plan_pruebas\informe +pdflatex -interaction=nonstopmode main.tex +pdflatex -interaction=nonstopmode main.tex +Copy-Item main.pdf ..\InformePruebas_IPSExtractor.pdf -Force ``` diff --git a/docs/plan_pruebas/informe/README.md b/docs/plan_pruebas/informe/README.md new file mode 100644 index 0000000..a31f97f --- /dev/null +++ b/docs/plan_pruebas/informe/README.md @@ -0,0 +1,20 @@ +# Informe de Pruebas IPSExtractor + +Entregable profesional de **resultados** (métricas, gráficas, capacidad 100k/mes). + +## Compilar + +```powershell +cd docs\plan_pruebas\informe +pdflatex -interaction=nonstopmode -file-line-error main.tex +pdflatex -interaction=nonstopmode -file-line-error main.tex +Copy-Item main.pdf ..\InformePruebas_IPSExtractor.pdf -Force +``` + +Salida: [`../InformePruebas_IPSExtractor.pdf`](../InformePruebas_IPSExtractor.pdf) + +## Datos + +- `data/metricas.csv` — latencias k6 +- `data/capacidad.csv` — modelo 100k/mes +- `data/memoria.csv` — RAM contenedores diff --git a/docs/plan_pruebas/informe/latexmkrc b/docs/plan_pruebas/informe/latexmkrc new file mode 100644 index 0000000..d11a0ff --- /dev/null +++ b/docs/plan_pruebas/informe/latexmkrc @@ -0,0 +1,3 @@ +$pdf_mode = 1; +$interaction = 'nonstopmode'; +$pdflatex = 'pdflatex -interaction=nonstopmode -file-line-error %O %S'; diff --git a/docs/plan_pruebas/informe/main.aux b/docs/plan_pruebas/informe/main.aux new file mode 100644 index 0000000..7b3bee6 --- /dev/null +++ b/docs/plan_pruebas/informe/main.aux @@ -0,0 +1,132 @@ +\relax +\providecommand \babel@aux [2]{\global \let \babel@toc \@gobbletwo } +\@nameuse{bbl@beforestart} +\catcode `"\active +\catcode `<\active +\catcode `>\active +\@nameuse{es@quoting} +\providecommand\hyper@newdestlabel[2]{} +\providecommand\HyField@AuxAddToFields[1]{} +\providecommand\HyField@AuxAddToCoFields[2]{} +\babel@aux{spanish}{} +\@writefile{toc}{\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{4}{chapter.1}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:resumen}{{1}{4}{Resumen ejecutivo}{chapter.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {1.1}Veredicto global}{4}{section.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{4}{section.1.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces KPIs ejecutivos de la campa\~na.}}{4}{table.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.3}Respuesta directa: 100\,000 facturas/mes}{4}{section.1.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.4}Riesgo principal}{5}{section.1.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {2}Objeto y alcance}{6}{chapter.2}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:alcance}{{2}{6}{Objeto y alcance}{chapter.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.1}Objeto}{6}{section.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {2.2}Alcance incluido}{6}{section.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {2.3}Fuera de alcance de esta campa\~na}{6}{section.2.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {2.4}Definici\'on de factura}{6}{section.2.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{7}{chapter.3}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:entorno}{{3}{7}{Entorno bajo prueba}{chapter.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.1}Hardware y sistema}{7}{section.3.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Host de ejecuci\'on.}}{7}{table.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{7}{section.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.3}Salud del stack}{7}{section.3.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Huella de memoria por contenedor (idle). MLflow domina el host.}}{8}{figure.3.1}\protected@file@percent } +\newlabel{fig:memoria}{{3.1}{8}{Huella de memoria por contenedor (idle). MLflow domina el host}{figure.3.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {4}Metodolog\'ia}{9}{chapter.4}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:metodologia}{{4}{9}{Metodolog\'ia}{chapter.4}{}} +\@writefile{toc}{\contentsline {section}{\numberline {4.1}Tipos de prueba}{9}{section.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.2}Herramientas}{9}{section.4.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.3}SLOs perfil \texttt {shared8g}}{9}{section.4.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.4}Comandos representativos}{9}{section.4.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{10}{chapter.5}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:unitarias}{{5}{10}{Resultados --- pruebas unitarias}{chapter.5}{}} +\@writefile{toc}{\contentsline {section}{\numberline {5.1}Scorecard}{10}{section.5.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Resultados unitarios. $^{*}$Base 3 PASS confirmados en Ubuntu; ampliaci\'on features/clustering (+8) y Timing sobre 3 PDFs reales requieren \texttt {go test} en host con Tesseract.}}{10}{table.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.2}Cobertura agent}{10}{section.5.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.3}Cobertura API store}{10}{section.5.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.4}Cobertura count-api}{10}{section.5.4}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {5.1}{\ignorespaces Unitarias ejecutadas vs pendientes.}}{11}{figure.5.1}\protected@file@percent } +\newlabel{fig:unitarias}{{5.1}{11}{Unitarias ejecutadas vs pendientes}{figure.5.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{12}{chapter.6}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:humo}{{6}{12}{Resultados --- humo y funcional}{chapter.6}{}} +\@writefile{toc}{\contentsline {section}{\numberline {6.1}Humo gateway}{12}{section.6.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {6.1}{\ignorespaces Humo HTTP.}}{12}{table.6.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.2}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{12}{section.6.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.3}Script preparado}{12}{section.6.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{13}{chapter.7}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:carga}{{7}{13}{Resultados --- pruebas de carga}{chapter.7}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.1}Tabla consolidada}{13}{section.7.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces M\'etricas de carga k6 (perfil shared8g).}}{13}{table.7.1}\protected@file@percent } +\newlabel{tab:carga}{{7.1}{13}{M\'etricas de carga k6 (perfil shared8g)}{table.7.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.2}ST-GW-01}{13}{section.7.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.3}ST-BOT-01 --- comparativa}{13}{section.7.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.4}Throughput gateway}{13}{section.7.4}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {7.1}{\ignorespaces p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla.}}{14}{figure.7.1}\protected@file@percent } +\newlabel{fig:latencias}{{7.1}{14}{p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla}{figure.7.1}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {7.2}{\ignorespaces Before/after heartbeats (eje limitado; valores exactos en tabla~\ref {tab:carga}).}}{14}{figure.7.2}\protected@file@percent } +\newlabel{fig:bot-before-after}{{7.2}{14}{Before/after heartbeats (eje limitado; valores exactos en tabla~\ref {tab:carga})}{figure.7.2}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {7.3}{\ignorespaces Throughput observado en health liveness (no equivale a PDFs/h).}}{14}{figure.7.3}\protected@file@percent } +\newlabel{fig:throughput}{{7.3}{14}{Throughput observado en health liveness (no equivale a PDFs/h)}{figure.7.3}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {8}Capacidad: 100\,000 facturas al mes}{15}{chapter.8}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:capacidad}{{8}{15}{Capacidad: 100\,000 facturas al mes}{chapter.8}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.1}Traducci\'on del objetivo}{15}{section.8.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {8.1}{\ignorespaces Throughput m\'inimo para cumplir 100k/mes.}}{15}{table.8.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.2}Modelo por etapa}{15}{section.8.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {8.2}{\ignorespaces Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger }$Con $N$ agents en paralelo.}}{15}{table.8.2}\protected@file@percent } +\newlabel{tab:capacidad}{{8.2}{15}{Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger }$Con $N$ agents en paralelo}{table.8.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.3}Interpretaci\'on}{15}{section.8.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {8.1}{\ignorespaces Brecha de capacidad. Gateway real $\gg $1000/h; se trunca para legibilidad.}}{16}{figure.8.1}\protected@file@percent } +\newlabel{fig:capacidad}{{8.1}{16}{Brecha de capacidad. Gateway real $\gg $1000/h; se trunca para legibilidad}{figure.8.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.4}Veredicto}{16}{section.8.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.5}Requisitos m\'inimos sugeridos para 100k/mes}{16}{section.8.5}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {9}An\'alisis y hallazgos}{17}{chapter.9}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:analisis}{{9}{17}{An\'alisis y hallazgos}{chapter.9}{}} +\@writefile{toc}{\contentsline {section}{\numberline {9.1}Hallazgos}{17}{section.9.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {9.2}Scorecard global}{17}{section.9.2}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {9.1}{\ignorespaces Distribuci\'on de resultados por tipo de prueba.}}{17}{figure.9.1}\protected@file@percent } +\newlabel{fig:scorecard}{{9.1}{17}{Distribuci\'on de resultados por tipo de prueba}{figure.9.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {9.3}Limitaciones}{17}{section.9.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {10}Conclusiones y recomendaciones}{18}{chapter.10}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:conclusiones}{{10}{18}{Conclusiones y recomendaciones}{chapter.10}{}} +\@writefile{toc}{\contentsline {section}{\numberline {10.1}Conclusiones}{18}{section.10.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {10.2}Recomendaciones}{18}{section.10.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {10.3}Pr\'oxima campa\~na sugerida}{18}{section.10.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {11}Anexos}{19}{chapter.11}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:anexos}{{11}{19}{Anexos}{chapter.11}{}} +\@writefile{toc}{\contentsline {section}{\numberline {11.1}Datos fuente}{19}{section.11.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.2}Extracto ST-GW-01}{19}{section.11.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.3}Extracto ST-BOT-01 \#2}{19}{section.11.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.4}Unitarias}{19}{section.11.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.5}Scripts}{19}{section.11.5}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.6}Reproducibilidad}{20}{section.11.6}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {12}Resultados --- latencia count-api (PDFs reales)}{21}{chapter.12}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:count-latency}{{12}{21}{Resultados --- latencia count-api (PDFs reales)}{chapter.12}{}} +\@writefile{toc}{\contentsline {section}{\numberline {12.1}Fixtures}{21}{section.12.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.2}Unitarias pipeline ampliadas}{21}{section.12.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.3}Timings por etapa}{21}{section.12.3}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {12.1}Medici\'on en Ubuntu}{21}{lstlisting.12.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Latencia por etapa (ms). Completar tras \texttt {-run Timing} en el host con Tesseract.}}{22}{table.12.1}\protected@file@percent } +\newlabel{tab:count-timing}{{12.1}{22}{Latencia por etapa (ms). Completar tras \texttt {-run Timing} en el host con Tesseract}{table.12.1}{}} +\gdef \@abspage@last{23} diff --git a/docs/plan_pruebas/informe/main.out b/docs/plan_pruebas/informe/main.out new file mode 100644 index 0000000..0cc7a3a --- /dev/null +++ b/docs/plan_pruebas/informe/main.out @@ -0,0 +1,58 @@ +\BOOKMARK [0][-]{chapter.1}{\376\377\000R\000e\000s\000u\000m\000e\000n\000\040\000e\000j\000e\000c\000u\000t\000i\000v\000o}{}% 1 +\BOOKMARK [1][-]{section.1.1}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.1}% 2 +\BOOKMARK [1][-]{section.1.2}{\376\377\000K\000P\000I\000s\000\040\000d\000e\000\040\000l\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.1}% 3 +\BOOKMARK [1][-]{section.1.3}{\376\377\000R\000e\000s\000p\000u\000e\000s\000t\000a\000\040\000d\000i\000r\000e\000c\000t\000a\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000/\000m\000e\000s}{chapter.1}% 4 +\BOOKMARK [1][-]{section.1.4}{\376\377\000R\000i\000e\000s\000g\000o\000\040\000p\000r\000i\000n\000c\000i\000p\000a\000l}{chapter.1}% 5 +\BOOKMARK [0][-]{chapter.2}{\376\377\000O\000b\000j\000e\000t\000o\000\040\000y\000\040\000a\000l\000c\000a\000n\000c\000e}{}% 6 +\BOOKMARK [1][-]{section.2.1}{\376\377\000O\000b\000j\000e\000t\000o}{chapter.2}% 7 +\BOOKMARK [1][-]{section.2.2}{\376\377\000A\000l\000c\000a\000n\000c\000e\000\040\000i\000n\000c\000l\000u\000i\000d\000o}{chapter.2}% 8 +\BOOKMARK [1][-]{section.2.3}{\376\377\000F\000u\000e\000r\000a\000\040\000d\000e\000\040\000a\000l\000c\000a\000n\000c\000e\000\040\000d\000e\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.2}% 9 +\BOOKMARK [1][-]{section.2.4}{\376\377\000D\000e\000f\000i\000n\000i\000c\000i\000\363\000n\000\040\000d\000e\000\040\000f\000a\000c\000t\000u\000r\000a}{chapter.2}% 10 +\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 11 +\BOOKMARK [1][-]{section.3.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.3}% 12 +\BOOKMARK [1][-]{section.3.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000m\000e\000m\000o\000r\000i\000a\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\051}{chapter.3}% 13 +\BOOKMARK [1][-]{section.3.3}{\376\377\000S\000a\000l\000u\000d\000\040\000d\000e\000l\000\040\000s\000t\000a\000c\000k}{chapter.3}% 14 +\BOOKMARK [0][-]{chapter.4}{\376\377\000M\000e\000t\000o\000d\000o\000l\000o\000g\000\355\000a}{}% 15 +\BOOKMARK [1][-]{section.4.1}{\376\377\000T\000i\000p\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.4}% 16 +\BOOKMARK [1][-]{section.4.2}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s}{chapter.4}% 17 +\BOOKMARK [1][-]{section.4.3}{\376\377\000S\000L\000O\000s\000\040\000p\000e\000r\000f\000i\000l\000\040\000s\000h\000a\000r\000e\000d\0008\000g}{chapter.4}% 18 +\BOOKMARK [1][-]{section.4.4}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000r\000e\000p\000r\000e\000s\000e\000n\000t\000a\000t\000i\000v\000o\000s}{chapter.4}% 19 +\BOOKMARK [0][-]{chapter.5}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 20 +\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d}{chapter.5}% 21 +\BOOKMARK [1][-]{section.5.2}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000a\000g\000e\000n\000t}{chapter.5}% 22 +\BOOKMARK [1][-]{section.5.3}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000A\000P\000I\000\040\000s\000t\000o\000r\000e}{chapter.5}% 23 +\BOOKMARK [1][-]{section.5.4}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.5}% 24 +\BOOKMARK [0][-]{chapter.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000h\000u\000m\000o\000\040\000y\000\040\000f\000u\000n\000c\000i\000o\000n\000a\000l}{}% 25 +\BOOKMARK [1][-]{section.6.1}{\376\377\000H\000u\000m\000o\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.6}% 26 +\BOOKMARK [1][-]{section.6.2}{\376\377\000F\000u\000n\000c\000i\000o\000n\000a\000l\000\040\000E\0002\000E\000-\0000\0007\000\040\000\050\000w\000o\000r\000k\000e\000r\000\040\000o\000f\000f\000l\000i\000n\000e\000\040\000\040\0004\0000\0009\000\051}{chapter.6}% 27 +\BOOKMARK [1][-]{section.6.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000p\000r\000e\000p\000a\000r\000a\000d\000o}{chapter.6}% 28 +\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{}% 29 +\BOOKMARK [1][-]{section.7.1}{\376\377\000T\000a\000b\000l\000a\000\040\000c\000o\000n\000s\000o\000l\000i\000d\000a\000d\000a}{chapter.7}% 30 +\BOOKMARK [1][-]{section.7.2}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.7}% 31 +\BOOKMARK [1][-]{section.7.3}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000c\000o\000m\000p\000a\000r\000a\000t\000i\000v\000a}{chapter.7}% 32 +\BOOKMARK [1][-]{section.7.4}{\376\377\000T\000h\000r\000o\000u\000g\000h\000p\000u\000t\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.7}% 33 +\BOOKMARK [0][-]{chapter.8}{\376\377\000C\000a\000p\000a\000c\000i\000d\000a\000d\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000\040\000a\000l\000\040\000m\000e\000s}{}% 34 +\BOOKMARK [1][-]{section.8.1}{\376\377\000T\000r\000a\000d\000u\000c\000c\000i\000\363\000n\000\040\000d\000e\000l\000\040\000o\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 35 +\BOOKMARK [1][-]{section.8.2}{\376\377\000M\000o\000d\000e\000l\000o\000\040\000p\000o\000r\000\040\000e\000t\000a\000p\000a}{chapter.8}% 36 +\BOOKMARK [1][-]{section.8.3}{\376\377\000I\000n\000t\000e\000r\000p\000r\000e\000t\000a\000c\000i\000\363\000n}{chapter.8}% 37 +\BOOKMARK [1][-]{section.8.4}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o}{chapter.8}% 38 +\BOOKMARK [1][-]{section.8.5}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000m\000\355\000n\000i\000m\000o\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000o\000s\000\040\000p\000a\000r\000a\000\040\0001\0000\0000\000k\000/\000m\000e\000s}{chapter.8}% 39 +\BOOKMARK [0][-]{chapter.9}{\376\377\000A\000n\000\341\000l\000i\000s\000i\000s\000\040\000y\000\040\000h\000a\000l\000l\000a\000z\000g\000o\000s}{}% 40 +\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.9}% 41 +\BOOKMARK [1][-]{section.9.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d\000\040\000g\000l\000o\000b\000a\000l}{chapter.9}% 42 +\BOOKMARK [1][-]{section.9.3}{\376\377\000L\000i\000m\000i\000t\000a\000c\000i\000o\000n\000e\000s}{chapter.9}% 43 +\BOOKMARK [0][-]{chapter.10}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s\000\040\000y\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{}% 44 +\BOOKMARK [1][-]{section.10.1}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s}{chapter.10}% 45 +\BOOKMARK [1][-]{section.10.2}{\376\377\000R\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{chapter.10}% 46 +\BOOKMARK [1][-]{section.10.3}{\376\377\000P\000r\000\363\000x\000i\000m\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000s\000u\000g\000e\000r\000i\000d\000a}{chapter.10}% 47 +\BOOKMARK [0][-]{chapter.11}{\376\377\000A\000n\000e\000x\000o\000s}{}% 48 +\BOOKMARK [1][-]{section.11.1}{\376\377\000D\000a\000t\000o\000s\000\040\000f\000u\000e\000n\000t\000e}{chapter.11}% 49 +\BOOKMARK [1][-]{section.11.2}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.11}% 50 +\BOOKMARK [1][-]{section.11.3}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\000\043\0002}{chapter.11}% 51 +\BOOKMARK [1][-]{section.11.4}{\376\377\000U\000n\000i\000t\000a\000r\000i\000a\000s}{chapter.11}% 52 +\BOOKMARK [1][-]{section.11.5}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.11}% 53 +\BOOKMARK [1][-]{section.11.6}{\376\377\000R\000e\000p\000r\000o\000d\000u\000c\000i\000b\000i\000l\000i\000d\000a\000d}{chapter.11}% 54 +\BOOKMARK [0][-]{chapter.12}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i\000\040\000\050\000P\000D\000F\000s\000\040\000r\000e\000a\000l\000e\000s\000\051}{}% 55 +\BOOKMARK [1][-]{section.12.1}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s}{chapter.12}% 56 +\BOOKMARK [1][-]{section.12.2}{\376\377\000U\000n\000i\000t\000a\000r\000i\000a\000s\000\040\000p\000i\000p\000e\000l\000i\000n\000e\000\040\000a\000m\000p\000l\000i\000a\000d\000a\000s}{chapter.12}% 57 +\BOOKMARK [1][-]{section.12.3}{\376\377\000T\000i\000m\000i\000n\000g\000s\000\040\000p\000o\000r\000\040\000e\000t\000a\000p\000a}{chapter.12}% 58 diff --git a/docs/plan_pruebas/informe/main.tex b/docs/plan_pruebas/informe/main.tex new file mode 100644 index 0000000..609f13c --- /dev/null +++ b/docs/plan_pruebas/informe/main.tex @@ -0,0 +1,36 @@ +\documentclass[11pt,a4paper,oneside]{report} + +\input{preamble} + +\begin{document} + +\makecover + +\chapter*{Control de cambios} +\begin{tabularx}{\textwidth}{@{}l l X@{}} +\toprule +\textbf{Versi\'on} & \textbf{Fecha} & \textbf{Descripci\'on} \\ +\midrule +1.0.0 & 13 jul 2026 & Informe profesional: campa\~na shared8g, m\'etricas, capacidad 100k/mes. \\ +1.1.0 & 13 jul 2026 & Unitarias count-api pipeline 3/3 PASS (Ubuntu). \\ +1.2.0 & 13 jul 2026 & Unitarias features/clustering ampliadas; timings PDF + scripts latencia. \\ +\bottomrule +\end{tabularx} + +\tableofcontents +\clearpage + +\input{sections/01_resumen_ejecutivo} +\input{sections/02_objeto_alcance} +\input{sections/03_entorno} +\input{sections/04_metodologia} +\input{sections/05_resultados_unitarias} +\input{sections/06_resultados_humo_funcional} +\input{sections/07_resultados_carga} +\input{sections/08_capacidad_100k} +\input{sections/09_analisis_hallazgos} +\input{sections/10_conclusiones} +\input{sections/11_anexos} +\input{sections/12_count_latency} + +\end{document} diff --git a/docs/plan_pruebas/informe/main.toc b/docs/plan_pruebas/informe/main.toc new file mode 100644 index 0000000..a70fcd9 --- /dev/null +++ b/docs/plan_pruebas/informe/main.toc @@ -0,0 +1,59 @@ +\babel@toc {spanish}{}\relax +\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{4}{chapter.1}% +\contentsline {section}{\numberline {1.1}Veredicto global}{4}{section.1.1}% +\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{4}{section.1.2}% +\contentsline {section}{\numberline {1.3}Respuesta directa: 100\,000 facturas/mes}{4}{section.1.3}% +\contentsline {section}{\numberline {1.4}Riesgo principal}{5}{section.1.4}% +\contentsline {chapter}{\numberline {2}Objeto y alcance}{6}{chapter.2}% +\contentsline {section}{\numberline {2.1}Objeto}{6}{section.2.1}% +\contentsline {section}{\numberline {2.2}Alcance incluido}{6}{section.2.2}% +\contentsline {section}{\numberline {2.3}Fuera de alcance de esta campa\~na}{6}{section.2.3}% +\contentsline {section}{\numberline {2.4}Definici\'on de factura}{6}{section.2.4}% +\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{7}{chapter.3}% +\contentsline {section}{\numberline {3.1}Hardware y sistema}{7}{section.3.1}% +\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{7}{section.3.2}% +\contentsline {section}{\numberline {3.3}Salud del stack}{7}{section.3.3}% +\contentsline {chapter}{\numberline {4}Metodolog\'ia}{9}{chapter.4}% +\contentsline {section}{\numberline {4.1}Tipos de prueba}{9}{section.4.1}% +\contentsline {section}{\numberline {4.2}Herramientas}{9}{section.4.2}% +\contentsline {section}{\numberline {4.3}SLOs perfil \texttt {shared8g}}{9}{section.4.3}% +\contentsline {section}{\numberline {4.4}Comandos representativos}{9}{section.4.4}% +\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{10}{chapter.5}% +\contentsline {section}{\numberline {5.1}Scorecard}{10}{section.5.1}% +\contentsline {section}{\numberline {5.2}Cobertura agent}{10}{section.5.2}% +\contentsline {section}{\numberline {5.3}Cobertura API store}{10}{section.5.3}% +\contentsline {section}{\numberline {5.4}Cobertura count-api}{10}{section.5.4}% +\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{12}{chapter.6}% +\contentsline {section}{\numberline {6.1}Humo gateway}{12}{section.6.1}% +\contentsline {section}{\numberline {6.2}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{12}{section.6.2}% +\contentsline {section}{\numberline {6.3}Script preparado}{12}{section.6.3}% +\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{13}{chapter.7}% +\contentsline {section}{\numberline {7.1}Tabla consolidada}{13}{section.7.1}% +\contentsline {section}{\numberline {7.2}ST-GW-01}{13}{section.7.2}% +\contentsline {section}{\numberline {7.3}ST-BOT-01 --- comparativa}{13}{section.7.3}% +\contentsline {section}{\numberline {7.4}Throughput gateway}{13}{section.7.4}% +\contentsline {chapter}{\numberline {8}Capacidad: 100\,000 facturas al mes}{15}{chapter.8}% +\contentsline {section}{\numberline {8.1}Traducci\'on del objetivo}{15}{section.8.1}% +\contentsline {section}{\numberline {8.2}Modelo por etapa}{15}{section.8.2}% +\contentsline {section}{\numberline {8.3}Interpretaci\'on}{15}{section.8.3}% +\contentsline {section}{\numberline {8.4}Veredicto}{16}{section.8.4}% +\contentsline {section}{\numberline {8.5}Requisitos m\'inimos sugeridos para 100k/mes}{16}{section.8.5}% +\contentsline {chapter}{\numberline {9}An\'alisis y hallazgos}{17}{chapter.9}% +\contentsline {section}{\numberline {9.1}Hallazgos}{17}{section.9.1}% +\contentsline {section}{\numberline {9.2}Scorecard global}{17}{section.9.2}% +\contentsline {section}{\numberline {9.3}Limitaciones}{17}{section.9.3}% +\contentsline {chapter}{\numberline {10}Conclusiones y recomendaciones}{18}{chapter.10}% +\contentsline {section}{\numberline {10.1}Conclusiones}{18}{section.10.1}% +\contentsline {section}{\numberline {10.2}Recomendaciones}{18}{section.10.2}% +\contentsline {section}{\numberline {10.3}Pr\'oxima campa\~na sugerida}{18}{section.10.3}% +\contentsline {chapter}{\numberline {11}Anexos}{19}{chapter.11}% +\contentsline {section}{\numberline {11.1}Datos fuente}{19}{section.11.1}% +\contentsline {section}{\numberline {11.2}Extracto ST-GW-01}{19}{section.11.2}% +\contentsline {section}{\numberline {11.3}Extracto ST-BOT-01 \#2}{19}{section.11.3}% +\contentsline {section}{\numberline {11.4}Unitarias}{19}{section.11.4}% +\contentsline {section}{\numberline {11.5}Scripts}{19}{section.11.5}% +\contentsline {section}{\numberline {11.6}Reproducibilidad}{20}{section.11.6}% +\contentsline {chapter}{\numberline {12}Resultados --- latencia count-api (PDFs reales)}{21}{chapter.12}% +\contentsline {section}{\numberline {12.1}Fixtures}{21}{section.12.1}% +\contentsline {section}{\numberline {12.2}Unitarias pipeline ampliadas}{21}{section.12.2}% +\contentsline {section}{\numberline {12.3}Timings por etapa}{21}{section.12.3}% diff --git a/docs/plan_pruebas/informe/preamble.tex b/docs/plan_pruebas/informe/preamble.tex new file mode 100644 index 0000000..fee6653 --- /dev/null +++ b/docs/plan_pruebas/informe/preamble.tex @@ -0,0 +1,102 @@ +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[spanish,es-tabla]{babel} +\usepackage{lmodern} +\usepackage{microtype} +\usepackage[ + a4paper, + margin=2.4cm, + headheight=14pt +]{geometry} +\usepackage{setspace} +\onehalfspacing + +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small IPSExtractor --- Informe de Pruebas} +\fancyhead[R]{\small\thepage} +\fancyfoot[C]{\footnotesize Confidencial --- Uso interno} +\renewcommand{\headrulewidth}{0.4pt} + +\usepackage{titlesec} +\titleformat{\chapter}[display] + {\normalfont\huge\bfseries}{\chaptertitlename\ \thechapter}{14pt}{\Huge} +\titleformat{\section}{\normalfont\Large\bfseries}{\thesection}{1em}{} + +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{tabularx} +\usepackage{multirow} +\usepackage{array} +\usepackage{enumitem} +\setlist[itemize]{nosep,leftmargin=1.4em} +\setlist[enumerate]{nosep,leftmargin=1.4em} + +\usepackage{siunitx} +\usepackage{xcolor} +\definecolor{ipsblue}{HTML}{1B4F72} +\definecolor{ipsgrey}{HTML}{566573} +\definecolor{codebg}{HTML}{F4F6F7} +\definecolor{passgreen}{HTML}{1E8449} +\definecolor{failred}{HTML}{C0392B} +\definecolor{warnorange}{HTML}{B9770E} + +\usepackage{listings} +\lstset{ + basicstyle=\ttfamily\footnotesize, + backgroundcolor=\color{codebg}, + frame=single, + framerule=0.3pt, + breaklines=true, + columns=fullflexible, + showstringspaces=false +} + +\usepackage{pgfplots} +\pgfplotsset{compat=1.18} +\usepgfplotslibrary{statistics} + +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning} + +\usepackage{graphicx} +\usepackage{hyperref} +\hypersetup{ + colorlinks=true, + linkcolor=ipsblue, + urlcolor=ipsblue, + pdftitle={Informe de Pruebas IPSExtractor}, + pdfauthor={IPSExtractor QA} +} + +\newcommand{\docversion}{1.2.0} +\newcommand{\docdate}{13 de julio de 2026} + +\newcommand{\pass}{\textcolor{passgreen}{\textbf{PASS}}} +\newcommand{\fail}{\textcolor{failred}{\textbf{FAIL}}} +\newcommand{\parcial}{\textcolor{warnorange}{\textbf{PARCIAL}}} +\newcommand{\neje}{\textcolor{ipsgrey}{\textbf{N/E}}} + +\newcommand{\makecover}{% + \begin{titlepage} + \centering + \vspace*{1.2cm} + {\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[0.35cm] + {\Huge\bfseries IPSExtractor}\\[0.25cm] + {\Large\color{ipsgrey} Extracci\'on y an\'alisis de facturas m\'edicas}\\[0.35cm] + {\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[1.5cm] + {\LARGE\bfseries Informe de Pruebas}\\[0.25cm] + {\large Resultados $\cdot$ M\'etricas $\cdot$ Capacidad 100\,000/mes}\\[1.8cm] + \begin{tabular}{@{}ll@{}} + \textbf{Versi\'on} & \docversion \\ + \textbf{Fecha} & \docdate \\ + \textbf{Campa\~na} & 2026-07-13 (perfil \texttt{shared8g}) \\ + \textbf{Host} & ubuntu --- ASUS X441UAK, 8\,GiB \\ + \textbf{Clasificaci\'on} & Uso interno \\ + \end{tabular} + \vfill + {\small Documento de \emph{resultados} (no plan). Incluye an\'alisis de viabilidad de 100\,000 facturas/mes.}\\[0.25cm] + {\color{ipsblue}\rule{0.45\textwidth}{0.6pt}} + \end{titlepage} +} diff --git a/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex b/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex new file mode 100644 index 0000000..38e0f82 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex @@ -0,0 +1,41 @@ +\chapter{Resumen ejecutivo} +\label{ch:resumen} + +\section{Veredicto global} +La campa\~na de pruebas del \textbf{13 de julio de 2026} sobre el stack IPSExtractor desplegado en el host \texttt{ubuntu} (perfil \texttt{shared8g}) concluye: + +\begin{center} +\large +Stack \textbf{operativo y estable} para orquestaci\'on y control remoto del bot.\\[0.3em] +\normalsize +Carga ligera del gateway y heartbeats: \pass\ bajo contenci\'on moderada.\\[0.2em] +Pipeline completo a \textbf{100\,000 facturas/mes} en este host: \fail\ (ver cap.~\ref{ch:capacidad}). +\end{center} + +\section{KPIs de la campa\~na} +\begin{table}[ht] +\centering +\begin{tabular}{@{}lrrl@{}} +\toprule +\textbf{Indicador} & \textbf{Valor} & \textbf{Objetivo} & \textbf{Estado} \\ +\midrule +ST-GW-01 p95 & 40{,}85\,ms & $<$500\,ms & \pass \\ +ST-GW-01 fail rate & 0\,\% & $<$1\,\% & \pass \\ +ST-BOT-01 p95 (reintento) & 85{,}55\,ms & $<$800\,ms & \pass \\ +Unitarias agent bot & 19/19 & 100\,\% & \pass \\ +Unitarias API bot store & 7/7 & 100\,\% & \pass \\ +Unitarias count-api (pipeline) & 3/3 & 100\,\% & \pass \\ +\bottomrule +\end{tabular} +\caption{KPIs ejecutivos de la campa\~na.} +\end{table} + +\section{Respuesta directa: 100\,000 facturas/mes} +\begin{description} + \item[Host actual (8\,GiB + MLflow/n8n/MinIO/\ldots)] \fail\ para el \emph{pipeline completo} (conteo + extracci\'on + bot + limpieza). + \item[Infra dedicada dimensionada] \pass\ \emph{condicional}: workers LLM, OCR con RAM $\geq$16--32\,GiB, $N$ agents Selenium, cuota API, sin zoo Docker competidor. +\end{description} +Detalle cuantitativo en el cap\'itulo~\ref{ch:capacidad}. + +\section{Riesgo principal} +Contenci\'on de plataforma: swap $\approx$2{,}1\,GiB y \texttt{mlflow-server} $\approx$1{,}84\,GiB provocaron un outlier de 23\,s en heartbeats (corrida 1). Tras aliviar carga, p95 cay\'o a 86\,ms. Los fallos de latencia deben correlacionarse siempre con \texttt{free -h} / \texttt{docker stats}. diff --git a/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex b/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex new file mode 100644 index 0000000..361bde8 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex @@ -0,0 +1,24 @@ +\chapter{Objeto y alcance} +\label{ch:alcance} + +\section{Objeto} +Documentar de forma profesional los \textbf{resultados} de pruebas de IPSExtractor (humo, unitarias, carga) y evaluar la viabilidad de procesar \textbf{100\,000 facturas/mes}. + +\section{Alcance incluido} +\begin{itemize} + \item Stack Docker IPS: gateway, extraction, workers, cleaning, count-api, bot-descarga, PostgreSQL, Redis. + \item Perfil de carga \texttt{shared8g} en host de staging compartido. + \item Unitarias del agent (\texttt{bots/bot\_descarga}) y del store API (\texttt{services/bot\_descarga}). + \item Modelo anal\'itico de capacidad por etapa del pipeline. +\end{itemize} + +\section{Fuera de alcance de esta campa\~na} +\begin{itemize} + \item ST-COUNT-01 / OCR masivo y ST-EXT bajo LLM real a escala. + \item E2E Imaging/Selenium con VPN. + \item ST-BOT-03 (events) --- requiere \texttt{DOWNLOAD\_JOB\_ID} de un job real (\neje). + \item Procesar 100\,000 PDFs reales. +\end{itemize} + +\section{Definici\'on de factura} +Una \textbf{factura} = un PDF de cuenta m\'edica evaluado por etapa (conteo OCR, extracci\'on LLM, descarga bot, limpieza). El veredicto 100k/mes se expresa por etapa y para el pipeline completo. diff --git a/docs/plan_pruebas/informe/sections/03_entorno.tex b/docs/plan_pruebas/informe/sections/03_entorno.tex new file mode 100644 index 0000000..7af9366 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/03_entorno.tex @@ -0,0 +1,64 @@ +\chapter{Entorno bajo prueba} +\label{ch:entorno} + +\section{Hardware y sistema} +\begin{table}[ht] +\centering +\begin{tabular}{@{}lp{8.5cm}@{}} +\toprule +\textbf{Campo} & \textbf{Valor} \\ +\midrule +Hostname & \texttt{ubuntu} \\ +SO & Ubuntu 24.04.4 LTS \\ +CPU & Intel Core i5-7200U @ 2{,}50\,GHz (2C/4T) \\ +RAM & 7{,}6\,GiB (l\'imite Docker $\approx$ 7{,}64\,GiB) \\ +Disco & HDD 500\,GB + USB 1\,TB \\ +Docker & 29.1.3 \\ +Gateway & \texttt{http://127.0.0.1:8189} \\ +\bottomrule +\end{tabular} +\caption{Host de ejecuci\'on.} +\end{table} + +\section{Baseline de memoria (2026-07-13)} +\begin{itemize} + \item Usados $\approx$4{,}4\,GiB; disponibles $\approx$3{,}3\,GiB; \textbf{swap 2{,}1\,GiB}. + \item IPS idle $\approx$230--250\,MiB total. + \item \texttt{mlflow-server} $\approx$1{,}84\,GiB; n8n $\approx$291\,MiB; MinIO $\approx$136\,MiB. +\end{itemize} + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=0.95\textwidth, + height=6.5cm, + ylabel={RAM (MiB)}, + symbolic x coords={api,extraction,w1,w2,cleaning,bot,count,db,redis,mlflow,n8n,minio}, + xtick=data, + x tick label style={rotate=45, anchor=east, font=\scriptsize}, + nodes near coords, + nodes near coords style={font=\tiny, rotate=90, anchor=west}, + bar width=8pt, + ymin=0, + enlarge x limits=0.08, + grid=major, + legend style={at={(0.98,0.98)},anchor=north east,font=\scriptsize} +] +\addplot[fill=ipsblue!70] coordinates { + (api,43) (extraction,30) (w1,29) (w2,32) (cleaning,51) + (bot,17) (count,13) (db,26) (redis,8) (mlflow,1884) (n8n,291) (minio,136) +}; +\end{axis} +\end{tikzpicture} +\caption{Huella de memoria por contenedor (idle). MLflow domina el host.} +\label{fig:memoria} +\end{figure} + +\section{Salud del stack} +\begin{itemize} + \item Contenedores IPS: \texttt{Up} / healthy ($\sim$17\,h). + \item \texttt{/health/live}: ok, $\approx$11\,ms. + \item \texttt{/health}: ok (extraction, cleaning, counting, bot\_descarga), $\approx$194\,ms. +\end{itemize} diff --git a/docs/plan_pruebas/informe/sections/04_metodologia.tex b/docs/plan_pruebas/informe/sections/04_metodologia.tex new file mode 100644 index 0000000..52de27d --- /dev/null +++ b/docs/plan_pruebas/informe/sections/04_metodologia.tex @@ -0,0 +1,30 @@ +\chapter{Metodolog\'ia} +\label{ch:metodologia} + +\section{Tipos de prueba} +\begin{enumerate} + \item \textbf{Unitarias} --- pytest (agent y API bot). + \item \textbf{Humo / funcional} --- curl y checks HTTP del gateway. + \item \textbf{Carga} --- k6 perfil \texttt{shared8g}. + \item \textbf{Capacidad} --- modelo anal\'itico (no carga de 100k PDFs). +\end{enumerate} + +\section{Herramientas} +k6 (Grafana), pytest, Docker stats, curl, pdflatex/pgfplots. + +\section{SLOs perfil \texttt{shared8g}} +\begin{itemize} + \item Gateway \texttt{/health/live}: p95 $<$500\,ms; fail $<$1\,\%. + \item Heartbeats bot: p95 $<$800\,ms; checks $>$95\,\%. + \item No usar \texttt{/health} agregado como SLO de latencia. +\end{itemize} + +\section{Comandos representativos} +\begin{lstlisting}[language=bash] +k6 run -e PROFILE=shared8g -e BASE_URL=http://127.0.0.1:8189 \ + k6_gateway_health.js +k6 run -e PROFILE=shared8g -e BOT_API_TOKEN=... \ + k6_bot_heartbeats.js +python -m pytest bots/bot_descarga/tests -q +cd services/bot_descarga && python -m pytest tests -q +\end{lstlisting} diff --git a/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex b/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex new file mode 100644 index 0000000..0c3b8c1 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex @@ -0,0 +1,57 @@ +\chapter{Resultados --- pruebas unitarias} +\label{ch:unitarias} + +\section{Scorecard} +\begin{table}[ht] +\centering +\begin{tabular}{@{}llrl@{}} +\toprule +\textbf{Suite} & \textbf{Resultado} & \textbf{Passed} & \textbf{Duraci\'on} \\ +\midrule +\texttt{bots/bot\_descarga/tests} & \pass & 19/19 & 3{,}21\,s \\ +\texttt{services/bot\_descarga/tests} & \pass & 7/7 & 1{,}45\,s \\ +\texttt{go test} count-api (\texttt{internal/pipeline}) & \pass$^{*}$ & 3+8 nuevos & ver Timing \\ +Vitest desktop & \neje & --- & No ejecutado en esta sesi\'on \\ +\bottomrule +\end{tabular} +\caption{Resultados unitarios. $^{*}$Base 3 PASS confirmados en Ubuntu; ampliaci\'on features/clustering (+8) y Timing sobre 3 PDFs reales requieren \texttt{go test} en host con Tesseract.} +\end{table} + +\section{Cobertura agent} +Incluye excel reader, PDF validator, file manager, cancel, browser fatal, MinIO keys, GUI settings (19 casos). + +\section{Cobertura API store} +Incluye helpers MinIO, register/heartbeat/offline, claim at\'omico, cancel, events, filtros por job padre (7 casos). + +\section{Cobertura count-api} +Ejecutado en Ubuntu (deps Tesseract/Leptonica): +tests originales de features (3) \pass. +Ampliaci\'on: \texttt{features\_extra\_test.go} + \texttt{clustering\_test.go} (+8 casos). +Medici\'on de documentos: \texttt{processor\_timing\_test.go} sobre +\texttt{docs/plan\_pruebas/data/*.pdf} (3 archivos); ver cap.~\ref{ch:count-latency}. +\texttt{PDFResult} ahora incluye \texttt{render\_ms}/\texttt{ocr\_ms}/\texttt{cluster\_ms}/\texttt{infer\_ms}/\texttt{total\_ms}. + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + xbar, + width=0.85\textwidth, + height=4.2cm, + xlabel={Casos}, + symbolic y coords={agent,api-store,count-api,desktop}, + ytick=data, + xmin=0, xmax=22, + nodes near coords, + bar width=10pt, + legend style={at={(0.98,0.35)},anchor=east,font=\small} +] +\addplot[fill=passgreen!70] coordinates {(19,agent) (7,api-store) (3,count-api) (0,desktop)}; +\addlegendentry{PASS} +\addplot[fill=ipsgrey!40] coordinates {(0,agent) (0,api-store) (0,count-api) (1,desktop)}; +\addlegendentry{N/E} +\end{axis} +\end{tikzpicture} +\caption{Unitarias ejecutadas vs pendientes.} +\label{fig:unitarias} +\end{figure} diff --git a/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex b/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex new file mode 100644 index 0000000..5d5e035 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex @@ -0,0 +1,23 @@ +\chapter{Resultados --- humo y funcional} +\label{ch:humo} + +\section{Humo gateway} +\begin{table}[ht] +\centering +\begin{tabular}{@{}llrl@{}} +\toprule +\textbf{Check} & \textbf{Resultado} & \textbf{Latencia} & \textbf{Notas} \\ +\midrule +\texttt{GET /health/live} & \pass & 11\,ms & Liveness \\ +\texttt{GET /health} & \pass & 194\,ms & 4 servicios ok \\ +Contenedores IPS healthy & \pass & --- & 17\,h uptime \\ +\bottomrule +\end{tabular} +\caption{Humo HTTP.} +\end{table} + +\section{Funcional E2E-07 (worker offline $\rightarrow$ 409)} +\neje\ en esta campa\~na (no se envi\'o \texttt{POST /bot-descarga/jobs} contra worker offline). Queda como follow-up de 2 minutos en staging. + +\section{Script preparado} +Se a\~nadi\'o \texttt{docs/plan\_pruebas/scripts/k6\_smoke\_suite.js} para repeticiones automatizadas de live/health/config. diff --git a/docs/plan_pruebas/informe/sections/07_resultados_carga.tex b/docs/plan_pruebas/informe/sections/07_resultados_carga.tex new file mode 100644 index 0000000..98e878c --- /dev/null +++ b/docs/plan_pruebas/informe/sections/07_resultados_carga.tex @@ -0,0 +1,110 @@ +\chapter{Resultados --- pruebas de carga} +\label{ch:carga} + +\section{Tabla consolidada} +\begin{table}[ht] +\centering +\begin{tabular}{@{}lrrrrrl@{}} +\toprule +\textbf{Caso} & \textbf{Reqs} & \textbf{Fail\%} & \textbf{avg} & \textbf{p95} & \textbf{max} & \textbf{Res.} \\ +\midrule +ST-GW-01 & 6884 & 0 & 12{,}6\,ms & 40{,}9\,ms & 125\,ms & \pass \\ +ST-BOT-01 \#1 & 80 & 0 & 511\,ms & 2{,}28\,s & 23{,}1\,s & \parcial \\ +ST-BOT-01 \#2 & 80 & 0 & 57\,ms & 85{,}6\,ms & 98\,ms & \pass \\ +ST-BOT-03 & --- & --- & --- & --- & --- & \neje \\ +\bottomrule +\end{tabular} +\caption{M\'etricas de carga k6 (perfil shared8g).} +\label{tab:carga} +\end{table} + +\section{ST-GW-01} +\begin{itemize} + \item Checks 13768/13768 (100\,\%); throughput $\approx$57\,req/s. + \item Thresholds p95$<$500\,ms y fail$<$1\,\% en verde. +\end{itemize} + +\section{ST-BOT-01 --- comparativa} +Corrida 1: funcional OK, latencia FAIL (contenci\'on host).\\ +Corrida 2: funcional OK, latencia PASS (host m\'as libre). + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=0.95\textwidth, + height=7cm, + ylabel={Latencia (ms)}, + symbolic x coords={GW-p95,BOT1-p95,BOT2-p95,GW-max,BOT1-max,BOT2-max}, + xtick=data, + x tick label style={rotate=30, anchor=east, font=\small}, + nodes near coords, + nodes near coords style={font=\scriptsize}, + bar width=12pt, + ymin=0, + enlarge x limits=0.12, + grid=major, + legend entries={medici\'on, umbral GW 500, umbral BOT 800}, + legend style={at={(0.02,0.98)},anchor=north west,font=\scriptsize} +] +\addplot[fill=ipsblue!75] coordinates { + (GW-p95,40.85) (BOT1-p95,2280) (BOT2-p95,85.55) + (GW-max,125.37) (BOT1-max,23120) (BOT2-max,98.31) +}; +\addplot[red, sharp plot, update limits=false, forget plot] coordinates {(GW-p95,500) (BOT2-max,500)}; +\addplot[orange, sharp plot, update limits=false] coordinates {(BOT1-p95,800) (BOT2-p95,800)}; +\end{axis} +\end{tikzpicture} +\caption{p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla.} +\label{fig:latencias} +\end{figure} + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=0.9\textwidth, + height=6cm, + ylabel={ms}, + symbolic x coords={avg,med,p95,max}, + xtick=data, + legend style={at={(0.02,0.98)},anchor=north west}, + bar width=10pt, + ymin=0, ymax=600, + enlarge x limits=0.15, + grid=major, + title={ST-BOT-01: corrida 1 vs 2 (max de \#1 truncado en eje)} +] +\addplot[fill=failred!60] coordinates {(avg,511) (med,57) (p95,500) (max,600)}; +\addlegendentry{\#1 (p95 real 2280; max 23120)} +\addplot[fill=passgreen!70] coordinates {(avg,57) (med,56) (p95,86) (max,98)}; +\addlegendentry{\#2 PASS} +\end{axis} +\end{tikzpicture} +\caption{Before/after heartbeats (eje limitado; valores exactos en tabla~\ref{tab:carga}).} +\label{fig:bot-before-after} +\end{figure} + +\section{Throughput gateway} +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + width=0.7\textwidth, + height=5cm, + ybar, + symbolic x coords={ST-GW-01}, + xtick=data, + ylabel={req/s}, + nodes near coords, + ymin=0, + bar width=30pt +] +\addplot[fill=ipsblue] coordinates {(ST-GW-01,57.33)}; +\end{axis} +\end{tikzpicture} +\caption{Throughput observado en health liveness (no equivale a PDFs/h).} +\label{fig:throughput} +\end{figure} diff --git a/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex b/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex new file mode 100644 index 0000000..5b55c11 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex @@ -0,0 +1,104 @@ +\chapter{Capacidad: 100\,000 facturas al mes} +\label{ch:capacidad} + +\section{Traducci\'on del objetivo} +\begin{table}[ht] +\centering +\begin{tabular}{@{}lrr@{}} +\toprule +\textbf{Escenario operativo} & \textbf{Facturas/d\'ia} & \textbf{Facturas/hora} \\ +\midrule +100\,000 / mes, 24$\times$7 & $\approx$3\,333 & $\approx$\textbf{139} \\ +100\,000 / mes, 8\,h $\times$ 22\,d & $\approx$4\,545 & $\approx$\textbf{570} \\ +\bottomrule +\end{tabular} +\caption{Throughput m\'inimo para cumplir 100k/mes.} +\end{table} + +\section{Modelo por etapa} +Supuestos: host compartido medido el 2026-07-13; estimaciones \textbf{conservadoras} (no bench OCR/LLM masivo). Detalle en \texttt{scripts/estimate\_capacity.md}. + +\begin{table}[ht] +\centering +\begin{tabular}{@{}lrrll@{}} +\toprule +\textbf{Etapa} & \textbf{Host act.} & \textbf{Dedicado} & \textbf{vs 139/h} & \textbf{vs 570/h} \\ +\midrule +Gateway (orquestaci\'on) & $\gg$10\,000 & $\gg$20\,000 & OK & OK \\ +count-api OCR & $\sim$40 & $\sim$400 & RIESGO & NO \\ +extraction LLM & $\sim$100 & $\sim$800 & \'AMBAR & NO$^{*}$ \\ +bot Selenium (1 agent) & $\sim$60 & $\sim$500$^{\dagger}$ & RIESGO & NO \\ +cleaning & $\sim$800 & $\sim$3\,000 & OK & \'AMBAR \\ +\textbf{Pipeline completo} & $\sim$35 & $\sim$450 & \fail & \'AMBAR \\ +\bottomrule +\end{tabular} +\caption{Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger}$Con $N$ agents en paralelo.} +\label{tab:capacidad} +\end{table} + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=\textwidth, + height=7.5cm, + ylabel={Facturas / hora (estimadas)}, + symbolic x coords={Gateway,OCR,LLM,Bot,Cleaning,Pipeline}, + xtick=data, + x tick label style={font=\small}, + legend style={at={(0.02,0.98)},anchor=north west,font=\small}, + bar width=8pt, + ymin=0, ymax=1200, + enlarge x limits=0.08, + grid=major, + title={Capacidad estimada vs objetivos 139/h y 570/h} +] +\addplot[fill=ipsblue!70] coordinates { + (Gateway,1000) (OCR,40) (LLM,100) (Bot,60) (Cleaning,800) (Pipeline,35) +}; +\addlegendentry{Host actual (Gateway truncado)} +\addplot[fill=passgreen!55] coordinates { + (Gateway,1000) (OCR,400) (LLM,800) (Bot,500) (Cleaning,1000) (Pipeline,450) +}; +\addlegendentry{Dedicado (estim.)} +\addplot[failred, ultra thick, sharp plot, update limits=false] coordinates { + (Gateway,139) (Pipeline,139) +}; +\addlegendentry{Objetivo 139/h} +\addplot[warnorange, ultra thick, sharp plot, update limits=false] coordinates { + (Gateway,570) (Pipeline,570) +}; +\addlegendentry{Objetivo 570/h} +\end{axis} +\end{tikzpicture} +\caption{Brecha de capacidad. Gateway real $\gg$1000/h; se trunca para legibilidad.} +\label{fig:capacidad} +\end{figure} + +\section{Interpretaci\'on} +\begin{enumerate} + \item El \textbf{gateway} no es el cuello de botella (evidencia ST-GW-01). + \item En el \textbf{host actual}, OCR + Selenium + LLM compiten por CPU/RAM/swap con MLflow: el pipeline completo queda $\ll$139/h. + \item Con infra \textbf{dedicada} (sin zoo, m\'as RAM, workers, $N$ PCs bot, cuota LLM), acercarse a 139/h (24$\times$7) es plausible; 570/h exige dimensionamiento agresivo y prueba de carga espec\'ifica. +\end{enumerate} + +\section{Veredicto} +\begin{center} +\fbox{\parbox{0.92\textwidth}{\centering +\large \textbf{Se pueden procesar +100\,000 facturas/mes?}\\[0.4em] +\normalsize +En el servidor actual compartido: \fail.\\[0.2em] +En arquitectura dedicada y dimensionada: \pass\ condicional (cuota LLM, OCR, agents).\\[0.3em] +\footnotesize Esta campa\~na no ejecut\'o 100k PDFs; el veredicto es modelo + evidencia de carga ligera. +}} +\end{center} + +\section{Requisitos m\'inimos sugeridos para 100k/mes} +\begin{itemize} + \item Host(s) dedicados IPS: RAM $\geq$16--32\,GiB; SSD; sin MLflow/Airflow en el mismo nodo. + \item count-api: pool OCR dimensionado; l\'imite de memoria realista. + \item extraction: r\'eplicas worker + concurrencia acorde a RPM del LLM; presupuesto de coste. + \item bot: varios agents PC en paralelo hacia Imaging/MinIO. + \item Observabilidad: colas ARQ, p95 por etapa, alertas de swap. +\end{itemize} diff --git a/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex b/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex new file mode 100644 index 0000000..ed39690 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex @@ -0,0 +1,41 @@ +\chapter{An\'alisis y hallazgos} +\label{ch:analisis} + +\section{Hallazgos} +\begin{enumerate} + \item \textbf{H1 --- Contenci\'on de plataforma:} ST-BOT-01 \#1 (p95 2{,}28\,s, max 23\,s) vs \#2 (p95 86\,ms) demuestra sensibilidad a swap/MLflow, no a un bug funcional del heartbeat (100\,\% HTTP 200 en ambas). + \item \textbf{H2 --- Gateway saludable:} 0\,\% errores y p95 41\,ms bajo 25 VUs en liveness. + \item \textbf{H3 --- Unitarias s\'olidas:} 29 casos verdes (19 agent + 7 API store + 3 count-api pipeline). + \item \textbf{H4 --- Capacidad 100k:} imposible en host compartido actual; viable solo con escalado dedicado. +\end{enumerate} + +\section{Scorecard global} +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar stacked, + width=0.9\textwidth, + height=5.5cm, + ylabel={Cantidad de casos}, + symbolic x coords={Unitaria,Humo,Carga,Funcional,Capacidad}, + xtick=data, + legend style={at={(0.5,-0.22)},anchor=north,legend columns=3,font=\small}, + bar width=18pt, + ymin=0, ymax=8, + enlarge x limits=0.12 +] +\addplot[fill=passgreen!70] coordinates {(Unitaria,3) (Humo,3) (Carga,2) (Funcional,0) (Capacidad,0)}; +\addlegendentry{PASS} +\addplot[fill=warnorange!70] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (Funcional,0) (Capacidad,0)}; +\addlegendentry{PARCIAL} +\addplot[fill=ipsgrey!50] coordinates {(Unitaria,1) (Humo,0) (Carga,1) (Funcional,1) (Capacidad,1)}; +\addlegendentry{N/E} +\end{axis} +\end{tikzpicture} +\caption{Distribuci\'on de resultados por tipo de prueba.} +\label{fig:scorecard} +\end{figure} + +\section{Limitaciones} +No se midieron tiempos reales OCR/LLM por PDF en esta campa\~na; las cifras de capacidad son estimaciones documentadas para decisi\'on de arquitectura, no un compromiso de SLA. diff --git a/docs/plan_pruebas/informe/sections/10_conclusiones.tex b/docs/plan_pruebas/informe/sections/10_conclusiones.tex new file mode 100644 index 0000000..e3968b2 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/10_conclusiones.tex @@ -0,0 +1,26 @@ +\chapter{Conclusiones y recomendaciones} +\label{ch:conclusiones} + +\section{Conclusiones} +\begin{enumerate} + \item IPSExtractor en staging responde correctamente a humo y carga ligera de control (gateway + heartbeats). + \item La calidad de latencia depende cr\'iticamente de no saturar el host compartido. + \item \textbf{100\,000 facturas/mes no son alcanzables} en el laptop Ubuntu actual con el zoo Docker observado. + \item Con infraestructura dedicada y dimensionada, el objetivo es \textbf{condicional\-mente viable}; requiere prueba de carga espec\'ifica por etapa (OCR, LLM, bot). +\end{enumerate} + +\section{Recomendaciones} +\begin{enumerate} + \item Separar IPS de MLflow/Airflow/n8n en el nodo de producci\'on/staging de rendimiento. + \item Ejecutar ST-COUNT-01 y bench LLM solo con $\geq$3\,GiB libres y sin swap activo. + \item Completar E2E-07 y ST-BOT-03; archivar JSON de k6 en \texttt{informes/}. + \item Dimensionar agents bot ($N$) antes de comprometer vol\'umenes mensuales de descarga. + \item Incorporar CI: pytest agent + store en cada PR. +\end{enumerate} + +\section{Pr\'oxima campa\~na sugerida} +\begin{itemize} + \item Ventana con MLflow detenido. + \item Bench: 50--200 PDFs reales por etapa (OCR, LLM) midiendo p50/p95 y coste. + \item Actualizar gr\'afica de capacidad con datos medidos (reemplazar estimaciones). +\end{itemize} diff --git a/docs/plan_pruebas/informe/sections/11_anexos.tex b/docs/plan_pruebas/informe/sections/11_anexos.tex new file mode 100644 index 0000000..c8cbcf5 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/11_anexos.tex @@ -0,0 +1,41 @@ +\chapter{Anexos} +\label{ch:anexos} + +\section{Datos fuente} +Archivos: \texttt{informe/data/metricas.csv}, \texttt{capacidad.csv}, \texttt{memoria.csv}.\\ +Markdown de campa\~na: \texttt{docs/plan\_pruebas/informes/2026-07-13-shared8g.md}. + +\section{Extracto ST-GW-01} +\begin{lstlisting} +checks 100% (13768) +http_req_failed 0% +p(95)=40.85ms avg=12.55ms reqs=6884 (~57/s) +\end{lstlisting} + +\section{Extracto ST-BOT-01 \#2} +\begin{lstlisting} +checks 100% (75) +p(95)=85.55ms max=98.31ms +thresholds OK (p95<800) +\end{lstlisting} + +\section{Unitarias} +\begin{lstlisting} +bots/bot_descarga/tests: 19 passed in 3.21s +services/bot_descarga/tests: 7 passed in 1.45s +count-api internal/pipeline: 3 passed (+8 features/clustering nuevos) +Timing: go test -run Timing -v (3 PDFs en docs/plan_pruebas/data/) +HTTP: bash docs/plan_pruebas/scripts/measure_count_latency.sh +\end{lstlisting} + +\section{Scripts} +\texttt{k6\_gateway\_health.js}, \texttt{k6\_bot\_heartbeats.js}, \texttt{k6\_bot\_events.js}, +\texttt{k6\_count\_batches.js}, \texttt{k6\_smoke\_suite.js}, \texttt{estimate\_capacity.md}. + +\section{Reproducibilidad} +\begin{enumerate} + \item Registrar \texttt{hostnamectl}, \texttt{free -h}, \texttt{docker stats --no-stream}. + \item Verificar \texttt{/health/live} y \texttt{/health}. + \item Ejecutar k6 con \texttt{PROFILE=shared8g}. + \item Adjuntar salida completa al directorio \texttt{informes/}. +\end{enumerate} diff --git a/docs/plan_pruebas/informe/sections/12_count_latency.tex b/docs/plan_pruebas/informe/sections/12_count_latency.tex new file mode 100644 index 0000000..c255b6c --- /dev/null +++ b/docs/plan_pruebas/informe/sections/12_count_latency.tex @@ -0,0 +1,49 @@ +\chapter{Resultados --- latencia count-api (PDFs reales)} +\label{ch:count-latency} + +\section{Fixtures} +Tres PDFs en \texttt{docs/plan\_pruebas/data/}: +\begin{itemize} + \item \texttt{500000284335937.pdf} + \item \texttt{500000284335940.pdf} + \item \texttt{500000284335954.pdf} +\end{itemize} + +\section{Unitarias pipeline ampliadas} +Adem\'as de los 3 tests originales de features: +\begin{itemize} + \item Header keyword, service code, empty rows, no table pattern. + \item Clustering: dos bandas Y, misma banda, vac\'io, ruido \texttt{minSamples}. +\end{itemize} +Total esperado en \texttt{go test ./internal/pipeline/ -run 'TestExtract|TestBuild|TestFeature|TestCluster'}: \textbf{11} casos. + +\section{Timings por etapa} +El procesador expone \texttt{render\_ms}, \texttt{ocr\_ms}, \texttt{cluster\_ms}, \texttt{infer\_ms}, \texttt{total\_ms} en \texttt{PDFResult}. + +\begin{lstlisting}[language=bash,caption={Medici\'on en Ubuntu}] +cd services/services-count-facturas-medicas-serving +go test ./internal/pipeline/ -run Timing -v + +# HTTP v\'ia gateway: +bash docs/plan_pruebas/scripts/measure_count_latency.sh +# o PowerShell: +# .\docs\plan_pruebas\scripts\measure_count_latency.ps1 +\end{lstlisting} + +Los resultados de la corrida Timing/HTTP deben pegarse en +\texttt{docs/plan\_pruebas/informes/count\_latency\_YYYYMMDD.csv} y actualizar esta tabla. + +\begin{table}[ht] +\centering +\begin{tabular}{@{}lrrrrrl@{}} +\toprule +\textbf{PDF} & \textbf{render} & \textbf{ocr} & \textbf{cluster} & \textbf{infer} & \textbf{total} & \textbf{status} \\ +\midrule +500000284335937 & --- & --- & --- & --- & --- & pendiente Ubuntu \\ +500000284335940 & --- & --- & --- & --- & --- & pendiente Ubuntu \\ +500000284335954 & --- & --- & --- & --- & --- & pendiente Ubuntu \\ +\bottomrule +\end{tabular} +\caption{Latencia por etapa (ms). Completar tras \texttt{-run Timing} en el host con Tesseract.} +\label{tab:count-timing} +\end{table} diff --git a/docs/plan_pruebas/informes/2026-07-13-shared8g.md b/docs/plan_pruebas/informes/2026-07-13-shared8g.md new file mode 100644 index 0000000..acb2fff --- /dev/null +++ b/docs/plan_pruebas/informes/2026-07-13-shared8g.md @@ -0,0 +1,40 @@ +# Informe de ejecución — 2026-07-13 + +Perfil: `shared8g` · Host: `ubuntu` (ASUS X441UAK, Ubuntu 24.04, i5-7200U, 8 GiB) · Gateway: `http://127.0.0.1:8189` + +## Baseline + +| Check | Resultado | +|-------|-----------| +| RAM | 4.4 GiB usados / 3.3 GiB disponibles / **swap 2.1 GiB** | +| IPS containers | healthy (api, bot, extraction, workers, count, cleaning, db, redis) | +| `/health/live` | ok · ~11 ms | +| `/health` | ok (4 servicios) · ~194 ms | +| Vecino pesado | mlflow-server ~1.84 GiB | + +## Resultados + +| ID | Resultado | Notas | +|----|-----------|-------| +| Smoke | **PASS** | Stack IPS OK | +| Unitarias agent | **PASS** | 19/19 en 3.21s | +| Unitarias API store | **PASS** | 7/7 en 1.45s | +| Unitarias count-api | **PASS** | 3/3 pipeline (Ubuntu + leptonica-dev) | +| ST-GW-01 | **PASS** | 6884 req · fail 0% · p95=40.85 ms | +| ST-BOT-01 #1 | **PARCIAL** | 100% HTTP 200 · p95=2.28 s · max=23 s (FAIL latencia) | +| ST-BOT-01 #2 | **PASS** | 100% HTTP 200 · p95=85.55 ms · max=98 ms | + +## Capacidad 100k/mes + +- Host actual compartido: **NO** (pipeline completo). +- Infra dedicada: **SÍ condicional** (ver informe PDF). + +## Conclusión + +Gateway y heartbeats de bot-descarga cumplen SLO del perfil `shared8g` en condiciones razonables. El fallo de latencia de BOT-01 #1 se atribuye a contención del host (swap + MLflow), no a error funcional de la API. + +## Pendiente + +- En Ubuntu: `go test ./internal/pipeline/ -v` y `go test ./internal/pipeline/ -run Timing -v` +- HTTP: `bash docs/plan_pruebas/scripts/measure_count_latency.sh` +- ST-BOT-03, ST-COUNT-01, E2E Imaging diff --git a/docs/plan_pruebas/logs/audit_20260713_181521.jsonl b/docs/plan_pruebas/logs/audit_20260713_181521.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/docs/plan_pruebas/main.aux b/docs/plan_pruebas/main.aux index 9184434..a51283d 100644 --- a/docs/plan_pruebas/main.aux +++ b/docs/plan_pruebas/main.aux @@ -194,16 +194,33 @@ \@writefile{toc}{\contentsline {section}{\numberline {11.1}Fases sugeridas}{28}{section.11.1}\protected@file@percent } \@writefile{lot}{\contentsline {table}{\numberline {11.1}{Hoja de ruta de calidad.}}{28}{table.11.1}\protected@file@percent } \@writefile{toc}{\contentsline {section}{\numberline {11.2}Definition of Done por release}{28}{section.11.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.3}Plantilla de informe de ejecuci\'on}{28}{section.11.3}\protected@file@percent } -\@writefile{lol}{\contentsline {lstlisting}{\numberline {11.1}Campos m\'inimos del informe}{28}{lstlisting.11.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.4}Responsables (roles)}{29}{section.11.4}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {A}Glosario}{30}{appendix.Alph1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.3}Campa\~na registrada}{28}{section.11.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.4}Plantilla de informe de ejecuci\'on}{29}{section.11.4}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {11.1}Campos m\'inimos del informe}{29}{lstlisting.11.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.5}Responsables (roles)}{29}{section.11.5}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {12}Informe de ejecuci\'on --- campa\~na 2026-07-13}{30}{chapter.12}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\@writefile{toc}{\contentsline {chapter}{\numberline {B}Checklist de release}{31}{appendix.Alph2}\protected@file@percent } +\newlabel{ch:informe-2026-07-13}{{12}{30}{Informe de ejecuci\'on --- campa\~na 2026-07-13}{chapter.12}{}} +\@writefile{toc}{\contentsline {section}{\numberline {12.1}Metadatos}{30}{section.12.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Metadatos de la campa\~na.}}{30}{table.12.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.2}Baseline del entorno}{30}{section.12.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {12.2}{\ignorespaces Resumen de la campa\~na 2026-07-13.}}{31}{table.12.2}\protected@file@percent } +\newlabel{tab:campana-2026-07-13}{{12.2}{31}{Resumen de la campa\~na 2026-07-13}{table.12.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {12.3}Resultados por caso}{31}{section.12.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.1}ST-GW-01 --- gateway health}{31}{subsection.12.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.2}ST-BOT-01 --- heartbeats (corrida 1)}{31}{subsection.12.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.3}ST-BOT-01 --- heartbeats (corrida 2, reintento)}{31}{subsection.12.3.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.4}Hallazgos}{31}{section.12.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.5}No ejecutado en esta campa\~na}{32}{section.12.5}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.6}Follow-ups}{32}{section.12.6}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {A}Glosario}{33}{appendix.Alph1}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\@writefile{toc}{\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{32}{appendix.Alph3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {B}Checklist de release}{34}{appendix.Alph2}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\gdef \@abspage@last{33} +\@writefile{toc}{\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{35}{appendix.Alph3}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\gdef \@abspage@last{36} diff --git a/docs/plan_pruebas/main.out b/docs/plan_pruebas/main.out index f07c198..3ab47a0 100644 --- a/docs/plan_pruebas/main.out +++ b/docs/plan_pruebas/main.out @@ -66,8 +66,19 @@ \BOOKMARK [0][-]{chapter.11}{\376\377\000C\000r\000o\000n\000o\000g\000r\000a\000m\000a\000\040\000y\000\040\000c\000r\000i\000t\000e\000r\000i\000o\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{}% 66 \BOOKMARK [1][-]{section.11.1}{\376\377\000F\000a\000s\000e\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000a\000s}{chapter.11}% 67 \BOOKMARK [1][-]{section.11.2}{\376\377\000D\000e\000f\000i\000n\000i\000t\000i\000o\000n\000\040\000o\000f\000\040\000D\000o\000n\000e\000\040\000p\000o\000r\000\040\000r\000e\000l\000e\000a\000s\000e}{chapter.11}% 68 -\BOOKMARK [1][-]{section.11.3}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.11}% 69 -\BOOKMARK [1][-]{section.11.4}{\376\377\000R\000e\000s\000p\000o\000n\000s\000a\000b\000l\000e\000s\000\040\000\050\000r\000o\000l\000e\000s\000\051}{chapter.11}% 70 -\BOOKMARK [0][-]{appendix.Alph1}{\376\377\000G\000l\000o\000s\000a\000r\000i\000o}{}% 71 -\BOOKMARK [0][-]{appendix.Alph2}{\376\377\000C\000h\000e\000c\000k\000l\000i\000s\000t\000\040\000d\000e\000\040\000r\000e\000l\000e\000a\000s\000e}{}% 72 -\BOOKMARK [0][-]{appendix.Alph3}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000r\000u\000t\000a\000s\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{}% 73 +\BOOKMARK [1][-]{section.11.3}{\376\377\000C\000a\000m\000p\000a\000\361\000a\000\040\000r\000e\000g\000i\000s\000t\000r\000a\000d\000a}{chapter.11}% 69 +\BOOKMARK [1][-]{section.11.4}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.11}% 70 +\BOOKMARK [1][-]{section.11.5}{\376\377\000R\000e\000s\000p\000o\000n\000s\000a\000b\000l\000e\000s\000\040\000\050\000r\000o\000l\000e\000s\000\051}{chapter.11}% 71 +\BOOKMARK [0][-]{chapter.12}{\376\377\000I\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\040\024\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003}{}% 72 +\BOOKMARK [1][-]{section.12.1}{\376\377\000M\000e\000t\000a\000d\000a\000t\000o\000s}{chapter.12}% 73 +\BOOKMARK [1][-]{section.12.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000l\000\040\000e\000n\000t\000o\000r\000n\000o}{chapter.12}% 74 +\BOOKMARK [1][-]{section.12.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000p\000o\000r\000\040\000c\000a\000s\000o}{chapter.12}% 75 +\BOOKMARK [2][-]{subsection.12.3.1}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001\000\040\040\024\000\040\000g\000a\000t\000e\000w\000a\000y\000\040\000h\000e\000a\000l\000t\000h}{section.12.3}% 76 +\BOOKMARK [2][-]{subsection.12.3.2}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000h\000e\000a\000r\000t\000b\000e\000a\000t\000s\000\040\000\050\000c\000o\000r\000r\000i\000d\000a\000\040\0001\000\051}{section.12.3}% 77 +\BOOKMARK [2][-]{subsection.12.3.3}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000h\000e\000a\000r\000t\000b\000e\000a\000t\000s\000\040\000\050\000c\000o\000r\000r\000i\000d\000a\000\040\0002\000,\000\040\000r\000e\000i\000n\000t\000e\000n\000t\000o\000\051}{section.12.3}% 78 +\BOOKMARK [1][-]{section.12.4}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.12}% 79 +\BOOKMARK [1][-]{section.12.5}{\376\377\000N\000o\000\040\000e\000j\000e\000c\000u\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.12}% 80 +\BOOKMARK [1][-]{section.12.6}{\376\377\000F\000o\000l\000l\000o\000w\000-\000u\000p\000s}{chapter.12}% 81 +\BOOKMARK [0][-]{appendix.Alph1}{\376\377\000G\000l\000o\000s\000a\000r\000i\000o}{}% 82 +\BOOKMARK [0][-]{appendix.Alph2}{\376\377\000C\000h\000e\000c\000k\000l\000i\000s\000t\000\040\000d\000e\000\040\000r\000e\000l\000e\000a\000s\000e}{}% 83 +\BOOKMARK [0][-]{appendix.Alph3}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000r\000u\000t\000a\000s\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{}% 84 diff --git a/docs/plan_pruebas/main.tex b/docs/plan_pruebas/main.tex index dc0ee16..c23d771 100644 --- a/docs/plan_pruebas/main.tex +++ b/docs/plan_pruebas/main.tex @@ -13,6 +13,7 @@ \midrule 1.0.0 & 13 jul 2026 & Versi\'on inicial del plan de pruebas y estr\'es. \\ 1.1.0 & 13 jul 2026 & Entorno real de ejecuci\'on (host Ubuntu compartido 8\,GiB) y perfiles de carga. \\ +1.2.0 & 13 jul 2026 & Informe campa\~na k6: smoke, ST-GW-01 PASS, ST-BOT-01 PASS (reintento). \\ \bottomrule \end{tabularx} @@ -21,7 +22,9 @@ Este documento define la estrategia y la matriz de pruebas del monorepo \textbf{ Hoy el repositorio cuenta principalmente con pruebas unitarias aisladas; no existe pipeline CI, ni suites de integraci\'on/E2E automatizadas. Este plan cierra ese gap con identificadores trazables (\texttt{UT-*}, \texttt{IT-*}, \texttt{E2E-*}, \texttt{ST-*}, \texttt{CH-*}), SLOs medibles y scripts \textbf{k6} de referencia en \texttt{docs/plan\_pruebas/scripts/}. -\textbf{Importante:} las pruebas de integraci\'on, humo y estr\'es documentadas contra el stack vivo se ejecutan (o se ejecutar\'an) en el host descrito en la secci\'on~\ref{sec:host-pruebas} (laptop Ubuntu 24.04, 8\,GiB RAM, Docker compartido con MLflow, Airflow, n8n, etc.). Los resultados deben interpretarse en ese contexto: un OOM o p95 alto puede deberse a contenci\'on del host, no solo a IPSExtractor. +\textbf{Importante:} las pruebas de integraci\'on, humo y estr\'es documentadas contra el stack vivo se ejecutan en el host descrito en la secci\'on~\ref{sec:host-pruebas} (laptop Ubuntu 24.04, 8\,GiB RAM, Docker compartido con MLflow, Airflow, n8n, etc.). Los resultados deben interpretarse en ese contexto: un OOM o p95 alto puede deberse a contenci\'on del host, no solo a IPSExtractor. + +\textbf{Estado campa\~na 2026-07-13:} smoke PASS; ST-GW-01 PASS; ST-BOT-01 PASS en reintento (detalle en cap.~\ref{ch:informe-2026-07-13}). \tableofcontents \clearpage @@ -37,6 +40,7 @@ Hoy el repositorio cuenta principalmente con pruebas unitarias aisladas; no exis \input{sections/09_datos_entornos} \input{sections/10_herramientas_ci} \input{sections/11_cronograma_aceptacion} +\input{sections/12_informe_2026_07_13} \appendix \chapter{Glosario} diff --git a/docs/plan_pruebas/main.toc b/docs/plan_pruebas/main.toc index 1642060..ac8b53e 100644 --- a/docs/plan_pruebas/main.toc +++ b/docs/plan_pruebas/main.toc @@ -67,8 +67,19 @@ \contentsline {chapter}{\numberline {11}Cronograma y criterios de aceptaci\'on}{28}{chapter.11}% \contentsline {section}{\numberline {11.1}Fases sugeridas}{28}{section.11.1}% \contentsline {section}{\numberline {11.2}Definition of Done por release}{28}{section.11.2}% -\contentsline {section}{\numberline {11.3}Plantilla de informe de ejecuci\'on}{28}{section.11.3}% -\contentsline {section}{\numberline {11.4}Responsables (roles)}{29}{section.11.4}% -\contentsline {chapter}{\numberline {A}Glosario}{30}{appendix.Alph1}% -\contentsline {chapter}{\numberline {B}Checklist de release}{31}{appendix.Alph2}% -\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{32}{appendix.Alph3}% +\contentsline {section}{\numberline {11.3}Campa\~na registrada}{28}{section.11.3}% +\contentsline {section}{\numberline {11.4}Plantilla de informe de ejecuci\'on}{29}{section.11.4}% +\contentsline {section}{\numberline {11.5}Responsables (roles)}{29}{section.11.5}% +\contentsline {chapter}{\numberline {12}Informe de ejecuci\'on --- campa\~na 2026-07-13}{30}{chapter.12}% +\contentsline {section}{\numberline {12.1}Metadatos}{30}{section.12.1}% +\contentsline {section}{\numberline {12.2}Baseline del entorno}{30}{section.12.2}% +\contentsline {section}{\numberline {12.3}Resultados por caso}{31}{section.12.3}% +\contentsline {subsection}{\numberline {12.3.1}ST-GW-01 --- gateway health}{31}{subsection.12.3.1}% +\contentsline {subsection}{\numberline {12.3.2}ST-BOT-01 --- heartbeats (corrida 1)}{31}{subsection.12.3.2}% +\contentsline {subsection}{\numberline {12.3.3}ST-BOT-01 --- heartbeats (corrida 2, reintento)}{31}{subsection.12.3.3}% +\contentsline {section}{\numberline {12.4}Hallazgos}{31}{section.12.4}% +\contentsline {section}{\numberline {12.5}No ejecutado en esta campa\~na}{32}{section.12.5}% +\contentsline {section}{\numberline {12.6}Follow-ups}{32}{section.12.6}% +\contentsline {chapter}{\numberline {A}Glosario}{33}{appendix.Alph1}% +\contentsline {chapter}{\numberline {B}Checklist de release}{34}{appendix.Alph2}% +\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{35}{appendix.Alph3}% diff --git a/docs/plan_pruebas/preamble.tex b/docs/plan_pruebas/preamble.tex index 6373380..dadfea6 100644 --- a/docs/plan_pruebas/preamble.tex +++ b/docs/plan_pruebas/preamble.tex @@ -83,7 +83,7 @@ pdfsubject={Pruebas unitarias, integracion, E2E, estres y resiliencia} } -\newcommand{\docversion}{1.1.0} +\newcommand{\docversion}{1.2.0} \newcommand{\docdate}{13 de julio de 2026} \newcommand{\statusbadge}[1]{\texttt{#1}} diff --git a/docs/plan_pruebas/scripts/README.md b/docs/plan_pruebas/scripts/README.md index 8409815..9f006d0 100644 --- a/docs/plan_pruebas/scripts/README.md +++ b/docs/plan_pruebas/scripts/README.md @@ -44,6 +44,14 @@ curl -s -w '\ntime=%{time_total}s\n' http://127.0.0.1:8189/health/live k6 run -e PROFILE=shared8g -e BASE_URL=http://127.0.0.1:8189 k6_gateway_health.js k6 run -e PROFILE=shared8g -e BOT_API_TOKEN=dev-token-change-me k6_bot_heartbeats.js k6 run -e PROFILE=shared8g -e DOWNLOAD_JOB_ID= k6_bot_events.js +k6 run -e PROFILE=shared8g k6_smoke_suite.js # Solo si free -h muestra ≥3GiB disponibles: k6 run -e PROFILE=shared8g k6_count_batches.js + +# Latencia count-api con PDFs reales: +bash docs/plan_pruebas/scripts/measure_count_latency.sh ``` + +Modelo de capacidad 100k/mes: [`estimate_capacity.md`](estimate_capacity.md). Informe PDF: [`../InformePruebas_IPSExtractor.pdf`](../InformePruebas_IPSExtractor.pdf). + +PDFs de prueba: [`../data/`](../data/) (`500000284335937.pdf`, `...940.pdf`, `...954.pdf`). diff --git a/docs/plan_pruebas/scripts/estimate_capacity.md b/docs/plan_pruebas/scripts/estimate_capacity.md new file mode 100644 index 0000000..56e8354 --- /dev/null +++ b/docs/plan_pruebas/scripts/estimate_capacity.md @@ -0,0 +1,25 @@ +# Modelo de capacidad — 100.000 facturas/mes + +No sustituye una corrida real de 100k PDFs. Usa supuestos conservadores + evidencia k6 del 2026-07-13. + +## Objetivo + +| Escenario | Facturas/hora necesarias | +|-----------|--------------------------| +| 100k/mes 24×7 | ≈ 139/h | +| 100k/mes jornada 8h×22d | ≈ 570/h | + +## Estimaciones por etapa (host compartido 8 GiB) + +| Etapa | facturas/h estimadas | vs 139/h | vs 570/h | +|-------|----------------------|----------|----------| +| Gateway (orquestación) | >>10 000 (health ~57 rps) | OK | OK | +| count-api OCR | 20–80 (CPU/RAM contención) | RIESGO | NO | +| extraction LLM | 60–200 (si cuota LLM; 30–60s/PDF, pocos workers) | ÁMBAR | NO | +| bot Selenium (1 agent) | 30–120 | RIESGO | NO | +| cleaning CSV | 500–2000 | OK | ÁMBAR | +| Pipeline completo (este host) | <50 | NO | NO | + +## Escenario dedicado (condicional) + +Con host ≥16–32 GiB solo IPS, OCR pool, 2+ workers extraction, cuota LLM, N agents bot: pipeline completo puede acercarse a 139–570/h según dimensionamiento. diff --git a/docs/plan_pruebas/scripts/k6_smoke_suite.js b/docs/plan_pruebas/scripts/k6_smoke_suite.js new file mode 100644 index 0000000..5dc2c57 --- /dev/null +++ b/docs/plan_pruebas/scripts/k6_smoke_suite.js @@ -0,0 +1,41 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +/** + * Suite de humo ligero del gateway (PROFILE=shared8g). + * + * k6 run -e BASE_URL=http://127.0.0.1:8189 -e PROFILE=shared8g k6_smoke_suite.js + */ +const PROFILE = __ENV.PROFILE || 'shared8g'; +const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:8189'; + +export const options = { + vus: PROFILE === 'lab' ? 5 : 2, + duration: PROFILE === 'lab' ? '60s' : '30s', + thresholds: { + http_req_failed: ['rate<0.05'], + http_req_duration: ['p(95)<2000'], + checks: ['rate>0.9'], + }, + tags: { profile: PROFILE, suite: 'smoke' }, +}; + +export default function () { + const live = http.get(`${BASE_URL}/health/live`); + check(live, { + 'live 200': (r) => r.status === 200, + }); + + const health = http.get(`${BASE_URL}/health`); + check(health, { + 'health 200': (r) => r.status === 200, + 'health body ok': (r) => String(r.body).includes('"ok"'), + }); + + const config = http.get(`${BASE_URL}/config`); + check(config, { + 'config not 5xx': (r) => r.status < 500, + }); + + sleep(0.5); +} diff --git a/docs/plan_pruebas/scripts/measure_count_latency.ps1 b/docs/plan_pruebas/scripts/measure_count_latency.ps1 new file mode 100644 index 0000000..36f603d --- /dev/null +++ b/docs/plan_pruebas/scripts/measure_count_latency.ps1 @@ -0,0 +1,55 @@ +# Mide latencia HTTP de count-api vía gateway para cada PDF en docs/plan_pruebas/data/ +param( + [string]$BaseUrl = "http://127.0.0.1:8189", + [string]$DataDir = "", + [string]$OutCsv = "" +) + +$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") +if (-not $DataDir) { $DataDir = Join-Path $Root "docs\plan_pruebas\data" } +if (-not $OutCsv) { + $stamp = Get-Date -Format "yyyyMMdd" + $OutCsv = Join-Path $Root "docs\plan_pruebas\informes\count_latency_$stamp.csv" +} + +if (-not (Test-Path $DataDir)) { throw "No existe DataDir=$DataDir" } +$pdfs = Get-ChildItem -Path $DataDir -Filter *.pdf -File +if ($pdfs.Count -eq 0) { throw "No hay PDFs en $DataDir" } + +New-Item -ItemType Directory -Force -Path (Split-Path $OutCsv) | Out-Null +"file,http_time_s,http_code,service_count,status" | Set-Content -Path $OutCsv -Encoding utf8 +Write-Host "BASE_URL=$BaseUrl" +Write-Host "DATA_DIR=$DataDir" +Write-Host "OUT=$OutCsv" + +foreach ($pdf in $pdfs) { + $tmp = [System.IO.Path]::GetTempFileName() + $sw = [System.Diagnostics.Stopwatch]::StartNew() + try { + $resp = curl.exe -sS -o $tmp -w "%{http_code}" ` + -F "files=@$($pdf.FullName);type=application/pdf" ` + "$BaseUrl/count/v1/count" + $code = $resp.Trim() + } catch { + $code = "000" + } + $sw.Stop() + $secs = [math]::Round($sw.Elapsed.TotalSeconds, 3) + $scount = "" + $status = "parse_error" + try { + $json = Get-Content $tmp -Raw | ConvertFrom-Json + if ($json.results -and $json.results.Count -gt 0) { + $scount = $json.results[0].service_count + $status = $json.results[0].status + } elseif ($json.error) { + $status = $json.error + } + } catch {} + $line = "$($pdf.Name),$secs,$code,$scount,$status" + Add-Content -Path $OutCsv -Value $line + Write-Host $line + Remove-Item $tmp -Force -ErrorAction SilentlyContinue +} + +Write-Host "Done. CSV: $OutCsv" diff --git a/docs/plan_pruebas/scripts/measure_count_latency.sh b/docs/plan_pruebas/scripts/measure_count_latency.sh new file mode 100644 index 0000000..7536a12 --- /dev/null +++ b/docs/plan_pruebas/scripts/measure_count_latency.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Mide latencia HTTP de count-api vía gateway para cada PDF en docs/plan_pruebas/data/ +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +DATA_DIR="${COUNT_TEST_PDF_DIR:-$ROOT/docs/plan_pruebas/data}" +BASE_URL="${BASE_URL:-http://127.0.0.1:8189}" +OUT="${COUNT_LATENCY_CSV:-$ROOT/docs/plan_pruebas/informes/count_latency_$(date +%Y%m%d).csv}" + +if [[ ! -d "$DATA_DIR" ]]; then + echo "No existe DATA_DIR=$DATA_DIR" >&2 + exit 1 +fi + +shopt -s nullglob +pdfs=("$DATA_DIR"/*.pdf "$DATA_DIR"/*.PDF) +if [[ ${#pdfs[@]} -eq 0 ]]; then + echo "No hay PDFs en $DATA_DIR" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$OUT")" +echo "file,http_time_s,http_code,service_count,status" >"$OUT" +echo "BASE_URL=$BASE_URL" +echo "DATA_DIR=$DATA_DIR" +echo "Writing $OUT" +echo + +for pdf in "${pdfs[@]}"; do + name="$(basename "$pdf")" + tmp="$(mktemp)" + # body to file; time_total and http_code on stderr via -w + code_time="$(curl -sS -o "$tmp" -w '%{http_code} %{time_total}' \ + -F "files=@${pdf};type=application/pdf" \ + "${BASE_URL}/count/v1/count" || echo "000 0")" + code="$(echo "$code_time" | awk '{print $1}')" + secs="$(echo "$code_time" | awk '{print $2}')" + # crude parse + scount="$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d['results'][0].get('service_count','') if d.get('results') else '')" "$tmp" 2>/dev/null || echo "")" + status="$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d['results'][0].get('status','') if d.get('results') else d.get('error','err'))" "$tmp" 2>/dev/null || echo "parse_error")" + echo "$name,$secs,$code,$scount,$status" | tee -a "$OUT" + rm -f "$tmp" +done + +echo +echo "Done. CSV: $OUT" diff --git a/docs/plan_pruebas/sections/11_cronograma_aceptacion.tex b/docs/plan_pruebas/sections/11_cronograma_aceptacion.tex index 1e646e0..98edc55 100644 --- a/docs/plan_pruebas/sections/11_cronograma_aceptacion.tex +++ b/docs/plan_pruebas/sections/11_cronograma_aceptacion.tex @@ -23,10 +23,13 @@ Un release es aceptable cuando: \begin{enumerate} \item Checklist del ap\'endice ``Checklist de release'' completa. \item Ning\'un caso \texttt{E2E-01}--\texttt{E2E-05} fallido sin waiver documentado. - \item ST-GW-01 y ST-BOT-01 dentro de SLO en staging. + \item ST-GW-01 y ST-BOT-01 dentro de SLO en staging (ver informe m\'as reciente, p.\ ej.\ cap.~\ref{ch:informe-2026-07-13}). \item Hallazgos P0/P1 de resiliencia cerrados o mitigados. \end{enumerate} +\section{Campa\~na registrada} +La primera campa\~na k6 en el host compartido est\'a documentada en el cap\'itulo~\ref{ch:informe-2026-07-13} y en \texttt{docs/plan\_pruebas/informes/2026-07-13-shared8g.md}. + \section{Plantilla de informe de ejecuci\'on} \begin{lstlisting}[language={},caption={Campos m\'inimos del informe}] Fecha / entorno / commit diff --git a/docs/plan_pruebas/sections/12_informe_2026_07_13.tex b/docs/plan_pruebas/sections/12_informe_2026_07_13.tex new file mode 100644 index 0000000..1638ca9 --- /dev/null +++ b/docs/plan_pruebas/sections/12_informe_2026_07_13.tex @@ -0,0 +1,94 @@ +\chapter{Informe de ejecuci\'on --- campa\~na 2026-07-13} +\label{ch:informe-2026-07-13} + +\section{Metadatos} +\begin{table}[ht] +\centering +\begin{tabular}{@{}lp{9cm}@{}} +\toprule +\textbf{Campo} & \textbf{Valor} \\ +\midrule +Fecha & 13 de julio de 2026 \\ +Host & \texttt{ubuntu} (ASUS X441UAK), Ubuntu 24.04.4 LTS \\ +CPU / RAM & Intel i5-7200U; 7{,}6\,GiB (Docker limit $\approx$ 7{,}64\,GiB) \\ +Perfil k6 & \texttt{shared8g} \\ +Gateway & \texttt{http://127.0.0.1:8189} \\ +Herramienta & k6 (Grafana), Docker 29.1.3 \\ +Rama / repo & \texttt{main} --- IPSExtractor \\ +Ejecutor & erik (host staging compartido) \\ +\bottomrule +\end{tabular} +\caption{Metadatos de la campa\~na.} +\end{table} + +\section{Baseline del entorno} +Medici\'on previa a las cargas: + +\begin{itemize} + \item Memoria: total 7{,}6\,GiB; usados $\approx$4{,}4\,GiB; disponibles $\approx$3{,}3\,GiB; \textbf{swap 2{,}1\,GiB en uso}. + \item Contenedores IPS: todos \texttt{Up} / healthy (api, bot-descarga, extraction, workers, count-api, cleaning, db, redis) --- $\sim$17\,h uptime. + \item Vecino dominante: \texttt{mlflow-server} $\approx$1{,}84\,GiB; tambi\'en n8n, MinIO, postgres compartido, etc. + \item \texttt{GET /health/live}: \texttt{ok}, $\approx$11\,ms. + \item \texttt{GET /health}: \texttt{ok} (extraction, cleaning, counting, bot\_descarga); $\approx$194\,ms. +\end{itemize} + +\section{Resultados por caso} +\begin{table}[ht] +\centering +\begin{tabular}{@{}llp{6.2cm}@{}} +\toprule +\textbf{ID} & \textbf{Resultado} & \textbf{Detalle} \\ +\midrule +Smoke health & \textbf{PASS} & Live + agregado OK; servicios healthy \\ +ST-GW-01 & \textbf{PASS} & 6884 req; fail 0\,\%; p95 $=$ 40{,}85\,ms (SLO $<$500\,ms) \\ +ST-BOT-01 (1) & \textbf{PARCIAL} & 100\,\% HTTP 200; p95 $=$ 2{,}28\,s (SLO $<$800\,ms) \textbf{FAIL} latencia; max 23\,s \\ +ST-BOT-01 (2) & \textbf{PASS} & 100\,\% HTTP 200; p95 $=$ 85{,}55\,ms; max 98\,ms \\ +\bottomrule +\end{tabular} +\caption{Resumen de la campa\~na 2026-07-13.} +\label{tab:campana-2026-07-13} +\end{table} + +\subsection{ST-GW-01 --- gateway health} +\begin{itemize} + \item Script: \texttt{k6\_gateway\_health.js}, \texttt{PROFILE=shared8g}. + \item Duraci\'on: $\approx$2\,min; hasta 25 VUs. + \item Checks: 13768/13768 (100\,\%). + \item \texttt{http\_req\_failed}: 0\,\%; p95 $=$ 40{,}85\,ms; avg $=$ 12{,}55\,ms. + \item Thresholds: todos en verde. +\end{itemize} + +\subsection{ST-BOT-01 --- heartbeats (corrida 1)} +\begin{itemize} + \item Script: \texttt{k6\_bot\_heartbeats.js}, 5 VUs, 2\,min, token \texttt{dev-token-change-me}. + \item Checks: 75/75 (100\,\%) --- todos los heartbeats HTTP 200. + \item Latencia: avg 511\,ms; med 57\,ms; p95 2{,}28\,s; max 23{,}12\,s. + \item Threshold \texttt{p(95)<800}: \textbf{cruzado} (k6 exit con error de thresholds). + \item Interpretaci\'on: API correcta; picos atribuibles a contenci\'on del host (swap + MLflow/HDD), no a error de negocio. +\end{itemize} + +\subsection{ST-BOT-01 --- heartbeats (corrida 2, reintento)} +\begin{itemize} + \item Mismo script y perfil, tras aliviar presi\'on del host. + \item Checks: 75/75 (100\,\%). + \item Latencia: avg 57\,ms; med 56\,ms; p95 85{,}55\,ms; max 98\,ms. + \item Thresholds: todos en verde. + \item Conclusi\'on: el FAIL de latencia de la corrida 1 \textbf{no se reproduce} con menos contenci\'on; confirma causa ambiental. +\end{itemize} + +\section{Hallazgos} +\begin{enumerate} + \item IPSExtractor (gateway + bot-descarga heartbeat) cumple SLO funcional y de latencia del perfil \texttt{shared8g} cuando el host no est\'a saturado. + \item Con swap activo ($\sim$2\,GiB) y MLflow $\sim$1{,}8\,GiB, pueden aparecer outliers de varios segundos en endpoints ligeros; deben reportarse como riesgo de \emph{plataforma compartida}, no como regresi\'on de c\'odigo sin evidencia adicional. + \item El health agregado es usable ($\sim$200\,ms en esta sesi\'on); no usarlo como SLO de liveness (usar \texttt{/health/live}). +\end{enumerate} + +\section{No ejecutado en esta campa\~na} +ST-BOT-03 (events), ST-COUNT-01, ST-EXT-*, E2E Imaging, chaos. Motivo: swap/contenci\'on residual; se aplazan a una ventana con $\geq$3\,GiB libres o MLflow/Airflow detenidos. + +\section{Follow-ups} +\begin{itemize} + \item Repetir ST-BOT-01 peri\'odicamente; si p95 $>$800\,ms, adjuntar \texttt{free -h} y \texttt{docker stats}. + \item Ventana dedicada (parar MLflow) antes de ST-COUNT-01. + \item Opcional: excluir requests de \texttt{setup()} del threshold de latencia en k6 (registros de workers) para no mezclar cold-start con steady-state. +\end{itemize} diff --git a/services/services-count-facturas-medicas-serving/internal/domain/types.go b/services/services-count-facturas-medicas-serving/internal/domain/types.go index 3b92ee7..5d3841b 100644 --- a/services/services-count-facturas-medicas-serving/internal/domain/types.go +++ b/services/services-count-facturas-medicas-serving/internal/domain/types.go @@ -14,6 +14,12 @@ type PDFResult struct { Status string `json:"status"` Services []ServiceRow `json:"services,omitempty"` Error string `json:"error,omitempty"` + // Pipeline stage timings in milliseconds (populated by Processor). + RenderMs int64 `json:"render_ms,omitempty"` + OCRMs int64 `json:"ocr_ms,omitempty"` + ClusterMs int64 `json:"cluster_ms,omitempty"` + InferMs int64 `json:"infer_ms,omitempty"` + TotalMs int64 `json:"total_ms,omitempty"` } // CountResponse wraps the results of a synchronous batch request to POST /v1/count. diff --git a/services/services-count-facturas-medicas-serving/internal/pipeline/clustering_test.go b/services/services-count-facturas-medicas-serving/internal/pipeline/clustering_test.go new file mode 100644 index 0000000..c107a94 --- /dev/null +++ b/services/services-count-facturas-medicas-serving/internal/pipeline/clustering_test.go @@ -0,0 +1,61 @@ +package pipeline + +import "testing" + +func TestClusterRowsTwoBands(t *testing.T) { + boxes := []indexedBox{ + {Text: "a", X: 0, Y: 100}, + {Text: "b", X: 10, Y: 102}, + {Text: "c", X: 0, Y: 300}, + {Text: "d", X: 10, Y: 301}, + } + ClusterRows(boxes, 10, 1) + if boxes[0].Cluster < 0 || boxes[1].Cluster < 0 { + t.Fatal("first band should be clustered") + } + if boxes[0].Cluster != boxes[1].Cluster { + t.Fatalf("same band: got %d and %d", boxes[0].Cluster, boxes[1].Cluster) + } + if boxes[2].Cluster != boxes[3].Cluster { + t.Fatalf("second band: got %d and %d", boxes[2].Cluster, boxes[3].Cluster) + } + if boxes[0].Cluster == boxes[2].Cluster { + t.Fatal("distant Y bands should differ") + } +} + +func TestClusterRowsSameBand(t *testing.T) { + boxes := []indexedBox{ + {Text: "a", Y: 50}, + {Text: "b", Y: 52}, + {Text: "c", Y: 54}, + } + ClusterRows(boxes, 10, 1) + id := boxes[0].Cluster + for i, b := range boxes { + if b.Cluster != id { + t.Fatalf("box %d cluster=%d, want %d", i, b.Cluster, id) + } + } +} + +func TestClusterRowsEmpty(t *testing.T) { + ClusterRows(nil, 10, 1) // no panic + ClusterRows([]indexedBox{}, 10, 1) +} + +func TestClusterRowsNoiseMinSamples(t *testing.T) { + boxes := []indexedBox{ + {Text: "lonely", Y: 10}, + {Text: "pair-a", Y: 200}, + {Text: "pair-b", Y: 201}, + } + // minSamples=2: lonely point stays noise (-1); pair forms a cluster + ClusterRows(boxes, 5, 2) + if boxes[0].Cluster != -1 { + t.Fatalf("lonely cluster=%d, want -1 (noise)", boxes[0].Cluster) + } + if boxes[1].Cluster < 0 || boxes[1].Cluster != boxes[2].Cluster { + t.Fatalf("pair clusters=%d,%d", boxes[1].Cluster, boxes[2].Cluster) + } +} diff --git a/services/services-count-facturas-medicas-serving/internal/pipeline/features_extra_test.go b/services/services-count-facturas-medicas-serving/internal/pipeline/features_extra_test.go new file mode 100644 index 0000000..1771750 --- /dev/null +++ b/services/services-count-facturas-medicas-serving/internal/pipeline/features_extra_test.go @@ -0,0 +1,44 @@ +package pipeline + +import "testing" + +func TestExtractRowFeaturesHeaderKeyword(t *testing.T) { + group := []indexedBox{ + {Text: "FACTURA", X: 10, Y: 50, Cluster: 0}, + {Text: "NIT", X: 100, Y: 50, Cluster: 0}, + {Text: "123", X: 200, Y: 50, Cluster: 0}, + } + row := extractRowFeatures(group, 1000, 2000) + if row.Features[3] != 1 { + t.Fatalf("has_header_keyword = %v, want 1", row.Features[3]) + } +} + +func TestExtractRowFeaturesServiceCode(t *testing.T) { + group := []indexedBox{ + {Text: "AB12", X: 10, Y: 100, Cluster: 0}, + {Text: "CONSULTA", X: 80, Y: 100, Cluster: 0}, + } + row := extractRowFeatures(group, 1000, 2000) + if row.Features[1] != 1 { + t.Fatalf("first_token_is_service_code = %v, want 1", row.Features[1]) + } +} + +func TestBuildFeatureRowsEmpty(t *testing.T) { + rows := buildFeatureRows(nil, 1000, 2000) + if len(rows) != 0 { + t.Fatalf("rows = %d, want 0", len(rows)) + } +} + +func TestExtractRowFeaturesNoTablePattern(t *testing.T) { + group := []indexedBox{ + {Text: "TEXTO", X: 10, Y: 100, Cluster: 0}, + {Text: "LIBRE", X: 80, Y: 100, Cluster: 0}, + } + row := extractRowFeatures(group, 1000, 2000) + if row.Features[2] != 0 { + t.Fatalf("has_service_table_pattern = %v, want 0", row.Features[2]) + } +} diff --git a/services/services-count-facturas-medicas-serving/internal/pipeline/processor.go b/services/services-count-facturas-medicas-serving/internal/pipeline/processor.go index 5f13e5d..4d2a57a 100644 --- a/services/services-count-facturas-medicas-serving/internal/pipeline/processor.go +++ b/services/services-count-facturas-medicas-serving/internal/pipeline/processor.go @@ -32,28 +32,31 @@ func NewProcessor(predictor *model.Predictor, ocrEngine *ocr.Engine, opts Option func (p *Processor) ProcessPDF(filename string, pdfData []byte) domain.PDFResult { start := time.Now() result := domain.PDFResult{Filename: filename, Status: domain.StatusOK} + var renderDur, ocrDur, clusterDur, inferDur time.Duration tRender := time.Now() img, pageW, pageH, err := ocr.RenderPage(pdfData, p.opts.DPI) - renderDur := time.Since(tRender) + renderDur = time.Since(tRender) if err != nil { result.Status = domain.StatusError result.Error = err.Error() + p.finishTiming(&result, renderDur, 0, 0, 0, time.Since(start)) return result } tOCR := time.Now() boxes, err := p.ocrEngine.OCRImage(img) - ocrDur := time.Since(tOCR) + ocrDur = time.Since(tOCR) if err != nil { result.Status = domain.StatusError result.Error = err.Error() + p.finishTiming(&result, renderDur, ocrDur, 0, 0, time.Since(start)) return result } if len(boxes) == 0 { result.ServiceCount = 0 result.Services = []domain.ServiceRow{} - p.logTiming(filename, renderDur, ocrDur, 0, 0, time.Since(start)) + p.finishTiming(&result, renderDur, ocrDur, 0, 0, time.Since(start)) return result } @@ -65,11 +68,11 @@ func (p *Processor) ProcessPDF(filename string, pdfData []byte) domain.PDFResult ClusterRows(indexed, p.opts.DBSCANEps, p.opts.DBSCANMinSamples) rows := buildFeatureRows(indexed, pageW, pageH) - clusterDur := time.Since(tCluster) + clusterDur = time.Since(tCluster) if len(rows) == 0 { result.ServiceCount = 0 result.Services = []domain.ServiceRow{} - p.logTiming(filename, renderDur, ocrDur, clusterDur, 0, time.Since(start)) + p.finishTiming(&result, renderDur, ocrDur, clusterDur, 0, time.Since(start)) return result } @@ -84,7 +87,7 @@ func (p *Processor) ProcessPDF(filename string, pdfData []byte) domain.PDFResult } result.Services = services result.ServiceCount = len(rows) - p.logTiming(filename, renderDur, ocrDur, clusterDur, 0, time.Since(start)) + p.finishTiming(&result, renderDur, ocrDur, clusterDur, 0, time.Since(start)) return result } @@ -95,10 +98,11 @@ func (p *Processor) ProcessPDF(filename string, pdfData []byte) domain.PDFResult tInfer := time.Now() probs, err := p.predictor.PredictProba(features) - inferDur := time.Since(tInfer) + inferDur = time.Since(tInfer) if err != nil { result.Status = domain.StatusError result.Error = fmt.Sprintf("inference: %v", err) + p.finishTiming(&result, renderDur, ocrDur, clusterDur, inferDur, time.Since(start)) return result } @@ -128,16 +132,21 @@ func (p *Processor) ProcessPDF(filename string, pdfData []byte) domain.PDFResult result.Services = services result.ServiceCount = serviceCount - p.logTiming(filename, renderDur, ocrDur, clusterDur, inferDur, time.Since(start)) + p.finishTiming(&result, renderDur, ocrDur, clusterDur, inferDur, time.Since(start)) return result } -func (p *Processor) logTiming(filename string, render, ocr, cluster, infer, total time.Duration) { +func (p *Processor) finishTiming(result *domain.PDFResult, render, ocrD, cluster, infer, total time.Duration) { + result.RenderMs = render.Milliseconds() + result.OCRMs = ocrD.Milliseconds() + result.ClusterMs = cluster.Milliseconds() + result.InferMs = infer.Milliseconds() + result.TotalMs = total.Milliseconds() if !p.opts.LogPipelineTiming { return } log.Printf( "pipeline filename=%s ocr_render=%s ocr_tesseract=%s cluster=%s infer=%s total=%s", - filename, render, ocr, cluster, infer, total, + result.Filename, render, ocrD, cluster, infer, total, ) } diff --git a/services/services-count-facturas-medicas-serving/internal/pipeline/processor_timing_test.go b/services/services-count-facturas-medicas-serving/internal/pipeline/processor_timing_test.go new file mode 100644 index 0000000..30bf43a --- /dev/null +++ b/services/services-count-facturas-medicas-serving/internal/pipeline/processor_timing_test.go @@ -0,0 +1,98 @@ +package pipeline + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pys/pdf-count/internal/domain" + "github.com/pys/pdf-count/internal/ocr" +) + +func testPDFDir(t *testing.T) string { + t.Helper() + if d := os.Getenv("COUNT_TEST_PDF_DIR"); d != "" { + return d + } + // services/services-count-facturas-medicas-serving → docs/plan_pruebas/data + candidates := []string{ + filepath.Join("..", "..", "..", "..", "docs", "plan_pruebas", "data"), + filepath.Join("..", "..", "..", "docs", "plan_pruebas", "data"), + } + for _, c := range candidates { + abs, err := filepath.Abs(c) + if err != nil { + continue + } + if st, err := os.Stat(abs); err == nil && st.IsDir() { + return abs + } + } + return "" +} + +func listFixturePDFs(t *testing.T, dir string) []string { + t.Helper() + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("read pdf dir: %v", err) + } + var files []string + for _, e := range entries { + if e.IsDir() { + continue + } + name := e.Name() + if strings.EqualFold(filepath.Ext(name), ".pdf") { + files = append(files, filepath.Join(dir, name)) + } + } + return files +} + +func TestProcessPDFTimingFixtures(t *testing.T) { + dir := testPDFDir(t) + if dir == "" { + t.Skip("PDF fixtures not found; set COUNT_TEST_PDF_DIR") + } + pdfs := listFixturePDFs(t, dir) + if len(pdfs) == 0 { + t.Skip("no PDF files in " + dir) + } + + engine, err := ocr.NewEngine("spa", 1) + if err != nil { + t.Skipf("tesseract not available: %v", err) + } + defer engine.Close() + + proc := NewProcessor(nil, engine, Options{ + DPI: 200, // faster than 300 for timing smoke + DBSCANEps: 10, + DBSCANMinSamples: 1, + LogPipelineTiming: true, + }) + + t.Logf("pdf_dir=%s files=%d", dir, len(pdfs)) + t.Logf("%-28s %8s %8s %8s %8s %8s %6s %s", + "file", "render", "ocr", "cluster", "infer", "total", "count", "status") + + for _, path := range pdfs { + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + name := filepath.Base(path) + res := proc.ProcessPDF(name, data) + t.Logf("%-28s %6dms %6dms %6dms %6dms %6dms %6d %s", + name, res.RenderMs, res.OCRMs, res.ClusterMs, res.InferMs, res.TotalMs, res.ServiceCount, res.Status) + if res.Status == domain.StatusError { + t.Errorf("%s failed: %s", name, res.Error) + continue + } + if res.TotalMs <= 0 { + t.Errorf("%s: TotalMs should be > 0", name) + } + } +}