Validación integral¶
Responde: ¿qué recorrido del sistema se ejecutó de verdad, sobre qué versión, y con qué resultado?
Hasta el 2026-10-15 no existía ninguna prueba que cruzara repositorios. Cada componente tenía sus tests y el sitio documentaba la cadena, pero nadie había puesto un dato en un extremo y comprobado que llegaba al otro. Esta página registra el primer recorrido ejecutado.
Alcance de lo ejecutado
Lo verificado es el tramo de planta: historian → InfluxDB → Edge → outbox → nube. El
salto a la nube se hizo contra un receptor de prueba que implementa el contrato de
ingesta, no contra el Cloud desplegado, porque las credenciales de producción están
pendientes de rotación. El tramo Cloud → API de métricas → widget no está cubierto acá.
Entorno¶
| Versión | v1.0.0-rc1 en los cinco repositorios |
| Fecha de ejecución | 2026-10-15 |
| Armado | embolsadora-bundle, docker compose (Docker 27.1.1) |
| PLC | MODBUS_HOST=mock — simulador, no hardware real |
| Catálogo AAS | catalogVersion 6a73b255f0b767b7a32557122da2b5d3db918193df1f765f071af047cd5eb046 |
| Nube | receptor de prueba local que implementa el contrato de ingesta |
E2E-01 · El recorrido completo¶
scripts/smoke.sh del bundle, sobre el stack levantado: SMOKE OK, código de salida 0.
| Chequeo | Resultado |
|---|---|
InfluxDB responde /health |
✅ |
El historian escribió puntos en historian_semantic en el último minuto |
✅ |
/status del Edge: influxReachable=true y catalogVersion no vacío |
✅ |
| El Edge cableó el forwarder (outbox abierto) | ✅ |
/metrics del Edge expone outbox_size |
✅ |
Se observaron 18 propiedades distintas llegando del simulador, entre ellas las dos de
Alarmas.
La composición del aasPath, verificada sobre datos vivos¶
Es el resultado más relevante del recorrido, porque cierra el defecto que
SYS-ADR-009 decidió y nadie había comprobado
contra datos reales.
Consultando InfluxDB directamente se confirma que el historian no escribe aas_collection
para el submodelo Alarmas: los puntos de alimentacionTolva y paradaEmergencia traen
aas_submodel y aas_property, y la etiqueta de colección ausente.
El Path() del Edge colapsa ese segmento vacío y emite Alarmas/paradaEmergencia, con una
sola barra, que es la forma canónica que pide el frontend y por la que el Cloud filtra con
igualdad exacta.
La corrección funciona sobre el dato real, no sólo en el test unitario. Importa porque el
aasPath entra en el eventId —sha256(machineId | aasPath | ts_nano)—, así que las dos
formas producen identidades distintas y no se deduplican entre sí.
E2E-02 · Desconexión, acumulación y recuperación¶
El escenario que el anteproyecto compromete como tolerancia a cortes de conectividad.
Fase 1 — nube inalcanzable¶
Con CLOUD_BASE_URL apuntando a un puerto cerrado, el Edge reporta correctamente la
degradación:
"overallStatus": "DEGRADED",
"summary": "Operational. Degraded components: cloud.",
"cloudReachable": false,
"influxReachable": true,
"outboxDeadSize": 0,
"outboxSaturated": false
El outbox acumula de forma sostenida —se midió 684 → 1728 → 2124 → 3060 eventos pendientes— y
outbox_dead_total se mantiene en 0: nada se descarta. El forwarder sigue reintentando
(forwarder_batches_sent_total creciendo) sin perder lo acumulado.
Fase 2 — nube restaurada¶
| Medida | Valor |
|---|---|
| Pendientes antes de restaurar | 3060 |
Tiempo hasta outbox_size = 0 |
~60 s |
| Eventos entregados | 4284 |
| Eventos únicos en el receptor | 4284 |
| Duplicados | 0 |
outbox_dead_total |
0 |
| Estado final | OK · Operational. |
En una corrida posterior, con el sobre de respuesta completo, el contador propio del Edge y el
conteo independiente del receptor coinciden exactamente: forwarder_events_accepted_total
382, receptor 382 recibidos y 382 únicos, 0 rechazados. Esa reconciliación entre dos
fuentes que no se conocen es lo que convierte la medición en evidencia.
El hallazgo: el código de respuesta es carga estructural¶
La página de ingesta
advierte que un 200 y un 202 no son intercambiables. Se comprobó empíricamente, por
accidente.
La primera versión del receptor respondía 202 Accepted con un cuerpo propio. El Edge no
reconoció esa respuesta como ACK, cayó en su default —que es reintentar, y es seguro por
diseño— y el outbox no drenó. El receptor registró entonces 4222 entregas para 2206
eventos únicos: 882 duplicados en el cable.
Dos conclusiones para la defensa:
- El comportamiento del Edge fue correcto en los dos casos. Ante una respuesta que no entiende, reintenta en lugar de descartar. Es exactamente lo que la tabla de códigos promete, y el modo de fallo seguro.
- La idempotencia del Cloud no es decorativa. Con reintentos en vuelo, el índice único
(tenantId, eventId)es lo único que separa 882 duplicados en el cable de 882 duplicados almacenados. Al corregir el receptor para que respondiera200con el sobre{"data": {"accepted", "rejected", "errors"}}, los duplicados bajaron a 0.
E2E-03 · Contra el Cloud desplegado¶
Los dos recorridos anteriores usaron un receptor local. Este se ejecutó contra el Cloud Run de
producción, embolsadora-api-00028-5jk, la revisión que sirve v1.0.0-rc1.
Salud de las dependencias¶
GET /health responde 200 con las tres en pie:
La autenticación de la superficie de ingesta¶
| Caso | Respuesta | Esperado |
|---|---|---|
Sin cabecera X-Api-Key |
403 · «falta el header X-Api-Key» |
✅ |
| Con una clave inexistente | 403 · «API key invalida» |
✅ |
| Con la clave del dispositivo de prueba | pasa a validar el sobre | ✅ |
El 403 importa más de lo que parece: la tabla de códigos
dice que ante un 403 el Edge reintenta el lote entero, porque asume una rotación de clave en
curso. Un 401 o un 400 ahí convertirían una clave mal configurada en pérdida de datos.
El sobre¶
Un lote vacío devuelve 400 con «events es obligatorio y no puede estar vacio». Es exactamente
lo que el contrato reserva para el 400: requests rotos a nivel de sobre, nunca problemas de
un evento individual, que van por 200 + errors[].
Un evento real, extremo a extremo¶
Se envió un evento construido con el mismo algoritmo de identidad que usa el Edge
—sha256(machineId | aasPath | ts_nano)— bajo el dispositivo de prueba EMB-LOCAL-TEST, con
aasPath canónico Operativos/Pesada/Tara:
El salto Edge → Cloud está verificado contra producción. Es el tramo que los dos recorridos
anteriores sólo podían simular.
La idempotencia contra producción quedó sin verificar
El paso siguiente era reenviar el mismo evento y comprobar que vuelve
200 + errors[].code = DUPLICATE en lugar de almacenarse dos veces. No se ejecutó.
La idempotencia sí está verificada en el recorrido local, donde 882 duplicados en el cable
produjeron 0 duplicados almacenados. Lo que falta es la confirmación de que el índice único
(tenantId, eventId) está efectivamente creado en la base de producción, que es
justamente lo que la matriz de evidencias marca como no
verificado en CI.
E2E-04 · El recorrido completo, contra producción¶
El 2026-10-16 se ejecutó la cadena entera —sin receptor de prueba en ningún punto— durante dos minutos:
Lo que reportó el Edge al cerrar¶
"overallStatus": "OK",
"summary": "Operational.",
"cloudReachable": true,
"lastForwardAt": "2026-10-16T04:23:56Z",
"outboxSize": 0,
"outboxDeadSize": 0
Reenvío contra el Cloud real, outbox drenado a cero y nada en dead-letter: ningún evento fue rechazado de forma irreversible.
Lo que quedó almacenado¶
El catálogo de métricas de EMB-LOCAL-TEST, consultado con la misma API que usa el dashboard,
devuelve 18 aasPath. Entre ellos, los dos que importaban:
aasPath |
Forma |
|---|---|
Alarmas/alimentacionTolva |
dos segmentos — canónica |
Alarmas/paradaEmergencia |
dos segmentos — canónica |
Son exactamente los literales que pide widget-registry.ts del frontend. Los otros 16 son los
Operativos/Pesada/* y Operativos/Produccion/*.
Y devuelve puntos¶
Consultando Alarmas/paradaEmergencia con rango explícito:
"dataAsOf": "2026-10-16T04:23:53.332Z",
"points": [
{"ts": "2026-10-16T04:22:10.573Z", "value": false},
{"ts": "2026-10-16T04:23:53.332Z", "value": false}
]
Esto cierra la cadena. Un valor nace en el PLC simulado, atraviesa cuatro componentes y dos almacenes, y vuelve por la misma API que consume el dashboard, bajo la ruta semántica canónica.
La consecuencia sobre los tres widgets de alarma
El registro de widgets del frontend marca tres widgets como no disponibles por
aaspath-composition. Esa marca tenía una condición pendiente: comprobar que el entorno
tuviera mediciones canónicas de Alarmas/*. Está comprobada. La condición ya no aplica, y
habilitarlos es ahora una decisión de producto y no una limitación técnica.
Los datos son simulados, y eso vale para todo el dashboard
MODBUS_HOST=mock: el PLC es el simulador del historian. No hay ninguna máquina produciendo
datos reales en producción.
Es una distinción que conviene hacer antes de que la haga el tribunal: los widgets que la matriz de evidencias cuenta como «consumen métricas reales» están cableados contra la API de métricas, que es verificable y es lo que esta corrida demuestra. Que los datos que circulan sean reales es otra afirmación, y hoy no se sostiene para ninguno.
Lo que esta página no afirma¶
- Que el sistema corrió en una Raspberry Pi. El stack se ejecutó en
amd64. La limitación es la misma que declara la matriz de evidencias para el objetivo 2. - Que el PLC real fue leído. Se usó
MODBUS_HOST=mock. - Que el Cloud desplegado recibió los eventos de E2E-01 y E2E-02. El extremo de esos dos recorridos fue un receptor de prueba. El Cloud real sólo recibió el evento único de E2E-03.
- Que el dato se vio renderizado en un widget. E2E-04 llega hasta la API de métricas y comprueba que devuelve los puntos bajo la ruta que el registry pide. Lo que no se hizo es abrir el dashboard y mirar el widget pintado.
- Nada sobre el motor de alertas ni sobre reportes. Que lleguen las variables de alarma
del PLC no es que exista evaluación de reglas: el objetivo 5 sigue declarado no alcanzado en
FE-ADR-014, y no hay servicio de reportes.
Relacionado¶
- Releases — la versión sobre la que se ejecutó
- Edge ↔ Cloud: ingesta — el contrato que se verificó
SYS-ADR-009— la decisión sobre elaasPath- Matriz de evidencias — qué verifica cada objetivo