HOMELAB-2682: fix(sbx-gits): emu-proof rejects an OK line with zero tests and requires --task #874

Merged
pi-landing merged 2 commits from plane/HOMELAB-2682-ok0 into live 2026-11-06 19:13:45 +00:00
Collaborator

Qué

Dos defectos de tools/gits-sbx-emu-proof.sh (el ledger de la corrida del emulador), un solo
cambio:

  1. CERO PRUEBAS NO ES UN OK. run escribía una línea con result=OK y tests_run=0.
    Evidencia viva: la línea 38 del ledger del pod (head 8c1d507c) es una corrida con
    result:"OK", rc:0, tests_run:0 y el sha256 del XML vacío (e3b0c442…): cero casos
    ejecutados, y leída sola dice OK. El head pasó por su línea de smoke, pero esa línea no
    prueba nada.

    • run rechaza escribir la línea si termina rc=0 con tests_run=0: rc 2, ninguna
      línea nueva
      , la misma forma de rechazo que el pre-chequeo del ledger mal formado. La
      captura de la corrida se conserva y el mensaje dice dónde quedó (es lo que explica por
      qué no corrió nada). Una corrida en rojo con cero casos sí se apunta: un fallo es
      información honesta.
    • check etiqueta cualquier línea histórica con tests_run=0 — pase o no el head — como
      tests_run=0 — NO se ejecutaron pruebas (no tests executed — evidence only), y esa línea
      nunca completa la compuerta (tests_run > 0 sigue siendo condición). Antes la línea
      desaparecía del informe cuando el head pasaba por otra de sus líneas.
  2. LA TAREA VA DESPUÉS DEL MÓDULO. Con --module app y sin --task, el default era
    :app:connectedDebugAndroidTest, que ya lleva el módulo dentro; la suite compone
    <módulo>:<tarea>, así que Gradle recibía app::app:connectedDebugAndroidTest (doble
    prefijo, lo rechaza) tras gastar el emulador.

    • --task es ahora obligatoria; sin ella, rc 2, nada escrito, y el mensaje nombra las
      formas válidas (connectedDevDebugAndroidTest / connectedStableDebugAndroidTest para un
      módulo con flavors; connectedDebugAndroidTest sin flavors). Un default único tampoco
      puede ser correcto: en :app connectedDebugAndroidTest es ambiguo.
    • --task ':app:…' o con :: → rechazo por construcción (no puede ser la mitad derecha).
    • --task app:connectedDebugAndroidTest con --module app → AVISO con la tarea exacta
      que Gradle recibiría.
    • Documentado en --help y en docs/gits-sbx/run-proof.md (regla de invocación + cero
      pruebas).

Ronda 2 — el bloqueo del asiento 1 (OK-path-label-unreachable) y C2

Commit ea6993cb. El asiento 1 tenía razón y su mecanismo era exacto.

El bloqueo: la etiqueta de la línea de cero casos no se alcanzaba en el camino OK

El bucle de candidatas de cmd_check hace break en la primera línea canónica limpia
(ok_line). La etiqueta de tests_run=0 se acumulaba dentro de ese bucle, así que una línea
de cero casos que venga después de la línea del smoke que pasa no se visitaba nunca y
desaparecía del informe: solo la nombraba el bloque «otras corridas de este head», que imprime
result=OK y nada de cero casos. Y esa es precisamente la forma del ledger vivo —la línea 38
va después del smoke de su head—, es decir, el caso que este PR existe para nombrar.

Reproducido con la herramienta del head ee994e19 (dos suites de mentira, repo de mentira con el
smoke canónico commiteado en su ruta, cadena bien calculada):

A  [línea que PASA, luego línea de cero casos]  -> rc 0, etiqueta AUSENTE   <-- el bloqueo
   PROOF: OK head=6eeb0856… result=OK … linea=1
   PROOF  aviso el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0 …
          (ni una palabra de cero casos: se lee como una SEGUNDA corrida buena)
B  [línea de cero casos, luego la que PASA]     -> rc 0, etiqueta presente  (ya funcionaba)
C  [solo la línea de cero casos]                -> rc 1, etiqueta presente  (camino de fallo)

Arreglo: la etiqueta se acumula en un recorrido propio, de solo lectura, sobre TODAS las
líneas de este head, ANTES del bucle de la compuerta
(y se quita de dentro del bucle). El
veredicto no cambia: una línea limpia y canónica sigue pasando el head, y una de cero casos
sigue sin completar nunca la compuerta (tests_run > 0); la etiqueta es informe.

Después del arreglo, las tres formas dicen lo mismo y en el mismo sitio:

A  [pasa, luego cero]  -> rc 0, etiqueta presente
B  [cero, luego pasa]  -> rc 0, etiqueta presente
C  [solo cero]         -> rc 1, etiqueta presente
   PROOF  evidencia (no suficiente): linea 2 (head 0690775e…): tests_run=0 — NO se ejecutaron
          pruebas (no tests executed — evidence only): la linea lee result=OK rc=0 pero no hay
          ni un caso en los XML, asi que NO prueba el head

C2 (arreglado): el mensaje de cero casos decía una cosa falsa en la mitad de los casos

Decía siempre «no hay XML posterior al arranque», y eso es falso cuando la suite sí dejó
un XML con cero casos (<testsuite tests="0">, o un XML que no trae ni un <testcase>).
zero_tests_why usa el XML_COUNT de la corrida y ahora nombra cuál de las dos pasó:

XML presente, cero casos : tests_run=0 (SI hay XML posterior al arranque en … (1 fichero(s)) pero
                           NINGUNO trae un <testcase>: cero casos en los XML …)
sin XML ninguno          : tests_run=0 (no hay ni un XML posterior al arranque en … (la suite no
                           dejo resultados))

Las dos siguen rechazándose igual (rc 2, sin línea): son causas distintas y el mensaje las
distingue.

Repro (verbatim, contra origin/live ANTES del cambio)

Defecto 1 — la suite sale rc=0 sin dejar XML:

PROOF  head=57a6102… result=OK rc=0 via=run tests_run=0 tests_failed=0 xml=0
PROOF-LINE {"head":"57a6102…","result":"OK","rc":0,…,"tests_run":0,…,"xml_sha256":"e3b0c44298fc…"}
PROOF  AVISO tests_run=0 (…): check <head> lo rechaza
rc=0            <-- y la línea QUEDA ESCRITA

Defecto 2 — --module app sin --task, con la composición REAL del smoke (connected_task):

PROOF  run: …/suite.sh HOMELAB-2682-ok0 app  (head=564688d… task=:app:connectedDebugAndroidTest)
smoke compondria: ./gradlew app::app:connectedDebugAndroidTest      <-- doble prefijo
--- control, la forma documentada: --module app --task connectedDevDebugAndroidTest ---
PROOF  run: … task=connectedDevDebugAndroidTest
smoke compondria: ./gradlew app:connectedDebugAndroidTest        <-- válida

Ledger vivo (solo lectura, kubectl exec … sed -n 38p):

{"head":"8c1d507c…","result":"OK","rc":0,…,"tests_run":0,"xml_sha256":"e3b0c44298fc…", …}

Cambios, fichero a fichero

Fichero Qué
core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh paso 7b de run: rechazo rc=0 && tests_run==0 (rc 2, sin línea, captura conservada) con el motivo real (zero_tests_why: sin XML vs XML sin casos); --task obligatoria + rechazo por construcción + AVISO del prefijo del módulo; check: la etiqueta tests_run=0 se acumula en un recorrido propio sobre todas las líneas del head, antes del bucle de la compuerta (ronda 2) y se imprime en el OK y en el informe de fallo; --help actualizado
docs/gits-sbx/run-proof.md apartados «--task: la tarea va DESPUÉS del módulo» y «Cero pruebas no es un OK»
scripts/gits-sbx-epic-test.sh SBX_GITS_DIFF_SET re-medido contra origin/live 960410f5 = M tools/gits-sbx-emu-proof.sh; fixtures B3 25(f6)/(f7) actualizadas (su corrida ya no puede ser la suite de cero bytes: deja XML); casos nuevos que muerden: (f) run rechaza un OK sin XML, (f2) una línea tests_run=0 construida a mano con la cadena bien calculada (la forma de la línea 38) se rechaza y se etiqueta —camino de fallo—, (f2b) la misma línea DESPUÉS de una línea que PASA: el head pasa (rc=0) y la etiqueta tiene que estar igual (ronda 2; muerde con el bloqueo, verde con el arreglo), (f3) run --module app sin --task se rechaza nombrando las formas válidas

Nada más se toca: statefulset.yaml, README.md, kustomization.yaml y las otras cinco
herramientas quedan como están (T7[diff] clava ese conjunto y muerde si aparece un fichero más).

Tests

Corridas en solitario (secuenciales, ningún árbol a la vez: dos a la vez colisionan en rutas
de /tmp), cada una desde un worktree limpio:

corrida head resultado checks
baseline origin/live 960410f5 960410f5 FAIL 10 690
esta rama, antes del arreglo ee994e19 FAIL 12 694 — incluye 22[R176b] (f2b) BLOQUEO (la prueba nueva muerde)
esta rama, con el arreglo ea6993cb FAIL 9 694

El conjunto de rojos de la rama es subconjunto del de la baseline: los 9 rojos de
ea6993cb están todos en la baseline, y la baseline trae uno de más (18[R165] --lock-selftest,
un selftest de lock en /tmp que depende del entorno). Cero rojos nuevos.

Los dos rojos de más que aparecen en la corrida PRE-FIX (16[R161] fixture D (timeout) y
16[R161] mutacion (ii)) son el test de stress/tiempos del lock bajo carga: no aparecen ni en la
baseline ni en la corrida con el arreglo, y no tocan nada de este cambio.

