Saltar a contenido

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:

  1. 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.
  2. 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 respondiera 200 con 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:

{"checks":{"mongo":"ok","postgres":"ok","redis":"ok"},"status":"ok"}

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:

{"data":{"accepted":1,"rejected":0}}

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:

historian (PLC simulado) → InfluxDB → Edge → outbox → Cloud Run → API de métricas

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