--- id: "0167" title: "fn run de library function Go ejecuta go test del paquete entero (arrastra tests flaky vecinos)" status: completado type: enhancement domain: - registry-quality scope: registry-only priority: media depends: [] blocks: [] related: ["0077"] created: 2026-06-03 updated: 2026-06-03 tags: [fn-run, go, testing, flaky, dag-engine, reliability] --- # 0167 — fn run de library function Go ejecuta go test del paquete entero ## APP Metadata | Campo | Valor | |-------|-------| | **ID** | 0167 | | **Estado** | pendiente | | **Prioridad** | media | | **Tipo** | enhancement — dispatcher de `fn run` | ## Contexto Cuando `fn run ` recibe una **library function Go sin `main.go`** que tiene tests declarados (`tested: true` + `test_file_path`), el dispatcher (`cmd/fn/run.go:171-181`) ejecuta: ``` go test -v -count=1 -tags fts5 ./functions/ # el PAQUETE ENTERO ``` Es decir, no ejecuta "la función" (no se puede: no tiene `main`), sino que corre **todos los tests del paquete**. Consecuencia: el éxito de `fn run miFuncion` depende de que pasen los tests de **todas las demás funciones del mismo paquete**, no solo los suyos. ### Cómo se manifestó Los DAGs `daily-registry-audit` y `weekly-deep-scan` del `dag_engine` invocaban funciones `*_go_infra` (`find_unused_functions`, `artefact_doctor`, etc.) como `function:` steps. Cada step disparaba `go test ./functions/infra` (paquete completo), que contiene tests impuros con recursos fijos: - `TestSSHTunnelOpenClose` → `bind [127.0.0.1]:19876: Address already in use` - `TestDockerContainerExec` → `listen unix .../docker_exec_test.sock: bind: invalid argument` (path de socket > 108 chars con TMPDIR largo) Al correr dos `function:` steps en paralelo (ambos `depends` del mismo padre), las dos invocaciones de `go test ./functions/infra` colisionaban en el **puerto fijo 19876** → una pasaba y la otra fallaba de forma no determinista. Resultado: el DAG fallaba sin auditar nada, y el fallo parecía "la auditoría encontró un problema" cuando en realidad era un test de red vecino. > Nota: el síntoma operativo en los DAGs ya se resolvió por otra vía (2026-06-03): los > steps ahora usan `audit_doctor_snapshot_bash_infra` (Bash), que ejecuta `fn doctor ` > real en vez de `go test` del paquete. Este issue es la **causa raíz general** del > dispatcher, que sigue afectando a cualquier `fn run `. ## Problema 1. `fn run` de una library function NO ejecuta la función — corre el paquete de test entero. 2. Los tests impuros de un paquete (puertos/sockets/red fijos) no son seguros para ejecuciones concurrentes ni reproducibles en cualquier entorno (TMPDIR, CI). 3. Un único test flaky en `functions/infra` rompe `fn run` de las ~N funciones testeadas del paquete, y por extensión cualquier DAG/cron que las invoque. ## Opciones de solución (decidir en implementación) ### Opción A — library Go sin main → siempre compile-check (`go vet`/`go build`) `fn run ` significa "verifica que la función va"; para código sin `main` eso es "compila". Testear es responsabilidad de `go test` / CI, no de `fn run` en un cron. - **Pro**: determinista, rápido, elimina el flaky de raíz. - **Contra**: rompe el comportamiento documentado en `CLAUDE.md` ("`fn run filter_slice_go_core` → Go function con tests → `go test -v`"). Perderíamos la capacidad de correr los tests de una función vía `fn run`. ### Opción B — go test acotado con `-run` a los tests de la función Si la función declara sus tests, ejecutar solo esos: ``` go test -v -count=1 -tags fts5 -run '^(TestX|TestY)$' ./functions/ ``` - **Pro**: aísla del flaky vecino manteniendo "fn run corre mis tests". - **Contra / RIESGO**: si los nombres de `fn.Tests` (frontmatter YAML, `registry/parser.go:32`) tienen **drift** respecto al código, `-run` no matchea y `go test` sale 0 con "no tests to run" → **falso-verde** en una primitiva crítica de todo el ecosistema. Mitigación obligatoria si se elige B: reconciliar `fn.Tests` con los tests extraídos por el indexer (`registry/test_parser.go::parseGoTests`, que ya puebla `unit_tests`) y/o detectar "0 tests ejecutados" parseando el output y tratarlo como fallo. ### Opción C — aislar los tests impuros del paquete Hacer robustos los tests culpables: puerto efímero (`:0` en vez de `19876`), socket en path corto bajo `/tmp` con nombre acotado, `t.Parallel`-safe. No cambia el dispatcher pero reduce la probabilidad de colisión. - **Pro**: no toca `fn run` (cero blast radius sistémico). - **Contra**: no resuelve el problema conceptual (sigue corriendo el paquete entero); otros paquetes pueden introducir tests impuros nuevos y reincidir. ## Recomendación Combinar **C** (saneamiento inmediato de `TestSSHTunnelOpenClose` y `TestDockerContainerExec`, bajo riesgo) con **B** endurecida (acotar `-run` + guard anti-falso-verde apoyado en `unit_tests` extraídos, no en el frontmatter manual). La Opción A es la más limpia conceptualmente pero rompe comportamiento documentado; evaluar si ese comportamiento ("fn run corre los tests") aún se usa de verdad o puede deprecarse hacia `go test` directo. ## Definition of Done | Escenario | Tipo | Comando / evidencia | Resultado esperado | |---|---|---|---| | Golden: `fn run` de library fn testeada | e2e | `./fn run find_unused_functions_go_infra` | exit 0 sin depender de tests de funciones vecinas | | Edge: dos `fn run` concurrentes del mismo paquete | e2e | dos invocaciones en paralelo de funciones de `functions/infra` | ambas exit 0, sin colisión de puerto/socket | | Error: nombres de test con drift (si se elige B) | unit | `fn.Tests` con un nombre inexistente | NO produce falso-verde (se detecta "0 tests run" → fallo) | | Tests impuros saneados | unit | `go test -run 'TestSSHTunnelOpenClose\|TestDockerContainerExec' ./functions/infra` repetido 5× | 5/5 PASS deterministas | ## Resolución (2026-06-03) Implementada la combinación **C + B** recomendada. ### C — Tests impuros saneados (`functions/infra/`) - `ssh_tunnel_test.go`: el puerto fijo `19876` pasa a **puerto efímero** (`freeTCPPort` pide `:0` al kernel). Elimina el `bind: address already in use` bajo concurrencia. - `docker_container_exec_test.go`: el socket Unix deja de colgar de `t.TempDir()` (path largo con el nombre del subtest) y usa un **directorio corto** bajo `/tmp` (`os.MkdirTemp("/tmp", "dk")` + cleanup). Elimina el `bind: invalid argument` por exceder los ~108 bytes de `sun_path`. - Verificado: `go test -run 'TestSSHTunnelOpenClose|TestDockerContainerExec' -count=5 ./functions/infra/` → `ok` (5×, determinista). ### B — `fn run` acota los tests a la función (`cmd/fn/run.go`) - Para una library Go function con tests, el dispatcher ahora añade `-run '^()$'` con los nombres **extraídos por el indexer** (`unit_tests`, vía `db.GetUnitTestsByFunction`), no los del frontmatter `.md` (que pueden driftar). Así `fn run` ejecuta solo los tests de esa función, aislándola de tests flaky de funciones vecinas del mismo paquete. Si no hay nombres extraídos, cae al paquete entero (comportamiento previo). - **Guard anti-falso-verde**: `cmdRun` refleja el output de un `go test -run` a un buffer; si go test reporta `no tests to run` (que sale con exit 0), el run se trata como **fallo** (exit 1 + mensaje pidiendo `fn index`). Evita que un drift de nombres produzca un verde silencioso. ### Evidencia (DoD) | Escenario | Resultado | |---|---| | Golden: `fn run find_unused_functions_go_infra` | Corre solo sus 2 tests (`TestFindUnusedFunctions_*`) en 0.06s, exit 0. No toca SSH/Docker. | | Edge concurrente: 2 `fn run` del paquete `infra` en paralelo | Ambos exit 0, sin colisión de puerto. | | Error/drift: `unit_tests` con nombre inexistente | `go test` da `[no tests to run]`; el guard lo intercepta → exit 1 con mensaje. NO falso-verde. | | Tests saneados 5× | `ok` determinista. | `go vet ./cmd/fn/` y `go test ./cmd/fn/` verdes tras los cambios. ## Notas - Archivos clave: `cmd/fn/run.go` (dispatcher, líneas 145-194), `registry/parser.go` (campo `Tests`), `registry/test_parser.go` (extracción de nombres de test), `functions/infra/ssh_tunnel_open_close_test.go` y `functions/infra/docker_container_exec_test.go` (tests culpables). - Relacionado con 0077 (fn-run-bash-output-mudo): familia de issues sobre la semántica y observabilidad de `fn run`.