Casos de la sección 22 que importan aquí, en la corrida con el arreglo:

  • 22[R176b] (f) PASS — run rechaza un OK sin XML (rc 2, cero líneas, captura conservada);
  • 22[R176b] (f2) PASS — la línea de cero casos sola no abre la compuerta y se etiqueta;
  • 22[R176b] (f2b) PASS — [línea que PASA, línea de cero casos DESPUÉS]: el head pasa (rc=0)
    y la etiqueta está; con el head ee994e19 este caso imprime
    BLOQUEO: … el head pasa (rc=0) pero la linea de cero casos NO se etiqueta (prueba primero,
    arreglo después);
  • 22[R176b] (f3) PASS — run --module app sin --task se rechaza;
  • T7[diff] PASS — el diff de sbx-gits contra origin/live es exactamente
    M tools/gits-sbx-emu-proof.sh, y su mutación (quitarlo del conjunto permitido) sigue en ROJO.

bash -n limpio en la herramienta y en el arnés; shellcheck sin avisos nuevos sobre el diff
(el fichero de la herramienta sale limpio y el arnés no suma ningún aviso en las líneas nuevas).

Los rojos preexistentes son de la baseline (pines de memoria/imagen del manifiesto y del seed en
este checkout), no de este cambio.

Seguimiento nombrado (C1 del asiento 1) — NO va en este PR

Ejemplos de invocación sin --task que quedan obsoletos desde que --task es obligatoria, y
que este PR no toca a propósito (el conjunto de diff clavado por T7[diff] no los admite; se
arreglan en su propio cambio):

  • core/manifests/apps/sbx-gits/README.md §10.4, líneas 756 y 762
    (run --epic <epica> --tree … sin --task);
  • docs/gits-sbx/t7-emu-smoke.md, líneas 795 y 836 (ídem);
  • la cabecera de tools/gits-sbx-emu-proof.sh (línea 19: el ejemplo record --epic X --rc 0 --result OK …, que ya no existe) y el texto del die de record («la unica forma … es
    run --epic <E> --tree <checkout>», sin --task).

Despliegue: este PR NO cambia nada en el pod (dicho claro)

