# Testing

El registry tiene dos niveles de tests:

- **Unit tests** (`unit_tests` en `registry.db`) — tests individuales extraidos automaticamente de los archivos de test de cada funcion.
- **E2E tests** (`e2e_tests` en `operations.db`) — tests de integracion que verifican como las funciones se componen dentro de una app.

---

## Unit tests

`fn index` lee cada archivo de test referenciado por `test_file_path` en las funciones testeadas, extrae los test cases individuales con su codigo, y los inserta en la tabla `unit_tests`.

### Tabla `unit_tests`

| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | string | `{function_id}_t{n}` (ej: `filter_slice_go_core_t0`) |
| `function_id` | string | FK a `functions.id` |
| `name` | string | Nombre del test case |
| `code` | string | Codigo fuente completo del test |
| `file_path` | string | Ruta relativa al archivo de test |
| `lang` | string | Lenguaje (go, py, bash) |
| `created_at` | datetime | Fecha de indexacion |
| `updated_at` | datetime | Fecha de ultima indexacion |

FTS5 disponible sobre `id`, `name`, `code`, `function_id`, `lang`.

### Consultas utiles

```bash
# Todos los tests de una funcion
sqlite3 registry.db "SELECT id, name FROM unit_tests WHERE function_id = 'filter_slice_go_core';"

# Buscar tests por contenido (FTS5)
sqlite3 registry.db "SELECT id, function_id, name FROM unit_tests WHERE id IN (SELECT id FROM unit_tests_fts WHERE unit_tests_fts MATCH 'retry') LIMIT 10;"

# Tests por lenguaje
sqlite3 registry.db "SELECT lang, COUNT(*) FROM unit_tests GROUP BY lang;"

# Ver codigo de un test
sqlite3 registry.db "SELECT code FROM unit_tests WHERE id = 'cache_decorator_py_core_t0';"
```

---

## Convenciones de test por lenguaje

El parser automatico de `fn index` detecta test cases segun el lenguaje. Para que los tests se extraigan correctamente, seguir estas convenciones.

### Go

Convencion estandar de Go. El parser detecta funciones `func TestXxx(t *testing.T)`:

```go
func TestFilterSlice(t *testing.T) {
    t.Run("filtra pares", func(t *testing.T) {
        got := FilterSlice([]int{1, 2, 3, 4, 5}, func(n int) bool { return n%2 == 0 })
        if len(got) != 2 || got[0] != 2 || got[1] != 4 {
            t.Errorf("got %v, want [2 4]", got)
        }
    })

    t.Run("slice vacio retorna vacio", func(t *testing.T) {
        got := FilterSlice([]int{}, func(n int) bool { return true })
        if len(got) != 0 {
            t.Errorf("got %v, want []", got)
        }
    })
}
```

**Deteccion:** `^func (Test\w+)\s*\(` — cada `func Test...` es un test case. Los subtests (`t.Run`) se incluyen dentro del codigo del test padre.

**Archivo:** `{domain}/{name}_test.go` (convencion Go estandar).

### Python

Convencion estandar de pytest. El parser detecta funciones `def test_xxx(`:

```python
def test_funcion_llamada_una_vez(store):
    calls = []

    @cache_decorator(store, ttl=60)
    def compute(x: int) -> int:
        calls.append(x)
        return x * 10

    assert compute(5) == 50
    assert compute(5) == 50
    assert len(calls) == 1


def test_ttl_expirado(store):
    # ...
```

**Deteccion:** `^def (test_\w+)\s*\(` — cada funcion top-level `def test_...` es un test case. El codigo incluye todo hasta la siguiente `def test_` o fin de archivo.

**Archivo:** `{domain}/{name}_test.py`.

### Bash

Bash no tiene framework estandar de testing. El parser soporta tres convenciones, en orden de prioridad:

#### Opcion 1: funciones `test_xxx()` (preferida)

La mas explicita y la que mejor se parsea:

```bash
#!/usr/bin/env bash
source "$(dirname "$0")/mi_funcion.sh"

PASS=0; FAIL=0

assert_eq() {
    local name="$1" got="$2" want="$3"
    if [ "$got" = "$want" ]; then echo "  PASS: $name"; ((PASS++))
    else echo "  FAIL: $name (got='$got', want='$want')"; ((FAIL++)); fi
}

test_caso_basico() {
    local got
    got=$(mi_funcion "input")
    assert_eq "caso basico" "$got" "expected"
}

test_caso_vacio() {
    local got
    got=$(mi_funcion "")
    assert_eq "input vacio" "$got" ""
}

# Ejecutar todos los tests
test_caso_basico
test_caso_vacio

echo "Resultados: $PASS passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1
```

