fa09ff9866
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
8.3 KiB
Markdown
163 lines
8.3 KiB
Markdown
---
|
||
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 <id>` 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/<domain> # 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 <sub>`
|
||
> 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 <library_go_fn_con_tests>`.
|
||
|
||
## 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 <lib_fn>` 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/<domain>
|
||
```
|
||
|
||
- **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 '^(<tests>)$'` 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`.
|