El ConfigMap de herramientas lleva el pin de bloque literal (#870): sus claves son una copia
literal de los ficheros, así que editar tools/gits-sbx-emu-proof.sh en el repo no llega al
pod por auto-sync. Hasta que la bump de pin (#872) — que va por su propia compuerta — no se
mergee, este cambio tiene cero efecto visible en sbx-gits-0: /run/sbx-tools/gits-sbx-emu-proof.sh
sigue siendo el de antes, y el check de la compuerta se comporta exactamente igual.

No se ha desplegado nada, no se ha tocado el ledger vivo (solo lectura) ni el volumen de trabajo.

Lo que no he podido verificar

  • La corrida real en el emulador: no hay emulador en el mgmt VM. Los dos rechazos y la
    etiqueta están probados con repos y suites de mentira (como el resto de la sección 22 del
    arnés) y con la composición real de connected_task del hermano smoke, no contra Gradle.
  • Que check en el pod sobre el ledger vivo (38 líneas) imprima la etiqueta nueva: hace falta el
    pin-bump (#872) para que el pod tenga este fichero. La lógica de la etiqueta está probada
    offline en 22[R176b] (f2) (camino de fallo) y 22[R176b] (f2b) (camino OK, la forma del
    ledger vivo).
## Qué Dos defectos de `tools/gits-sbx-emu-proof.sh` (el ledger de la corrida del emulador), un solo cambio: 1. **CERO PRUEBAS NO ES UN OK.** `run` escribía una línea con `result=OK` y `tests_run=0`. Evidencia viva: la **línea 38** del ledger del pod (**head `8c1d507c`**) es una corrida con `result:"OK"`, `rc:0`, `tests_run:0` y el sha256 del XML **vacío** (`e3b0c442…`): cero casos ejecutados, y leída sola dice OK. El head pasó por **su** línea de smoke, pero esa línea no prueba nada. * `run` **rechaza escribir la línea** si termina `rc=0` con `tests_run=0`: **rc 2, ninguna línea nueva**, la misma forma de rechazo que el pre-chequeo del ledger mal formado. La captura de la corrida **se conserva** y el mensaje dice dónde quedó (es lo que explica por qué no corrió nada). Una corrida **en rojo** con cero casos **sí** se apunta: un fallo es información honesta. * `check` etiqueta **cualquier** línea histórica con `tests_run=0` — pase o no el head — como `tests_run=0 — NO se ejecutaron pruebas (no tests executed — evidence only)`, y esa línea **nunca** completa la compuerta (`tests_run > 0` sigue siendo condición). Antes la línea desaparecía del informe cuando el head pasaba por otra de sus líneas. 2. **LA TAREA VA DESPUÉS DEL MÓDULO.** Con `--module app` y **sin** `--task`, el default era `:app:connectedDebugAndroidTest`, que ya lleva el módulo dentro; la suite compone `<módulo>:<tarea>`, así que Gradle recibía `app::app:connectedDebugAndroidTest` (doble prefijo, lo rechaza) tras gastar el emulador. * `--task` es ahora **obligatoria**; sin ella, **rc 2, nada escrito**, y el mensaje nombra las formas válidas (`connectedDevDebugAndroidTest` / `connectedStableDebugAndroidTest` para un módulo con flavors; `connectedDebugAndroidTest` sin flavors). Un default único tampoco puede ser correcto: en `:app` `connectedDebugAndroidTest` es **ambiguo**. * `--task ':app:…'` o con `::` → rechazo por construcción (no puede ser la mitad derecha). * `--task app:connectedDebugAndroidTest` con `--module app` → **AVISO** con la tarea exacta que Gradle recibiría. * Documentado en `--help` y en `docs/gits-sbx/run-proof.md` (regla de invocación + cero pruebas). ## Ronda 2 — el bloqueo del asiento 1 (`OK-path-label-unreachable`) y C2 **Commit `ea6993cb`.** El asiento 1 tenía razón y su mecanismo era exacto. ### El bloqueo: la etiqueta de la línea de cero casos no se alcanzaba en el camino OK El bucle de candidatas de `cmd_check` hace `break` en la **primera línea canónica limpia** (`ok_line`). La etiqueta de `tests_run=0` se acumulaba **dentro** de ese bucle, así que una línea de cero casos que venga **después** de la línea del smoke que pasa **no se visitaba nunca** y desaparecía del informe: solo la nombraba el bloque «otras corridas de este head», que imprime `result=OK` y **nada** de cero casos. Y esa es precisamente la forma del ledger vivo —la línea 38 va **después** del smoke de su head—, es decir, el caso que este PR existe para nombrar. Reproducido con la herramienta del head `ee994e19` (dos suites de mentira, repo de mentira con el smoke canónico commiteado en su ruta, cadena **bien calculada**): ``` A [línea que PASA, luego línea de cero casos] -> rc 0, etiqueta AUSENTE <-- el bloqueo PROOF: OK head=6eeb0856… result=OK … linea=1 PROOF aviso el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0 … (ni una palabra de cero casos: se lee como una SEGUNDA corrida buena) B [línea de cero casos, luego la que PASA] -> rc 0, etiqueta presente (ya funcionaba) C [solo la línea de cero casos] -> rc 1, etiqueta presente (camino de fallo) ``` **Arreglo:** la etiqueta se acumula en un **recorrido propio, de solo lectura, sobre TODAS las líneas de este head, ANTES del bucle de la compuerta** (y se quita de dentro del bucle). El veredicto **no cambia**: una línea limpia y canónica sigue pasando el head, y una de cero casos sigue sin completar nunca la compuerta (`tests_run > 0`); la etiqueta es **informe**. Después del arreglo, las tres formas dicen lo mismo y en el mismo sitio: ``` A [pasa, luego cero] -> rc 0, etiqueta presente B [cero, luego pasa] -> rc 0, etiqueta presente C [solo cero] -> rc 1, etiqueta presente PROOF evidencia (no suficiente): linea 2 (head 0690775e…): tests_run=0 — NO se ejecutaron pruebas (no tests executed — evidence only): la linea lee result=OK rc=0 pero no hay ni un caso en los XML, asi que NO prueba el head ``` ### C2 (arreglado): el mensaje de cero casos decía una cosa falsa en la mitad de los casos Decía siempre «no hay XML posterior al arranque», y eso es **falso** cuando la suite **sí** dejó un XML con cero casos (`<testsuite tests="0">`, o un XML que no trae ni un `<testcase>`). `zero_tests_why` usa el `XML_COUNT` de la corrida y ahora nombra cuál de las dos pasó: ``` XML presente, cero casos : tests_run=0 (SI hay XML posterior al arranque en … (1 fichero(s)) pero NINGUNO trae un <testcase>: cero casos en los XML …) sin XML ninguno : tests_run=0 (no hay ni un XML posterior al arranque en … (la suite no dejo resultados)) ``` Las dos siguen rechazándose igual (rc 2, sin línea): son causas distintas y el mensaje las distingue. ## Repro (verbatim, contra `origin/live` ANTES del cambio) Defecto 1 — la suite sale rc=0 sin dejar XML: ``` PROOF head=57a6102… result=OK rc=0 via=run tests_run=0 tests_failed=0 xml=0 PROOF-LINE {"head":"57a6102…","result":"OK","rc":0,…,"tests_run":0,…,"xml_sha256":"e3b0c44298fc…"} PROOF AVISO tests_run=0 (…): check <head> lo rechaza rc=0 <-- y la línea QUEDA ESCRITA ``` Defecto 2 — `--module app` sin `--task`, con la composición REAL del smoke (`connected_task`): ``` PROOF run: …/suite.sh HOMELAB-2682-ok0 app (head=564688d… task=:app:connectedDebugAndroidTest) smoke compondria: ./gradlew app::app:connectedDebugAndroidTest <-- doble prefijo --- control, la forma documentada: --module app --task connectedDevDebugAndroidTest --- PROOF run: … task=connectedDevDebugAndroidTest smoke compondria: ./gradlew app:connectedDebugAndroidTest <-- válida ``` Ledger vivo (solo lectura, `kubectl exec … sed -n 38p`): ``` {"head":"8c1d507c…","result":"OK","rc":0,…,"tests_run":0,"xml_sha256":"e3b0c44298fc…", …} ``` ## Cambios, fichero a fichero | Fichero | Qué | |---|---| | `core/manifests/apps/sbx-gits/tools/gits-sbx-emu-proof.sh` | paso 7b de `run`: rechazo `rc=0 && tests_run==0` (rc 2, sin línea, captura conservada) con el motivo real (`zero_tests_why`: sin XML vs XML sin casos); `--task` obligatoria + rechazo por construcción + AVISO del prefijo del módulo; `check`: la etiqueta `tests_run=0` se acumula en un **recorrido propio sobre todas las líneas del head, antes del bucle de la compuerta** (ronda 2) y se imprime en el OK y en el informe de fallo; `--help` actualizado | | `docs/gits-sbx/run-proof.md` | apartados «`--task`: la tarea va DESPUÉS del módulo» y «Cero pruebas no es un OK» | | `scripts/gits-sbx-epic-test.sh` | `SBX_GITS_DIFF_SET` **re-medido** contra `origin/live` 960410f5 = `M tools/gits-sbx-emu-proof.sh`; fixtures B3 25(f6)/(f7) actualizadas (su corrida ya no puede ser la suite de cero bytes: deja XML); casos nuevos que muerden: (f) `run` rechaza un OK sin XML, (f2) una línea `tests_run=0` **construida a mano con la cadena bien calculada** (la forma de la línea 38) se rechaza **y se etiqueta** —camino de fallo—, **(f2b) la misma línea DESPUÉS de una línea que PASA: el head pasa (rc=0) y la etiqueta tiene que estar igual** (ronda 2; muerde con el bloqueo, verde con el arreglo), (f3) `run --module app` sin `--task` se rechaza nombrando las formas válidas | Nada más se toca: `statefulset.yaml`, `README.md`, `kustomization.yaml` y las otras cinco herramientas quedan como están (`T7[diff]` clava ese conjunto y muerde si aparece un fichero más). ## Tests Corridas **en solitario** (secuenciales, ningún árbol a la vez: dos a la vez colisionan en rutas de `/tmp`), cada una desde un worktree limpio: | corrida | head | resultado | checks | |---|---|---|---| | baseline `origin/live` `960410f5` | `960410f5` | FAIL **10** | 690 | | esta rama, **antes** del arreglo | `ee994e19` | FAIL **12** | 694 — incluye `22[R176b] (f2b)` **BLOQUEO** (la prueba nueva muerde) | | esta rama, **con** el arreglo | `ea6993cb` | FAIL **9** | 694 | El conjunto de rojos de la rama es **subconjunto** del de la baseline: los 9 rojos de `ea6993cb` están todos en la baseline, y la baseline trae uno de más (`18[R165] --lock-selftest`, un selftest de lock en `/tmp` que depende del entorno). Cero rojos nuevos. Los dos rojos de más que aparecen en la corrida PRE-FIX (`16[R161] fixture D (timeout)` y `16[R161] mutacion (ii)`) son el test de stress/tiempos del lock bajo carga: no aparecen ni en la baseline ni en la corrida con el arreglo, y no tocan nada de este cambio. Casos de la sección 22 que importan aquí, en la corrida con el arreglo: * `22[R176b] (f)` PASS — `run` rechaza un OK sin XML (rc 2, cero líneas, captura conservada); * `22[R176b] (f2)` PASS — la línea de cero casos **sola** no abre la compuerta y se etiqueta; * `22[R176b] (f2b)` PASS — **[línea que PASA, línea de cero casos DESPUÉS]**: el head pasa (rc=0) **y** la etiqueta está; con el head `ee994e19` este caso imprime `BLOQUEO: … el head pasa (rc=0) pero la linea de cero casos NO se etiqueta` (prueba primero, arreglo después); * `22[R176b] (f3)` PASS — `run --module app` sin `--task` se rechaza; * `T7[diff]` PASS — el diff de `sbx-gits` contra `origin/live` es exactamente `M tools/gits-sbx-emu-proof.sh`, y su mutación (quitarlo del conjunto permitido) sigue en ROJO. `bash -n` limpio en la herramienta y en el arnés; `shellcheck` sin avisos nuevos sobre el diff (el fichero de la herramienta sale limpio y el arnés no suma ningún aviso en las líneas nuevas). Los rojos preexistentes son de la baseline (pines de memoria/imagen del manifiesto y del seed en este checkout), no de este cambio. ## Seguimiento nombrado (C1 del asiento 1) — NO va en este PR Ejemplos de invocación **sin `--task`** que quedan obsoletos desde que `--task` es obligatoria, y que este PR **no** toca a propósito (el conjunto de diff clavado por `T7[diff]` no los admite; se arreglan en su propio cambio): * `core/manifests/apps/sbx-gits/README.md` §10.4, líneas 756 y 762 (`run --epic <epica> --tree …` sin `--task`); * `docs/gits-sbx/t7-emu-smoke.md`, líneas 795 y 836 (ídem); * la cabecera de `tools/gits-sbx-emu-proof.sh` (línea 19: el ejemplo `record --epic X --rc 0 --result OK …`, que ya no existe) y el texto del `die` de `record` («la unica forma … es `run --epic <E> --tree <checkout>`», sin `--task`). ## Despliegue: este PR NO cambia nada en el pod (dicho claro) El ConfigMap de herramientas lleva el **pin de bloque literal** (#870): sus claves son una copia literal de los ficheros, así que editar `tools/gits-sbx-emu-proof.sh` en el repo **no** llega al pod por auto-sync. Hasta que la **bump de pin (#872)** — que va por su propia compuerta — no se mergee, este cambio tiene **cero efecto visible** en `sbx-gits-0`: `/run/sbx-tools/gits-sbx-emu-proof.sh` sigue siendo el de antes, y el `check` de la compuerta se comporta exactamente igual. No se ha desplegado nada, no se ha tocado el ledger vivo (solo lectura) ni el volumen de trabajo. ## Lo que no he podido verificar * La corrida **real** en el emulador: no hay emulador en el mgmt VM. Los dos rechazos y la etiqueta están probados con repos y suites de mentira (como el resto de la sección 22 del arnés) y con la composición **real** de `connected_task` del hermano smoke, no contra Gradle. * Que `check` en el pod sobre el ledger vivo (38 líneas) imprima la etiqueta nueva: hace falta el pin-bump (#872) para que el pod tenga este fichero. La lógica de la etiqueta está probada offline en `22[R176b] (f2)` (camino de fallo) y `22[R176b] (f2b)` (camino OK, la forma del ledger vivo).
HOMELAB-2682: fix(sbx-gits): emu-proof refuses an OK line with zero tests and requires --task
All checks were successful
CI Review / runner-manifest-test (pull_request) Successful in 7s
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 13s
CI Review / alert-receiver-test (pull_request) Successful in 23s
Lint & Validate / shellcheck (pull_request) Successful in 7s
Lint & Validate / yaml-lint (pull_request) Successful in 19s
CI Review / record-sql-test (pull_request) Successful in 49s
Lint & Validate / terraform-validate (pull_request) Successful in 2m5s
CI Review / cockpit-test (pull_request) Successful in 3m8s
CI Review / cockpit-e2e (pull_request) Successful in 3m21s
CI Review / dispatcher-test (pull_request) Successful in 3m45s
ee994e19d2
Two defects, one change:

1. CERO PRUEBAS NO ES UN OK. run wrote result=OK with tests_run=0 (live
   ledger line 38, head 8c1d507c: OK, rc=0, empty-XML sha, zero cases).
   run now REFUSES to write that line (rc 2, no ledger line; the run
   capture is kept and named in the message), the same refusal shape as
   the malformed-ledger pre-check. A RED run with zero cases is still
   recorded (an honest failure). check now labels EVERY historical
   tests_run=0 line 'no tests executed - evidence only' - whether or not
   the head passes on another line - and such a line never satisfies the
   gate.

2. LA TAREA VA DESPUES DEL MODULO. --task is now REQUIRED: the suite
   composes '<module>:<task>', so the old default
   ':<module>:connectedDebugAndroidTest' produced
   'app::app:connectedDebugAndroidTest' (double prefix, Gradle rejects
   it). An absent --task is refused (rc 2, nothing written, valid forms
   named); --task ':x'/'::' is refused by construction; a task that
   already carries the module prefix gets an explicit AVISO with the task
   Gradle would receive. Documented in --help and docs/gits-sbx/run-proof.md.

Harness: re-measured SBX_GITS_DIFF_SET against origin/live 960410f5 to the
one file this branch touches, updated the 25(f6)/(f7) B3 fixtures (their
run can no longer be a zero-byte suite: it leaves XML now) and added three
cases that bite - (f) run refuses an OK line with no XML, (f2) a hand-built
tests_run=0 line (the line-38 shape, valid chain) is rejected AND labelled,
(f3) 'run --module app' without --task is refused naming the valid forms.

Deployment: the tools ConfigMap carries a literal-block pin (#870), so this
source edit produces ZERO pod-visible change until the pin-bump PR (#872).
Collaborator

Verdict of record — ld-reviewer — head ee994e19d2: REQUEST_CHANGES
model: deepseek-flash
session: ses-874-seat1-b3f7a2

Blockers

  • OK-path-label-unreachable: blocker — claim 3 is not delivered in the OK report. In cmd_check the candidate loop breaks at the first clean canonical line (gits-sbx-emu-proof.sh, the ok_line=$ln … break in the anchor branch, ~L1876), so a tests_run=0 line that comes AFTER the head's passing smoke line is never appended to the new zero_tests[] array and is never labelled. It is then only reported by the pre-existing "otras corridas de este head en el ledger" loop, which prints result=OK rc=0 via=run — … es OTRA suite and says nothing about zero tests. That is precisely the live line-38 shape the PR sets out to fix (and the shape the code comment promises: "se acumula para TODA linea … se imprime tanto si el head pasa … como si no"), and the new harness case 22[R176b] (f2) cannot see it: (f2) writes a ledger whose ONLY line is the zero-tests line, i.e. the failure path.

    Reproduced in my own worktrees with fake suites (no emulator, both orders, same head H, canonical fake smoke committed at the canonical relpath so the anchor resolves):

    • [passing 2-case canonical line, then line-38-shaped OK/tests_run=0 line] → check H rc 0, report line: PROOF otras corridas de este head en el ledger (NO son el smoke canonico, NO lo prueban): linea 2 … result=OK rc=0 via=run — su sha256 NO es el ancla_blob, and no no tests executed / NO se ejecutaron pruebas anywhere in the output.
    • same two lines in the other order (zero line first) → the label DOES print: PROOF evidencia (no suficiente): linea 1 … tests_run=0 — NO se ejecutaron pruebas (no tests executed — evidence only).
    • failure path (zero line is the head's only line) → the label prints twice (gate line + evidence line). So half the claim is real and half is not; an instrumented copy of the same file shows only the first line ever reaches the tests_run block.
    • Note the live ledger's own line 38 sits after its head's smoke line, so on the real file the label would still be absent — the one case the ticket exists for.

    The fix is small (accumulate zero_tests[] in a pass over every head-matching line independent of the anchor break, or carry the label into the "otras corridas" loop), and (f2) should gain a second ledger — passing line FIRST, zero line second — so the test bites for the OK path it claims.

What I verified (executed, not read)

Both defects reproduce at origin/live 960410f5 first, then the fixes at the head:

  1. Zero-test OK line (claim 1) — verified. origin/live tool, fake suite exit 0 + no XML: line written with "result":"OK","rc":0,"tests_run":0,"xml_sha256":"e3b0c442…", rc=0, and task=":app:connectedDebugAndroidTest" (defect 2 riding along in the same run). At the head: rc 2, ledger file not even created, capture kept and named (ledger.runs/<head>.<sha>.log), message names the cause. The condition is exactly rc == 0 && TESTS_RUN == 0: an rc=1 + zero-case run is still recorded (result=FAIL, tests_run=0, rc=0 overall, capture kept). Empty-XML-directory and a suite that writes <testsuite tests="0"> are both refused — refusing there is right (nothing was proven), see C2 for the message.
  2. --task required (claim 2) — verified. absent → rc 2, nothing written, message names both valid forms; --task ':app:…' and --task 'x::y' refused by construction; --task 'app:connectedDevDebugAndroidTest' with --module app → AVISO naming app:app:connectedDevDebugAndroidTest; a normal 2-case run still writes a clean OK line with tests_run=2. The composition model matches the smoke (task="$TEST_MODULE:$(connected_task)" + CONNECTED_TASK wins).
  3. check label (claim 3) — half; blocker above. Failure path works, OK path works only when the zero line precedes the passing line; tamper detection is not weakened (the label is a report-side accumulation; the chain/anchor/capture checks are untouched and the labelled line still went through chain verification, and the same line still fails the gate).
  4. Harness (claim 4) — partially verified. In my solo run of the head tree the three new cases pass: 22[R176b] (f), (f2), (f3). git diff --stat 960410f5..ee994e19 is exactly 3 files with only tools/gits-sbx-emu-proof.sh under sbx-gits, consistent with the re-measured T7 diff set. I could not independently certify the red-set equality (9 red/690 vs 9 red/693) — see C4.
  5. Deployment honesty (claim 5) — consistent. kustomization.yaml's literal copy still carries the old help (run --epic <E> --tree <dir> [--module <m>] [--task <t>] optional), which is what the pod keeps until the pin bump; nothing in this PR is pod-visible.

Concerns

  • C1 stale in-file usage, and docs that now refuse. The file's own header still advertises [--task <t>] as optional (L120), and the record refusal message still tells callers the only way is '$PROG run --epic <E> --tree <checkout>' — an invocation that now exits 2 (L~2049). Outside this file, the canonical examples in core/manifests/apps/sbx-gits/README.md (L756, L762) and docs/gits-sbx/t7-emu-smoke.md (L795, L836) also lack --task. Those two files cannot be touched here without breaking the T7 diff pin, so they need a follow-up ticket; the two occurrences inside gits-sbx-emu-proof.sh can and should be fixed now.
  • C2 diagnostic wording. When an XML exists but contains zero testcases (reproduced: suite writes <testsuite tests="0">), the refusal still says "no hay XML posterior al arranque en …". That is false and points the reader at a missing file when the real case is a filter that matched nothing (--class). The message should distinguish "no XML" from "XML with zero cases".
  • C3 caller impact — no gate breakage found. Grepping the repo, the smoke, the docs and scripts/gits-sbx-epic-test.sh: nothing automated invokes run without --task (the harness passes --task), so no GITS merge would start refusing. The only callers that now rc 2 are the doc examples in C1 and the tool's own message.
  • C4 harness comparison not independently certified. I ran the harness in both worktrees concurrently and they collide on fixed /tmp paths (e.g. /tmp/h2v3-objects-sbx-sendrast-*.txt), which produced one spurious extra red on the head (H2v3[3] render … anadidos: ConfigMap/sbx-sendrast-models-json) that I do not treat as evidence against this PR — the two worktrees' sendrast directories are byte-identical and resolve the same origin/live. The landing worker should re-measure the red lists with sequential solo runs before merge.
  • Nit: --task is validated for shape but not against the module's real tasks (by design, the suite resolves it) — fine, just noting the AVISO is the only guard against a plausible-but-wrong task name.
**Verdict of record — ld-reviewer — head ee994e19d24569d72cb3e1258190a114608493ad: REQUEST_CHANGES** model: deepseek-flash session: ses-874-seat1-b3f7a2 ## Blockers - `OK-path-label-unreachable`: **blocker** — claim 3 is not delivered in the OK report. In `cmd_check` the candidate loop `break`s at the first clean canonical line (`gits-sbx-emu-proof.sh`, the `ok_line=$ln … break` in the anchor branch, ~L1876), so a `tests_run=0` line that comes AFTER the head's passing smoke line is never appended to the new `zero_tests[]` array and is never labelled. It is then only reported by the pre-existing "otras corridas de este head en el ledger" loop, which prints `result=OK rc=0 via=run — … es OTRA suite` and says nothing about zero tests. That is precisely the live line-38 shape the PR sets out to fix (and the shape the code comment promises: "se acumula para TODA linea … se imprime tanto si el head pasa … como si no"), and the new harness case `22[R176b] (f2)` cannot see it: (f2) writes a ledger whose ONLY line is the zero-tests line, i.e. the failure path. Reproduced in my own worktrees with fake suites (no emulator, both orders, same head H, canonical fake smoke committed at the canonical relpath so the anchor resolves): - `[passing 2-case canonical line, then line-38-shaped OK/tests_run=0 line]` → `check H` rc 0, report line: `PROOF otras corridas de este head en el ledger (NO son el smoke canonico, NO lo prueban): linea 2 … result=OK rc=0 via=run — su sha256 NO es el ancla_blob`, and no `no tests executed` / `NO se ejecutaron pruebas` anywhere in the output. - same two lines in the other order (zero line first) → the label DOES print: `PROOF evidencia (no suficiente): linea 1 … tests_run=0 — NO se ejecutaron pruebas (no tests executed — evidence only)`. - failure path (zero line is the head's only line) → the label prints twice (gate line + evidence line). So half the claim is real and half is not; an instrumented copy of the same file shows only the first line ever reaches the `tests_run` block. - Note the live ledger's own line 38 sits after its head's smoke line, so on the real file the label would still be absent — the one case the ticket exists for. The fix is small (accumulate `zero_tests[]` in a pass over every head-matching line independent of the anchor `break`, or carry the label into the "otras corridas" loop), and (f2) should gain a second ledger — passing line FIRST, zero line second — so the test bites for the OK path it claims. ## What I verified (executed, not read) Both defects reproduce at origin/live `960410f5` first, then the fixes at the head: 1. **Zero-test OK line (claim 1) — verified.** origin/live tool, fake suite `exit 0` + no XML: line written with `"result":"OK","rc":0,"tests_run":0,"xml_sha256":"e3b0c442…"`, `rc=0`, and `task=":app:connectedDebugAndroidTest"` (defect 2 riding along in the same run). At the head: rc 2, ledger file not even created, capture kept and named (`ledger.runs/<head>.<sha>.log`), message names the cause. The condition is exactly `rc == 0 && TESTS_RUN == 0`: an `rc=1` + zero-case run is still recorded (`result=FAIL`, `tests_run=0`, `rc=0` overall, capture kept). Empty-XML-directory and a suite that writes `<testsuite tests="0">` are both refused — refusing there is right (nothing was proven), see C2 for the message. 2. **`--task` required (claim 2) — verified.** absent → rc 2, nothing written, message names both valid forms; `--task ':app:…'` and `--task 'x::y'` refused by construction; `--task 'app:connectedDevDebugAndroidTest'` with `--module app` → AVISO naming `app:app:connectedDevDebugAndroidTest`; a normal 2-case run still writes a clean OK line with `tests_run=2`. The composition model matches the smoke (`task="$TEST_MODULE:$(connected_task)"` + `CONNECTED_TASK` wins). 3. **`check` label (claim 3) — half; blocker above.** Failure path works, OK path works only when the zero line precedes the passing line; tamper detection is not weakened (the label is a report-side accumulation; the chain/anchor/capture checks are untouched and the labelled line still went through chain verification, and the same line still fails the gate). 4. **Harness (claim 4) — partially verified.** In my solo run of the head tree the three new cases pass: `22[R176b] (f)`, `(f2)`, `(f3)`. `git diff --stat 960410f5..ee994e19` is exactly 3 files with only `tools/gits-sbx-emu-proof.sh` under sbx-gits, consistent with the re-measured T7 diff set. I could not independently certify the red-set equality (`9 red/690` vs `9 red/693`) — see C4. 5. **Deployment honesty (claim 5) — consistent.** `kustomization.yaml`'s literal copy still carries the old help (`run --epic <E> --tree <dir> [--module <m>] [--task <t>]` optional), which is what the pod keeps until the pin bump; nothing in this PR is pod-visible. ## Concerns - **C1 stale in-file usage, and docs that now refuse.** The file's own header still advertises `[--task <t>]` as optional (L120), and the `record` refusal message still tells callers the only way is `'$PROG run --epic <E> --tree <checkout>'` — an invocation that now exits 2 (L~2049). Outside this file, the canonical examples in `core/manifests/apps/sbx-gits/README.md` (L756, L762) and `docs/gits-sbx/t7-emu-smoke.md` (L795, L836) also lack `--task`. Those two files cannot be touched here without breaking the T7 diff pin, so they need a follow-up ticket; the two occurrences inside `gits-sbx-emu-proof.sh` can and should be fixed now. - **C2 diagnostic wording.** When an XML exists but contains zero testcases (reproduced: suite writes `<testsuite tests="0">`), the refusal still says "no hay XML posterior al arranque en …". That is false and points the reader at a missing file when the real case is a filter that matched nothing (`--class`). The message should distinguish "no XML" from "XML with zero cases". - **C3 caller impact — no gate breakage found.** Grepping the repo, the smoke, the docs and `scripts/gits-sbx-epic-test.sh`: nothing automated invokes `run` without `--task` (the harness passes `--task`), so no GITS merge would start refusing. The only callers that now rc 2 are the doc examples in C1 and the tool's own message. - **C4 harness comparison not independently certified.** I ran the harness in both worktrees concurrently and they collide on fixed `/tmp` paths (e.g. `/tmp/h2v3-objects-sbx-sendrast-*.txt`), which produced one spurious extra red on the head (`H2v3[3] render … anadidos: ConfigMap/sbx-sendrast-models-json`) that I do not treat as evidence against this PR — the two worktrees' sendrast directories are byte-identical and resolve the same `origin/live`. The landing worker should re-measure the red lists with sequential solo runs before merge. - Nit: `--task` is validated for shape but not against the module's real tasks (by design, the suite resolves it) — fine, just noting the AVISO is the only guard against a plausible-but-wrong task name.
HOMELAB-2682: fix(sbx-gits): check labels zero-test lines after the pass too
All checks were successful
CI Review / runner-manifest-test (pull_request) Successful in 6s
CI Review / receiver-test (pull_request) Successful in 6s
CI Review / helm-validate (pull_request) Successful in 11s
Lint & Validate / shellcheck (pull_request) Successful in 7s
CI Review / alert-receiver-test (pull_request) Successful in 21s
Lint & Validate / yaml-lint (pull_request) Successful in 23s
CI Review / record-sql-test (pull_request) Successful in 1m6s
Lint & Validate / terraform-validate (pull_request) Successful in 2m2s
CI Review / cockpit-test (pull_request) Successful in 2m28s
CI Review / cockpit-e2e (pull_request) Successful in 2m56s
CI Review / dispatcher-test (pull_request) Successful in 3m35s
ea6993cb14
Seat 1 (round 1) blocker `OK-path-label-unreachable`: the candidate loop in
`cmd_check` breaks at the first clean canonical line, so a `tests_run=0` line
that comes AFTER the head's passing smoke line never reached the label block
and vanished from the report — exactly the live ledger's line 38 shape (it
sits after its head's smoke line), the case this change exists for.

`check` now walks EVERY line of the head in a dedicated read-only pass before
the gate loop and labels each `tests_run=0` line, so the label is printed in
both the OK path and the failure path regardless of where the gate loop stops.
The gate decision is untouched: a passing canonical line still passes the head;
a zero-test line still never completes the gate.

C2 from the same seat: the zero-test message claimed always "no hay XML
posterior al arranque", which is false when the suite wrote an XML with zero
cases. `zero_tests_why` now names which of the two happened (no XML at all vs
XML present with no <testcase>).

Harness: new biting case 22[R176b] (f2b) — a ledger with [passing canonical
line, then line-38-shaped zero line] must still label the zero line; it fails
on the previous head and passes on this one. (f2) (only the zero line) keeps
covering the failure path, (f)/(f2)/(f3) unchanged.
Collaborator

Verdict of record — ld-reviewer — head ea6993cb14: APPROVE
model: deepseek-flash
session: ses-874r2-seat1-4f18c07b

Blockers

None.

The round-1 blocker is genuinely fixed and I reproduced both sides of it myself: with the pre-fix head (ee994e19) the [line that PASSES, then zero-test line] shape gives rc 0 and the label is absent (and the report prints only "el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0", i.e. a second good run); with this head the same shape gives rc 0 and the label. The zero-test line never completes the gate, and the pre-pass changes no verdict: over six ledger shapes the pre-fix and this head print byte-identical output except the added label line.

Concerns

  • C-R2-1 — deployment claim: #872 does not carry this file's bytes, and the body reads as if it does. Verified: origin/plane/HOMELAB-2682-pin-bump pins gits-sbx-emu-proof.sh to 211be2b5… (1957 lines / 125320 B — the post-#871 tool), while this head's tools/gits-sbx-emu-proof.sh hashes 43e5f04e0a1d71ec931109268cc2e4a576ab4d235280813b0f75e57f7a84be1b (2090 lines / 136638 B). The narrow claim is true and I confirmed the mechanism (the sbx-gits-tools ConfigMap is a literals: block with no files:, so editing tools/ renders identically and reaches no pod until the block is re-cut): zero pod-visible change from this PR. But "hace falta el pin-bump (#872) para que el pod tenga este fichero" is false as things stand — merging #872 leaves sbx-gits-0 on the pre-#874 bytes, so the live ledger's zero-test lines stay unlabelled while the record implies #872 shipped the fix. Fix by correcting the body, or by re-cutting #872's literal block to 43e5f04e… before #872 merges. No code change needed, which is why this is not a blocker.
  • C-R2-2 — C1 (inherited) is still real and still has no tracker. The stale --task-less invocations remain exactly where round 1 found them: core/manifests/apps/sbx-gits/README.md §10.4 (~lines 756, 762, and the --class example at ~766), docs/gits-sbx/t7-emu-smoke.md (~795, ~836), and the tool's own header (record --epic X --rc 0 --result OK …, now lines 19–21) plus record's die text, which still names run --epic <E> --tree <checkout> with no --task. Every one of those now exits rc 2. Declaring them out of scope is honest and the frozen T7[diff] set genuinely does not admit those files — what is missing is a named tracker: the PR names no ticket, and the identifier the branch uses (HOMELAB-2682) resolves in Plane to an unrelated LD intent ("ld-1: see my programs in Lab Director"), while the sbx-gits epic HOMELAB-2648 resolves correctly. Please confirm the reference and file the follow-up, or README and the runbook keep documenting a command that now fails.
  • C-R2-3 — the OK report still reads as two good runs, one line above the label. On the live line-38 shape the report prints PROOF aviso el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0 via=run … and only then PROOF evidencia (no suficiente): linea 2 … tests_run=0 …. The pair is now truthful, but that first line still says nothing about tests_run, so a reader who stops there reads a second good run — the exact misreading round 1 named. Additive fix: say tests_run=$tests_run inside the (4d) "otras corridas" block (or drop that line when tests_run=0), or print the zero-test evidence before it.
  • C-R2-4 — the pre-pass re-parses work the chain loop already did (minor, measurable). It calls jstr/jraw four more times per candidate instead of reusing the $h/$result/$rc/$tests_run the chain loop had in hand. Cost on a pathological 38-line ledger whose 38 lines all belong to the checked head: check 4.37 s → 5.55 s (median of 5; min 4.23 → 4.81). On the real ledger (38 lines, a handful of candidates for any one head) the cost is negligible, so this is a simplification note, not a defect.
  • C-R2-5 — the label cannot say WHICH zero-test cause it is (precision parity with C2, minor). check's label still reads "no hay ni un caso en los XML", true by the definition of tests_run, but unlike run's message it does not distinguish "the suite left no XML" from "XML present with no <testcase>". When the line carries results_sha256, the XML did exist and had no cases; that could be said. Not false in either case, so cosmetic.

What I verified first-hand

All runs solo and sequential (never two trees at once), from clean detached worktrees.

1. Round-1 blocker, both sides. Fake repo with the canonical smoke committed at core/manifests/apps/sbx-gits/tools/gits-sbx-emu-smoke.sh, fake suite, and a hand-built zero-test line chained with the tool's own chain_text (tests_run=0, xml_sha256=e3b0c442…, its own PROOF-RUN capture, script_sha256 = the anchor):

shape ee994e19 (pre-fix) ea6993cb (this head)
[pass, then zero] rc 0, label ABSENT ("tambien corrio … en la linea 2: result=OK") rc 0, label PRESENT
[zero, then pass] rc 0, label present rc 0, label present
[only zero] rc 1, label present rc 1, label present

2. Additive-only, proven by diff. Same ledger, both binaries, six shapes — [pass, then zero], [zero, then pass], [only zero], red canonical + green canonical, another suite (evidence line), and a hand-edited line (broken chain). Output identical in all six except (a) the single new evidencia (no suficiente) line in the [pass, then zero] case and (b) nothing else — no refusal text, no verdict, no ordering changed. Round-1 test still holds: a clean canonical line passes the head; a zero-test line never completes it; rc=1 with zero cases is still recorded (result":"FAIL","rc":1,"tests_run":0) with the "SI hay XML … pero NINGUNO trae un <testcase>" AVISO.

3. (f2b) bites. I extracted epic-test §22b (T176) into a standalone runner and drove it twice: real head tool → 40 checks, 1 FAIL (an extraction artifact, present in both runs), (f2b) PASS; then a surgical mutation of the head tool that removes only the pre-pass and puts the label back inside the gate loop (the round-1 shape) → (f2b) FAIL: "BLOQUEO: … el head pasa (rc=0) pero la linea de cero casos NO se etiqueta", while (f2), (f), (f3) stay PASS. The case fails for exactly the reason it claims.

4. Harness honesty, solo and sequential. bash scripts/gits-sbx-epic-test.sh:

  • baseline origin/live 960410f5: FAIL 10 of 690 (8 skipped);
  • this head ea6993cb: FAIL 9 of 694 (6 skipped);
  • head FAIL set ⊆ baseline FAIL set, zero new reds; the one baseline-only extra is 16[R161] mutacion (ii) (the lock-under-load self-test flake). The skip delta is the branch exercising two checks the baseline skips: T7[diff] and 17[R159] (d) PASS here, with the diff of sbx-gits vs origin/live exactly M tools/gits-sbx-emu-proof.sh, and the mutation ("remove that line from the allowed set") still red. git diff --name-status 960410f5..ea6993cb is three files: the tool, docs/gits-sbx/run-proof.md, scripts/gits-sbx-epic-test.sh — no new files.

5. run's refusals, re-checked at this head. no --task → rc 2, nothing executed, no line, valid forms named; --task ':app:…' → rc 2; --task 'app::…' → rc 2; guard is exactly rc==0 && TESTS_RUN==0. C2: both new strings reachable and true in the case that produces them — XML present with zero <testcase> (<testsuite tests="0">, and the FAKE_TESTS=0 suite) gives "SI hay XML posterior al arranque … pero NINGUNO trae un <testcase>"; FAKE_NOWRITE=1 gives "no hay ni un XML posterior al arranque". The pre-scan keeps "posterior al arranque" honest (a stale untouched XML is excluded).

6. Chain/anchor/integrity. The round-2 delta touches no hash, chain, anchor, artifact-binding or results_sha256 logic; the pre-pass is read-only over cand/candn (this head's lines only) and its locals cannot leak into the verdict. The gate's tests_run > 0, via=run, tree_clean/tree_clean_after, smoke@commit anchor and capture binding are unchanged, and the harness's attack cases ((b), (c1)–(c3), (d)–(d4), (i), (i2), (o)) all still PASS.

**Verdict of record — ld-reviewer — head ea6993cb149d9a32484bb664101cf2a1d2b264f4: APPROVE** model: deepseek-flash session: ses-874r2-seat1-4f18c07b ## Blockers None. The round-1 blocker is genuinely fixed and I reproduced both sides of it myself: with the pre-fix head (`ee994e19`) the `[line that PASSES, then zero-test line]` shape gives rc 0 and the label is **absent** (and the report prints only "el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0", i.e. a second good run); with this head the same shape gives rc 0 **and** the label. The zero-test line never completes the gate, and the pre-pass changes no verdict: over six ledger shapes the pre-fix and this head print byte-identical output except the added label line. ## Concerns - **C-R2-1 — deployment claim: #872 does not carry this file's bytes, and the body reads as if it does.** Verified: `origin/plane/HOMELAB-2682-pin-bump` pins `gits-sbx-emu-proof.sh` to `211be2b5…` (1957 lines / 125320 B — the post-#871 tool), while this head's `tools/gits-sbx-emu-proof.sh` hashes `43e5f04e0a1d71ec931109268cc2e4a576ab4d235280813b0f75e57f7a84be1b` (2090 lines / 136638 B). The narrow claim is true and I confirmed the mechanism (the `sbx-gits-tools` ConfigMap is a `literals:` block with no `files:`, so editing `tools/` renders identically and reaches no pod until the block is re-cut): zero pod-visible change from this PR. But "hace falta el pin-bump (#872) para que el pod tenga este fichero" is false as things stand — merging #872 leaves `sbx-gits-0` on the pre-#874 bytes, so the live ledger's zero-test lines stay unlabelled while the record implies #872 shipped the fix. Fix by correcting the body, or by re-cutting #872's literal block to `43e5f04e…` before #872 merges. No code change needed, which is why this is not a blocker. - **C-R2-2 — C1 (inherited) is still real and still has no tracker.** The stale `--task`-less invocations remain exactly where round 1 found them: `core/manifests/apps/sbx-gits/README.md` §10.4 (~lines 756, 762, and the `--class` example at ~766), `docs/gits-sbx/t7-emu-smoke.md` (~795, ~836), and the tool's own header (`record --epic X --rc 0 --result OK …`, now lines 19–21) plus `record`'s `die` text, which still names `run --epic <E> --tree <checkout>` with no `--task`. Every one of those now exits rc 2. Declaring them out of scope is honest and the frozen `T7[diff]` set genuinely does not admit those files — what is missing is a named tracker: the PR names no ticket, and the identifier the branch uses (`HOMELAB-2682`) resolves in Plane to an unrelated LD intent ("ld-1: see my programs in Lab Director"), while the sbx-gits epic `HOMELAB-2648` resolves correctly. Please confirm the reference and file the follow-up, or README and the runbook keep documenting a command that now fails. - **C-R2-3 — the OK report still reads as two good runs, one line above the label.** On the live line-38 shape the report prints `PROOF aviso el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0 via=run …` and only then `PROOF evidencia (no suficiente): linea 2 … tests_run=0 …`. The pair is now truthful, but that first line still says nothing about `tests_run`, so a reader who stops there reads a second good run — the exact misreading round 1 named. Additive fix: say `tests_run=$tests_run` inside the (4d) "otras corridas" block (or drop that line when `tests_run=0`), or print the zero-test evidence before it. - **C-R2-4 — the pre-pass re-parses work the chain loop already did (minor, measurable).** It calls `jstr`/`jraw` four more times per candidate instead of reusing the `$h`/`$result`/`$rc`/`$tests_run` the chain loop had in hand. Cost on a pathological 38-line ledger whose 38 lines all belong to the checked head: `check` 4.37 s → 5.55 s (median of 5; min 4.23 → 4.81). On the real ledger (38 lines, a handful of candidates for any one head) the cost is negligible, so this is a simplification note, not a defect. - **C-R2-5 — the label cannot say WHICH zero-test cause it is (precision parity with C2, minor).** `check`'s label still reads "no hay ni un caso en los XML", true by the definition of `tests_run`, but unlike `run`'s message it does not distinguish "the suite left no XML" from "XML present with no `<testcase>`". When the line carries `results_sha256`, the XML did exist and had no cases; that could be said. Not false in either case, so cosmetic. ## What I verified first-hand All runs solo and sequential (never two trees at once), from clean detached worktrees. **1. Round-1 blocker, both sides.** Fake repo with the canonical smoke committed at `core/manifests/apps/sbx-gits/tools/gits-sbx-emu-smoke.sh`, fake suite, and a hand-built zero-test line chained with the tool's own `chain_text` (`tests_run=0`, `xml_sha256=e3b0c442…`, its own `PROOF-RUN` capture, `script_sha256` = the anchor): | shape | `ee994e19` (pre-fix) | `ea6993cb` (this head) | |---|---|---| | [pass, then zero] | rc **0**, label **ABSENT** ("tambien corrio … en la linea 2: result=OK") | rc **0**, label **PRESENT** | | [zero, then pass] | rc 0, label present | rc 0, label present | | [only zero] | rc 1, label present | rc 1, label present | **2. Additive-only, proven by diff.** Same ledger, both binaries, six shapes — `[pass, then zero]`, `[zero, then pass]`, `[only zero]`, red canonical + green canonical, another suite (evidence line), and a hand-edited line (broken chain). Output identical in all six except (a) the single new `evidencia (no suficiente)` line in the `[pass, then zero]` case and (b) nothing else — no refusal text, no verdict, no ordering changed. Round-1 test still holds: a clean canonical line passes the head; a zero-test line never completes it; `rc=1` with zero cases is still **recorded** (`result":"FAIL","rc":1,"tests_run":0`) with the "SI hay XML … pero NINGUNO trae un `<testcase>`" AVISO. **3. `(f2b)` bites.** I extracted epic-test §22b (T176) into a standalone runner and drove it twice: real head tool → 40 checks, 1 FAIL (an extraction artifact, present in both runs), `(f2b)` PASS; then a surgical mutation of the head tool that removes only the pre-pass and puts the label back inside the gate loop (the round-1 shape) → `(f2b)` **FAIL: "BLOQUEO: … el head pasa (rc=0) pero la linea de cero casos NO se etiqueta"**, while `(f2)`, `(f)`, `(f3)` stay PASS. The case fails for exactly the reason it claims. **4. Harness honesty, solo and sequential.** `bash scripts/gits-sbx-epic-test.sh`: - baseline `origin/live` `960410f5`: **FAIL 10 of 690** (8 skipped); - this head `ea6993cb`: **FAIL 9 of 694** (6 skipped); - head FAIL set ⊆ baseline FAIL set, **zero new reds**; the one baseline-only extra is `16[R161] mutacion (ii)` (the lock-under-load self-test flake). The skip delta is the branch exercising two checks the baseline skips: `T7[diff]` and `17[R159] (d)` **PASS** here, with the diff of `sbx-gits` vs `origin/live` exactly `M tools/gits-sbx-emu-proof.sh`, and the mutation ("remove that line from the allowed set") still red. `git diff --name-status 960410f5..ea6993cb` is three files: the tool, `docs/gits-sbx/run-proof.md`, `scripts/gits-sbx-epic-test.sh` — no new files. **5. `run`'s refusals, re-checked at this head.** no `--task` → rc 2, nothing executed, no line, valid forms named; `--task ':app:…'` → rc 2; `--task 'app::…'` → rc 2; guard is exactly `rc==0 && TESTS_RUN==0`. C2: both new strings reachable and true in the case that produces them — XML present with zero `<testcase>` (`<testsuite tests="0">`, and the `FAKE_TESTS=0` suite) gives "SI hay XML posterior al arranque … pero NINGUNO trae un `<testcase>`"; `FAKE_NOWRITE=1` gives "no hay ni un XML posterior al arranque". The pre-scan keeps "posterior al arranque" honest (a stale untouched XML is excluded). **6. Chain/anchor/integrity.** The round-2 delta touches no hash, chain, anchor, artifact-binding or `results_sha256` logic; the pre-pass is read-only over `cand`/`candn` (this head's lines only) and its locals cannot leak into the verdict. The gate's `tests_run > 0`, `via=run`, `tree_clean`/`tree_clean_after`, `smoke@commit` anchor and capture binding are unchanged, and the harness's attack cases (`(b)`, `(c1)`–`(c3)`, `(d)`–`(d4)`, `(i)`, `(i2)`, `(o)`) all still PASS.
Collaborator

Verdict of record — ld-reviewer — head ea6993cb14: APPROVE

model: deepseek-flash
session: ses-874r2-seat2-54a5f625

Blockers

None.

The round-1 blocker is closed, and I reproduced it first-hand before accepting the fix.

What I verified myself (no prior verdict used as evidence)

Independent fixtures: fake repo with the canonical smoke committed at its real path (identical bytes to the
suite run executes), a fake suite that leaves 3 cases, and the hand-built tests_run=0 line chained with
the tool's own chain_text (the live ledger line-38 shape). Tools taken straight out of the objects:
ee994e19 (pre-fix), ea6993cb (head), 960410f5 (origin/live).

1. The blocker: label reachable on the OK path — REPRODUCED, then GONE

shape ee994e19 ea6993cb
[line that PASSES, then the zero line] rc=0, label ABSENT rc=0, label PRESENT
[zero line, then the line that passes] rc=0, label present rc=0, label present
[only the zero line] rc=1, label present rc=1, label present

At the pre-fix head the OK path prints nothing about the second line's zero cases — it says only
aviso el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0 … SI es el smoke canonico, NO otra suite, i.e. it reads as a second good run. That is exactly the live ledger shape (line 38 after the
smoke line) and exactly the seat-1 mechanism. At ea6993cb the same fixture adds:

PROOF  evidencia (no suficiente): linea 2 (head …): tests_run=0 — NO se ejecutaron pruebas
(no tests executed — evidence only): la linea lee result=OK rc=0 pero no hay ni un caso en los XML,
asi que NO prueba el head

Gate semantics did not move: a clean canonical line still returns rc=0 (shapes 1 and 2), the zero-test line
never completes the head (shape 3 stays rc=1, and its own tests_run>0 refusal is still there), and
ok_line can never be a zero-test line.

2. The new pre-pass is additive-only

I diffed the full check report of the same fixture at ee994e19 vs ea6993cb (fake head sha normalised):
shape 1 gains exactly one line (the label); shapes 2 and 3 are byte-identical. No refusal text changed, no
verdict flipped, no new reason appears. In code the pass only reads cand/candn into a new zero_tests[]
array; its locals (zi zln zc zh zres zrc zrun) are declared and do not shadow the gate loop's.

Perf: a 41-line ledger for one head with 40 zero lines costs 5.2 s vs 3.7 s for the same size with no zero
lines (~37 ms per candidate line). Real ledgers are 38 lines total with 1-3 lines for a given head, so the
added cost is tens of milliseconds. No issue.

3. run refusal is exactly rc==0 && tests_run==0

  • rc=0 with no XML → rc 2, no line written, message names the cause, and the capture is kept and named
    (…queda en '<art>'); I read <art> back — it is the run's own log.
  • rc=1 (RED) with no XML → the line IS written with result=FAIL, and the AVISO says
    tests_run=0 (…) : result=FAIL sin casos ejecutados — check <head> NO la acepta como prueba.
  • A zero-case suite that DOES leave XML (<testsuite tests="0">) → refused too, with the other cause named
    (see C2 below).
    The guard sits after the run and before chaining, so a refusal cannot leave a partial append.

4. --task

  • absent (and --task '') → rc 2, nothing written, nothing executed, message names both valid forms
    (connectedDevDebugAndroidTest / connectedStableDebugAndroidTest for a flavoured module,
    connectedDebugAndroidTest without flavours).
  • --task ':app:connectedDebugAndroidTest' and --task 'a::b' → rc 2, nothing written, refused by
    construction.
  • --task 'app:connectedDebugAndroidTest' with --module app → AVISO naming the exact task Gradle would
    receive (app:app:connectedDebugAndroidTest) and the corrected invocation; the run proceeds.
    The check is at step 0b, i.e. before the emulator is spent.

5. C2 messages are reachable and true

At ee994e19 the zero-case suite printed tests_run=0 (no hay XML posterior al arranque …) while the run
had left one XML
(<testsuite tests="0">) — the false sentence. At ea6993cb the same fixture prints
tests_run=0 (SI hay XML posterior al arranque en … (1 fichero(s)) pero NINGUNO trae un <testcase>: cero casos en los XML …), and the no-XML case prints no hay ni un XML posterior al arranque en … (la suite no dejo resultados). Both branches observed; both refusals still rc 2 with no line.

6. Harness — SOLO, SEQUENTIAL (clean worktrees, never two at once)

run head result
baseline 960410f5 FAIL 9 of 690 checks (8 skipped, 0 pending)
this branch ea6993cb FAIL 9 of 694 checks (6 skipped, 0 pending)

The FAIL sets (after normalising tmp paths and shas) are identical: branch ⊆ baseline with zero new reds
(the nine are the known environment/pin reds: manifest[2]/[3]/[3b], T7 emulador, T7+R99[10b],
11[H11] x3, the probe --dir case). My baseline came out 9, not the 10 the PR table reports — the
18[R165] --lock-selftest red did not reproduce on this box; that only makes the subset relation cleaner,
it does not weaken the claim.

  • T7[diff] PASS with the pinned set exactly M tools/gits-sbx-emu-proof.sh, and its mutation
    (removing that entry from the allowed set) is RED — the set bites.
  • git diff --name-status 960410f5..ea6993cb = M tools/gits-sbx-emu-proof.sh, M docs/gits-sbx/run-proof.md,
    M scripts/gits-sbx-epic-test.sh. No new files; the two round-1 files sit outside core/manifests/apps/sbx-gits/
    so the pinned set is correct.
  • New checks vs the baseline run: 22[R176b] (f2), (f2b), (f3) — all PASS at the head; the rewritten
    (f) PASSes too.
  • (f2b) bites: its assertion is check rc=0 AND the label present; on the pre-fix tool the same fixture
    gives rc=0 with the label absent, which is precisely the case's BLOQUEO branch. Test first, fix after —
    that is what the object graph actually does, not just what the PR body says.

7. Chain / anchor / integrity — nothing weakened

chain_text is untouched (the tamper cases (b), (d), (d2), (n), (o2), (q2) all still PASS), the
anchor is still the committed blob, and both new behaviours fail closed (refuse to write) rather than
loosen anything. The only verdict-shaped risk in this diff is the label, and the label is provably report-only.

8. Deployment note intact

The sbx-gits-tools ConfigMap is still a literal pin (kustomization records gits-sbx-emu-proof.sh sha
ac29a440… = the tool at a848b717), and that is neither origin/live's 211be2b5… nor this head's
43e5f04e…. The pod's /run/sbx-tools/gits-sbx-emu-proof.sh is unchanged by this PR; #872 is open and is
the only route to the pod. The claim "zero pod-visible change until the pin bump" is true.

Concerns

Non-blocking. None of these changes the verdict.

  • C-1 (doc precision, the C2 conflation survived in the prose). The runtime message now distinguishes the
    two causes and the PR argues at length that conflating them is misleading — but the new
    docs/gits-sbx/run-proof.md section and the tool's --help still describe zero-tests as one cause only
    ("ningún XML posterior al arranque en <árbol>/<módulo>/build/outputs/androidTest-results"). A run that
    leaves <testsuite tests="0"> or an XML without <testcase> is refused with the other message. One
    sentence in each place ("o XML sin ningún <testcase>") closes it.
  • C-2 (C1 follow-up needs an owner). The deferred list is accurate — I spot-checked all of it:
    core/manifests/apps/sbx-gits/README.md lines 755/762 and docs/gits-sbx/t7-emu-smoke.md lines 795/836 all
    show run --epic … --tree … with no --task and would now be refused (rc 2), and so does the live
    record die text (La unica forma de anadir una linea es EJECUTAR la suite: '<prog> run --epic <E> --tree <checkout>') — a user-facing instruction that now always fails. Please make sure the follow-up carries a
    Plane id; the PR text names files but no ticket. Minor: the header's record --epic X --rc 0 --result OK …
    example is historical framing ("why record is gone"), not a stale instruction — only the die text is.
  • C-3 (note, not a finding). (f2)'s message still reads "no hay ni un caso en los XML" for a line that may
    have carried no XML at all; harmless (the line is refused either way) and out of scope, but the same
    distinction C2 makes in run could be made here too.

Limits of this review

Read-only: I ran the tool and the harness against throwaway fixtures/worktrees, I did not edit, commit, push or
merge, and this comment is my only write. I did not see any other round-2 verdict, and no emulator, Gradle or
cluster was involved — the two refusals and the label are proven against fake suites and a fake repo, the same
way section 22 of the harness proves them.

**Verdict of record — ld-reviewer — head ea6993cb149d9a32484bb664101cf2a1d2b264f4: APPROVE** model: deepseek-flash session: ses-874r2-seat2-54a5f625 ## Blockers None. The round-1 blocker is closed, and I reproduced it first-hand before accepting the fix. ## What I verified myself (no prior verdict used as evidence) Independent fixtures: fake repo with the canonical smoke committed at its real path (identical bytes to the suite `run` executes), a fake suite that leaves 3 cases, and the hand-built `tests_run=0` line chained with the tool's own `chain_text` (the live ledger line-38 shape). Tools taken straight out of the objects: `ee994e19` (pre-fix), `ea6993cb` (head), `960410f5` (origin/live). ### 1. The blocker: label reachable on the OK path — REPRODUCED, then GONE | shape | `ee994e19` | `ea6993cb` | |---|---|---| | [line that PASSES, then the zero line] | rc=0, **label ABSENT** | rc=0, **label PRESENT** | | [zero line, then the line that passes] | rc=0, label present | rc=0, label present | | [only the zero line] | rc=1, label present | rc=1, label present | At the pre-fix head the OK path prints nothing about the second line's zero cases — it says only `aviso el smoke CANONICO tambien corrio en este head, en la linea 2: result=OK rc=0 … SI es el smoke canonico, NO otra suite`, i.e. it reads as a second good run. That is exactly the live ledger shape (line 38 after the smoke line) and exactly the seat-1 mechanism. At `ea6993cb` the same fixture adds: ``` PROOF evidencia (no suficiente): linea 2 (head …): tests_run=0 — NO se ejecutaron pruebas (no tests executed — evidence only): la linea lee result=OK rc=0 pero no hay ni un caso en los XML, asi que NO prueba el head ``` Gate semantics did not move: a clean canonical line still returns rc=0 (shapes 1 and 2), the zero-test line never completes the head (shape 3 stays rc=1, and its own `tests_run>0` refusal is still there), and `ok_line` can never be a zero-test line. ### 2. The new pre-pass is additive-only I diffed the full `check` report of the same fixture at `ee994e19` vs `ea6993cb` (fake head sha normalised): shape 1 gains **exactly one line** (the label); shapes 2 and 3 are byte-identical. No refusal text changed, no verdict flipped, no new reason appears. In code the pass only reads `cand`/`candn` into a new `zero_tests[]` array; its locals (`zi zln zc zh zres zrc zrun`) are declared and do not shadow the gate loop's. Perf: a 41-line ledger for one head with 40 zero lines costs 5.2 s vs 3.7 s for the same size with no zero lines (~37 ms per candidate line). Real ledgers are 38 lines total with 1-3 lines for a given head, so the added cost is tens of milliseconds. No issue. ### 3. `run` refusal is exactly `rc==0 && tests_run==0` - rc=0 with no XML → **rc 2, no line written**, message names the cause, and the capture is kept and named (`…queda en '<art>'`); I read `<art>` back — it is the run's own log. - rc=1 (RED) with no XML → **the line IS written** with `result=FAIL`, and the AVISO says `tests_run=0 (…) : result=FAIL sin casos ejecutados — check <head> NO la acepta como prueba`. - A zero-case suite that DOES leave XML (`<testsuite tests="0">`) → refused too, with the other cause named (see C2 below). The guard sits after the run and before chaining, so a refusal cannot leave a partial append. ### 4. `--task` - absent (and `--task ''`) → rc 2, nothing written, nothing executed, message names both valid forms (`connectedDevDebugAndroidTest` / `connectedStableDebugAndroidTest` for a flavoured module, `connectedDebugAndroidTest` without flavours). - `--task ':app:connectedDebugAndroidTest'` and `--task 'a::b'` → rc 2, nothing written, refused by construction. - `--task 'app:connectedDebugAndroidTest'` with `--module app` → AVISO naming the exact task Gradle would receive (`app:app:connectedDebugAndroidTest`) and the corrected invocation; the run proceeds. The check is at step 0b, i.e. before the emulator is spent. ### 5. C2 messages are reachable and true At `ee994e19` the zero-case suite printed `tests_run=0 (no hay XML posterior al arranque …)` **while the run had left one XML** (`<testsuite tests="0">`) — the false sentence. At `ea6993cb` the same fixture prints `tests_run=0 (SI hay XML posterior al arranque en … (1 fichero(s)) pero NINGUNO trae un <testcase>: cero casos en los XML …)`, and the no-XML case prints `no hay ni un XML posterior al arranque en … (la suite no dejo resultados)`. Both branches observed; both refusals still rc 2 with no line. ### 6. Harness — SOLO, SEQUENTIAL (clean worktrees, never two at once) | run | head | result | |---|---|---| | baseline | `960410f5` | **FAIL 9** of **690** checks (8 skipped, 0 pending) | | this branch | `ea6993cb` | **FAIL 9** of **694** checks (6 skipped, 0 pending) | The FAIL sets (after normalising tmp paths and shas) are **identical**: branch ⊆ baseline with zero new reds (the nine are the known environment/pin reds: `manifest[2]/[3]/[3b]`, `T7 emulador`, `T7+R99[10b]`, `11[H11]` x3, the `probe --dir` case). My baseline came out **9**, not the 10 the PR table reports — the `18[R165] --lock-selftest` red did not reproduce on this box; that only makes the subset relation cleaner, it does not weaken the claim. - `T7[diff]` **PASS** with the pinned set exactly `M tools/gits-sbx-emu-proof.sh`, and its mutation (removing that entry from the allowed set) is **RED** — the set bites. - `git diff --name-status 960410f5..ea6993cb` = `M tools/gits-sbx-emu-proof.sh`, `M docs/gits-sbx/run-proof.md`, `M scripts/gits-sbx-epic-test.sh`. No new files; the two round-1 files sit outside `core/manifests/apps/sbx-gits/` so the pinned set is correct. - New checks vs the baseline run: `22[R176b] (f2)`, `(f2b)`, `(f3)` — all PASS at the head; the rewritten `(f)` PASSes too. - `(f2b)` **bites**: its assertion is `check rc=0 AND the label present`; on the pre-fix tool the same fixture gives rc=0 with the label absent, which is precisely the case's BLOQUEO branch. Test first, fix after — that is what the object graph actually does, not just what the PR body says. ### 7. Chain / anchor / integrity — nothing weakened `chain_text` is untouched (the tamper cases `(b)`, `(d)`, `(d2)`, `(n)`, `(o2)`, `(q2)` all still PASS), the anchor is still the committed blob, and both new behaviours fail **closed** (refuse to write) rather than loosen anything. The only verdict-shaped risk in this diff is the label, and the label is provably report-only. ### 8. Deployment note intact The `sbx-gits-tools` ConfigMap is still a literal pin (kustomization records `gits-sbx-emu-proof.sh` sha `ac29a440…` = the tool at `a848b717`), and that is **neither** `origin/live`'s `211be2b5…` nor this head's `43e5f04e…`. The pod's `/run/sbx-tools/gits-sbx-emu-proof.sh` is unchanged by this PR; #872 is open and is the only route to the pod. The claim "zero pod-visible change until the pin bump" is true. ## Concerns Non-blocking. None of these changes the verdict. - **C-1 (doc precision, the C2 conflation survived in the prose).** The runtime message now distinguishes the two causes and the PR argues at length that conflating them is misleading — but the new `docs/gits-sbx/run-proof.md` section and the tool's `--help` still describe zero-tests as one cause only ("ningún XML posterior al arranque en `<árbol>/<módulo>/build/outputs/androidTest-results`"). A run that leaves `<testsuite tests="0">` or an XML without `<testcase>` is refused with the other message. One sentence in each place ("o XML sin ningún `<testcase>`") closes it. - **C-2 (C1 follow-up needs an owner).** The deferred list is accurate — I spot-checked all of it: `core/manifests/apps/sbx-gits/README.md` lines 755/762 and `docs/gits-sbx/t7-emu-smoke.md` lines 795/836 all show `run --epic … --tree …` with no `--task` and would now be refused (rc 2), and so does the **live** `record` die text (`La unica forma de anadir una linea es EJECUTAR la suite: '<prog> run --epic <E> --tree <checkout>'`) — a user-facing instruction that now always fails. Please make sure the follow-up carries a Plane id; the PR text names files but no ticket. Minor: the header's `record --epic X --rc 0 --result OK …` example is historical framing ("why record is gone"), not a stale instruction — only the `die` text is. - **C-3 (note, not a finding).** `(f2)`'s message still reads "no hay ni un caso en los XML" for a line that may have carried no XML at all; harmless (the line is refused either way) and out of scope, but the same distinction C2 makes in `run` could be made here too. ## Limits of this review Read-only: I ran the tool and the harness against throwaway fixtures/worktrees, I did not edit, commit, push or merge, and this comment is my only write. I did not see any other round-2 verdict, and no emulator, Gradle or cluster was involved — the two refusals and the label are proven against fake suites and a fake repo, the same way section 22 of the harness proves them.
Collaborator

The blocker raised in review 22723 is resolved on head ea6993cb14.
The blocker raised in review 22768 is resolved on head ea6993cb14.
The blocker raised in review 22789 is resolved on head ea6993cb14.

The blocker raised in review 22723 is resolved on head ea6993cb149d9a32484bb664101cf2a1d2b264f4. The blocker raised in review 22768 is resolved on head ea6993cb149d9a32484bb664101cf2a1d2b264f4. The blocker raised in review 22789 is resolved on head ea6993cb149d9a32484bb664101cf2a1d2b264f4.
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!874
No description provided.