**Deteccion:** `^(test_\w+)\s*\(\)\s*\{` — cada funcion `test_xxx() { ... }` es un test case.

#### Opcion 2: secciones `=== nombre ===`

Para tests que agrupan multiples asserts bajo secciones nombradas:

```bash
#!/usr/bin/env bash
source "$(dirname "$0")/mi_funcion.sh"

echo "=== caso basico ==="

got=$(mi_funcion "input")
assert_eq "retorna expected" "$got" "expected"

echo "=== caso edge ==="

got=$(mi_funcion "")
assert_eq "input vacio" "$got" ""

echo "=== errores ==="

assert_fail "input invalido" mi_funcion "--bad"
```

**Deteccion:** `^(echo\s+["'])?===\s*(\w[\w\s]*\w)\s*===(["'])?\s*$` — cada linea con `=== nombre ===` (con o sin `echo`) abre una seccion. El nombre debe contener al menos dos caracteres alfanumericos (las lineas de separacion puras como `======` se ignoran).

#### Opcion 3: comentarios `# Test:`

Para scripts simples donde cada test se marca con un comentario:

```bash
#!/usr/bin/env bash
source "$(dirname "$0")/mi_funcion.sh"

# Test: caso basico
got=$(mi_funcion "input")
[ "$got" = "expected" ] || { echo "FAIL"; exit 1; }

# Test: input vacio
got=$(mi_funcion "")
[ "$got" = "" ] || { echo "FAIL"; exit 1; }
```

**Deteccion:** `^#\s*[Tt]est:\s*(.+)` — cada comentario `# Test: nombre` abre un bloque.

#### Recomendacion

Usar **opcion 1** (funciones `test_xxx()`) para tests nuevos. Es la mas explicita, cada test esta aislado en su propia funcion, y se parsea sin ambiguedad.

La **opcion 2** (secciones `===`) es aceptable cuando ya existe el patron en el archivo (como `pass_test.sh`).

**Archivo:** `{domain}/{name}_test.sh`.

---

## E2E tests

Los e2e tests viven en `operations.db` de cada app. No se extraen automaticamente — se crean manualmente o por el bucle reactivo cuando se necesita verificar que un flujo end-to-end funciona.

### Tabla `e2e_tests`

| Campo | Tipo | Descripcion |
|---|---|---|
| `id` | string | Identificador unico |
| `name` | string | Nombre descriptivo del test |
| `description` | string | Que verifica este test |
| `relation_id` | string | FK a `relations.id` — que pipeline/relacion prueba |
| `steps` | []string | Funciones involucradas en orden |
| `input_fixture` | JSON | Datos de entrada para el test |
| `expected` | JSON | Resultado esperado |
| `last_status` | string | pass, fail, skip, o vacio |
| `last_run_at` | datetime | Ultima ejecucion |
| `execution_id` | string | Referencia a la ejecucion que lo corrio |
| `duration_ms` | int | Duracion en milisegundos |
| `created_at` | datetime | Fecha de creacion |
| `updated_at` | datetime | Ultima actualizacion |

FTS5 disponible sobre `id`, `name`, `description`, `steps`.

### Diferencia con assertions

| | Assertions | E2E tests |
|---|---|---|
| **Que son** | Reglas declarativas sobre datos | Ejecuciones concretas de flujos |
| **Cuando corren** | En cada ejecucion del bucle reactivo | Bajo demanda o en CI |
| **Sobre que** | Una entity (`precio > 0`) | Un flujo completo (input → pipeline → output) |
| **Resultado** | pass/fail sobre el valor actual | pass/fail comparando output vs expected |

### Ejemplo de uso

```bash
# Crear un e2e test para un pipeline
fn ops e2e add --name "metabase_setup_completo" \
  --relation-id "rel_setup_metabase" \
  --steps '["docker_pull_image_go_infra","init_metabase_go_pipelines"]' \
  --input '{"project":"test"}' \
  --expected '{"status":"running"}'

# Listar e2e tests
fn ops e2e list

# Ver resultado
fn ops e2e show <id>
```
