# proposals

id: proposal_1775083169379485942
kind: improve_function
target_id: bar_chart_typescript_ui
title: bar_chart: horizontal mode no intercambia ejes XAxis/YAxis
evidence: {}
status: implemented
created_by: reactive_loop
reviewed_by: fn-constructor
created_at: 2026-04-01T22:39:29Z
updated_at: 2026-04-01T22:48:22Z
description:
  Al usar options.horizontal=true, el componente solo pasa layout='vertical' al BarChart de Recharts pero no intercambia XAxis/YAxis. Las barras horizontales no se renderizan.

  Fix: cuando horizontal=true, usar YAxis con dataKey={xKey} type="category" y XAxis type="number". Ajustar radius de barras a [0,4,4,0].

  Evidencia: descubierto al replicar dashboard Metabase en rapid_dashboards. Fix aplicado en apps/rapid_dashboards/frontend/src/components/widgets/BarChartWidget.tsx.


id: proposal_1775083177926727090
kind: new_function
title: pie_chart: componente PieChart Recharts para dashboards
evidence: {}
status: implemented
created_by: reactive_loop
reviewed_by: fn-constructor
created_at: 2026-04-01T22:39:37Z
updated_at: 2026-04-01T22:48:22Z
description:
  No existe un componente pie_chart en el registry. Es necesario para dashboards donde bar_chart no comunica bien proporciones (distribuciones por dominio, lenguaje, purity).

  Specs: PieChart Recharts con Cell por segmento, colores automaticos, labels con nombre+porcentaje, Legend, Tooltip tematico, soporte donut (innerRadius), conversion automatica de valores a number.

  Referencia: implementado en apps/rapid_dashboards/frontend/src/components/widgets/PieChartWidget.tsx. Extraer como componente reutilizable en frontend/functions/ui/.


id: proposal_1775083183562379637
kind: new_function
title: data_table: tabla con heatmap, sorting y formato condicional
evidence: {}
status: implemented
created_by: reactive_loop
reviewed_by: fn-constructor
created_at: 2026-04-01T22:39:43Z
updated_at: 2026-04-01T22:48:22Z
description:
  No existe un componente data_table reutilizable en el registry. Las tablas de datos son fundamentales para dashboards y TUIs.

  Specs: tabla con columnas configurables, heatmap por columna (colorea celdas segun intensidad numerica), sticky header, overflow scroll, formato por columna (number, datetime, currency), hover rows. Prop heatmap_columns: string[] para activar heatmap selectivo.

  Referencia: implementado en apps/rapid_dashboards/frontend/src/components/widgets/TableWidget.tsx con heatmap funcional.


id: proposal_1775083192134382057
kind: improve_function
target_id: kpi_card_typescript_ui
title: kpi_card: permitir eliminar border y shadow via className
evidence: {}
status: implemented
created_by: reactive_loop
reviewed_by: fn-constructor
created_at: 2026-04-01T22:39:52Z
updated_at: 2026-04-01T22:48:22Z
description:
  KPICard tiene border hardcodeado en className ('rounded-lg border bg-card p-4'). En dashboards dark donde las cards no llevan borde, no hay forma limpia de eliminarlo sin className override.

  Fix: usar cn() para merge de clases, permitiendo que className="border-0 shadow-none" sobreescriba los defaults. Ya funciona si el componente usa cn() correctamente, pero documentar el patron.

  Evidencia: en rapid_dashboards los KPIs con borde blanco rompian la estetica dark. Workaround: className="border-0 shadow-none".


id: proposal_1775083198546778981
kind: improve_function
target_id: card_ts_ui
title: card: variante borderless/ghost para dashboards dark
evidence: {}
status: implemented
created_by: reactive_loop
reviewed_by: fn-constructor
created_at: 2026-04-01T22:39:58Z
updated_at: 2026-04-01T22:48:22Z
description:
  El componente Card tiene border por defecto via CSS. En dashboards dark los bordes blancos/claros rompen la estetica. Se necesita una variante borderless o ghost.

  Fix: anadir prop variant='default'|'borderless'|'ghost' que controle border y shadow. O exponer data-slot para override CSS global. Actualmente se usa [data-slot="card"] { border: none } como workaround.

  Evidencia: rapid_dashboards requirio CSS global para eliminar bordes de todas las cards.


id: proposal_1775083211082150334
kind: improve_function
target_id: sqlite_open_go_infra
title: sqlite_open: resolver paths relativos desde un base_dir configurable
evidence: {}
status: implemented
created_by: reactive_loop
reviewed_by: fn-constructor
created_at: 2026-04-01T22:40:11Z
updated_at: 2026-04-01T22:48:22Z
description:
  SQLiteOpen recibe un path que se resuelve relativo al cwd del proceso. Cuando la config (YAML) define paths relativos a su propia ubicacion, el resultado es incorrecto si el binario corre desde otro directorio.

  Propuesta: anadir parametro opcional basePath. Si se provee, resolver path relativo a basePath en lugar de cwd. filepath.Join(basePath, path) si path no es absoluto.

  Evidencia: en rapid_dashboards, el YAML tenia path: ../../../registry.db (relativo al YAML en examples/) pero el binario corria desde apps/rapid_dashboards/. Resultado: abria un SQLite vacio sin tablas.


id: proposal_1775083218894134980
kind: new_function
title: widget_renderer: componente generico de fetch+render+countdown para widgets de dashboard
evidence: {}
status: pending
created_by: reactive_loop
created_at: 2026-04-01T22:40:18Z
updated_at: 2026-04-01T22:40:18Z
description:
  Componente React que encapsula: fetch de datos via callback, manejo de loading/error, auto-refetch por intervalo, countdown visual hasta proximo refresh, y delegacion de render a un widget registrado.

  Specs: recibe widgetDef, queryDef (con refreshMs), filters, getData callback. Muestra countdown en esquina superior derecha. Cache de datos por JSON serialization para evitar re-renders innecesarios.

  Referencia: implementado en apps/rapid_dashboards/frontend/src/components/WidgetRenderer.tsx.


id: proposal_1775083225381497218
kind: new_function
title: dashboard_shell: layout de dashboard con header, filtros y secciones grid
evidence: {}
status: pending
created_by: reactive_loop
created_at: 2026-04-01T22:40:25Z
updated_at: 2026-04-01T22:40:25Z
description:
  Componente React de layout para dashboards: header con titulo, FilterBar interactivo (select, date_range, text con debounce), secciones colapsables con grid CSS responsive.

  Specs: recibe DashboardConfig, filter state, getData callback. Cada seccion tiene su propio grid columns override. Widgets se posicionan con span/row_span. Layout denso para maximizar datos visibles.

  Referencia: implementado en apps/rapid_dashboards/frontend/src/components/DashboardShell.tsx + Section.tsx + FilterBar.tsx.


# apps

id: docker_tui_go_infra
name: docker_tui
lang: go
domain: infra
description: TUI interactiva para gestion de contenedores, imagenes, volumenes y redes Docker.
tags: ["docker","tui","bubbletea","containers"]
uses_types: []
framework: bubbletea
entry_point: main.go
dir_path: apps/docker_tui
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T11:22:32Z
content_hash: fc380ff9c4f3fa460c6a8894b7a050d9dc3ea999aa17520b7f18f8be00812abf
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/docker_tui
uses_functions:
  ["docker_pull_image_go_infra","docker_list_containers_go_infra","docker_remove_container_go_infra","docker_stop_container_go_infra","docker_start_container_go_infra","docker_list_images_go_infra","docker_remove_image_go_infra","docker_remove_network_go_infra","docker_create_network_go_infra","docker_inspect_container_go_infra","docker_run_container_go_infra","docker_container_logs_go_infra"]

notes:
  Aplicacion TUI con pestanas para contenedores, imagenes, volumenes, redes y compose. Construida con Bubble Tea (Charmbracelet).


id: metabase_registry_py_analytics
name: metabase_registry
lang: py
domain: analytics
tags: ["metabase","dashboard","analytics","visualization","operations"]
uses_types: []
framework: httpx
entry_point: main.py
dir_path: apps/metabase_registry
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T11:22:32Z
content_hash: 49e8c15fbc68b6514e6b71a5ffff4895a41537686405b8890131f36f4bfa04bf
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/metabase_registry
description:
  Setup y dashboards automaticos de Metabase para visualizar metricas del fn-registry y operations.db de cada app.

uses_functions:
  ["metabase_auth_py_infra","metabase_create_card_py_infra","metabase_create_dashboard_py_infra","metabase_update_dashboard_py_infra","metabase_list_databases_py_infra","metabase_add_database_py_infra","metabase_list_dashboards_py_infra","metabase_delete_dashboard_py_infra","metabase_create_user_py_infra"]

documentation:
  ## Arquitectura

  Metabase corre en Docker (`fn_registry-metabase`) con Postgres como backend interno.
  Las bases de datos SQLite del proyecto se montan como bind mounts RW en `/data/`:

  | Database | Mount en container | Contenido |
  |----------|-------------------|-----------|
  | registry.db | `/data/registry/registry.db` | functions, types, proposals, apps |
  | ops-docker-tui | `/data/ops-docker-tui/operations.db` | entities, relations, executions |
  | ops-metabase-registry | `/data/ops-metabase-registry/operations.db` | entities, relations, executions |
  | ops-pipeline-launcher | `/data/ops-pipeline-launcher/operations.db` | entities, relations, executions |

  ## Dashboards

  | Dashboard | Contenido |
  |-----------|-----------|
  | fn-registry Overview | KPIs, distribucion y analisis del registry |
  | fn-registry Apps | Apps por lenguaje, dominio, dependencias |
  | ops: \<app-name\> | Dashboard operativo por app (entities, relations, executions, assertions) |

  ## Permisos SQLite en Docker

  Metabase corre Java como UID 2000 (usuario `metabase`). SQLite necesita crear journal/WAL
  files en el mismo directorio que la BD. Reglas:

  - **NUNCA** hacer `chown` dentro del container: se propaga al host via bind mount y rompe permisos locales.
  - **Usar `chmod`**: `chmod 777` en directorios, `chmod 666` en archivos `.db`.
  - **Pipeline automatico**: `./fn run metabase_fix_permissions` arregla todos los permisos.
  - **Ejecutar despues de**: recrear container, añadir nueva database, o ver error `SQLITE_READONLY_DIRECTORY`.

  ## Flujo para app nueva

  ```bash
  # 1. Crear operations.db
  ./fn ops init apps/nueva_app

  # 2. Recrear container con el nuevo mount
  docker stop fn_registry-metabase && docker rm fn_registry-metabase
  docker run -d \
    --name fn_registry-metabase \
    --network fn_registry-net \
    -p 3000:3000 \
    -e MB_DB_TYPE=postgres -e MB_DB_DBNAME=metabase \
    -e MB_DB_PORT=5432 -e MB_DB_USER=metabase \
    -e MB_DB_PASS=metabase -e MB_DB_HOST=fn_registry-postgres \
    -v /home/lucas/fn_registry:/registry:ro \
    -v /home/lucas/fn_registry/registry.db:/data/registry/registry.db \
    -v /home/lucas/fn_registry/apps/docker_tui:/data/ops-docker-tui \
    -v /home/lucas/fn_registry/apps/metabase_registry:/data/ops-metabase-registry \
    -v /home/lucas/fn_registry/apps/pipeline_launcher:/data/ops-pipeline-launcher \
    -v /home/lucas/fn_registry/apps/nueva_app:/data/ops-nueva-app \
    metabase/metabase:latest

  # 3. Fix permisos
  ./fn run metabase_fix_permissions

  # 4. Registrar database en Metabase
  ./fn run metabase_add_ops_db nueva_app

  # 5. Crear dashboard operativo
  ./fn run metabase_create_ops_dashboard nueva_app
  ```

  ## Scripts

  | Script | Funcion |
  |--------|---------|
  | `main.py` | Setup inicial: datasource + cards basicas + dashboard Overview |
  | `create_registry_dashboard.py` | Dashboard fn-registry Overview (18 cards) |
  | `create_apps_dashboard.py` | Dashboard fn-registry Apps (10 cards) |

  ## Pipelines relacionados

  | Pipeline | ID | Funcion |
  |----------|-----|---------|
  | `metabase_add_ops_db` | `metabase_add_ops_db_py_pipelines` | Registra operations.db de una app |
  | `metabase_create_ops_dashboard` | `metabase_create_ops_dashboard_py_pipelines` | Crea dashboard operativo para una app |
  | `metabase_fix_permissions` | `metabase_fix_permissions_py_pipelines` | Arregla SQLITE_READONLY_DIRECTORY |

  ## Credenciales

  En `.env` local (NO commitear).


id: pipeline_launcher_go_tools
name: pipeline_launcher
lang: go
domain: tools
description: TUI para lanzar y monitorear pipelines del fn-registry con historial de ejecuciones.
tags: ["pipeline","tui","bubbletea","runner","launcher"]
uses_functions: []
uses_types: []
framework: bubbletea
entry_point: main.go
dir_path: apps/pipeline_launcher
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T11:22:32Z
content_hash: 83544a964e6fdd49ce628466ffe96492fc5f157bd1d98a08ce4c58d9e08ff92c
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/pipeline_launcher
notes:
  Aplicacion TUI que lista pipelines con tag `launcher` del registry, permite ejecutarlos y muestra historial de ejecuciones desde operations.db.


id: script_navegador_go_infra
name: script_navegador
lang: go
domain: infra
tags: ["cdp","chrome","browser","automation","yaml"]
uses_types: []
entry_point: main.go
dir_path: apps/script_navegador
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T11:22:32Z
content_hash: 8194a844f0ffd603c008ae7348913745943e262595c167bd9cc53be7497584e2
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/script_navegador
description:
  Ejecutor de scripts de navegador CDP sobre Chrome. Lee pasos desde YAML y los ejecuta en secuencia registrando cada resultado en operations.db.

uses_functions:
  ["chrome_launch_go_browser","cdp_connect_go_browser","cdp_navigate_go_browser","cdp_click_go_browser","cdp_type_text_go_browser","cdp_wait_element_go_browser","cdp_evaluate_go_browser","cdp_get_html_go_browser","cdp_screenshot_go_browser","cdp_close_go_browser"]

documentation:
  ## Descripcion

  CLI Go que lee un archivo YAML con pasos de navegacion CDP y los ejecuta sobre Chrome, registrando cada paso y su resultado en `operations.db`.

  ## Uso

  ```bash
  # Conectarse a Chrome ya corriendo en puerto 9222
  go run . --script examples/busqueda_google.yaml

  # Lanzar Chrome nuevo (headless)
  go run . --script examples/busqueda_google.yaml --launch --headless

  # Puerto personalizado
  go run . --script examples/busqueda_google.yaml --port 9333
  ```

  ## Formato del script YAML

  ```yaml
  name: "nombre_del_script"
  steps:
    - action: navigate
      url: "https://ejemplo.com"

    - action: wait
      selector: "#elemento"
      timeout_ms: 5000           # opcional, default 10000

    - action: click
      selector: "#boton"
      continue_on_error: true    # opcional, default false

    - action: type
      selector: "input[name=q]"  # hace click primero para enfocar
      text: "texto a escribir"

    - action: screenshot
      path: "/tmp/captura.png"
      full_page: false           # opcional, default false

    - action: evaluate
      expr: "document.title"

    - action: get_html
      # sin parametros adicionales

    - action: sleep
      ms: 500                    # pausa en milisegundos
  ```

  ## Acciones soportadas

  | Accion      | Parametros obligatorios | Parametros opcionales         |
  |-------------|-------------------------|-------------------------------|
  | `navigate`  | `url`                   |                               |
  | `wait`      | `selector`              | `timeout_ms` (default 10000)  |
  | `click`     | `selector`              | `continue_on_error`           |
  | `type`      | `selector`, `text`      | `continue_on_error`           |
  | `screenshot`| `path`                  | `full_page`, `continue_on_error` |
  | `evaluate`  | `expr`                  | `continue_on_error`           |
  | `get_html`  | —                       | `continue_on_error`           |
  | `sleep`     | `ms`                    |                               |

  ## Registro en operations.db

  - **Entity `script_run`**: una por ejecucion del script, con metadata del script y resultado final
  - **Execution**: una por ejecucion, con `pipeline_id = "script_navegador"`, duration_ms, records_in=pasos totales, records_out=pasos exitosos
  - **Logs**: un log por cada paso ejecutado con nivel info/error

notes:
  - Si Chrome no esta corriendo y no se pasa `--launch`, la conexion falla con error claro
  - `continue_on_error: true` por paso permite continuar aunque ese paso falle
  - Flag global `--abort-on-error` (default false) aborta todo el script al primer error
  - Al terminar (exito o error), siempre se ejecuta `cdp_close` para limpiar recursos
  - operations.db se inicializa automaticamente si no existe usando `fn ops init`


# analysis

id: finanzas_personales_py_finance
name: finanzas_personales
lang: py
domain: finance
description: Exploracion y analisis de finanzas personales con Jupyter notebooks.
tags: ["finanzas","jupyter","pandas","matplotlib"]
uses_functions: []
uses_types: []
framework: jupyterlab
entry_point: notebooks/
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/finanzas_personales
dir_path: analysis/finanzas_personales
content_hash: 0c5339b48ec7ec8294308c8e5c100483e15aa16a36580ef1d605818c6775a69a
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T11:22:32Z
notes:
  Analisis independiente de finanzas personales usando Jupyter Lab con pandas, numpy y matplotlib.
  Incluye kernel startup con acceso al registry y gestion de dependencias con uv.


# unit_tests

id: cdp_connect_go_browser_t0
function_id: cdp_connect_go_browser
name: TestFindChrome
file_path: functions/browser/chrome_launch_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestFindChrome(t *testing.T) {
  	path, err := findChrome()
  	if err != nil {
  		t.Skipf("Chrome no encontrado (entorno sin Chrome): %v", err)
  	}
  	if path == "" {
  		t.Error("findChrome retorno path vacio")
  	}
  	t.Logf("Chrome encontrado en: %s", path)
  }

  // TestChromeLaunchAndConnect lanza Chrome, conecta CDP, navega a about:blank y cierra.


id: cdp_connect_go_browser_t1
function_id: cdp_connect_go_browser
name: TestChromeLaunchAndConnect
file_path: functions/browser/chrome_launch_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestChromeLaunchAndConnect(t *testing.T) {
  	if testing.Short() {
  		t.Skip("skip: requiere Chrome real (use -short=false para ejecutar)")
  	}

  	// Verificar que Chrome esta disponible
  	if _, err := findChrome(); err != nil {
  		t.Skipf("Chrome no disponible: %v", err)
  	}

  	opts := ChromeLaunchOpts{
  		Port:        9223, // puerto alternativo para no interferir con sesiones existentes
  		UserDataDir: t.TempDir(),
  		Headless:    true,
  	}

  	pid, err := ChromeLaunch(opts)
  	if err != nil {
  		t.Fatalf("ChromeLaunch: %v", err)
  	}
  	t.Logf("Chrome lanzado con PID %d en puerto %d", pid, opts.Port)

  	defer func() {
  		if err := CdpClose(nil, pid); err != nil {
  			t.Logf("CdpClose (pid only): %v", err)
  		}
  	}()

  	// Conectar CDP
  	conn, err := CdpConnect(opts.Port)
  	if err != nil {
  		t.Fatalf("CdpConnect: %v", err)
  	}
  	defer func() {
  		if err := CdpClose(conn, 0); err != nil {
  			t.Logf("CdpClose (conn): %v", err)
  		}
  	}()

  	// Navegar a about:blank
  	if err := CdpNavigate(conn, "about:blank"); err != nil {
  		t.Fatalf("CdpNavigate about:blank: %v", err)
  	}
  	t.Log("Navegacion a about:blank exitosa")
  }

  // TestCdpEvaluate ejecuta JS simple en Chrome y verifica el resultado.


id: cdp_connect_go_browser_t2
function_id: cdp_connect_go_browser
name: TestCdpEvaluate
file_path: functions/browser/chrome_launch_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCdpEvaluate(t *testing.T) {
  	if testing.Short() {
  		t.Skip("skip: requiere Chrome real (use -short=false para ejecutar)")
  	}

  	if _, err := findChrome(); err != nil {
  		t.Skipf("Chrome no disponible: %v", err)
  	}

  	opts := ChromeLaunchOpts{
  		Port:        9224,
  		UserDataDir: t.TempDir(),
  		Headless:    true,
  	}

  	pid, err := ChromeLaunch(opts)
  	if err != nil {
  		t.Fatalf("ChromeLaunch: %v", err)
  	}
  	defer CdpClose(nil, pid)

  	conn, err := CdpConnect(opts.Port)
  	if err != nil {
  		t.Fatalf("CdpConnect: %v", err)
  	}
  	defer CdpClose(conn, 0)

  	if err := CdpNavigate(conn, "about:blank"); err != nil {
  		t.Fatalf("CdpNavigate: %v", err)
  	}

  	t.Run("expresion aritmetica simple", func(t *testing.T) {
  		result, err := CdpEvaluate(conn, "1 + 2")
  		if err != nil {
  			t.Fatalf("CdpEvaluate: %v", err)
  		}
  		if result != "3" {
  			t.Errorf("esperado '3', got %q", result)
  		}
  	})

  	t.Run("string literal", func(t *testing.T) {
  		result, err := CdpEvaluate(conn, `"hola mundo"`)
  		if err != nil {
  			t.Fatalf("CdpEvaluate: %v", err)
  		}
  		if result != "hola mundo" {
  			t.Errorf("esperado 'hola mundo', got %q", result)
  		}
  	})

  	t.Run("typeof window", func(t *testing.T) {
  		result, err := CdpEvaluate(conn, "typeof window")
  		if err != nil {
  			t.Fatalf("CdpEvaluate: %v", err)
  		}
  		if result != "object" {
  			t.Errorf("esperado 'object', got %q", result)
  		}
  	})
  }

  // TestCdpGetHTML obtiene el HTML de about:blank y verifica que contiene elementos basicos.


id: cdp_connect_go_browser_t3
function_id: cdp_connect_go_browser
name: TestCdpGetHTML
file_path: functions/browser/chrome_launch_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCdpGetHTML(t *testing.T) {
  	if testing.Short() {
  		t.Skip("skip: requiere Chrome real (use -short=false para ejecutar)")
  	}

  	if _, err := findChrome(); err != nil {
  		t.Skipf("Chrome no disponible: %v", err)
  	}

  	opts := ChromeLaunchOpts{
  		Port:        9225,
  		UserDataDir: t.TempDir(),
  		Headless:    true,
  	}

  	pid, err := ChromeLaunch(opts)
  	if err != nil {
  		t.Fatalf("ChromeLaunch: %v", err)
  	}
  	defer CdpClose(nil, pid)

  	conn, err := CdpConnect(opts.Port)
  	if err != nil {
  		t.Fatalf("CdpConnect: %v", err)
  	}
  	defer CdpClose(conn, 0)

  	if err := CdpNavigate(conn, "about:blank"); err != nil {
  		t.Fatalf("CdpNavigate: %v", err)
  	}

  	t.Run("html de about blank contiene html y body", func(t *testing.T) {
  		html, err := CdpGetHTML(conn)
  		if err != nil {
  			t.Fatalf("CdpGetHTML: %v", err)
  		}
  		lower := strings.ToLower(html)
  		if !strings.Contains(lower, "<html") {
  			t.Errorf("HTML no contiene tag <html>: %q", html[:min(200, len(html))])
  		}
  		if !strings.Contains(lower, "<body") {
  			t.Errorf("HTML no contiene tag <body>: %q", html[:min(200, len(html))])
  		}
  		t.Logf("HTML (primeros 200 chars): %s", html[:min(200, len(html))])
  	})
  }

  // TestCdpScreenshot toma un screenshot de about:blank y verifica que se crea el archivo PNG.


id: cdp_connect_go_browser_t4
function_id: cdp_connect_go_browser
name: TestCdpScreenshot
file_path: functions/browser/chrome_launch_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCdpScreenshot(t *testing.T) {
  	if testing.Short() {
  		t.Skip("skip: requiere Chrome real (use -short=false para ejecutar)")
  	}

  	if _, err := findChrome(); err != nil {
  		t.Skipf("Chrome no disponible: %v", err)
  	}

  	opts := ChromeLaunchOpts{
  		Port:        9226,
  		UserDataDir: t.TempDir(),
  		Headless:    true,
  	}

  	pid, err := ChromeLaunch(opts)
  	if err != nil {
  		t.Fatalf("ChromeLaunch: %v", err)
  	}
  	defer CdpClose(nil, pid)

  	conn, err := CdpConnect(opts.Port)
  	if err != nil {
  		t.Fatalf("CdpConnect: %v", err)
  	}
  	defer CdpClose(conn, 0)

  	if err := CdpNavigate(conn, "about:blank"); err != nil {
  		t.Fatalf("CdpNavigate: %v", err)
  	}

  	// Esperar un momento para que la pagina cargue
  	time.Sleep(500 * time.Millisecond)

  	t.Run("screenshot png se crea correctamente", func(t *testing.T) {
  		outputPath := t.TempDir() + "/screenshot.png"
  		err := CdpScreenshot(conn, outputPath, CdpScreenshotOpts{FullPage: false})
  		if err != nil {
  			t.Fatalf("CdpScreenshot: %v", err)
  		}

  		info, err := os.Stat(outputPath)
  		if err != nil {
  			t.Fatalf("screenshot no encontrado: %v", err)
  		}
  		if info.Size() == 0 {
  			t.Error("screenshot vacio (0 bytes)")
  		}
  		t.Logf("Screenshot creado: %s (%d bytes)", outputPath, info.Size())
  	})
  }


id: filter_slice_go_core_t0
function_id: filter_slice_go_core
name: TestFilterSlice
file_path: functions/core/filter_slice_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  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)
  		}
  	})

  	t.Run("ningun match retorna vacio", func(t *testing.T) {
  		got := FilterSlice([]int{1, 3, 5}, func(n int) bool { return n%2 == 0 })
  		if len(got) != 0 {
  			t.Errorf("got %v, want []", got)
  		}
  	})
  }


id: join_by_key_go_core_t0
function_id: join_by_key_go_core
name: TestJoinByKey
file_path: functions/core/join_by_key_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestJoinByKey(t *testing.T) {
  	t.Run("Inner join solo matches", func(t *testing.T) {
  		left := []map[string]any{{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}}
  		right := []map[string]any{{"id": 1, "dept": "eng"}, {"id": 3, "dept": "sales"}}
  		result := JoinByKey(left, right, "id", "inner")
  		if len(result) != 1 {
  			t.Fatalf("got %d rows, want 1", len(result))
  		}
  		if result[0]["id"] != 1 || result[0]["name"] != "Alice" || result[0]["dept"] != "eng" {
  			t.Errorf("unexpected row: %v", result[0])
  		}
  	})

  	t.Run("Left join todos los left con nil para right sin match", func(t *testing.T) {
  		left := []map[string]any{{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}}
  		right := []map[string]any{{"id": 1, "dept": "eng"}}
  		result := JoinByKey(left, right, "id", "left")
  		if len(result) != 2 {
  			t.Fatalf("got %d rows, want 2", len(result))
  		}
  		var alice, bob map[string]any
  		for _, r := range result {
  			if r["id"] == 1 {
  				alice = r
  			} else {
  				bob = r
  			}
  		}
  		if alice["dept"] != "eng" {
  			t.Errorf("alice dept = %v, want eng", alice["dept"])
  		}
  		if bob["dept"] != nil {
  			t.Errorf("bob dept = %v, want nil", bob["dept"])
  		}
  	})

  	t.Run("Right join", func(t *testing.T) {
  		left := []map[string]any{{"id": 1, "name": "Alice"}}
  		right := []map[string]any{{"id": 1, "dept": "eng"}, {"id": 2, "dept": "sales"}}
  		result := JoinByKey(left, right, "id", "right")
  		if len(result) != 2 {
  			t.Fatalf("got %d rows, want 2", len(result))
  		}
  		var eng, sales map[string]any
  		for _, r := range result {
  			if r["id"] == 1 {
  				eng = r
  			} else {
  				sales = r
  			}
  		}
  		if eng["name"] != "Alice" {
  			t.Errorf("eng name = %v, want Alice", eng["name"])
  		}
  		if sales["name"] != nil {
  			t.Errorf("sales name = %v, want nil", sales["name"])
  		}
  	})

  	t.Run("Outer join", func(t *testing.T) {
  		left := []map[string]any{{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}}
  		right := []map[string]any{{"id": 1, "dept": "eng"}, {"id": 3, "dept": "sales"}}
  		result := JoinByKey(left, right, "id", "outer")
  		ids := map[any]bool{}
  		for _, r := range result {
  			ids[r["id"]] = true
  		}
  		if len(ids) != 3 || !ids[1] || !ids[2] || !ids[3] {
  			t.Errorf("outer join ids = %v, want {1, 2, 3}", ids)
  		}
  	})

  	t.Run("Campos duplicados con sufijo _right", func(t *testing.T) {
  		left := []map[string]any{{"id": 1, "name": "Alice", "score": 90}}
  		right := []map[string]any{{"id": 1, "score": 85, "dept": "eng"}}
  		result := JoinByKey(left, right, "id", "inner")
  		if len(result) != 1 {
  			t.Fatalf("got %d rows, want 1", len(result))
  		}
  		if result[0]["score"] != 90 {
  			t.Errorf("score = %v, want 90", result[0]["score"])
  		}
  		if result[0]["score_right"] != 85 {
  			t.Errorf("score_right = %v, want 85", result[0]["score_right"])
  		}
  		if result[0]["dept"] != "eng" {
  			t.Errorf("dept = %v, want eng", result[0]["dept"])
  		}
  	})

  	t.Run("Key ausente en alguna fila", func(t *testing.T) {
  		left := []map[string]any{{"id": 1, "name": "Alice"}, {"name": "Bob"}} // Bob sin id
  		right := []map[string]any{{"id": 1, "dept": "eng"}}
  		result := JoinByKey(left, right, "id", "inner")
  		// Solo Alice matchea (Bob tiene key=nil, right no tiene nil)
  		if len(result) != 1 {
  			t.Fatalf("got %d rows, want 1", len(result))
  		}
  		if result[0]["name"] != "Alice" {
  			t.Errorf("name = %v, want Alice", result[0]["name"])
  		}
  	})
  }


id: map_slice_go_core_t0
function_id: map_slice_go_core
name: TestMapSlice
file_path: functions/core/map_slice_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestMapSlice(t *testing.T) {
  	t.Run("transforma enteros a strings", func(t *testing.T) {
  		got := MapSlice([]int{1, 2, 3}, func(n int) string { return fmt.Sprintf("%d", n) })
  		if len(got) != 3 || got[0] != "1" || got[1] != "2" || got[2] != "3" {
  			t.Errorf("got %v", got)
  		}
  	})

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

  	t.Run("preserva orden", func(t *testing.T) {
  		got := MapSlice([]int{3, 1, 2}, func(n int) int { return n * 10 })
  		if got[0] != 30 || got[1] != 10 || got[2] != 20 {
  			t.Errorf("got %v, want [30 10 20]", got)
  		}
  	})
  }


id: next_cron_time_go_core_t0
function_id: next_cron_time_go_core
name: TestNextCronTime
file_path: functions/core/next_cron_time_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestNextCronTime(t *testing.T) {
  	utc := time.UTC

  	t.Run("0 * * * * desde :30 retorna la proxima hora en punto", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 * * * *")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		after := time.Date(2024, 1, 15, 14, 30, 0, 0, utc)
  		got := NextCronTime(sched, after)
  		want := time.Date(2024, 1, 15, 15, 0, 0, 0, utc)
  		if !got.Equal(want) {
  			t.Errorf("got %v, want %v", got, want)
  		}
  	})

  	t.Run("@weekly desde viernes retorna proximo domingo a medianoche", func(t *testing.T) {
  		// @weekly = "0 0 * * 0" (Sunday)
  		sched, err := ParseCronExpr("@weekly")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		// 2024-01-19 is a Friday
  		after := time.Date(2024, 1, 19, 10, 0, 0, 0, utc)
  		got := NextCronTime(sched, after)
  		// Next Sunday = 2024-01-21
  		want := time.Date(2024, 1, 21, 0, 0, 0, 0, utc)
  		if !got.Equal(want) {
  			t.Errorf("got %v, want %v", got, want)
  		}
  	})

  	t.Run("0 9 * * 1-5 desde viernes retorna proximo lunes a las 9", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 * * 1-5")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		// 2024-01-19 is a Friday, after 9am so today is already past.
  		after := time.Date(2024, 1, 19, 10, 0, 0, 0, utc)
  		got := NextCronTime(sched, after)
  		// Next weekday = Monday 2024-01-22
  		want := time.Date(2024, 1, 22, 9, 0, 0, 0, utc)
  		if !got.Equal(want) {
  			t.Errorf("got %v, want %v", got, want)
  		}
  	})

  	t.Run("schedule imposible retorna zero time", func(t *testing.T) {
  		// 30 Feb does not exist — will exhaust 366-day limit quickly for a specific year.
  		// Use a schedule matching only Feb 30, which never occurs.
  		sched := CronSchedule{
  			Minute:     []int{0},
  			Hour:       []int{0},
  			DayOfMonth: []int{30},
  			Month:      []int{2},
  			DayOfWeek:  []int{0, 1, 2, 3, 4, 5, 6},
  			Raw:        "0 0 30 2 *",
  		}
  		after := time.Date(2023, 3, 1, 0, 0, 0, 0, utc)
  		got := NextCronTime(sched, after)
  		if !got.IsZero() {
  			t.Errorf("expected zero time for impossible schedule, got %v", got)
  		}
  	})
  }


id: parse_cron_expr_go_core_t0
function_id: parse_cron_expr_go_core
name: TestParseCronExpr
file_path: functions/core/parse_cron_expr_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestParseCronExpr(t *testing.T) {
  	t.Run("*/15 expande minutos a [0 15 30 45]", func(t *testing.T) {
  		sched, err := ParseCronExpr("*/15 * * * *")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		want := []int{0, 15, 30, 45}
  		if !reflect.DeepEqual(sched.Minute, want) {
  			t.Errorf("Minute = %v, want %v", sched.Minute, want)
  		}
  		// Hour should be all 24 hours
  		if len(sched.Hour) != 24 {
  			t.Errorf("Hour len = %d, want 24", len(sched.Hour))
  		}
  	})

  	t.Run("@daily resuelve a 0 0 en todos los campos restantes", func(t *testing.T) {
  		sched, err := ParseCronExpr("@daily")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if !reflect.DeepEqual(sched.Minute, []int{0}) {
  			t.Errorf("Minute = %v, want [0]", sched.Minute)
  		}
  		if !reflect.DeepEqual(sched.Hour, []int{0}) {
  			t.Errorf("Hour = %v, want [0]", sched.Hour)
  		}
  		// DayOfMonth should be all days
  		if len(sched.DayOfMonth) != 31 {
  			t.Errorf("DayOfMonth len = %d, want 31", len(sched.DayOfMonth))
  		}
  	})

  	t.Run("0 9 1,15 * * expande dias a [1 15]", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 1,15 * *")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if !reflect.DeepEqual(sched.Minute, []int{0}) {
  			t.Errorf("Minute = %v, want [0]", sched.Minute)
  		}
  		if !reflect.DeepEqual(sched.Hour, []int{9}) {
  			t.Errorf("Hour = %v, want [9]", sched.Hour)
  		}
  		if !reflect.DeepEqual(sched.DayOfMonth, []int{1, 15}) {
  			t.Errorf("DayOfMonth = %v, want [1, 15]", sched.DayOfMonth)
  		}
  	})

  	t.Run("0 9 * * 1-5 expande dia de semana a [1 2 3 4 5]", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 * * 1-5")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		want := []int{1, 2, 3, 4, 5}
  		if !reflect.DeepEqual(sched.DayOfWeek, want) {
  			t.Errorf("DayOfWeek = %v, want %v", sched.DayOfWeek, want)
  		}
  	})

  	t.Run("expresion con 4 campos retorna error", func(t *testing.T) {
  		_, err := ParseCronExpr("0 9 * *")
  		if err == nil {
  			t.Error("expected error for 4-field expression, got nil")
  		}
  	})

  	t.Run("minuto fuera de rango retorna error", func(t *testing.T) {
  		_, err := ParseCronExpr("60 * * * *")
  		if err == nil {
  			t.Error("expected error for minute=60, got nil")
  		}
  	})
  }


id: validate_struct_fields_go_core_t0
function_id: validate_struct_fields_go_core
name: TestValidateStructFields
file_path: functions/core/validate_struct_fields_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestValidateStructFields(t *testing.T) {
  	t.Run("campo required presente y ausente", func(t *testing.T) {
  		rules := map[string]string{"name": "required"}

  		valid, errs := ValidateStructFields(map[string]any{"name": "Alice"}, rules)
  		if !valid || len(errs) != 0 {
  			t.Errorf("expected valid, got errors: %v", errs)
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{}, rules)
  		if valid2 || len(errs2) == 0 {
  			t.Errorf("expected invalid for missing required field")
  		}
  	})

  	t.Run("type validation string como int falla", func(t *testing.T) {
  		rules := map[string]string{"count": "type=int"}

  		valid, _ := ValidateStructFields(map[string]any{"count": 5}, rules)
  		if !valid {
  			t.Error("expected int 5 to pass type=int")
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{"count": "five"}, rules)
  		if valid2 || len(errs2) == 0 {
  			t.Error("expected string to fail type=int")
  		}
  	})

  	t.Run("numeric ranges", func(t *testing.T) {
  		rules := map[string]string{"score": "min=0,max=100"}

  		valid, _ := ValidateStructFields(map[string]any{"score": 50}, rules)
  		if !valid {
  			t.Error("expected 50 to pass min=0,max=100")
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{"score": 150}, rules)
  		if valid2 || !strings.Contains(errs2[0], "max") {
  			t.Errorf("expected max violation, got: %v", errs2)
  		}

  		valid3, errs3 := ValidateStructFields(map[string]any{"score": -1}, rules)
  		if valid3 || !strings.Contains(errs3[0], "min") {
  			t.Errorf("expected min violation, got: %v", errs3)
  		}
  	})

  	t.Run("string lengths", func(t *testing.T) {
  		rules := map[string]string{"tag": "minlen=2,maxlen=10"}

  		valid, _ := ValidateStructFields(map[string]any{"tag": "go"}, rules)
  		if !valid {
  			t.Error("expected 'go' to pass minlen=2,maxlen=10")
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{"tag": "a"}, rules)
  		if valid2 || !strings.Contains(errs2[0], "minlen") {
  			t.Errorf("expected minlen violation, got: %v", errs2)
  		}

  		valid3, errs3 := ValidateStructFields(map[string]any{"tag": "averylongtag"}, rules)
  		if valid3 || !strings.Contains(errs3[0], "maxlen") {
  			t.Errorf("expected maxlen violation, got: %v", errs3)
  		}
  	})

  	t.Run("oneof validation", func(t *testing.T) {
  		rules := map[string]string{"status": "oneof=active|inactive|pending"}

  		valid, _ := ValidateStructFields(map[string]any{"status": "active"}, rules)
  		if !valid {
  			t.Error("expected 'active' to pass oneof")
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{"status": "deleted"}, rules)
  		if valid2 || len(errs2) == 0 {
  			t.Errorf("expected oneof violation, got: %v", errs2)
  		}
  	})

  	t.Run("pattern matching", func(t *testing.T) {
  		rules := map[string]string{"email": `pattern=^[^@]+@[^@]+\.[^@]+$`}

  		valid, _ := ValidateStructFields(map[string]any{"email": "user@example.com"}, rules)
  		if !valid {
  			t.Error("expected valid email to pass pattern")
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{"email": "not-an-email"}, rules)
  		if valid2 || !strings.Contains(errs2[0], "pattern") {
  			t.Errorf("expected pattern violation, got: %v", errs2)
  		}
  	})

  	t.Run("multiples reglas combinadas", func(t *testing.T) {
  		rules := map[string]string{
  			"name":  "required,type=string,minlen=2,maxlen=50",
  			"score": "required,type=float,min=0,max=10",
  		}

  		valid, _ := ValidateStructFields(map[string]any{"name": "Alice", "score": float64(8.5)}, rules)
  		if !valid {
  			t.Error("expected all rules to pass")
  		}

  		valid2, errs2 := ValidateStructFields(map[string]any{"name": "A", "score": float64(11)}, rules)
  		if valid2 || len(errs2) < 2 {
  			t.Errorf("expected at least 2 errors, got: %v", errs2)
  		}
  	})

  	t.Run("map vacio con reglas required", func(t *testing.T) {
  		rules := map[string]string{
  			"id":   "required",
  			"name": "required",
  		}

  		valid, errs := ValidateStructFields(map[string]any{}, rules)
  		if valid || len(errs) < 2 {
  			t.Errorf("expected 2 required errors, got: %v", errs)
  		}
  	})
  }


id: diff_entities_go_datascience_t0
function_id: diff_entities_go_datascience
name: TestDiffEntities
file_path: functions/datascience/diff_entities_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDiffEntities(t *testing.T) {
  	t.Run("entity añadida", func(t *testing.T) {
  		before := []map[string]any{
  			{"id": "1", "name": "Alice"},
  		}
  		after := []map[string]any{
  			{"id": "1", "name": "Alice"},
  			{"id": "2", "name": "Bob"},
  		}
  		result := DiffEntities(before, after, "id", nil)
  		added := result["added"].([]map[string]any)
  		if len(added) != 1 {
  			t.Errorf("expected 1 added, got %d", len(added))
  		}
  		if added[0]["id"] != "2" {
  			t.Errorf("expected added id=2, got %v", added[0]["id"])
  		}
  		if result["unchanged"].(int) != 1 {
  			t.Errorf("expected 1 unchanged, got %v", result["unchanged"])
  		}
  	})

  	t.Run("entity eliminada", func(t *testing.T) {
  		before := []map[string]any{
  			{"id": "1", "name": "Alice"},
  			{"id": "2", "name": "Bob"},
  		}
  		after := []map[string]any{
  			{"id": "1", "name": "Alice"},
  		}
  		result := DiffEntities(before, after, "id", nil)
  		removed := result["removed"].([]map[string]any)
  		if len(removed) != 1 {
  			t.Errorf("expected 1 removed, got %d", len(removed))
  		}
  		if removed[0]["id"] != "2" {
  			t.Errorf("expected removed id=2, got %v", removed[0]["id"])
  		}
  	})

  	t.Run("entity modificada con detalle de campos", func(t *testing.T) {
  		before := []map[string]any{
  			{"id": "1", "name": "Alice", "status": "active"},
  		}
  		after := []map[string]any{
  			{"id": "1", "name": "Alice", "status": "inactive"},
  		}
  		result := DiffEntities(before, after, "id", nil)
  		modified := result["modified"].([]map[string]any)
  		if len(modified) != 1 {
  			t.Errorf("expected 1 modified, got %d", len(modified))
  		}
  		changes := modified[0]["changes"].(map[string]any)
  		statusChange, ok := changes["status"].(map[string]any)
  		if !ok {
  			t.Fatalf("expected status change, got %v", changes)
  		}
  		if statusChange["old"] != "active" {
  			t.Errorf("expected old=active, got %v", statusChange["old"])
  		}
  		if statusChange["new"] != "inactive" {
  			t.Errorf("expected new=inactive, got %v", statusChange["new"])
  		}
  	})

  	t.Run("entities identicas → unchanged", func(t *testing.T) {
  		entities := []map[string]any{
  			{"id": "1", "name": "Alice"},
  			{"id": "2", "name": "Bob"},
  		}
  		result := DiffEntities(entities, entities, "id", nil)
  		if result["unchanged"].(int) != 2 {
  			t.Errorf("expected 2 unchanged, got %v", result["unchanged"])
  		}
  		if len(result["added"].([]map[string]any)) != 0 {
  			t.Errorf("expected 0 added")
  		}
  		if len(result["modified"].([]map[string]any)) != 0 {
  			t.Errorf("expected 0 modified")
  		}
  	})

  	t.Run("ignore_fields funciona", func(t *testing.T) {
  		before := []map[string]any{
  			{"id": "1", "name": "Alice", "updated_at": "2024-01-01"},
  		}
  		after := []map[string]any{
  			{"id": "1", "name": "Alice", "updated_at": "2024-06-01"},
  		}
  		// Default ignores updated_at
  		result := DiffEntities(before, after, "id", nil)
  		if result["unchanged"].(int) != 1 {
  			t.Errorf("expected 1 unchanged (updated_at ignored), got %v", result["unchanged"])
  		}
  		modified := result["modified"].([]map[string]any)
  		if len(modified) != 0 {
  			t.Errorf("expected 0 modified when updated_at is ignored, got %d", len(modified))
  		}
  	})

  	t.Run("lista vacia vs lista con datos", func(t *testing.T) {
  		before := []map[string]any{}
  		after := []map[string]any{
  			{"id": "1", "name": "Alice"},
  		}
  		result := DiffEntities(before, after, "id", nil)
  		added := result["added"].([]map[string]any)
  		if len(added) != 1 {
  			t.Errorf("expected 1 added, got %d", len(added))
  		}
  		if result["unchanged"].(int) != 0 {
  			t.Errorf("expected 0 unchanged")
  		}
  	})

  	t.Run("summary format correcto", func(t *testing.T) {
  		before := []map[string]any{
  			{"id": "1", "name": "Alice"},
  			{"id": "3", "name": "Carol"},
  		}
  		after := []map[string]any{
  			{"id": "1", "name": "Alice Changed"},
  			{"id": "2", "name": "Bob"},
  		}
  		result := DiffEntities(before, after, "id", nil)
  		summary := result["summary"].(string)
  		expected := "1 added, 1 removed, 1 modified, 0 unchanged"
  		if summary != expected {
  			t.Errorf("expected summary %q, got %q", expected, summary)
  		}
  	})
  }


id: pivot_go_datascience_t0
function_id: pivot_go_datascience
name: TestPivot
file_path: functions/datascience/pivot_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestPivot(t *testing.T) {
  	t.Run("Pivot basico con sum", func(t *testing.T) {
  		rows := []map[string]any{
  			{"region": "US", "product": "A", "sales": 10},
  			{"region": "US", "product": "B", "sales": 20},
  			{"region": "EU", "product": "A", "sales": 15},
  		}
  		result := Pivot(rows, "region", "product", "sales", "sum")
  		if len(result) != 2 {
  			t.Fatalf("got %d rows, want 2", len(result))
  		}
  		var us, eu map[string]any
  		for _, r := range result {
  			if r["region"] == "US" {
  				us = r
  			} else {
  				eu = r
  			}
  		}
  		if us["A"] != 10.0 {
  			t.Errorf("US.A: got %v, want 10", us["A"])
  		}
  		if us["B"] != 20.0 {
  			t.Errorf("US.B: got %v, want 20", us["B"])
  		}
  		if eu["A"] != 15.0 {
  			t.Errorf("EU.A: got %v, want 15", eu["A"])
  		}
  		if eu["B"] != 0 {
  			t.Errorf("EU.B: got %v, want 0", eu["B"])
  		}
  	})

  	t.Run("Pivot con count y mean", func(t *testing.T) {
  		rows := []map[string]any{
  			{"region": "US", "product": "A", "sales": 10},
  			{"region": "US", "product": "A", "sales": 20},
  			{"region": "EU", "product": "A", "sales": 15},
  		}
  		resultCount := Pivot(rows, "region", "product", "sales", "count")
  		for _, r := range resultCount {
  			if r["region"] == "US" && r["A"] != 2 {
  				t.Errorf("count US.A: got %v, want 2", r["A"])
  			}
  		}

  		resultMean := Pivot(rows, "region", "product", "sales", "mean")
  		for _, r := range resultMean {
  			if r["region"] == "US" {
  				mean, ok := r["A"].(float64)
  				if !ok || mean != 15.0 {
  					t.Errorf("mean US.A: got %v, want 15.0", r["A"])
  				}
  			}
  		}
  	})

  	t.Run("Valores faltantes rellenados con 0", func(t *testing.T) {
  		rows := []map[string]any{
  			{"region": "US", "product": "A", "sales": 5},
  			{"region": "EU", "product": "B", "sales": 8},
  		}
  		result := Pivot(rows, "region", "product", "sales", "sum")
  		for _, r := range result {
  			if r["region"] == "US" && r["B"] != 0 {
  				t.Errorf("US.B: got %v, want 0", r["B"])
  			}
  			if r["region"] == "EU" && r["A"] != 0 {
  				t.Errorf("EU.A: got %v, want 0", r["A"])
  			}
  		}
  	})

  	t.Run("Una sola fila", func(t *testing.T) {
  		rows := []map[string]any{
  			{"region": "US", "product": "A", "sales": 42},
  		}
  		result := Pivot(rows, "region", "product", "sales", "sum")
  		if len(result) != 1 {
  			t.Fatalf("got %d rows, want 1", len(result))
  		}
  		if result[0]["A"] != 42.0 {
  			t.Errorf("got %v, want 42", result[0]["A"])
  		}
  	})

  	t.Run("Multiples valores por celda requieren agregacion", func(t *testing.T) {
  		rows := []map[string]any{
  			{"region": "US", "product": "A", "sales": 10},
  			{"region": "US", "product": "A", "sales": 30},
  		}
  		resultSum := Pivot(rows, "region", "product", "sales", "sum")
  		if resultSum[0]["A"] != 40.0 {
  			t.Errorf("sum: got %v, want 40.0", resultSum[0]["A"])
  		}
  		resultMin := Pivot(rows, "region", "product", "sales", "min")
  		if resultMin[0]["A"] != 10.0 {
  			t.Errorf("min: got %v, want 10.0", resultMin[0]["A"])
  		}
  		resultMax := Pivot(rows, "region", "product", "sales", "max")
  		if resultMax[0]["A"] != 30.0 {
  			t.Errorf("max: got %v, want 30.0", resultMax[0]["A"])
  		}
  	})
  }


id: cache_to_sqlite_go_infra_t0
function_id: cache_to_sqlite_go_infra
name: TestCacheToSQLite_SetGet
file_path: functions/infra/cache_to_sqlite_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCacheToSQLite_SetGet(t *testing.T) {
  	t.Run("Set/Get basico", func(t *testing.T) {
  		c, err := CacheToSQLite(tempDB(t), "default")
  		if err != nil {
  			t.Fatal(err)
  		}
  		defer c.Close()

  		payload, _ := json.Marshal(map[string]int{"x": 1})
  		if err := c.Set("foo", payload, 0); err != nil {
  			t.Fatal(err)
  		}
  		got, ok := c.Get("foo")
  		if !ok {
  			t.Fatal("expected cache hit")
  		}
  		var result map[string]int
  		json.Unmarshal(got, &result)
  		if result["x"] != 1 {
  			t.Errorf("got %v, want x=1", result)
  		}
  	})
  }


id: cache_to_sqlite_go_infra_t1
function_id: cache_to_sqlite_go_infra
name: TestCacheToSQLite_TTLExpirado
file_path: functions/infra/cache_to_sqlite_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCacheToSQLite_TTLExpirado(t *testing.T) {
  	t.Run("TTL expirado", func(t *testing.T) {
  		c, err := CacheToSQLite(tempDB(t), "default")
  		if err != nil {
  			t.Fatal(err)
  		}
  		defer c.Close()

  		payload, _ := json.Marshal("hello")
  		c.Set("temp", payload, 50*time.Millisecond)
  		time.Sleep(100 * time.Millisecond)

  		_, ok := c.Get("temp")
  		if ok {
  			t.Error("expected cache miss after TTL expiry")
  		}
  	})
  }


id: cache_to_sqlite_go_infra_t2
function_id: cache_to_sqlite_go_infra
name: TestCacheToSQLite_GetOrSet
file_path: functions/infra/cache_to_sqlite_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCacheToSQLite_GetOrSet(t *testing.T) {
  	t.Run("GetOrSet con factory", func(t *testing.T) {
  		c, err := CacheToSQLite(tempDB(t), "default")
  		if err != nil {
  			t.Fatal(err)
  		}
  		defer c.Close()

  		calls := 0
  		factory := func() ([]byte, error) {
  			calls++
  			return json.Marshal("computed")
  		}

  		v1, err := c.GetOrSet("k", factory, time.Minute)
  		if err != nil {
  			t.Fatal(err)
  		}
  		v2, err := c.GetOrSet("k", factory, time.Minute)
  		if err != nil {
  			t.Fatal(err)
  		}
  		if string(v1) != string(v2) {
  			t.Errorf("v1=%s v2=%s, want equal", v1, v2)
  		}
  		if calls != 1 {
  			t.Errorf("factory called %d times, want 1", calls)
  		}
  	})
  }


id: cache_to_sqlite_go_infra_t3
function_id: cache_to_sqlite_go_infra
name: TestCacheToSQLite_Concurrencia
file_path: functions/infra/cache_to_sqlite_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCacheToSQLite_Concurrencia(t *testing.T) {
  	t.Run("Concurrencia (goroutines)", func(t *testing.T) {
  		c, err := CacheToSQLite(tempDB(t), "parallel")
  		if err != nil {
  			t.Fatal(err)
  		}
  		defer c.Close()

  		var wg sync.WaitGroup
  		errs := make(chan error, 40)
  		for i := 0; i < 20; i++ {
  			wg.Add(1)
  			go func(n int) {
  				defer wg.Done()
  				key := fmt.Sprintf("key_%d", n)
  				payload, _ := json.Marshal(n)
  				if err := c.Set(key, payload, 0); err != nil {
  					errs <- err
  					return
  				}
  				got, ok := c.Get(key)
  				if !ok {
  					errs <- fmt.Errorf("miss for key %s", key)
  					return
  				}
  				var val int
  				json.Unmarshal(got, &val)
  				if val != n {
  					errs <- fmt.Errorf("key %s: got %d want %d", key, val, n)
  				}
  			}(i)
  		}
  		wg.Wait()
  		close(errs)
  		for err := range errs {
  			t.Error(err)
  		}
  	})
  }


id: cron_ticker_go_infra_t0
function_id: cron_ticker_go_infra
name: TestCronTicker
file_path: functions/infra/cron_ticker_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestCronTicker(t *testing.T) {
  	t.Run("context cancel cierra el channel", func(t *testing.T) {
  		sched := CronTickerSchedule{
  			Minute:     allMinutes(),
  			Hour:       allHours(),
  			DayOfMonth: allDays(),
  			Month:      allMonths(),
  			DayOfWeek:  allDOW(),
  		}
  		ctx, cancel := context.WithCancel(context.Background())
  		ch := CronTicker(sched, ctx)

  		// Cancel immediately.
  		cancel()

  		// Channel should close without blocking.
  		timeout := time.After(2 * time.Second)
  		select {
  		case _, ok := <-ch:
  			if ok {
  				// Might receive one tick before cancel propagates — acceptable.
  			}
  			// Drain remaining.
  			for range ch {
  			}
  		case <-timeout:
  			t.Error("channel did not close within 2s after context cancel")
  		}
  	})

  	t.Run("ticker emite al llegar el momento del schedule", func(t *testing.T) {
  		// Use a schedule that fires every minute (all minutes).
  		// The next tick is at most 60s away. We use a short-lived context
  		// to avoid waiting: instead we verify the channel is not nil and
  		// that cancellation closes it cleanly.
  		sched := CronTickerSchedule{
  			Minute:     allMinutes(),
  			Hour:       allHours(),
  			DayOfMonth: allDays(),
  			Month:      allMonths(),
  			DayOfWeek:  allDOW(),
  		}
  		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
  		defer cancel()

  		ch := CronTicker(sched, ctx)
  		if ch == nil {
  			t.Fatal("CronTicker returned nil channel")
  		}

  		// Wait for context to expire, then confirm channel closes.
  		<-ctx.Done()
  		timeout := time.After(2 * time.Second)
  		for {
  			select {
  			case _, ok := <-ch:
  				if !ok {
  					return // channel closed, test passes
  				}
  			case <-timeout:
  				t.Error("channel did not close within 2s after context timeout")
  				return
  			}
  		}
  	})
  }


id: docker_build_image_go_infra_t0
function_id: docker_build_image_go_infra
name: TestDockerBuildImage
file_path: functions/infra/docker_build_image_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDockerBuildImage(t *testing.T) {
  	t.Run("build sin build args retorna image ID", func(t *testing.T) {
  		// Crear un Dockerfile temporal minimo
  		dir := t.TempDir()
  		dockerfile := filepath.Join(dir, "Dockerfile")
  		if err := os.WriteFile(dockerfile, []byte("FROM scratch\n"), 0644); err != nil {
  			t.Skip("no se pudo crear Dockerfile temporal:", err)
  		}

  		id, err := DockerBuildImage(dir, "test-fn-registry-scratch:latest", nil)
  		if err != nil {
  			// Docker puede no estar disponible en CI — skip en vez de fail
  			t.Skipf("docker build no disponible: %v", err)
  		}
  		if id == "" {
  			t.Error("se esperaba un image ID no vacio")
  		}

  		// Limpiar
  		_ = DockerRemoveImage("test-fn-registry-scratch:latest", true)
  	})

  	t.Run("build con build args incluye --build-arg", func(t *testing.T) {
  		dir := t.TempDir()
  		dockerfile := filepath.Join(dir, "Dockerfile")
  		content := "FROM scratch\nARG MY_VERSION\n"
  		if err := os.WriteFile(dockerfile, []byte(content), 0644); err != nil {
  			t.Skip("no se pudo crear Dockerfile:", err)
  		}

  		id, err := DockerBuildImage(dir, "test-fn-registry-args:latest", map[string]string{
  			"MY_VERSION": "42",
  		})
  		if err != nil {
  			t.Skipf("docker build no disponible: %v", err)
  		}
  		if id == "" {
  			t.Error("se esperaba un image ID no vacio")
  		}

  		_ = DockerRemoveImage("test-fn-registry-args:latest", true)
  	})

  	t.Run("error si contextDir no existe", func(t *testing.T) {
  		_, err := DockerBuildImage("/ruta/que/no/existe/nunca", "test:latest", nil)
  		if err == nil {
  			t.Error("se esperaba error para directorio inexistente")
  		}
  	})
  }


id: docker_compose_down_go_infra_t0
function_id: docker_compose_down_go_infra
name: TestDockerComposeDown
file_path: functions/infra/docker_compose_down_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDockerComposeDown(t *testing.T) {
  	t.Run("removeVolumes true agrega flag -v al comando", func(t *testing.T) {
  		// Verificar que docker compose esta disponible
  		if err := exec.Command("docker", "compose", "version").Run(); err != nil {
  			t.Skip("docker compose no disponible")
  		}

  		// Con archivo inexistente debe fallar pero con el mensaje correcto
  		_, err := DockerComposeDown("/ruta/inexistente/docker-compose.yml", true)
  		if err == nil {
  			t.Error("se esperaba error para archivo inexistente")
  		}
  		errStr := strings.ToLower(err.Error())
  		if !strings.Contains(errStr, "compose") && !strings.Contains(errStr, "inexistente") {
  			t.Logf("error obtenido: %v", err)
  		}
  	})

  	t.Run("error si composeFile no existe", func(t *testing.T) {
  		_, err := DockerComposeDown("/ruta/que/no/existe/docker-compose.yml", false)
  		if err == nil {
  			t.Error("se esperaba error para archivo inexistente")
  		}
  	})
  }


id: docker_compose_up_go_infra_t0
function_id: docker_compose_up_go_infra
name: TestDockerComposeUp
file_path: functions/infra/docker_compose_up_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDockerComposeUp(t *testing.T) {
  	t.Run("detach true agrega flag -d al comando", func(t *testing.T) {
  		// Verificar que docker compose esta disponible
  		if err := exec.Command("docker", "compose", "version").Run(); err != nil {
  			t.Skip("docker compose no disponible")
  		}

  		// Con archivo inexistente y detach, debe fallar pero con el mensaje correcto
  		_, err := DockerComposeUp("/ruta/inexistente/docker-compose.yml", true)
  		if err == nil {
  			t.Error("se esperaba error para archivo inexistente")
  		}
  		// El error debe mencionar el archivo o compose
  		errStr := strings.ToLower(err.Error())
  		if !strings.Contains(errStr, "compose") && !strings.Contains(errStr, "inexistente") {
  			t.Logf("error obtenido: %v", err)
  		}
  	})

  	t.Run("error si composeFile no existe", func(t *testing.T) {
  		_, err := DockerComposeUp("/ruta/que/no/existe/docker-compose.yml", false)
  		if err == nil {
  			t.Error("se esperaba error para archivo inexistente")
  		}
  	})
  }


id: docker_volume_create_go_infra_t0
function_id: docker_volume_create_go_infra
name: TestDockerVolumeCreate
file_path: functions/infra/docker_volume_create_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDockerVolumeCreate(t *testing.T) {
  	t.Run("crea volume y retorna nombre", func(t *testing.T) {
  		name := "fn-registry-test-vol-create"

  		got, err := DockerVolumeCreate(name)
  		if err != nil {
  			t.Skipf("docker no disponible: %v", err)
  		}

  		if strings.TrimSpace(got) != name {
  			t.Errorf("got %q, want %q", got, name)
  		}

  		// Limpiar
  		_ = DockerVolumeRemove(name, false)
  	})

  	t.Run("idempotente si el volume ya existe", func(t *testing.T) {
  		name := "fn-registry-test-vol-idempotent"

  		first, err := DockerVolumeCreate(name)
  		if err != nil {
  			t.Skipf("docker no disponible: %v", err)
  		}
  		defer DockerVolumeRemove(name, false) //nolint

  		second, err := DockerVolumeCreate(name)
  		if err != nil {
  			t.Errorf("segunda llamada fallo: %v", err)
  		}
  		if first != second {
  			t.Errorf("nombres distintos: %q vs %q", first, second)
  		}
  	})
  }


id: docker_volume_list_go_infra_t0
function_id: docker_volume_list_go_infra
name: TestDockerVolumeList
file_path: functions/infra/docker_volume_list_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDockerVolumeList(t *testing.T) {
  	t.Run("lista vacia retorna nil sin error", func(t *testing.T) {
  		// Verificar que la funcion no falla incluso si no hay volumes
  		volumes, err := DockerVolumeList()
  		if err != nil {
  			t.Skipf("docker no disponible: %v", err)
  		}
  		// volumes puede ser nil o no segun el sistema — no es un error
  		_ = volumes
  	})

  	t.Run("parsea campos Driver y Name correctamente", func(t *testing.T) {
  		name := "fn-registry-test-vol-list"
  		_, err := DockerVolumeCreate(name)
  		if err != nil {
  			t.Skipf("docker no disponible: %v", err)
  		}
  		defer DockerVolumeRemove(name, false) //nolint

  		volumes, err := DockerVolumeList()
  		if err != nil {
  			t.Fatalf("DockerVolumeList: %v", err)
  		}

  		found := false
  		for _, v := range volumes {
  			if v["Name"] == name {
  				found = true
  				if v["Driver"] == "" {
  					t.Error("Driver vacio para volume existente")
  				}
  				break
  			}
  		}
  		if !found {
  			t.Errorf("volume %q no encontrado en la lista", name)
  		}
  	})
  }


id: docker_volume_remove_go_infra_t0
function_id: docker_volume_remove_go_infra
name: TestDockerVolumeRemove
file_path: functions/infra/docker_volume_remove_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestDockerVolumeRemove(t *testing.T) {
  	t.Run("error si volume no existe", func(t *testing.T) {
  		err := DockerVolumeRemove("fn-registry-vol-que-no-existe-xyz", false)
  		if err == nil {
  			t.Error("se esperaba error para volume inexistente")
  		}
  	})

  	t.Run("force flag incluye -f en el comando", func(t *testing.T) {
  		name := "fn-registry-test-vol-remove"
  		_, err := DockerVolumeCreate(name)
  		if err != nil {
  			t.Skipf("docker no disponible: %v", err)
  		}

  		err = DockerVolumeRemove(name, true)
  		if err != nil {
  			t.Errorf("DockerVolumeRemove con force: %v", err)
  		}
  	})
  }


id: generate_dockerfile_go_infra_t0
function_id: generate_dockerfile_go_infra
name: TestGenerateDockerfile
file_path: functions/infra/generate_dockerfile_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestGenerateDockerfile(t *testing.T) {
  	t.Run("contiene stage builder con golang:1.23-alpine", func(t *testing.T) {
  		got := GenerateDockerfile("myapp", 8080, nil)
  		if !strings.Contains(got, "FROM golang:1.23-alpine AS builder") {
  			t.Errorf("expected FROM golang:1.23-alpine AS builder, got:\n%s", got)
  		}
  	})

  	t.Run("contiene stage final con alpine:latest", func(t *testing.T) {
  		got := GenerateDockerfile("myapp", 8080, nil)
  		if !strings.Contains(got, "FROM alpine:latest") {
  			t.Errorf("expected FROM alpine:latest, got:\n%s", got)
  		}
  	})

  	t.Run("incluye EXPOSE cuando port mayor a cero", func(t *testing.T) {
  		got := GenerateDockerfile("myapp", 8080, nil)
  		if !strings.Contains(got, "EXPOSE 8080") {
  			t.Errorf("expected EXPOSE 8080, got:\n%s", got)
  		}
  	})

  	t.Run("no incluye EXPOSE cuando port es cero", func(t *testing.T) {
  		got := GenerateDockerfile("myapp", 0, nil)
  		if strings.Contains(got, "EXPOSE") {
  			t.Errorf("expected no EXPOSE directive, got:\n%s", got)
  		}
  	})

  	t.Run("env vars aparecen ordenadas alfabeticamente", func(t *testing.T) {
  		got := GenerateDockerfile("myapp", 8080, map[string]string{
  			"Z_VAR": "z",
  			"A_VAR": "a",
  			"M_VAR": "m",
  		})
  		posA := strings.Index(got, "ENV A_VAR=a")
  		posM := strings.Index(got, "ENV M_VAR=m")
  		posZ := strings.Index(got, "ENV Z_VAR=z")
  		if posA < 0 || posM < 0 || posZ < 0 {
  			t.Fatalf("ENV vars no encontradas en:\n%s", got)
  		}
  		if !(posA < posM && posM < posZ) {
  			t.Errorf("ENV vars no ordenadas: A=%d M=%d Z=%d", posA, posM, posZ)
  		}
  	})

  	t.Run("binaryName aparece en ENTRYPOINT", func(t *testing.T) {
  		got := GenerateDockerfile("mycli", 9090, nil)
  		if !strings.Contains(got, `ENTRYPOINT ["./mycli"]`) {
  			t.Errorf("expected ENTRYPOINT with mycli, got:\n%s", got)
  		}
  	})

  	t.Run("env vars vacias no generan instrucciones ENV", func(t *testing.T) {
  		got := GenerateDockerfile("myapp", 8080, map[string]string{})
  		if strings.Contains(got, "ENV ") {
  			t.Errorf("expected no ENV directives for empty map, got:\n%s", got)
  		}
  	})
  }


id: go_build_binary_go_infra_t0
function_id: go_build_binary_go_infra
name: TestGoBuildBinary
file_path: functions/infra/go_build_binary_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestGoBuildBinary(t *testing.T) {
  	t.Run("compila proyecto valido sin error", func(t *testing.T) {
  		// Crear un proyecto Go mínimo en un directorio temporal
  		tmpDir := t.TempDir()
  		goModContent := "module testapp\n\ngo 1.21\n"
  		mainContent := `package main

  import "fmt"

  func main() {
  	fmt.Println("hello")
  }
  `
  		if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goModContent), 0o644); err != nil {
  			t.Fatal(err)
  		}
  		if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainContent), 0o644); err != nil {
  			t.Fatal(err)
  		}

  		outputPath := filepath.Join(tmpDir, "bin", "testapp")
  		err := GoBuildBinary(tmpDir, outputPath, "", "")
  		if err != nil {
  			t.Errorf("unexpected error: %v", err)
  		}

  		// Verificar que el binario existe
  		if _, statErr := os.Stat(outputPath); os.IsNotExist(statErr) {
  			t.Errorf("binary not found at %s", outputPath)
  		}
  	})

  	t.Run("outputPath vacio usa build/dirname por defecto", func(t *testing.T) {
  		tmpDir := t.TempDir()
  		goModContent := "module myproject\n\ngo 1.21\n"
  		mainContent := "package main\n\nfunc main() {}\n"
  		if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goModContent), 0o644); err != nil {
  			t.Fatal(err)
  		}
  		if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte(mainContent), 0o644); err != nil {
  			t.Fatal(err)
  		}

  		err := GoBuildBinary(tmpDir, "", "", "")
  		if err != nil {
  			t.Errorf("unexpected error: %v", err)
  		}

  		expectedPath := filepath.Join(tmpDir, "build", filepath.Base(tmpDir))
  		if _, statErr := os.Stat(expectedPath); os.IsNotExist(statErr) {
  			t.Errorf("expected binary at %s, not found", expectedPath)
  		}
  	})

  	t.Run("ldflags vacio usa -s -w por defecto", func(t *testing.T) {
  		// Este test verifica que la función no falla con ldflags vacío
  		// (los flags por defecto -s -w son válidos para go build)
  		tmpDir := t.TempDir()
  		if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module x\n\ngo 1.21\n"), 0o644); err != nil {
  			t.Fatal(err)
  		}
  		if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil {
  			t.Fatal(err)
  		}

  		outputPath := filepath.Join(tmpDir, "out")
  		err := GoBuildBinary(tmpDir, outputPath, "", "")
  		if err != nil {
  			t.Errorf("ldflags vacío debería usar -s -w y compilar sin error: %v", err)
  		}
  	})

  	t.Run("error si projectDir no existe", func(t *testing.T) {
  		err := GoBuildBinary("/nonexistent/path/to/project", "/tmp/out", "", "")
  		if err == nil {
  			t.Error("expected error for nonexistent projectDir, got nil")
  		}
  	})
  }


id: health_check_http_go_infra_t0
function_id: health_check_http_go_infra
name: TestHealthCheckHTTP
file_path: functions/infra/health_check_http_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestHealthCheckHTTP(t *testing.T) {
  	t.Run("retorna nil cuando el servidor responde 200", func(t *testing.T) {
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusOK)
  		}))
  		defer srv.Close()

  		err := HealthCheckHTTP(srv.URL, 5, 100)
  		if err != nil {
  			t.Errorf("se esperaba nil, got: %v", err)
  		}
  	})

  	t.Run("retorna error si el timeout se agota", func(t *testing.T) {
  		// Servidor que nunca responde 200
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusServiceUnavailable)
  		}))
  		defer srv.Close()

  		err := HealthCheckHTTP(srv.URL, 1, 200)
  		if err == nil {
  			t.Error("se esperaba error por timeout")
  		}
  	})

  	t.Run("respeta el intervalo entre intentos", func(t *testing.T) {
  		attempts := 0
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			attempts++
  			if attempts >= 3 {
  				w.WriteHeader(http.StatusOK)
  			} else {
  				w.WriteHeader(http.StatusServiceUnavailable)
  			}
  		}))
  		defer srv.Close()

  		start := time.Now()
  		err := HealthCheckHTTP(srv.URL, 5, 150)
  		elapsed := time.Since(start)

  		if err != nil {
  			t.Errorf("se esperaba nil despues de 3 intentos, got: %v", err)
  		}
  		// Con 2 intervalos de 150ms debe haber pasado al menos 250ms
  		if elapsed < 250*time.Millisecond {
  			t.Errorf("elapsed %v demasiado rapido, se esperaban al menos 250ms", elapsed)
  		}
  		if attempts < 3 {
  			t.Errorf("se esperaban al menos 3 intentos, got %d", attempts)
  		}
  	})
  }


id: http_download_file_go_infra_t0
function_id: http_download_file_go_infra
name: TestHttpDownloadFile
file_path: functions/infra/http_download_file_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestHttpDownloadFile(t *testing.T) {
  	t.Run("httptest.Server sirve archivo binario", func(t *testing.T) {
  		content := []byte("\x00\x01\x02\x03binary content")
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.Header().Set("Content-Type", "application/octet-stream")
  			w.Write(content)
  		}))
  		defer srv.Close()

  		tmp := t.TempDir()
  		dest := filepath.Join(tmp, "out.bin")

  		n, err := HttpDownloadFile(srv.URL, dest, nil, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if n != int64(len(content)) {
  			t.Errorf("got %d bytes, want %d", n, len(content))
  		}
  		got, _ := os.ReadFile(dest)
  		if string(got) != string(content) {
  			t.Errorf("file content mismatch")
  		}
  	})

  	t.Run("Directorio creado automaticamente", func(t *testing.T) {
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.Write([]byte("data"))
  		}))
  		defer srv.Close()

  		tmp := t.TempDir()
  		dest := filepath.Join(tmp, "nested", "deep", "file.bin")

  		_, err := HttpDownloadFile(srv.URL, dest, nil, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if _, err := os.Stat(dest); os.IsNotExist(err) {
  			t.Error("dest file does not exist after download")
  		}
  	})

  	t.Run("Archivo temporal + rename (no deja basura si falla)", func(t *testing.T) {
  		// Server que falla a mitad de la transferencia cortando la conexion
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.Write([]byte("partial"))
  			// hijack y cierra bruscamente no disponible facilmente; simulamos con
  			// status 500 antes de escribir
  		}))
  		defer srv.Close()

  		// Verificar que un download exitoso no deja .download-* temporales
  		tmp := t.TempDir()
  		dest := filepath.Join(tmp, "file.bin")

  		HttpDownloadFile(srv.URL, dest, nil, 5*time.Second)

  		entries, _ := os.ReadDir(tmp)
  		for _, e := range entries {
  			if e.Name() != "file.bin" && filepath.Ext(e.Name()) != ".bin" {
  				t.Errorf("unexpected temp file left: %s", e.Name())
  			}
  		}
  	})

  	t.Run("Size retornado coincide", func(t *testing.T) {
  		content := make([]byte, 10000)
  		for i := range content {
  			content[i] = byte(i % 256)
  		}
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.Write(content)
  		}))
  		defer srv.Close()

  		tmp := t.TempDir()
  		dest := filepath.Join(tmp, "big.bin")

  		n, err := HttpDownloadFile(srv.URL, dest, nil, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if n != int64(len(content)) {
  			t.Errorf("got %d bytes, want %d", n, len(content))
  		}
  	})
  }


id: http_get_json_go_infra_t0
function_id: http_get_json_go_infra
name: TestHttpGetJSON
file_path: functions/infra/http_get_json_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestHttpGetJSON(t *testing.T) {
  	t.Run("httptest.Server con respuesta JSON", func(t *testing.T) {
  		payload := map[string]any{"ok": true, "value": float64(42)}
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.Header().Set("Content-Type", "application/json")
  			json.NewEncoder(w).Encode(payload)
  		}))
  		defer srv.Close()

  		result, err := HttpGetJSON(srv.URL, nil, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if result["ok"] != true {
  			t.Errorf("got ok=%v, want true", result["ok"])
  		}
  	})

  	t.Run("Status 404 → error", func(t *testing.T) {
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			http.Error(w, "not found", http.StatusNotFound)
  		}))
  		defer srv.Close()

  		_, err := HttpGetJSON(srv.URL, nil, 5*time.Second)
  		if err == nil {
  			t.Fatal("expected error, got nil")
  		}
  		if !strings.Contains(err.Error(), "404") {
  			t.Errorf("error should contain 404, got: %v", err)
  		}
  	})

  	t.Run("Timeout → error", func(t *testing.T) {
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			// No responde — bloquea hasta que el cliente cancela
  			<-r.Context().Done()
  		}))
  		defer srv.Close()

  		_, err := HttpGetJSON(srv.URL, nil, 50*time.Millisecond)
  		if err == nil {
  			t.Fatal("expected timeout error, got nil")
  		}
  	})

  	t.Run("Headers custom", func(t *testing.T) {
  		receivedHeaders := make(chan http.Header, 1)
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			receivedHeaders <- r.Header.Clone()
  			w.Header().Set("Content-Type", "application/json")
  			w.Write([]byte(`{}`))
  		}))
  		defer srv.Close()

  		headers := map[string]string{"X-Api-Key": "mytoken"}
  		_, err := HttpGetJSON(srv.URL, headers, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		h := <-receivedHeaders
  		if h.Get("X-Api-Key") != "mytoken" {
  			t.Errorf("X-Api-Key not sent, got: %v", h.Get("X-Api-Key"))
  		}
  		if h.Get("Accept") != "application/json" {
  			t.Errorf("Accept header missing, got: %v", h.Get("Accept"))
  		}
  	})
  }


id: http_post_json_go_infra_t0
function_id: http_post_json_go_infra
name: TestHttpPostJSON
file_path: functions/infra/http_post_json_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestHttpPostJSON(t *testing.T) {
  	t.Run("httptest.Server recibe body correcto", func(t *testing.T) {
  		received := make(chan map[string]any, 1)
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			var body map[string]any
  			data, _ := io.ReadAll(r.Body)
  			json.Unmarshal(data, &body)
  			received <- body
  			w.Header().Set("Content-Type", "application/json")
  			w.Write([]byte(`{"ok": true}`))
  		}))
  		defer srv.Close()

  		_, err := HttpPostJSON(srv.URL, map[string]any{"name": "Alice", "score": 100}, nil, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		body := <-received
  		if body["name"] != "Alice" {
  			t.Errorf("name not received correctly, got: %v", body["name"])
  		}
  	})

  	t.Run("Status 201 → exito", func(t *testing.T) {
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.Header().Set("Content-Type", "application/json")
  			w.WriteHeader(http.StatusCreated)
  			w.Write([]byte(`{"id": 42}`))
  		}))
  		defer srv.Close()

  		result, err := HttpPostJSON(srv.URL, map[string]any{"x": 1}, nil, 5*time.Second)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if result["id"] != float64(42) {
  			t.Errorf("got id=%v, want 42", result["id"])
  		}
  	})

  	t.Run("Status 500 → error con body parcial", func(t *testing.T) {
  		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			http.Error(w, "internal server error details", http.StatusInternalServerError)
  		}))
  		defer srv.Close()

  		_, err := HttpPostJSON(srv.URL, map[string]any{}, nil, 5*time.Second)
  		if err == nil {
  			t.Fatal("expected error, got nil")
  		}
  		if !strings.Contains(err.Error(), "500") {
  			t.Errorf("error should contain 500, got: %v", err)
  		}
  	})
  }


id: parse_nordvpn_status_go_infra_t0
function_id: parse_nordvpn_status_go_infra
name: TestParseNordVPNStatus_Connected
file_path: functions/infra/parse_nordvpn_status_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestParseNordVPNStatus_Connected(t *testing.T) {
  	input := `Status: Connected
  Hostname: es42.nordvpn.com
  IP: 185.230.124.42
  Country: Spain
  City: Madrid
  Current Technology: NordLynx
  Current Protocol: nordlynx
  Transfer: 1.2 MiB received, 500 KiB sent
  Uptime: 5 minutes 32 seconds`

  	got := ParseNordVPNStatus(input)

  	if !got.Connected {
  		t.Error("expected Connected=true")
  	}
  	if got.Hostname != "es42.nordvpn.com" {
  		t.Errorf("Hostname = %q, want es42.nordvpn.com", got.Hostname)
  	}
  	if got.IP != "185.230.124.42" {
  		t.Errorf("IP = %q, want 185.230.124.42", got.IP)
  	}
  	if got.Country != "Spain" {
  		t.Errorf("Country = %q, want Spain", got.Country)
  	}
  	if got.City != "Madrid" {
  		t.Errorf("City = %q, want Madrid", got.City)
  	}
  	if got.Technology != "NordLynx" {
  		t.Errorf("Technology = %q, want NordLynx", got.Technology)
  	}
  }


id: parse_nordvpn_status_go_infra_t1
function_id: parse_nordvpn_status_go_infra
name: TestParseNordVPNStatus_Disconnected
file_path: functions/infra/parse_nordvpn_status_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestParseNordVPNStatus_Disconnected(t *testing.T) {
  	input := `Status: Disconnected`

  	got := ParseNordVPNStatus(input)

  	if got.Connected {
  		t.Error("expected Connected=false")
  	}
  	if got.Status != "Disconnected" {
  		t.Errorf("Status = %q, want Disconnected", got.Status)
  	}
  }


id: parse_nordvpn_status_go_infra_t2
function_id: parse_nordvpn_status_go_infra
name: TestParseNordVPNStatus_WithANSI
file_path: functions/infra/parse_nordvpn_status_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestParseNordVPNStatus_WithANSI(t *testing.T) {
  	input := "\x1b[32mStatus: Connected\x1b[0m\n\x1b[32m- Hostname: us1234.nordvpn.com\x1b[0m"

  	got := ParseNordVPNStatus(input)

  	if !got.Connected {
  		t.Error("expected Connected=true with ANSI codes")
  	}
  	if got.Hostname != "us1234.nordvpn.com" {
  		t.Errorf("Hostname = %q, want us1234.nordvpn.com", got.Hostname)
  	}
  }


id: ssh_check_go_infra_t0
function_id: ssh_check_go_infra
name: TestSSHCheck
file_path: functions/infra/ssh_check_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestSSHCheck(t *testing.T) {
  	t.Run("conecta a organic-machine", func(t *testing.T) {
  		conn := sshTestConn()
  		err := SSHCheck(conn)
  		if err != nil {
  			t.Skipf("SSH no disponible: %v", err)
  		}
  	})

  	t.Run("falla con host inexistente", func(t *testing.T) {
  		conn := SSHConn{Host: "192.0.2.1", Port: 22, User: "nobody"}
  		err := SSHCheck(conn)
  		if err == nil {
  			t.Error("esperaba error con host inexistente")
  		}
  	})
  }


id: ssh_download_go_infra_t0
function_id: ssh_download_go_infra
name: TestSSHUploadDownload
file_path: functions/infra/ssh_transfer_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestSSHUploadDownload(t *testing.T) {
  	conn := skipIfNoSSH(t)

  	t.Run("upload y download roundtrip", func(t *testing.T) {
  		// Crear archivo temporal local
  		tmpDir := t.TempDir()
  		localFile := filepath.Join(tmpDir, "test_upload.txt")
  		content := "fn_registry ssh test: upload roundtrip"
  		if err := os.WriteFile(localFile, []byte(content), 0644); err != nil {
  			t.Fatal(err)
  		}

  		remotePath := "/tmp/fn_registry_ssh_test.txt"

  		// Upload
  		err := SSHUpload(conn, localFile, remotePath)
  		if err != nil {
  			t.Fatalf("SSHUpload: %v", err)
  		}

  		// Verificar que existe en remoto
  		stdout, _, code, err := SSHExec(conn, "cat "+remotePath)
  		if err != nil {
  			t.Fatalf("SSHExec cat: %v", err)
  		}
  		if code != 0 {
  			t.Fatalf("cat remoto fallo con code %d", code)
  		}
  		if strings.TrimSpace(stdout) != content {
  			t.Errorf("contenido remoto = %q, want %q", strings.TrimSpace(stdout), content)
  		}

  		// Download
  		downloadFile := filepath.Join(tmpDir, "test_download.txt")
  		err = SSHDownload(conn, remotePath, downloadFile)
  		if err != nil {
  			t.Fatalf("SSHDownload: %v", err)
  		}

  		got, err := os.ReadFile(downloadFile)
  		if err != nil {
  			t.Fatal(err)
  		}
  		if string(got) != content {
  			t.Errorf("downloaded = %q, want %q", string(got), content)
  		}

  		// Limpiar remoto
  		SSHExec(conn, "rm -f "+remotePath)
  	})
  }


id: ssh_exec_go_infra_t0
function_id: ssh_exec_go_infra
name: TestSSHExec
file_path: functions/infra/ssh_exec_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestSSHExec(t *testing.T) {
  	conn := skipIfNoSSH(t)

  	t.Run("echo simple", func(t *testing.T) {
  		stdout, stderr, code, err := SSHExec(conn, "echo hello")
  		if err != nil {
  			t.Fatalf("SSHExec error: %v", err)
  		}
  		if code != 0 {
  			t.Errorf("exit code = %d, want 0; stderr: %s", code, stderr)
  		}
  		if strings.TrimSpace(stdout) != "hello" {
  			t.Errorf("stdout = %q, want %q", strings.TrimSpace(stdout), "hello")
  		}
  	})

  	t.Run("captura stderr", func(t *testing.T) {
  		_, stderr, code, err := SSHExec(conn, "echo error >&2")
  		if err != nil {
  			t.Fatalf("SSHExec error: %v", err)
  		}
  		if code != 0 {
  			t.Errorf("exit code = %d, want 0", code)
  		}
  		if !strings.Contains(stderr, "error") {
  			t.Errorf("stderr = %q, want contener 'error'", stderr)
  		}
  	})

  	t.Run("exit code no cero", func(t *testing.T) {
  		_, _, code, err := SSHExec(conn, "exit 42")
  		if err != nil {
  			t.Fatalf("SSHExec error: %v", err)
  		}
  		if code != 42 {
  			t.Errorf("exit code = %d, want 42", code)
  		}
  	})

  	t.Run("comando multilinea", func(t *testing.T) {
  		stdout, _, code, err := SSHExec(conn, "hostname && uname -s")
  		if err != nil {
  			t.Fatalf("SSHExec error: %v", err)
  		}
  		if code != 0 {
  			t.Errorf("exit code = %d, want 0", code)
  		}
  		lines := strings.Split(strings.TrimSpace(stdout), "\n")
  		if len(lines) < 2 {
  			t.Errorf("esperaba al menos 2 lineas, got %d: %q", len(lines), stdout)
  		}
  	})
  }


id: ssh_tunnel_close_go_infra_t0
function_id: ssh_tunnel_close_go_infra
name: TestSSHTunnelOpenClose
file_path: functions/infra/ssh_tunnel_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestSSHTunnelOpenClose(t *testing.T) {
  	conn := skipIfNoSSH(t)

  	t.Run("abre tunel y lo cierra", func(t *testing.T) {
  		// Usar puerto alto aleatorio para evitar conflictos
  		localPort := 19876
  		// Tunel a localhost:22 del remoto (el propio sshd)
  		pid, err := SSHTunnelOpen(conn, localPort, "localhost", 22)
  		if err != nil {
  			t.Fatalf("SSHTunnelOpen: %v", err)
  		}
  		if pid <= 0 {
  			t.Fatalf("PID invalido: %d", pid)
  		}

  		// Verificar que el puerto local esta escuchando
  		time.Sleep(300 * time.Millisecond)
  		c, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", localPort), 2*time.Second)
  		if err != nil {
  			// Limpiar antes de fallar
  			SSHTunnelClose(pid)
  			t.Fatalf("no se puede conectar al tunel en localhost:%d: %v", localPort, err)
  		}
  		c.Close()

  		// Cerrar tunel
  		err = SSHTunnelClose(pid)
  		if err != nil {
  			t.Errorf("SSHTunnelClose: %v", err)
  		}

  		// Verificar que el puerto ya no escucha
  		time.Sleep(300 * time.Millisecond)
  		c, err = net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", localPort), 1*time.Second)
  		if err == nil {
  			c.Close()
  			t.Error("el tunel sigue abierto despues de SSHTunnelClose")
  		}
  	})
  }


id: write_dockerfile_go_infra_t0
function_id: write_dockerfile_go_infra
name: TestWriteDockerfile
file_path: functions/infra/write_dockerfile_test.go
lang: go
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  func TestWriteDockerfile(t *testing.T) {
  	t.Run("escribe Dockerfile en directorio existente", func(t *testing.T) {
  		tmpDir := t.TempDir()
  		content := "FROM alpine:latest\nCMD [\"sh\"]\n"

  		path, err := WriteDockerfile(tmpDir, content)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}

  		data, err := os.ReadFile(path)
  		if err != nil {
  			t.Fatalf("cannot read written file: %v", err)
  		}
  		if string(data) != content {
  			t.Errorf("content mismatch: got %q, want %q", string(data), content)
  		}
  	})

  	t.Run("crea directorio si no existe", func(t *testing.T) {
  		tmpDir := t.TempDir()
  		newDir := filepath.Join(tmpDir, "subdir", "nested")
  		content := "FROM scratch\n"

  		path, err := WriteDockerfile(newDir, content)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if _, statErr := os.Stat(newDir); os.IsNotExist(statErr) {
  			t.Error("expected directory to be created")
  		}
  		if _, statErr := os.Stat(path); os.IsNotExist(statErr) {
  			t.Errorf("expected Dockerfile at %s", path)
  		}
  	})

  	t.Run("retorna path absoluto correcto", func(t *testing.T) {
  		tmpDir := t.TempDir()
  		content := "FROM ubuntu:22.04\n"

  		path, err := WriteDockerfile(tmpDir, content)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}

  		if !filepath.IsAbs(path) {
  			t.Errorf("expected absolute path, got: %s", path)
  		}
  		if !strings.HasSuffix(path, "Dockerfile") {
  			t.Errorf("expected path to end with Dockerfile, got: %s", path)
  		}
  	})

  	t.Run("error si dir es path invalido", func(t *testing.T) {
  		// Intentar escribir en un path donde el padre es un archivo (no directorio)
  		tmpDir := t.TempDir()
  		// Crear un archivo donde esperamos un directorio
  		blockerPath := filepath.Join(tmpDir, "blocker")
  		if err := os.WriteFile(blockerPath, []byte("x"), 0o644); err != nil {
  			t.Fatal(err)
  		}

  		// Intentar usar ese archivo como directorio
  		_, err := WriteDockerfile(filepath.Join(blockerPath, "subdir"), "FROM scratch\n")
  		if err == nil {
  			t.Error("expected error when dir path goes through a file, got nil")
  		}
  	})
  }


id: pass_delete_bash_infra_t0
function_id: pass_delete_bash_infra
name: pass_get
file_path: bash/functions/infra/pass_test.sh
lang: bash
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  echo "=== pass_get ==="

  echo "  test: lee entrada existente (agentes/gitea-url)"
  got=$(pass_get agentes/gitea-url)
  assert_nonzero "lee entrada existente" "$got"

  echo "  test: falla con entrada inexistente"
  assert_fail "falla con entrada inexistente" pass_get "no/existe/xyz"

  echo ""


id: pass_delete_bash_infra_t1
function_id: pass_delete_bash_infra
name: pass_set
file_path: bash/functions/infra/pass_test.sh
lang: bash
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  echo "=== pass_set ==="

  echo "  test: inserta valor y lo lee de vuelta"
  pass_set "$TEST_ENTRY" "test-value-12345"
  got=$(pass_get "$TEST_ENTRY")
  assert_eq "inserta y lee" "$got" "test-value-12345"

  echo "  test: sobreescribe valor existente"
  pass_set "$TEST_ENTRY" "overwritten-value"
  got=$(pass_get "$TEST_ENTRY")
  assert_eq "sobreescribe" "$got" "overwritten-value"

  # Limpiar para siguiente test
  pass_cleanup

  echo ""


id: pass_delete_bash_infra_t2
function_id: pass_delete_bash_infra
name: pass_list
file_path: bash/functions/infra/pass_test.sh
lang: bash
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  echo "=== pass_list ==="

  echo "  test: lista todas las entradas"
  got=$(pass_list)
  assert_contains "lista todas" "$got" "dataforge-token"

  echo "  test: filtra por prefijo agentes"
  got=$(pass_list agentes)
  assert_contains "filtra agentes" "$got" "gitea-url"

  echo ""


id: pass_delete_bash_infra_t3
function_id: pass_delete_bash_infra
name: pass_generate
file_path: bash/functions/infra/pass_test.sh
lang: bash
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  echo "=== pass_generate ==="

  echo "  test: genera password de 16 chars"
  generated=$(pass_generate "$TEST_ENTRY" 16)
  assert_eq "longitud 16" "${#generated}" "16"

  echo "  test: valor almacenado coincide"
  stored=$(pass_get "$TEST_ENTRY")
  assert_eq "stored = generated" "$stored" "$generated"

  pass_cleanup

  echo "  test: default 24 chars"
  generated=$(pass_generate "$TEST_ENTRY")
  assert_eq "longitud default 24" "${#generated}" "24"

  pass_cleanup

  echo ""


id: pass_delete_bash_infra_t4
function_id: pass_delete_bash_infra
name: pass_delete
file_path: bash/functions/infra/pass_test.sh
lang: bash
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  echo "=== pass_delete ==="

  echo "  test: elimina entrada de test"
  pass_set "$TEST_ENTRY" "to-delete"
  pass_delete "$TEST_ENTRY"
  assert_fail "despues de delete no se puede leer" pass_get "$TEST_ENTRY"

  echo "  test: falla con entrada inexistente"
  assert_fail "delete inexistente" pass_delete "no/existe/xyz_delete_test"

  echo ""


id: pass_delete_bash_infra_t5
function_id: pass_delete_bash_infra
name: pass_sync
file_path: bash/functions/infra/pass_test.sh
lang: bash
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  echo "=== pass_sync ==="

  echo "  test: sincroniza con remoto"
  got=$(pass_sync)
  assert_contains "sync retorna json" "$got" "pull"

  echo ""
  echo "================================"
  echo "Resultados: $PASS passed, $FAIL failed"
  if [ "$FAIL" -gt 0 ]; then
      exit 1
  fi


id: cache_decorator_py_core_t0
function_id: cache_decorator_py_core
name: test_funcion_llamada_una_vez_segunda_vez_desde_cache
file_path: python/functions/core/cache_decorator_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_funcion_llamada_una_vez_segunda_vez_desde_cache(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


id: cache_decorator_py_core_t1
function_id: cache_decorator_py_core
name: test_ttl_expirado_llama_de_nuevo
file_path: python/functions/core/cache_decorator_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ttl_expirado_llama_de_nuevo(store):
      calls = []

      @cache_decorator(store, ttl=0.05)
      def work(n: int) -> int:
          calls.append(n)
          return n + 1

      work(3)
      time.sleep(0.1)
      work(3)
      assert len(calls) == 2


id: cache_decorator_py_core_t2
function_id: cache_decorator_py_core
name: test_key_fn_custom
file_path: python/functions/core/cache_decorator_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_fn_custom(store):
      calls = []

      def my_key_fn(func, args, kwargs):
          return f"custom:{args[0]}"

      @cache_decorator(store, ttl=60, key_fn=my_key_fn)
      def fn(x: int) -> str:
          calls.append(x)
          return f"result_{x}"

      fn(7)
      fn(7)
      assert len(calls) == 1


id: cache_decorator_py_core_t3
function_id: cache_decorator_py_core
name: test_argumentos_distintos_keys_distintas
file_path: python/functions/core/cache_decorator_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_argumentos_distintos_keys_distintas(store):
      calls = []

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

      fn(1)
      fn(2)
      fn(1)
      assert len(calls) == 2


id: cache_decorator_py_core_t4
function_id: cache_decorator_py_core
name: test_funciona_con_async
file_path: python/functions/core/cache_decorator_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_funciona_con_async(store):
      calls = []

      @cache_decorator(store, ttl=60)
      async def async_fn(x: int) -> int:
          calls.append(x)
          return x + 100

      async def run():
          r1 = await async_fn(5)
          r2 = await async_fn(5)
          return r1, r2

      r1, r2 = asyncio.run(run())
      assert r1 == 105
      assert r2 == 105
      assert len(calls) == 1


id: calculate_media_strategy_py_core_t2
function_id: calculate_media_strategy_py_core
name: test_10_imagenes_20_lineas_full_page_vlm
file_path: python/functions/core/calculate_media_strategy_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_10_imagenes_20_lineas_full_page_vlm():
      assert calculate_media_strategy(10, 20) == "full_page_vlm"


id: calculate_media_strategy_py_core_t3
function_id: calculate_media_strategy_py_core
name: test_5_imagenes_100_lineas_full_page_vlm
file_path: python/functions/core/calculate_media_strategy_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_5_imagenes_100_lineas_full_page_vlm():
      assert calculate_media_strategy(5, 100) == "full_page_vlm"


id: calculate_media_strategy_py_core_t4
function_id: calculate_media_strategy_py_core
name: test_0_lineas_division_por_cero_evitada
file_path: python/functions/core/calculate_media_strategy_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_0_lineas_division_por_cero_evitada():
      assert calculate_media_strategy(3, 0) == "text_only"


id: call_batch_with_retry_py_core_t0
function_id: call_batch_with_retry_py_core
name: test_todos_los_items_exito
file_path: python/functions/core/call_batch_with_retry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_todos_los_items_exito():
      results, failures = call_batch_with_retry(
          items=[1, 2, 3],
          process_func=lambda x: x * 2,
          max_retries=3,
      )
      assert results == [2, 4, 6]
      assert failures == []


id: call_batch_with_retry_py_core_t1
function_id: call_batch_with_retry_py_core
name: test_item_falla_permanentemente_continue_true
file_path: python/functions/core/call_batch_with_retry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_item_falla_permanentemente_continue_true():
      def process(x):
          if x == 2:
              raise ValueError("fallo permanente")
          return x * 10

      results, failures = call_batch_with_retry(
          items=[1, 2, 3],
          process_func=process,
          max_retries=2,
          initial_delay=0.0,
          continue_on_failure=True,
      )
      assert results == [10, 30]
      assert len(failures) == 1
      assert failures[0]["index"] == 1
      assert failures[0]["item"] == 2
      assert "fallo permanente" in failures[0]["error"]


id: call_batch_with_retry_py_core_t2
function_id: call_batch_with_retry_py_core
name: test_item_falla_abort_continue_false
file_path: python/functions/core/call_batch_with_retry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_item_falla_abort_continue_false():
      call_count = {"n": 0}

      def process(x):
          call_count["n"] += 1
          if x == 2:
              raise RuntimeError("error fatal")
          return x

      try:
          call_batch_with_retry(
              items=[1, 2, 3],
              process_func=process,
              max_retries=1,
              initial_delay=0.0,
              continue_on_failure=False,
          )
          assert False, "Deberia haber lanzado excepcion"
      except RuntimeError as e:
          assert "error fatal" in str(e)
      # item 3 nunca fue procesado
      assert call_count["n"] < 6  # 1 ok + 2 intentos para item 2 + 0 para item 3


id: call_batch_with_retry_py_core_t3
function_id: call_batch_with_retry_py_core
name: test_item_falla_luego_exito_retry_funciona
file_path: python/functions/core/call_batch_with_retry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_item_falla_luego_exito_retry_funciona():
      attempt_counts = {}

      def process(x):
          attempt_counts[x] = attempt_counts.get(x, 0) + 1
          # item 5 falla las primeras 2 veces, exito en la tercera
          if x == 5 and attempt_counts[x] < 3:
              raise ValueError("fallo temporal")
          return x * 2

      results, failures = call_batch_with_retry(
          items=[1, 5, 9],
          process_func=process,
          max_retries=3,
          initial_delay=0.0,
          continue_on_failure=True,
      )
      assert results == [2, 10, 18]
      assert failures == []
      assert attempt_counts[5] == 3


id: call_batch_with_retry_py_core_t4
function_id: call_batch_with_retry_py_core
name: test_failures_contiene_index_correcto
file_path: python/functions/core/call_batch_with_retry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_failures_contiene_index_correcto():
      def process(x):
          if x in (0, 2, 4):
              raise ValueError(f"fallo en {x}")
          return x

      results, failures = call_batch_with_retry(
          items=[0, 1, 2, 3, 4],
          process_func=process,
          max_retries=0,
          initial_delay=0.0,
          continue_on_failure=True,
      )
      assert results == [1, 3]
      assert [f["index"] for f in failures] == [0, 2, 4]
      assert [f["item"] for f in failures] == [0, 2, 4]


id: circuit_breaker_py_core_t0
function_id: circuit_breaker_py_core
name: test_closed_to_open_after_n_failures
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_closed_to_open_after_n_failures() -> None:
      """Transicion CLOSED → OPEN despues de N fallos"""
      cb = CircuitBreaker(failure_threshold=3, reset_timeout=60.0)

      cb.check()  # Should not raise

      cb.record_failure(_transient_error())
      cb.record_failure(_transient_error())
      assert cb._state is CircuitBreakerState.CLOSED  # Still closed after 2

      cb.record_failure(_transient_error())
      assert cb._state is CircuitBreakerState.OPEN

      try:
          cb.check()
          assert False, "Should have raised CircuitBreakerOpen"
      except CircuitBreakerOpen:
          pass

      print("PASS: Transicion CLOSED → OPEN despues de N fallos")


id: circuit_breaker_py_core_t1
function_id: circuit_breaker_py_core
name: test_open_to_half_open_after_timeout
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_open_to_half_open_after_timeout() -> None:
      """Transicion OPEN → HALF_OPEN despues de timeout"""
      cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.05)
      cb.record_failure(_transient_error())
      assert cb._state is CircuitBreakerState.OPEN

      time.sleep(0.1)

      cb.check()  # Should not raise — transitions to HALF_OPEN
      assert cb._state is CircuitBreakerState.HALF_OPEN

      print("PASS: Transicion OPEN → HALF_OPEN despues de timeout")


id: circuit_breaker_py_core_t2
function_id: circuit_breaker_py_core
name: test_half_open_to_closed_on_success
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_half_open_to_closed_on_success() -> None:
      """Transicion HALF_OPEN → CLOSED en exito"""
      cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.05)
      cb.record_failure(_transient_error())
      time.sleep(0.1)
      cb.check()  # enters HALF_OPEN
      assert cb._state is CircuitBreakerState.HALF_OPEN

      cb.record_success()
      assert cb._state is CircuitBreakerState.CLOSED

      cb.check()  # Should not raise

      print("PASS: Transicion HALF_OPEN → CLOSED en exito")


id: circuit_breaker_py_core_t3
function_id: circuit_breaker_py_core
name: test_half_open_to_open_on_failure
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_half_open_to_open_on_failure() -> None:
      """Transicion HALF_OPEN → OPEN en fallo"""
      cb = CircuitBreaker(failure_threshold=1, reset_timeout=0.05)
      cb.record_failure(_transient_error())
      time.sleep(0.1)
      cb.check()  # enters HALF_OPEN
      assert cb._state is CircuitBreakerState.HALF_OPEN

      cb.record_failure(_transient_error())
      assert cb._state is CircuitBreakerState.OPEN

      print("PASS: Transicion HALF_OPEN → OPEN en fallo")


id: circuit_breaker_py_core_t4
function_id: circuit_breaker_py_core
name: test_permanent_error_opens_immediately
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_permanent_error_opens_immediately() -> None:
      """Error permanente abre inmediatamente"""
      cb = CircuitBreaker(failure_threshold=10, reset_timeout=60.0)
      assert cb._state is CircuitBreakerState.CLOSED

      cb.record_failure(_permanent_error())
      assert cb._state is CircuitBreakerState.OPEN

      print("PASS: Error permanente abre inmediatamente")


id: circuit_breaker_py_core_t5
function_id: circuit_breaker_py_core
name: test_thread_safety
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_thread_safety() -> None:
      """Thread safety (concurrencia)"""
      cb = CircuitBreaker(failure_threshold=5, reset_timeout=60.0)
      errors: list[Exception] = []

      def worker() -> None:
          try:
              for _ in range(10):
                  cb.check()
                  cb.record_failure(_transient_error())
          except CircuitBreakerOpen:
              pass
          except Exception as exc:
              errors.append(exc)

      threads = [threading.Thread(target=worker) for _ in range(20)]
      for t in threads:
          t.start()
      for t in threads:
          t.join()

      assert not errors, f"Thread errors: {errors}"
      # After concurrent failures the circuit must be OPEN or HALF_OPEN
      assert cb._state in (CircuitBreakerState.OPEN, CircuitBreakerState.HALF_OPEN, CircuitBreakerState.CLOSED)

      print("PASS: Thread safety (concurrencia)")


id: circuit_breaker_py_core_t6
function_id: circuit_breaker_py_core
name: test_retry_after_returns_zero_when_not_open
file_path: python/functions/core/circuit_breaker_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_retry_after_returns_zero_when_not_open() -> None:
      """retry_after retorna 0 cuando no esta OPEN"""
      cb = CircuitBreaker(failure_threshold=5, reset_timeout=60.0)
      assert cb.retry_after == 0.0

      cb.record_failure(_transient_error())
      # Still CLOSED (threshold not reached)
      assert cb.retry_after == 0.0

      print("PASS: retry_after retorna 0 cuando no esta OPEN")


  if __name__ == "__main__":
      test_closed_to_open_after_n_failures()
      test_open_to_half_open_after_timeout()
      test_half_open_to_closed_on_success()
      test_half_open_to_open_on_failure()
      test_permanent_error_opens_immediately()
      test_thread_safety()
      test_retry_after_returns_zero_when_not_open()
      print("\nAll tests passed.")


id: classify_api_error_py_core_t0
function_id: classify_api_error_py_core
name: test_error_429_es_transitorio
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_429_es_transitorio():
      err = Exception("HTTP 429 TooManyRequests")
      assert classify_api_error(err) == "transient"


id: classify_api_error_py_core_t1
function_id: classify_api_error_py_core
name: test_error_401_es_permanente
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_401_es_permanente():
      err = Exception("HTTP 401 Unauthorized")
      assert classify_api_error(err) == "permanent"


id: classify_api_error_py_core_t2
function_id: classify_api_error_py_core
name: test_error_timeout_es_transitorio
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_timeout_es_transitorio():
      err = Exception("Connection timeout occurred")
      assert classify_api_error(err) == "transient"


id: classify_api_error_py_core_t3
function_id: classify_api_error_py_core
name: test_error_desconocido_retorna_unknown
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_desconocido_retorna_unknown():
      err = Exception("Something completely unexpected happened")
      assert classify_api_error(err) == "unknown"


id: classify_api_error_py_core_t4
function_id: classify_api_error_py_core
name: test_error_con___cause___transitorio
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_con___cause___transitorio():
      cause = Exception("Connection reset by peer")
      err = Exception("Request failed")
      err.__cause__ = cause
      assert classify_api_error(err) == "transient"


id: classify_api_error_py_core_t5
function_id: classify_api_error_py_core
name: test_permanente_tiene_prioridad_sobre_transitorio
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_permanente_tiene_prioridad_sobre_transitorio():
      # Mensaje que contiene patrones de ambos tipos: 401 (permanent) y 503 (transient)
      err = Exception("401 503 mixed error")
      assert classify_api_error(err) == "permanent"


id: classify_api_error_py_core_t6
function_id: classify_api_error_py_core
name: test_error_403_forbidden_es_permanente
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_403_forbidden_es_permanente():
      err = Exception("403 Forbidden")
      assert classify_api_error(err) == "permanent"


id: classify_api_error_py_core_t7
function_id: classify_api_error_py_core
name: test_error_500_es_transitorio
file_path: python/functions/core/classify_api_error_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_error_500_es_transitorio():
      err = Exception("Internal server error 500")
      assert classify_api_error(err) == "transient"


id: coerce_types_py_core_t0
function_id: coerce_types_py_core
name: test_string_42_a_int_42
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_string_42_a_int_42():
      result, warnings = coerce_types({"n": "42"}, {"n": "int"})
      assert result["n"] == 42
      assert isinstance(result["n"], int)
      assert warnings == []


id: coerce_types_py_core_t1
function_id: coerce_types_py_core
name: test_string_3_14_a_float_3_14
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_string_3_14_a_float_3_14():
      result, warnings = coerce_types({"x": "3.14"}, {"x": "float"})
      assert abs(result["x"] - 3.14) < 1e-9
      assert warnings == []


id: coerce_types_py_core_t2
function_id: coerce_types_py_core
name: test_string_true_a_bool_true
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_string_true_a_bool_true():
      result, warnings = coerce_types({"flag": "true"}, {"flag": "bool"})
      assert result["flag"] is True
      assert warnings == []

      result2, _ = coerce_types({"flag": "yes"}, {"flag": "bool"})
      assert result2["flag"] is True

      result3, _ = coerce_types({"flag": "1"}, {"flag": "bool"})
      assert result3["flag"] is True

      result4, _ = coerce_types({"flag": "false"}, {"flag": "bool"})
      assert result4["flag"] is False


id: coerce_types_py_core_t3
function_id: coerce_types_py_core
name: test_string_iso8601_a_datetime
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_string_iso8601_a_datetime():
      result, warnings = coerce_types(
          {"ts": "2024-01-15T10:30:00Z"}, {"ts": "datetime"}
      )
      assert isinstance(result["ts"], datetime)
      assert result["ts"].year == 2024
      assert result["ts"].month == 1
      assert result["ts"].day == 15
      assert warnings == []


id: coerce_types_py_core_t4
function_id: coerce_types_py_core
name: test_coercion_fallida_genera_warning_sin_crash
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_coercion_fallida_genera_warning_sin_crash():
      result, warnings = coerce_types({"n": "not-a-number"}, {"n": "int"})
      # mantiene el original
      assert result["n"] == "not-a-number"
      assert len(warnings) == 1
      assert "n" in warnings[0]


id: coerce_types_py_core_t5
function_id: coerce_types_py_core
name: test_dict_con_mix_de_tipos_ya_correctos_y_strings
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dict_con_mix_de_tipos_ya_correctos_y_strings():
      data = {"a": "10", "b": 3.14, "c": True, "d": "hello"}
      schema = {"a": "int", "b": "float", "c": "bool", "d": "str"}
      result, warnings = coerce_types(data, schema)
      assert result["a"] == 10
      assert abs(result["b"] - 3.14) < 1e-9
      assert result["c"] is True
      assert result["d"] == "hello"
      assert warnings == []


id: coerce_types_py_core_t6
function_id: coerce_types_py_core
name: test_campo_ausente_en_schema_pass_through_sin_tocar
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campo_ausente_en_schema_pass_through_sin_tocar():
      data = {"a": "42", "b": [1, 2, 3]}
      schema = {"a": "int"}  # "b" no esta en schema
      result, warnings = coerce_types(data, schema)
      assert result["a"] == 42
      assert result["b"] == [1, 2, 3]
      assert warnings == []


id: coerce_types_py_core_t7
function_id: coerce_types_py_core
name: test_string_lista_a_list_str
file_path: python/functions/core/coerce_types_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_string_lista_a_list_str():
      result, warnings = coerce_types(
          {"tags": "python, go, bash"}, {"tags": "list[str]"}
      )
      assert result["tags"] == ["python", "go", "bash"]
      assert warnings == []


id: compute_backoff_delay_py_core_t0
function_id: compute_backoff_delay_py_core
name: test_attempt_0_retorna_base_delay_sin_jitter
file_path: python/functions/core/compute_backoff_delay_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_attempt_0_retorna_base_delay_sin_jitter():
      result = compute_backoff_delay(0, base_delay=0.5, max_delay=8.0, jitter=False)
      assert result == 0.5


id: compute_backoff_delay_py_core_t1
function_id: compute_backoff_delay_py_core
name: test_attempt_alto_se_cappea_a_max_delay
file_path: python/functions/core/compute_backoff_delay_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_attempt_alto_se_cappea_a_max_delay():
      result = compute_backoff_delay(10, base_delay=0.5, max_delay=8.0, jitter=False)
      assert result == 8.0


id: compute_backoff_delay_py_core_t2
function_id: compute_backoff_delay_py_core
name: test_sin_jitter_es_determinista
file_path: python/functions/core/compute_backoff_delay_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sin_jitter_es_determinista():
      r1 = compute_backoff_delay(3, base_delay=1.0, max_delay=16.0, jitter=False)
      r2 = compute_backoff_delay(3, base_delay=1.0, max_delay=16.0, jitter=False)
      assert r1 == r2
      # attempt=3: 1.0 * 2^3 = 8.0
      assert r1 == 8.0


id: compute_backoff_delay_py_core_t3
function_id: compute_backoff_delay_py_core
name: test_escala_exponencial
file_path: python/functions/core/compute_backoff_delay_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_escala_exponencial():
      d0 = compute_backoff_delay(0, base_delay=1.0, max_delay=100.0, jitter=False)
      d1 = compute_backoff_delay(1, base_delay=1.0, max_delay=100.0, jitter=False)
      d2 = compute_backoff_delay(2, base_delay=1.0, max_delay=100.0, jitter=False)
      assert d0 == 1.0
      assert d1 == 2.0
      assert d2 == 4.0


id: compute_backoff_delay_py_core_t4
function_id: compute_backoff_delay_py_core
name: test_con_jitter_no_excede_max_delay_mas_base
file_path: python/functions/core/compute_backoff_delay_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_con_jitter_no_excede_max_delay_mas_base():
      # Con jitter, delay base + jitter <= max_delay + base_delay
      for attempt in range(5):
          result = compute_backoff_delay(attempt, base_delay=0.5, max_delay=8.0, jitter=True)
          assert result >= 0.5
          assert result <= 8.0 + 0.5


id: convert_github_to_raw_url_py_core_t0
function_id: convert_github_to_raw_url_py_core
name: test_url_github_blob
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_github_blob():
      """URL de GitHub blob se convierte correctamente a raw.githubusercontent.com."""
      url = "https://github.com/openai/whisper/blob/main/README.md"
      result = convert_github_to_raw_url(url)
      assert result == "https://raw.githubusercontent.com/openai/whisper/main/README.md"


id: convert_github_to_raw_url_py_core_t1
function_id: convert_github_to_raw_url_py_core
name: test_url_github_blob_subdirectorio
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_github_blob_subdirectorio():
      """URL de GitHub blob con subdirectorio se convierte correctamente."""
      url = "https://github.com/org/repo/blob/main/src/utils/helper.py"
      result = convert_github_to_raw_url(url)
      assert result == "https://raw.githubusercontent.com/org/repo/main/src/utils/helper.py"


id: convert_github_to_raw_url_py_core_t2
function_id: convert_github_to_raw_url_py_core
name: test_url_github_blob_otra_rama
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_github_blob_otra_rama():
      """URL de GitHub blob con rama distinta a main se convierte correctamente."""
      url = "https://github.com/org/repo/blob/develop/config.yaml"
      result = convert_github_to_raw_url(url)
      assert result == "https://raw.githubusercontent.com/org/repo/develop/config.yaml"


id: convert_github_to_raw_url_py_core_t3
function_id: convert_github_to_raw_url_py_core
name: test_url_gitlab_blob
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_gitlab_blob():
      """URL de GitLab blob se convierte a raw."""
      url = "https://gitlab.com/org/repo/-/blob/main/README.md"
      result = convert_github_to_raw_url(url)
      assert result == "https://gitlab.com/org/repo/-/raw/main/README.md"


id: convert_github_to_raw_url_py_core_t4
function_id: convert_github_to_raw_url_py_core
name: test_url_gitlab_blob_sin_guion
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_gitlab_blob_sin_guion():
      """URL de GitLab blob sin '/-/' tambien se convierte."""
      url = "https://gitlab.com/org/repo/blob/main/README.md"
      result = convert_github_to_raw_url(url)
      assert result == "https://gitlab.com/org/repo/raw/main/README.md"


id: convert_github_to_raw_url_py_core_t5
function_id: convert_github_to_raw_url_py_core
name: test_url_que_no_es_blob_retorna_sin_cambios
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_que_no_es_blob_retorna_sin_cambios():
      """URL de GitHub sin blob retorna sin cambios."""
      url = "https://github.com/org/repo"
      result = convert_github_to_raw_url(url)
      assert result == url


id: convert_github_to_raw_url_py_core_t6
function_id: convert_github_to_raw_url_py_core
name: test_url_github_tree_retorna_sin_cambios
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_github_tree_retorna_sin_cambios():
      """URL de GitHub tree (no blob) retorna sin cambios."""
      url = "https://github.com/org/repo/tree/main/src"
      result = convert_github_to_raw_url(url)
      assert result == url


id: convert_github_to_raw_url_py_core_t7
function_id: convert_github_to_raw_url_py_core
name: test_url_no_github_retorna_sin_cambios
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_no_github_retorna_sin_cambios():
      """URL de otro dominio retorna sin cambios."""
      url = "https://example.com/org/repo/blob/main/file.py"
      result = convert_github_to_raw_url(url)
      assert result == url


id: convert_github_to_raw_url_py_core_t8
function_id: convert_github_to_raw_url_py_core
name: test_url_vacia_retorna_sin_cambios
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_vacia_retorna_sin_cambios():
      """URL vacia retorna string vacio."""
      result = convert_github_to_raw_url("")
      assert result == ""


id: convert_github_to_raw_url_py_core_t9
function_id: convert_github_to_raw_url_py_core
name: test_url_raw_githubusercontent_retorna_sin_cambios
file_path: python/functions/core/convert_github_to_raw_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_raw_githubusercontent_retorna_sin_cambios():
      """URL ya en raw.githubusercontent.com no se modifica."""
      url = "https://raw.githubusercontent.com/org/repo/main/file.py"
      result = convert_github_to_raw_url(url)
      assert result == url


id: cursor_paginate_py_core_t0
function_id: cursor_paginate_py_core
name: test_api_retorna_3_paginas_de_10_items
file_path: python/functions/core/cursor_paginate_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_api_retorna_3_paginas_de_10_items():
      pages = [
          [{"id": i, "cursor": str(i)} for i in range(0, 10)],
          [{"id": i, "cursor": str(i)} for i in range(10, 20)],
          [{"id": i, "cursor": str(i)} for i in range(20, 30)],
          [],  # sentinel: empty page ends pagination
      ]
      api = make_api(pages)
      result = cursor_paginate(
          fetch_page=api,
          get_cursor=get_cursor,
          page_size=10,
          max_items=2000,
          max_retries=0,
      )
      assert len(result) == 30
      assert result[0]["id"] == 0
      assert result[-1]["id"] == 29


id: cursor_paginate_py_core_t1
function_id: cursor_paginate_py_core
name: test_api_falla_1_vez_por_pagina_retry_funciona
file_path: python/functions/core/cursor_paginate_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_api_falla_1_vez_por_pagina_retry_funciona():
      """fetch_page falla en el primer intento de cada llamada, pero el retry recupera."""
      call_counter = [0]
      # Cada pagina tiene 5 items. 2 paginas en total, luego vacio.
      items_by_page = [
          [{"id": i, "cursor": str(i)} for i in range(0, 5)],
          [{"id": i, "cursor": str(i)} for i in range(5, 10)],
      ]
      page_idx = [0]
      fail_flags = [True, True]  # falla una vez por pagina

      def fetch_page(limit: int, cursor: str | None) -> list[dict]:
          idx = page_idx[0]
          if idx < len(fail_flags) and fail_flags[idx]:
              fail_flags[idx] = False
              raise ConnectionError("transient failure")
          page_idx[0] += 1
          if idx >= len(items_by_page):
              return []
          return items_by_page[idx]

      result = cursor_paginate(
          fetch_page=fetch_page,
          get_cursor=get_cursor,
          page_size=5,
          max_items=2000,
          max_retries=3,
          retry_delay=0.0,
          retryable_exceptions=(ConnectionError, TimeoutError, OSError),
      )
      assert len(result) == 10


id: cursor_paginate_py_core_t2
function_id: cursor_paginate_py_core
name: test_max_items_limita_correctamente
file_path: python/functions/core/cursor_paginate_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_max_items_limita_correctamente():
      # 50 items disponibles en 5 paginas de 10, pero max_items=25
      pages = [
          [{"id": i, "cursor": str(i)} for i in range(j * 10, j * 10 + 10)]
          for j in range(5)
      ]
      api = make_api(pages)
      result = cursor_paginate(
          fetch_page=api,
          get_cursor=get_cursor,
          page_size=10,
          max_items=25,
          max_retries=0,
      )
      assert len(result) == 25
      assert result[-1]["id"] == 24


id: cursor_paginate_py_core_t3
function_id: cursor_paginate_py_core
name: test_api_retorna_pagina_parcial_ultima_pagina
file_path: python/functions/core/cursor_paginate_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_api_retorna_pagina_parcial_ultima_pagina():
      pages = [
          [{"id": i, "cursor": str(i)} for i in range(10)],  # full page
          [{"id": i, "cursor": str(i)} for i in range(10, 17)],  # partial — 7 items
      ]
      api = make_api(pages)
      result = cursor_paginate(
          fetch_page=api,
          get_cursor=get_cursor,
          page_size=10,
          max_items=2000,
          max_retries=0,
      )
      assert len(result) == 17
      assert result[-1]["id"] == 16


id: cursor_paginate_py_core_t4
function_id: cursor_paginate_py_core
name: test_cursor_none_en_ultimo_item_se_detiene
file_path: python/functions/core/cursor_paginate_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_cursor_none_en_ultimo_item_se_detiene():
      """Cuando el ultimo item no tiene cursor, la paginacion debe detenerse."""
      pages = [
          [{"id": i, "cursor": str(i)} for i in range(10)],
          # last item has no cursor — signals end of data
          [{"id": i, "cursor": (str(i) if i < 19 else None)} for i in range(10, 20)],
      ]
      api = make_api(pages)

      def get_cursor_nullable(item: dict) -> str | None:
          return item.get("cursor")

      result = cursor_paginate(
          fetch_page=api,
          get_cursor=get_cursor_nullable,
          page_size=10,
          max_items=2000,
          max_retries=0,
      )
      assert len(result) == 20
      assert result[-1]["id"] == 19


id: detect_url_type_py_core_t0
function_id: detect_url_type_py_core
name: test_url_pdf_por_extension
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_pdf_por_extension():
      """URL .pdf se detecta por extension sin hacer request HTTP."""
      url_type, metadata = detect_url_type("https://example.com/report.pdf")
      assert url_type == "pdf"
      assert metadata["detection"] == "extension"


id: detect_url_type_py_core_t1
function_id: detect_url_type_py_core
name: test_url_github_repo
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_github_repo():
      """URL de GitHub org/repo se detecta como code_repository por patron URL."""
      url_type, metadata = detect_url_type("https://github.com/openai/whisper")
      assert url_type == "code_repository"
      assert metadata["detection"] == "url_pattern"


id: detect_url_type_py_core_t2
function_id: detect_url_type_py_core
name: test_url_github_con_git_suffix
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_github_con_git_suffix():
      """URL github terminada en .git se detecta como code_repository."""
      url_type, metadata = detect_url_type("https://github.com/openai/whisper.git")
      assert url_type == "code_repository"


id: detect_url_type_py_core_t3
function_id: detect_url_type_py_core
name: test_url_markdown_por_extension
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_markdown_por_extension():
      """URL .md se detecta como markdown por extension."""
      url_type, metadata = detect_url_type("https://example.com/README.md")
      assert url_type == "markdown"
      assert metadata["detection"] == "extension"


id: detect_url_type_py_core_t4
function_id: detect_url_type_py_core
name: test_url_ssh_git
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_ssh_git():
      """URL SSH git@ se detecta como code_repository."""
      url_type, metadata = detect_url_type("git@github.com:openai/whisper.git")
      assert url_type == "code_repository"
      assert metadata["detection"] == "ssh_pattern"


id: detect_url_type_py_core_t5
function_id: detect_url_type_py_core
name: test_url_html_por_extension
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_html_por_extension():
      """URL .html se detecta como webpage por extension."""
      url_type, metadata = detect_url_type("https://example.com/page.html")
      assert url_type == "webpage"
      assert metadata["detection"] == "extension"


id: detect_url_type_py_core_t6
function_id: detect_url_type_py_core
name: test_url_txt_por_extension
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_url_txt_por_extension():
      """URL .txt se detecta como text por extension."""
      url_type, metadata = detect_url_type("https://example.com/data.txt")
      assert url_type == "text"


id: detect_url_type_py_core_t7
function_id: detect_url_type_py_core
name: test_github_subrepo_no_es_repo
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_github_subrepo_no_es_repo():
      """URL de GitHub apuntando a un issue/blob no se trata como code_repository."""
      # Debe intentar HEAD request (que fallara sin red) — verificamos que no clasifica como repo
      # Solo comprobamos que no devuelve code_repository por patron URL
      url = "https://github.com/openai/whisper/blob/main/README.md"
      # Extension .md deberia detectarse primero
      url_type, metadata = detect_url_type(url)
      assert url_type == "markdown"


id: detect_url_type_py_core_t8
function_id: detect_url_type_py_core
name: test_helper_type_from_extension
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_helper_type_from_extension():
      """_type_from_extension funciona para extensiones conocidas."""
      assert _type_from_extension("/doc.pdf") == "pdf"
      assert _type_from_extension("/README.md") == "markdown"
      assert _type_from_extension("/notes.txt") == "text"
      assert _type_from_extension("/unknown.xyz") is None


id: detect_url_type_py_core_t9
function_id: detect_url_type_py_core
name: test_helper_type_from_content_type
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_helper_type_from_content_type():
      """_type_from_content_type mapea headers correctamente."""
      assert _type_from_content_type("application/pdf; charset=utf-8") == "pdf"
      assert _type_from_content_type("text/html; charset=utf-8") == "webpage"
      assert _type_from_content_type("text/plain") == "text"
      assert _type_from_content_type("text/markdown") == "markdown"
      assert _type_from_content_type("application/octet-stream") == "webpage"


id: detect_url_type_py_core_t10
function_id: detect_url_type_py_core
name: test_helper_is_ssh_git_url
file_path: python/functions/core/detect_url_type_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_helper_is_ssh_git_url():
      """_is_ssh_git_url detecta formato git@."""
      assert _is_ssh_git_url("git@github.com:org/repo.git") is True
      assert _is_ssh_git_url("https://github.com/org/repo") is False
      assert _is_ssh_git_url("ssh://git@github.com/org/repo") is False


id: docx_to_markdown_py_core_t0
function_id: docx_to_markdown_py_core
name: test_docx_con_headings_y_parrafos
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_docx_con_headings_y_parrafos():
      """docx con headings y parrafos"""

      def build(doc):
          doc.add_heading("Titulo Principal", level=1)
          doc.add_paragraph("Primer parrafo de contenido.")
          doc.add_heading("Seccion", level=2)
          doc.add_paragraph("Segundo parrafo.")

      path = _make_docx(build)
      try:
          result = docx_to_markdown(path)
          assert "# Titulo Principal" in result
          assert "## Seccion" in result
          assert "Primer parrafo de contenido." in result
          assert "Segundo parrafo." in result
      finally:
          os.unlink(path)


id: docx_to_markdown_py_core_t1
function_id: docx_to_markdown_py_core
name: test_docx_con_tablas_intercaladas
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_docx_con_tablas_intercaladas():
      """docx con tablas intercaladas"""

      def build(doc):
          doc.add_paragraph("Texto antes de la tabla.")
          table = doc.add_table(rows=2, cols=3)
          table.cell(0, 0).text = "Col1"
          table.cell(0, 1).text = "Col2"
          table.cell(0, 2).text = "Col3"
          table.cell(1, 0).text = "a"
          table.cell(1, 1).text = "b"
          table.cell(1, 2).text = "c"
          doc.add_paragraph("Texto despues de la tabla.")

      path = _make_docx(build)
      try:
          result = docx_to_markdown(path)
          # Table must appear BETWEEN the two paragraphs
          before_idx = result.index("Texto antes de la tabla.")
          table_idx = result.index("| Col1")
          after_idx = result.index("Texto despues de la tabla.")
          assert before_idx < table_idx < after_idx
          assert "| Col2" in result
          assert "| a" in result
      finally:
          os.unlink(path)


id: docx_to_markdown_py_core_t2
function_id: docx_to_markdown_py_core
name: test_docx_con_formato_bold_italic
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_docx_con_formato_bold_italic():
      """docx con formato bold/italic"""

      def build(doc):
          para = doc.add_paragraph()
          run_bold = para.add_run("negrita")
          run_bold.bold = True
          run_normal = para.add_run(" texto normal ")
          run_italic = para.add_run("cursiva")
          run_italic.italic = True

      path = _make_docx(build)
      try:
          result = docx_to_markdown(path)
          assert "**negrita**" in result
          assert "*cursiva*" in result
          assert "texto normal" in result
      finally:
          os.unlink(path)


id: docx_to_markdown_py_core_t3
function_id: docx_to_markdown_py_core
name: test_docx_vacio
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_docx_vacio():
      """docx vacio"""

      def build(doc):
          # python-docx adds a default empty paragraph; remove all content
          # by just not adding anything — the default empty paragraph will
          # produce an empty string that gets filtered out.
          pass

      path = _make_docx(build)
      try:
          result = docx_to_markdown(path)
          # Empty document should produce empty or whitespace-only output
          assert result.strip() == ""
      finally:
          os.unlink(path)


id: docx_to_markdown_py_core_t4
function_id: docx_to_markdown_py_core
name: test_archivo_no_encontrado
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_no_encontrado():
      """archivo no encontrado lanza FileNotFoundError"""
      with pytest.raises(FileNotFoundError):
          docx_to_markdown("/tmp/nonexistent_file_fn_registry.docx")


  if __name__ == "__main__":
      test_docx_con_headings_y_parrafos()
      test_docx_con_tablas_intercaladas()
      test_docx_con_formato_bold_italic()
      test_docx_vacio()
      test_archivo_no_encontrado()
      print("All tests passed.")


id: epub_to_markdown_py_core_t0
function_id: epub_to_markdown_py_core
name: test_html_heading_conversion
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_heading_conversion():
      """conversion de headings h1-h3."""
      html = '<h1>Titulo</h1><h2>Subtitulo</h2><h3>Seccion</h3>'
      result = _html_to_markdown(html)
      assert '# Titulo' in result
      assert '## Subtitulo' in result
      assert '### Seccion' in result


id: epub_to_markdown_py_core_t1
function_id: epub_to_markdown_py_core
name: test_html_bold_italic
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_bold_italic():
      """conversion de bold e italic."""
      html = '<p><strong>negrita</strong> y <em>italica</em></p>'
      result = _html_to_markdown(html)
      assert '**negrita**' in result
      assert '*italica*' in result


id: epub_to_markdown_py_core_t2
function_id: epub_to_markdown_py_core
name: test_html_script_style_removed
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_script_style_removed():
      """script y style se eliminan del output."""
      html = '<script>alert(1)</script><style>body{}</style><p>Contenido</p>'
      result = _html_to_markdown(html)
      assert 'alert' not in result
      assert 'body{}' not in result
      assert 'Contenido' in result


id: epub_to_markdown_py_core_t3
function_id: epub_to_markdown_py_core
name: test_html_entities_unescaped
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_entities_unescaped():
      """HTML entities se convierten a caracteres."""
      html = '<p>Tom &amp; Jerry &lt;show&gt;</p>'
      result = _html_to_markdown(html)
      assert 'Tom & Jerry' in result
      assert '<show>' in result


  # ---------------------------------------------------------------------------
  # Tests de epub_via_zipfile (sin ebooklib)
  # ---------------------------------------------------------------------------


id: epub_to_markdown_py_core_t4
function_id: epub_to_markdown_py_core
name: test_epub_via_zipfile_extrae_html
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_epub_via_zipfile_extrae_html():
      """epub sin ebooklib extrae texto de archivos html."""
      path = _build_epub({
          'chapter.html': '<html><body><h1>Capitulo Uno</h1><p>Hola mundo.</p></body></html>',
      })
      try:
          result = _epub_via_zipfile(path)
          assert 'Capitulo Uno' in result
          assert 'Hola mundo' in result
      finally:
          os.unlink(path)


  # ---------------------------------------------------------------------------
  # Tests de epub_to_markdown (integracion)
  # ---------------------------------------------------------------------------


id: epub_to_markdown_py_core_t5
function_id: epub_to_markdown_py_core
name: test_epub_con_ebooklib_metadata
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_epub_con_ebooklib_metadata():
      """epub con ebooklib incluye titulo y autor en el output."""
      pytest.importorskip('ebooklib')
      path = _build_epub_with_opf(
          title='Mi Libro',
          author='Ana Perez',
          body_html='<h1>Introduccion</h1><p>Primer parrafo.</p>',
      )
      try:
          result = epub_to_markdown(path)
          assert '# Mi Libro' in result
          assert 'Ana Perez' in result
          assert 'Introduccion' in result
      finally:
          os.unlink(path)


id: epub_to_markdown_py_core_t6
function_id: epub_to_markdown_py_core
name: test_epub_corrupto_lanza_excepcion
file_path: python/functions/core/epub_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_epub_corrupto_lanza_excepcion():
      """epub corrupto lanza Exception."""
      import tempfile
      tmp = tempfile.NamedTemporaryFile(suffix='.epub', delete=False)
      tmp.write(b'esto no es un epub valido')
      tmp.close()
      try:
          with pytest.raises(Exception):
              epub_to_markdown(tmp.name)
      finally:
          os.unlink(tmp.name)


id: estimate_token_count_py_core_t0
function_id: estimate_token_count_py_core
name: test_extract_frontmatter_con_frontmatter
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extract_frontmatter_con_frontmatter():
      content = "---\ntitle: Hello\nauthor: Alice\n---\n# Body\n"
      remaining, data = extract_frontmatter(content)
      assert data is not None
      assert data.get("title") in ("Hello", "Hello")
      assert "# Body" in remaining


id: estimate_token_count_py_core_t1
function_id: estimate_token_count_py_core
name: test_extract_frontmatter_sin_frontmatter
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extract_frontmatter_sin_frontmatter():
      content = "# Just a heading\n\nSome text."
      remaining, data = extract_frontmatter(content)
      assert data is None
      assert remaining == content


id: estimate_token_count_py_core_t2
function_id: estimate_token_count_py_core
name: test_extract_frontmatter_frontmatter_vacio
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extract_frontmatter_frontmatter_vacio():
      content = "---\n\n---\n# Body\n"
      remaining, data = extract_frontmatter(content)
      # An empty frontmatter block — data may be None or empty dict
      assert "# Body" in remaining


id: estimate_token_count_py_core_t3
function_id: estimate_token_count_py_core
name: test_extract_frontmatter_con_listas
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extract_frontmatter_con_listas():
      content = "---\ntags: [go, python, bash]\nauthor: Bob\n---\nContent here\n"
      remaining, data = extract_frontmatter(content)
      assert data is not None
      assert "Content here" in remaining


  # ---------------------------------------------------------------------------
  # find_headings
  # ---------------------------------------------------------------------------


id: estimate_token_count_py_core_t4
function_id: estimate_token_count_py_core
name: test_find_headings_normales
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_find_headings_normales():
      content = "# Title\n\nSome text\n\n## Section\n\n### Sub\n"
      headings = find_headings(content)
      titles = [h[2] for h in headings]
      levels = [h[3] for h in headings]
      assert titles == ["Title", "Section", "Sub"]
      assert levels == [1, 2, 3]


id: estimate_token_count_py_core_t5
function_id: estimate_token_count_py_core
name: test_find_headings_dentro_de_code_blocks
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_find_headings_dentro_de_code_blocks():
      content = "# Real\n\n```\n# Not a heading\n```\n\n## Also real\n"
      headings = find_headings(content)
      titles = [h[2] for h in headings]
      assert "Not a heading" not in titles
      assert "Real" in titles
      assert "Also real" in titles


id: estimate_token_count_py_core_t6
function_id: estimate_token_count_py_core
name: test_find_headings_escapados
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_find_headings_escapados():
      content = "# Normal\n\n\\# Escaped heading\n\n## Another\n"
      headings = find_headings(content)
      titles = [h[2] for h in headings]
      # Escaped heading should not appear
      assert "Escaped heading" not in titles
      assert "Normal" in titles


id: estimate_token_count_py_core_t7
function_id: estimate_token_count_py_core
name: test_find_headings_en_html_comments
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_find_headings_en_html_comments():
      content = "# Visible\n\n<!-- # Hidden -->\n\n## Also visible\n"
      headings = find_headings(content)
      titles = [h[2] for h in headings]
      assert "Hidden" not in titles
      assert "Visible" in titles


  # ---------------------------------------------------------------------------
  # estimate_token_count
  # ---------------------------------------------------------------------------


id: estimate_token_count_py_core_t9
function_id: estimate_token_count_py_core
name: test_estimate_token_count_solo_latin
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_estimate_token_count_solo_latin():
      # "hello" = 5 non-whitespace chars * 0.3 = 1 (int)
      result = estimate_token_count("hello")
      assert isinstance(result, int)
      assert result >= 0


id: estimate_token_count_py_core_t10
function_id: estimate_token_count_py_core
name: test_estimate_token_count_solo_cjk
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_estimate_token_count_solo_cjk():
      # 3 CJK chars * 0.7 = 2 (int)
      result = estimate_token_count("\u4e2d\u6587\u8bed")
      assert isinstance(result, int)
      assert result >= 1


id: estimate_token_count_py_core_t11
function_id: estimate_token_count_py_core
name: test_estimate_token_count_texto_mixto
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_estimate_token_count_texto_mixto():
      # Mix of CJK and latin
      result = estimate_token_count("Hello \u4e16\u754c")
      assert isinstance(result, int)
      assert result > 0


  # ---------------------------------------------------------------------------
  # smart_split_content
  # ---------------------------------------------------------------------------


id: estimate_token_count_py_core_t12
function_id: estimate_token_count_py_core
name: test_smart_split_content_contenido_corto
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_smart_split_content_contenido_corto():
      content = "Short paragraph.\n\nAnother short one."
      parts = smart_split_content(content, max_tokens=1024, max_chars=8000)
      assert len(parts) == 1
      assert parts[0] == content


id: estimate_token_count_py_core_t13
function_id: estimate_token_count_py_core
name: test_smart_split_content_contenido_largo
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_smart_split_content_contenido_largo():
      # Create content with many small paragraphs that together exceed max_tokens
      paragraphs = ["word " * 10] * 200  # 200 paragraphs of ~10 words each
      content = "\n\n".join(paragraphs)
      parts = smart_split_content(content, max_tokens=50, max_chars=8000)
      assert len(parts) > 1
      for part in parts:
          assert estimate_token_count(part) <= 50 + estimate_token_count("word " * 10)


id: estimate_token_count_py_core_t14
function_id: estimate_token_count_py_core
name: test_smart_split_content_parrafo_gigante
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_smart_split_content_parrafo_gigante():
      # Single paragraph that exceeds max_chars — must be force-cut
      long_para = "x" * 20000
      parts = smart_split_content(long_para, max_tokens=1024, max_chars=8000)
      assert len(parts) >= 3  # 20000 / 8000 = at least 3 chunks
      for part in parts:
          assert len(part) <= 8000


  # ---------------------------------------------------------------------------
  # sanitize_for_path
  # ---------------------------------------------------------------------------


id: estimate_token_count_py_core_t15
function_id: estimate_token_count_py_core
name: test_sanitize_for_path_texto_normal
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sanitize_for_path_texto_normal():
      assert sanitize_for_path("Hello World") == "Hello_World"


id: estimate_token_count_py_core_t16
function_id: estimate_token_count_py_core
name: test_sanitize_for_path_caracteres_especiales
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sanitize_for_path_caracteres_especiales():
      result = sanitize_for_path("Hello! @World#2024")
      assert "!" not in result
      assert "@" not in result
      assert "#" not in result


id: estimate_token_count_py_core_t17
function_id: estimate_token_count_py_core
name: test_sanitize_for_path_texto_muy_largo
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sanitize_for_path_texto_muy_largo():
      long_text = "a" * 100
      result = sanitize_for_path(long_text, max_length=50)
      assert len(result) <= 50
      # Should have hash suffix
      assert "_" in result


id: estimate_token_count_py_core_t19
function_id: estimate_token_count_py_core
name: test_sanitize_for_path_texto_cjk
file_path: python/functions/core/parse_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sanitize_for_path_texto_cjk():
      result = sanitize_for_path("\u4e2d\u6587\u6807\u9898")
      assert len(result) > 0
      assert result != "section"


id: excel_to_markdown_py_core_t0
function_id: excel_to_markdown_py_core
name: test_xlsx_multiples_sheets
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_xlsx_multiples_sheets():
      """xlsx con multiples sheets produce una seccion H2 por sheet."""
      path = _make_xlsx(
          {
              "Ventas": [["Producto", "Precio", "Cantidad"], ["Manzana", 1.5, 100], ["Pera", 2.0, 50]],
              "Resumen": [["Total", "Importe"], ["150", "225.0"]],
          },
          "multi.xlsx",
      )
      result = excel_to_markdown(path)

      assert "## Sheet: Ventas" in result
      assert "## Sheet: Resumen" in result
      assert "Producto" in result
      assert "Manzana" in result
      assert "Total" in result


id: excel_to_markdown_py_core_t1
function_id: excel_to_markdown_py_core
name: test_sheet_vacio
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sheet_vacio():
      """Sheet sin filas produce nota de sheet vacio."""
      path = _make_xlsx({"Vacio": []}, "empty.xlsx")
      result = excel_to_markdown(path)

      assert "## Sheet: Vacio" in result
      assert "empty sheet" in result


id: excel_to_markdown_py_core_t2
function_id: excel_to_markdown_py_core
name: test_sheet_truncado
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sheet_truncado():
      """Sheet con mas filas que max_rows_per_sheet se trunca con nota."""
      rows = [["col"]] + [[str(i)] for i in range(20)]
      path = _make_xlsx({"Data": rows}, "big.xlsx")
      result = excel_to_markdown(path, max_rows_per_sheet=5)

      assert "omitted" in result
      # 21 filas totales, 5 mostradas -> 16 omitidas
      assert "16 rows omitted" in result


id: excel_to_markdown_py_core_t3
function_id: excel_to_markdown_py_core
name: test_sheet_con_formulas_data_only
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sheet_con_formulas_data_only():
      """Archivo xlsx abierto con data_only=True muestra valores calculados (o None si no guardados)."""
      wb = openpyxl.Workbook()
      ws = wb.active
      ws.title = "Formulas"
      ws.append(["A", "B", "Suma"])
      ws.append([1, 2, "=A2+B2"])
      path = os.path.join(tempfile.mkdtemp(), "formulas.xlsx")
      wb.save(path)

      result = excel_to_markdown(path)
      assert "## Sheet: Formulas" in result
      # La celda formula puede ser None con data_only=True si no fue guardada con valor
      assert "Suma" in result


id: excel_to_markdown_py_core_t4
function_id: excel_to_markdown_py_core
name: test_xls_legacy_con_fechas
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_xls_legacy_con_fechas():
      """xls legacy: la funcion debe aceptar .xls (via xlrd) y manejar fechas."""
      # Creamos un .xls usando xlwt si disponible, si no lo saltamos
      pytest.importorskip("xlwt", reason="xlwt no disponible para crear .xls de prueba")
      import xlwt

      wb = xlwt.Workbook()
      ws = wb.add_sheet("Fechas")
      ws.write(0, 0, "Nombre")
      ws.write(0, 1, "Fecha")
      ws.write(1, 0, "Evento A")

      date_format = xlwt.XFStyle()
      date_format.num_format_str = "YYYY-MM-DD"
      ws.write(1, 1, datetime.date(2024, 1, 15).toordinal() - 693594, date_format)

      path = os.path.join(tempfile.mkdtemp(), "legacy.xls")
      wb.save(path)

      result = excel_to_markdown(path)
      assert "## Sheet: Fechas" in result
      assert "Evento A" in result


id: excel_to_markdown_py_core_t5
function_id: excel_to_markdown_py_core
name: test_extension_no_soportada
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extension_no_soportada():
      """Extension no soportada lanza ValueError."""
      path = os.path.join(tempfile.mkdtemp(), "data.csv")
      with open(path, "w") as f:
          f.write("a,b\n1,2\n")

      with pytest.raises(ValueError, match="Unsupported extension"):
          excel_to_markdown(path)


id: excel_to_markdown_py_core_t6
function_id: excel_to_markdown_py_core
name: test_archivo_no_existe
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_no_existe():
      """Archivo inexistente lanza FileNotFoundError."""
      with pytest.raises(FileNotFoundError):
          excel_to_markdown("/tmp/no_existe_para_nada.xlsx")


id: excel_to_markdown_py_core_t7
function_id: excel_to_markdown_py_core
name: test_dimensiones_en_metadata
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dimensiones_en_metadata():
      """El markdown incluye dimensiones del sheet."""
      path = _make_xlsx({"Hoja1": [["A", "B"], [1, 2], [3, 4]]}, "dims.xlsx")
      result = excel_to_markdown(path)
      assert "**Dimensions:**" in result
      assert "3 x 2" in result


id: excel_to_markdown_py_core_t8
function_id: excel_to_markdown_py_core
name: test_tabla_markdown_formato
file_path: python/functions/core/excel_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tabla_markdown_formato():
      """La tabla tiene formato correcto con separador de header."""
      path = _make_xlsx({"Datos": [["Col1", "Col2"], ["val1", "val2"]]}, "fmt.xlsx")
      result = excel_to_markdown(path)
      # Debe tener linea separadora con ---
      assert "| --- |" in result or "| --- | --- |" in result
      assert "Col1" in result
      assert "val1" in result


id: extract_text_from_file_py_core_t0
function_id: extract_text_from_file_py_core
name: test_pdf_con_texto_extrae_contenido_correctamente
file_path: python/functions/core/extract_text_from_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pdf_con_texto_extrae_contenido_correctamente():
      """PDF con texto extrae contenido correctamente."""
      try:
          import fitz
      except ImportError:
          pytest.skip("PyMuPDF no instalado")

      # Create a minimal in-memory PDF using PyMuPDF and write it to a temp file
      doc = fitz.open()
      page = doc.new_page()
      page.insert_text((72, 72), "Hello from PDF")
      with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
          tmp_path = f.name
      try:
          doc.save(tmp_path)
          doc.close()
          result = extract_text_from_file(tmp_path)
          assert "Hello from PDF" in result
      finally:
          os.unlink(tmp_path)


id: extract_text_from_file_py_core_t1
function_id: extract_text_from_file_py_core
name: test_archivo_md_utf8_retorna_contenido
file_path: python/functions/core/extract_text_from_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_md_utf8_retorna_contenido():
      """archivo MD UTF-8 retorna contenido."""
      content = "# Titulo\n\nParrafo con texto UTF-8: cafe, senor, japon.\n"
      with tempfile.NamedTemporaryFile(
          suffix=".md", mode="wb", delete=False
      ) as f:
          f.write(content.encode("utf-8"))
          tmp_path = f.name
      try:
          result = extract_text_from_file(tmp_path)
          assert "# Titulo" in result
          assert "cafe" in result
      finally:
          os.unlink(tmp_path)


id: extract_text_from_file_py_core_t2
function_id: extract_text_from_file_py_core
name: test_archivo_txt_latin1_detecta_encoding
file_path: python/functions/core/extract_text_from_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_txt_latin1_detecta_encoding():
      """archivo TXT latin-1 detecta encoding."""
      content = "Texto en latin-1: cafe, hotel, naive\n"
      with tempfile.NamedTemporaryFile(
          suffix=".txt", mode="wb", delete=False
      ) as f:
          f.write(content.encode("latin-1"))
          tmp_path = f.name
      try:
          result = extract_text_from_file(tmp_path)
          # The word "cafe" or similar should appear in the decoded result
          assert len(result) > 0
          assert "cafe" in result or "caf" in result
      finally:
          os.unlink(tmp_path)


id: extract_text_from_file_py_core_t3
function_id: extract_text_from_file_py_core
name: test_archivo_inexistente_lanza_filenotfounderror
file_path: python/functions/core/extract_text_from_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_inexistente_lanza_filenotfounderror():
      """archivo inexistente lanza FileNotFoundError."""
      with pytest.raises(FileNotFoundError):
          extract_text_from_file("/tmp/no_existe_este_archivo_12345.txt")


id: extract_text_from_file_py_core_t4
function_id: extract_text_from_file_py_core
name: test_extension_no_soportada_lanza_valueerror
file_path: python/functions/core/extract_text_from_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extension_no_soportada_lanza_valueerror():
      """extension no soportada lanza ValueError."""
      with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as f:
          f.write(b"fake docx content")
          tmp_path = f.name
      try:
          with pytest.raises(ValueError, match="Unsupported file extension"):
              extract_text_from_file(tmp_path)
      finally:
          os.unlink(tmp_path)


id: format_iso8601_py_core_t0
function_id: format_iso8601_py_core
name: test_datetime_naive_formateado_como_utc
file_path: python/functions/core/format_iso8601_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_datetime_naive_formateado_como_utc():
      dt = datetime(2026, 2, 21, 13, 20, 23, 147000)
      result = format_iso8601(dt)
      assert result == "2026-02-21T13:20:23.147Z"


id: format_iso8601_py_core_t1
function_id: format_iso8601_py_core
name: test_datetime_con_timezone_convertido_a_utc
file_path: python/functions/core/format_iso8601_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_datetime_con_timezone_convertido_a_utc():
      tz8 = timezone(timedelta(hours=8))
      dt = datetime(2026, 2, 21, 21, 20, 23, 147000, tzinfo=tz8)
      result = format_iso8601(dt)
      assert result == "2026-02-21T13:20:23.147Z"


id: format_iso8601_py_core_t2
function_id: format_iso8601_py_core
name: test_datetime_utc_sin_conversion
file_path: python/functions/core/format_iso8601_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_datetime_utc_sin_conversion():
      dt = datetime(2026, 6, 15, 9, 0, 0, 500000, tzinfo=timezone.utc)
      result = format_iso8601(dt)
      assert result == "2026-06-15T09:00:00.500Z"


id: format_simplified_py_core_t0
function_id: format_simplified_py_core
name: test_mismo_dia_muestra_formato_hora
file_path: python/functions/core/format_simplified_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_mismo_dia_muestra_formato_hora():
      now = datetime(2026, 2, 21, 15, 0, 0)
      dt = datetime(2026, 2, 21, 9, 30, 45)
      result = format_simplified(dt, now)
      assert result == "09:30:45"


id: format_simplified_py_core_t1
function_id: format_simplified_py_core
name: test_dia_anterior_muestra_formato_fecha
file_path: python/functions/core/format_simplified_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dia_anterior_muestra_formato_fecha():
      now = datetime(2026, 2, 21, 15, 0, 0)
      dt = datetime(2026, 2, 20, 9, 30, 45)
      result = format_simplified(dt, now)
      assert result == "2026-02-20"


id: format_simplified_py_core_t2
function_id: format_simplified_py_core
name: test_exactamente_24h_muestra_formato_fecha
file_path: python/functions/core/format_simplified_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_exactamente_24h_muestra_formato_fecha():
      now = datetime(2026, 2, 21, 15, 0, 0)
      dt = now - timedelta(hours=24)
      result = format_simplified(dt, now)
      assert result == "2026-02-20"


id: format_table_to_markdown_py_core_t0
function_id: format_table_to_markdown_py_core
name: test_tabla_normal
file_path: python/functions/core/format_table_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tabla_normal():
      rows = [["Name", "Age", "City"], ["Alice", "30", "Madrid"], ["Bob", "25", "Berlin"]]
      result = format_table_to_markdown(rows)
      assert "| Name  | Age | City   |" in result
      assert "| ---   | --- | ---    |" in result or "---" in result
      assert "| Alice | 30  | Madrid |" in result
      assert "| Bob   | 25  | Berlin |" in result


id: format_table_to_markdown_py_core_t1
function_id: format_table_to_markdown_py_core
name: test_tabla_con_celdas_vacias
file_path: python/functions/core/format_table_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tabla_con_celdas_vacias():
      rows = [["A", "B"], ["", "x"], ["y", ""]]
      result = format_table_to_markdown(rows)
      assert "|" in result
      lines = result.split("\n")
      assert len(lines) == 4  # header + separator + 2 data rows


id: format_table_to_markdown_py_core_t2
function_id: format_table_to_markdown_py_core
name: test_tabla_con_1_fila
file_path: python/functions/core/format_table_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tabla_con_1_fila():
      rows = [["Solo", "Row"]]
      result = format_table_to_markdown(rows)
      lines = result.split("\n")
      # header + separator (no data rows)
      assert len(lines) == 2
      assert "Solo" in lines[0]
      assert "---" in lines[1]


id: format_table_to_markdown_py_core_t4
function_id: format_table_to_markdown_py_core
name: test_celdas_con_pipes
file_path: python/functions/core/format_table_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_celdas_con_pipes():
      rows = [["Header"], ["cell|with|pipes"]]
      result = format_table_to_markdown(rows)
      assert "\\|" in result


id: format_table_to_markdown_py_core_t5
function_id: format_table_to_markdown_py_core
name: test_sin_header
file_path: python/functions/core/format_table_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sin_header():
      rows = [["A", "B"], ["C", "D"]]
      result = format_table_to_markdown(rows, has_header=False)
      assert "---" not in result
      lines = result.split("\n")
      assert len(lines) == 2


  if __name__ == "__main__":
      test_tabla_normal()
      test_tabla_con_celdas_vacias()
      test_tabla_con_1_fila()
      test_tabla_vacia()
      test_celdas_con_pipes()
      test_sin_header()
      print("All tests passed.")


id: from_csv_py_core_t0
function_id: from_csv_py_core
name: test_csv_simple_con_header
file_path: python/functions/core/from_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_csv_simple_con_header():
      text = "nombre,edad\r\nAna,30\r\nBob,25"
      result = from_csv(text)
      assert len(result) == 2
      assert result[0] == {"nombre": "Ana", "edad": "30"}
      assert result[1] == {"nombre": "Bob", "edad": "25"}


id: from_csv_py_core_t1
function_id: from_csv_py_core
name: test_campos_con_escaping
file_path: python/functions/core/from_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campos_con_escaping():
      text = 'a,b\r\n"dijo ""hola""","uno,dos"'
      result = from_csv(text)
      assert result[0]["a"] == 'dijo "hola"'
      assert result[0]["b"] == "uno,dos"


id: from_csv_py_core_t2
function_id: from_csv_py_core
name: test_sin_header_keys_generadas
file_path: python/functions/core/from_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sin_header_keys_generadas():
      text = "foo,bar\nbaz,qux"
      result = from_csv(text, has_header=False)
      assert result[0] == {"col_0": "foo", "col_1": "bar"}
      assert result[1] == {"col_0": "baz", "col_1": "qux"}


id: from_csv_py_core_t3
function_id: from_csv_py_core
name: test_lineas_vacias_ignoradas
file_path: python/functions/core/from_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lineas_vacias_ignoradas():
      text = "x,y\n\n1,2\n\n3,4\n"
      result = from_csv(text)
      assert len(result) == 2
      assert result[0] == {"x": "1", "y": "2"}


id: from_csv_py_core_t4
function_id: from_csv_py_core
name: test_un_solo_campo_por_fila
file_path: python/functions/core/from_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_un_solo_campo_por_fila():
      text = "valor\nhola\nmundo"
      result = from_csv(text)
      assert len(result) == 2
      assert result[0] == {"valor": "hola"}
      assert result[1] == {"valor": "mundo"}


id: from_jsonl_py_core_t0
function_id: from_jsonl_py_core
name: test_jsonl_valido
file_path: python/functions/core/from_jsonl_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_jsonl_valido():
      text = '{"a": 1}\n{"b": 2}'
      result = from_jsonl(text)
      assert result == [{"a": 1}, {"b": 2}]


id: from_jsonl_py_core_t1
function_id: from_jsonl_py_core
name: test_lineas_vacias_intercaladas
file_path: python/functions/core/from_jsonl_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lineas_vacias_intercaladas():
      text = '{"x": 1}\n\n{"x": 2}\n\n'
      result = from_jsonl(text)
      assert len(result) == 2
      assert result[0] == {"x": 1}
      assert result[1] == {"x": 2}


id: from_jsonl_py_core_t2
function_id: from_jsonl_py_core
name: test_linea_invalida_raise_con_numero
file_path: python/functions/core/from_jsonl_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_linea_invalida_raise_con_numero():
      text = '{"ok": 1}\nnot-json\n{"ok": 3}'
      with pytest.raises(ValueError, match="linea 2"):
          from_jsonl(text)


id: generate_html_report_py_core_t0
function_id: generate_html_report_py_core
name: test_reporte_con_una_tabla
file_path: python/functions/core/generate_html_report_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_reporte_con_una_tabla():
      sections = [
          {
              "heading": "Datos",
              "type": "table",
              "data": [{"nombre": "Ana", "score": 99}, {"nombre": "Bob", "score": 87}],
          }
      ]
      html = generate_html_report("Reporte", sections)
      assert "<!DOCTYPE html>" in html
      assert "<title>Reporte</title>" in html
      assert "<th>nombre</th>" in html
      assert "<td>Ana</td>" in html
      assert "zebra" in html  # segunda fila tiene class zebra


id: generate_html_report_py_core_t1
function_id: generate_html_report_py_core
name: test_reporte_con_multiples_secciones_mixtas
file_path: python/functions/core/generate_html_report_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_reporte_con_multiples_secciones_mixtas():
      sections = [
          {"heading": "Texto", "type": "text", "data": "Hola mundo"},
          {"heading": "Lista", "type": "list", "data": ["uno", "dos", "tres"]},
          {"heading": "KPIs", "type": "kpi", "data": [{"label": "Revenue", "value": "1M", "delta": None}]},
      ]
      html = generate_html_report("Multi", sections)
      assert "<p>Hola mundo</p>" in html
      assert "<li>uno</li>" in html
      assert "Revenue" in html
      assert "1M" in html


id: generate_html_report_py_core_t2
function_id: generate_html_report_py_core
name: test_kpi_con_deltas_positivos_y_negativos
file_path: python/functions/core/generate_html_report_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_kpi_con_deltas_positivos_y_negativos():
      sections = [
          {
              "heading": "Metricas",
              "type": "kpi",
              "data": [
                  {"label": "Ganancia", "value": "5K", "delta": "+12%"},
                  {"label": "Perdida", "value": "2K", "delta": "-5%"},
                  {"label": "Estable", "value": "1K", "delta": "0%"},
              ],
          }
      ]
      html = generate_html_report("KPIs", sections)
      assert 'class="delta-pos"' in html
      assert 'class="delta-neg"' in html
      assert 'class="delta-neutral"' in html
      assert "+12%" in html
      assert "-5%" in html


id: generate_html_report_py_core_t3
function_id: generate_html_report_py_core
name: test_caracteres_especiales_html_escapados_en_data
file_path: python/functions/core/generate_html_report_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_caracteres_especiales_html_escapados_en_data():
      sections = [
          {
              "heading": "Codigo",
              "type": "table",
              "data": [{"expr": "<script>alert('xss')</script>"}],
          }
      ]
      html = generate_html_report("Seguro", sections)
      assert "<script>" not in html
      assert "&lt;script&gt;" in html


id: generate_html_report_py_core_t4
function_id: generate_html_report_py_core
name: test_titulo_con_caracteres_especiales
file_path: python/functions/core/generate_html_report_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_titulo_con_caracteres_especiales():
      html = generate_html_report("Reporte & Analisis <2024>", [])
      assert "Reporte &amp; Analisis &lt;2024&gt;" in html
      assert "<title>Reporte &amp; Analisis &lt;2024&gt;</title>" in html


id: get_text_stats_py_core_t0
function_id: get_text_stats_py_core
name: test_texto_normal_con_palabras_y_lineas
file_path: python/functions/core/get_text_stats_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_normal_con_palabras_y_lineas():
      result = get_text_stats("hello world\nfoo bar")
      assert result == {"total_chars": 19, "total_lines": 2, "total_words": 4}


id: get_text_stats_py_core_t1
function_id: get_text_stats_py_core
name: test_texto_vacio_retorna_ceros
file_path: python/functions/core/get_text_stats_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_vacio_retorna_ceros():
      result = get_text_stats("")
      assert result == {"total_chars": 0, "total_lines": 1, "total_words": 0}


id: get_text_stats_py_core_t2
function_id: get_text_stats_py_core
name: test_texto_con_solo_newlines
file_path: python/functions/core/get_text_stats_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_solo_newlines():
      result = get_text_stats("\n\n")
      assert result == {"total_chars": 2, "total_lines": 3, "total_words": 0}


id: html_to_markdown_py_core_t0
function_id: html_to_markdown_py_core
name: test_html_con_nav_y_footer_filtra_boilerplate
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_con_nav_y_footer_filtra_boilerplate():
      """HTML con nav/footer: el contenido principal debe extraerse (nav no aparece en output)."""
      html = """
      <html>
        <body>
          <nav><a href="/">Home</a><a href="/about">About</a></nav>
          <main>
            <h1>Titulo principal</h1>
            <p>Este es el contenido relevante del articulo.</p>
          </main>
          <footer><p>Copyright 2026</p></footer>
        </body>
      </html>
      """
      result = html_to_markdown(html)
      assert "Titulo principal" in result
      assert "contenido relevante" in result


id: html_to_markdown_py_core_t1
function_id: html_to_markdown_py_core
name: test_html_limpio_se_convierte_correctamente
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_limpio_se_convierte_correctamente():
      """HTML limpio sin boilerplate: headings y parrafos se convierten correctamente."""
      html = """
      <html>
        <body>
          <h1>Hello World</h1>
          <p>Parrafo de prueba con <strong>texto en negrita</strong>.</p>
          <h2>Seccion dos</h2>
          <p>Mas contenido aqui.</p>
        </body>
      </html>
      """
      result = html_to_markdown(html)
      assert "Hello World" in result
      assert "Parrafo de prueba" in result
      assert "Seccion dos" in result


id: html_to_markdown_py_core_t2
function_id: html_to_markdown_py_core
name: test_html_con_imagenes_lazy_loaded
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_con_imagenes_lazy_loaded():
      """HTML con imagenes lazy-loaded: data-src debe reemplazar src en el output."""
      html = """
      <html>
        <body>
          <p>Articulo con imagen</p>
          <img src="placeholder.gif" data-src="imagen-real.jpg" alt="foto real" />
        </body>
      </html>
      """
      # Verificar preprocesamiento
      preprocessed = _preprocess_html(html)
      assert "imagen-real.jpg" in preprocessed
      # El resultado final debe contener la URL real
      result = html_to_markdown(html)
      assert "imagen-real.jpg" in result


id: html_to_markdown_py_core_t3
function_id: html_to_markdown_py_core
name: test_preprocess_lazy_loading_reemplaza_src
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_preprocess_lazy_loading_reemplaza_src():
      """_preprocess_html reemplaza src con data-src en imagenes."""
      html = '<img src="placeholder.gif" data-src="real.jpg" alt="x" />'
      result = _preprocess_html(html)
      assert 'src="real.jpg"' in result


id: html_to_markdown_py_core_t4
function_id: html_to_markdown_py_core
name: test_preprocess_lazy_loading_sin_src_anade_src
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_preprocess_lazy_loading_sin_src_anade_src():
      """_preprocess_html agrega src cuando la imagen no tiene atributo src."""
      html = '<img data-src="real.jpg" alt="foto" />'
      result = _preprocess_html(html)
      assert 'src="real.jpg"' in result


id: html_to_markdown_py_core_t5
function_id: html_to_markdown_py_core
name: test_html_vacio_retorna_string
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_vacio_retorna_string():
      """HTML vacio no lanza excepcion."""
      result = html_to_markdown("")
      assert isinstance(result, str)


id: html_to_markdown_py_core_t6
function_id: html_to_markdown_py_core
name: test_html_solo_texto
file_path: python/functions/core/html_to_markdown_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_solo_texto():
      """HTML con solo texto plano se convierte sin error."""
      html = "<p>Solo texto</p>"
      result = html_to_markdown(html)
      assert "Solo texto" in result


id: is_git_repo_url_py_core_t0
function_id: is_git_repo_url_py_core
name: test_parse_git_url_https_github
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_git_url_https_github():
      result = parse_git_url("https://github.com/psf/requests")
      assert result == "psf/requests"


id: is_git_repo_url_py_core_t1
function_id: is_git_repo_url_py_core
name: test_parse_git_url_ssh_git_at
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_git_url_ssh_git_at():
      result = parse_git_url("git@github.com:psf/requests.git")
      assert result == "psf/requests"


id: is_git_repo_url_py_core_t2
function_id: is_git_repo_url_py_core
name: test_parse_git_url_git_suffix_stripped
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_git_url_git_suffix_stripped():
      result = parse_git_url("https://github.com/psf/requests.git")
      assert result == "psf/requests"


id: is_git_repo_url_py_core_t3
function_id: is_git_repo_url_py_core
name: test_parse_git_url_path_extra_ignored
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_git_url_path_extra_ignored():
      result = parse_git_url("https://github.com/psf/requests/issues/123")
      assert result == "psf/requests"


id: is_git_repo_url_py_core_t4
function_id: is_git_repo_url_py_core
name: test_parse_git_url_unknown_host_returns_none
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_git_url_unknown_host_returns_none():
      result = parse_git_url("https://bitbucket.org/org/repo")
      assert result is None


  # ── is_git_repo_url ───────────────────────────────────────────────────────────


id: is_git_repo_url_py_core_t5
function_id: is_git_repo_url_py_core
name: test_is_git_repo_url_valid_repo
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_is_git_repo_url_valid_repo():
      assert is_git_repo_url("https://github.com/psf/requests") is True


id: is_git_repo_url_py_core_t6
function_id: is_git_repo_url_py_core
name: test_is_git_repo_url_issue_rejected
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_is_git_repo_url_issue_rejected():
      assert is_git_repo_url("https://github.com/psf/requests/issues/123") is False


id: is_git_repo_url_py_core_t7
function_id: is_git_repo_url_py_core
name: test_is_git_repo_url_blob_rejected
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_is_git_repo_url_blob_rejected():
      assert is_git_repo_url("https://github.com/psf/requests/blob/main/README.md") is False


id: is_git_repo_url_py_core_t8
function_id: is_git_repo_url_py_core
name: test_is_git_repo_url_tree_accepted
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_is_git_repo_url_tree_accepted():
      assert is_git_repo_url("https://github.com/psf/requests/tree/main") is True


  # ── validate_git_ssh_uri ──────────────────────────────────────────────────────


id: is_git_repo_url_py_core_t9
function_id: is_git_repo_url_py_core
name: test_validate_git_ssh_uri_valid
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_validate_git_ssh_uri_valid():
      # Should not raise
      validate_git_ssh_uri("git@github.com:org/repo.git")


id: is_git_repo_url_py_core_t10
function_id: is_git_repo_url_py_core
name: test_validate_git_ssh_uri_no_git_at
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_validate_git_ssh_uri_no_git_at():
      try:
          validate_git_ssh_uri("https://github.com/org/repo")
          assert False, "Expected ValueError"
      except ValueError as e:
          assert "git@" in str(e)


id: is_git_repo_url_py_core_t11
function_id: is_git_repo_url_py_core
name: test_validate_git_ssh_uri_no_colon
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_validate_git_ssh_uri_no_colon():
      try:
          validate_git_ssh_uri("git@github.com/org/repo")
          assert False, "Expected ValueError"
      except ValueError as e:
          assert ":" in str(e)


id: is_git_repo_url_py_core_t12
function_id: is_git_repo_url_py_core
name: test_validate_git_ssh_uri_empty_path
file_path: python/functions/core/parse_git_url_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_validate_git_ssh_uri_empty_path():
      try:
          validate_git_ssh_uri("git@github.com:")
          assert False, "Expected ValueError"
      except ValueError as e:
          assert "non-empty path" in str(e)


  if __name__ == "__main__":
      test_parse_git_url_https_github()
      test_parse_git_url_ssh_git_at()
      test_parse_git_url_git_suffix_stripped()
      test_parse_git_url_path_extra_ignored()
      test_parse_git_url_unknown_host_returns_none()
      test_is_git_repo_url_valid_repo()
      test_is_git_repo_url_issue_rejected()
      test_is_git_repo_url_blob_rejected()
      test_is_git_repo_url_tree_accepted()
      test_validate_git_ssh_uri_valid()
      test_validate_git_ssh_uri_no_git_at()
      test_validate_git_ssh_uri_no_colon()
      test_validate_git_ssh_uri_empty_path()
      print("All tests passed.")


id: join_by_key_py_core_t0
function_id: join_by_key_py_core
name: test_inner_join_solo_matches
file_path: python/functions/core/join_by_key_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_inner_join_solo_matches():
      """Inner join solo matches."""
      left = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
      right = [{"id": 1, "dept": "eng"}, {"id": 3, "dept": "sales"}]
      result = join_by_key(left, right, key="id", how="inner")
      assert len(result) == 1
      assert result[0]["id"] == 1
      assert result[0]["name"] == "Alice"
      assert result[0]["dept"] == "eng"


id: join_by_key_py_core_t1
function_id: join_by_key_py_core
name: test_left_join_todos_los_left_con_none_para_right_sin_match
file_path: python/functions/core/join_by_key_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_left_join_todos_los_left_con_none_para_right_sin_match():
      """Left join todos los left con None para right sin match."""
      left = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
      right = [{"id": 1, "dept": "eng"}]
      result = join_by_key(left, right, key="id", how="left")
      assert len(result) == 2
      alice = next(r for r in result if r["id"] == 1)
      bob = next(r for r in result if r["id"] == 2)
      assert alice["dept"] == "eng"
      assert bob["dept"] is None


id: join_by_key_py_core_t2
function_id: join_by_key_py_core
name: test_right_join
file_path: python/functions/core/join_by_key_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_right_join():
      """Right join."""
      left = [{"id": 1, "name": "Alice"}]
      right = [{"id": 1, "dept": "eng"}, {"id": 2, "dept": "sales"}]
      result = join_by_key(left, right, key="id", how="right")
      assert len(result) == 2
      eng = next(r for r in result if r["id"] == 1)
      sales = next(r for r in result if r["id"] == 2)
      assert eng["name"] == "Alice"
      assert sales.get("name") is None


id: join_by_key_py_core_t3
function_id: join_by_key_py_core
name: test_outer_join
file_path: python/functions/core/join_by_key_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_outer_join():
      """Outer join."""
      left = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
      right = [{"id": 1, "dept": "eng"}, {"id": 3, "dept": "sales"}]
      result = join_by_key(left, right, key="id", how="outer")
      ids = {r["id"] for r in result}
      assert ids == {1, 2, 3}


id: join_by_key_py_core_t4
function_id: join_by_key_py_core
name: test_campos_duplicados_con_sufijo_right
file_path: python/functions/core/join_by_key_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campos_duplicados_con_sufijo_right():
      """Campos duplicados con sufijo _right."""
      left = [{"id": 1, "name": "Alice", "score": 90}]
      right = [{"id": 1, "score": 85, "dept": "eng"}]
      result = join_by_key(left, right, key="id", how="inner")
      assert len(result) == 1
      assert result[0]["score"] == 90
      assert result[0]["score_right"] == 85
      assert result[0]["dept"] == "eng"


id: join_by_key_py_core_t5
function_id: join_by_key_py_core
name: test_key_ausente_en_alguna_fila
file_path: python/functions/core/join_by_key_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_ausente_en_alguna_fila():
      """Key ausente en alguna fila."""
      left = [{"id": 1, "name": "Alice"}, {"name": "Bob"}]  # Bob sin id
      right = [{"id": 1, "dept": "eng"}]
      result = join_by_key(left, right, key="id", how="inner")
      # Solo Alice matchea
      assert len(result) == 1
      assert result[0]["name"] == "Alice"


id: load_translations_py_core_t0
function_id: load_translations_py_core
name: test_carga_multiples_locales
file_path: python/functions/core/load_translations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_carga_multiples_locales():
      tmp = tempfile.mkdtemp()
      try:
          with open(os.path.join(tmp, "en.json"), "w") as f:
              json.dump({"greeting": "Hello"}, f)
          with open(os.path.join(tmp, "es.json"), "w") as f:
              json.dump({"greeting": "Hola"}, f)

          result = load_translations(tmp)
          assert "en" in result, "Debe contener locale 'en'"
          assert "es" in result, "Debe contener locale 'es'"
          assert result["en"]["greeting"] == "Hello"
          assert result["es"]["greeting"] == "Hola"
      finally:
          shutil.rmtree(tmp)


id: load_translations_py_core_t1
function_id: load_translations_py_core
name: test_directorio_inexistente_retorna_dict_vacio
file_path: python/functions/core/load_translations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_directorio_inexistente_retorna_dict_vacio():
      result = load_translations("/tmp/directorio_que_no_existe_xyz_12345")
      assert result == {}, f"Expected {{}}, got {result}"


id: load_translations_py_core_t2
function_id: load_translations_py_core
name: test_ignora_archivos_no_json
file_path: python/functions/core/load_translations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ignora_archivos_no_json():
      tmp = tempfile.mkdtemp()
      try:
          with open(os.path.join(tmp, "en.json"), "w") as f:
              json.dump({"key": "value"}, f)
          with open(os.path.join(tmp, "README.md"), "w") as f:
              f.write("# Locales")
          with open(os.path.join(tmp, "notes.txt"), "w") as f:
              f.write("some notes")

          result = load_translations(tmp)
          assert list(result.keys()) == ["en"], f"Expected only 'en', got {list(result.keys())}"
      finally:
          shutil.rmtree(tmp)


id: load_translations_py_core_t3
function_id: load_translations_py_core
name: test_locale_con_estructura_anidada
file_path: python/functions/core/load_translations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_locale_con_estructura_anidada():
      tmp = tempfile.mkdtemp()
      try:
          nested = {"report": {"sectionStart": "Section: {title}", "done": "Done"}}
          with open(os.path.join(tmp, "en.json"), "w") as f:
              json.dump(nested, f)

          result = load_translations(tmp)
          assert result["en"]["report"]["done"] == "Done"
          assert result["en"]["report"]["sectionStart"] == "Section: {title}"
      finally:
          shutil.rmtree(tmp)


  if __name__ == "__main__":
      test_carga_multiples_locales()
      print("PASS: carga multiples locales")

      test_directorio_inexistente_retorna_dict_vacio()
      print("PASS: directorio inexistente retorna dict vacio")

      test_ignora_archivos_no_json()
      print("PASS: ignora archivos no json")

      test_locale_con_estructura_anidada()
      print("PASS: locale con estructura anidada")

      print("---")
      print("All tests passed.")


id: merge_entity_attributes_py_core_t0
function_id: merge_entity_attributes_py_core
name: test_atributos_complementarios
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_atributos_complementarios():
      """Atributos complementarios (A tiene full_name, B tiene nationality) -> ambos."""
      a = {"full_name": "Alice Smith"}
      b = {"nationality": "US"}
      result = merge_entity_attributes([a, b])
      assert result["full_name"] == "Alice Smith"
      assert result["nationality"] == "US"


id: merge_entity_attributes_py_core_t1
function_id: merge_entity_attributes_py_core
name: test_atributos_conflictivos_risk_score_max
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_atributos_conflictivos_risk_score_max():
      """Atributos conflictivos en risk_score -> max."""
      a = {"risk_score": 3.5}
      b = {"risk_score": 7.2}
      result = merge_entity_attributes([a, b])
      assert result["risk_score"] == 7.2


id: merge_entity_attributes_py_core_t2
function_id: merge_entity_attributes_py_core
name: test_atributos_first_seen_conflictivos_min
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_atributos_first_seen_conflictivos_min():
      """Atributos first_seen conflictivos -> min."""
      a = {"first_seen": "2023-01-01"}
      b = {"first_seen": "2022-05-15"}
      result = merge_entity_attributes([a, b])
      assert result["first_seen"] == "2022-05-15"


id: merge_entity_attributes_py_core_t3
function_id: merge_entity_attributes_py_core
name: test_todos_null
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_todos_null():
      """Todos null -> null."""
      a = {"score": None}
      b = {"score": None}
      result = merge_entity_attributes([a, b])
      assert result["score"] is None


id: merge_entity_attributes_py_core_t4
function_id: merge_entity_attributes_py_core
name: test_listas_union_sin_duplicados
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_listas_union_sin_duplicados():
      """Listas -> union sin duplicados."""
      a = {"name_servers": ["ns1.example.com", "ns2.example.com"]}
      b = {"name_servers": ["ns2.example.com", "ns3.example.com"]}
      result = merge_entity_attributes([a, b])
      assert set(result["name_servers"]) == {"ns1.example.com", "ns2.example.com", "ns3.example.com"}
      assert len(result["name_servers"]) == 3


id: merge_entity_attributes_py_core_t5
function_id: merge_entity_attributes_py_core
name: test_boolean_or
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_boolean_or():
      """Boolean verified -> True si alguno es True."""
      a = {"verified": False}
      b = {"verified": True}
      result = merge_entity_attributes([a, b])
      assert result["verified"] is True


id: merge_entity_attributes_py_core_t6
function_id: merge_entity_attributes_py_core
name: test_string_mas_largo
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_string_mas_largo():
      """String conflictivo -> usar el mas largo."""
      a = {"description": "Short desc"}
      b = {"description": "A much longer and more detailed description"}
      result = merge_entity_attributes([a, b])
      assert result["description"] == "A much longer and more detailed description"


id: merge_entity_attributes_py_core_t7
function_id: merge_entity_attributes_py_core
name: test_valores_iguales
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_valores_iguales():
      """Valores iguales -> usar ese valor."""
      a = {"alias": "Alice"}
      b = {"alias": "Alice"}
      result = merge_entity_attributes([a, b])
      assert result["alias"] == "Alice"


id: merge_entity_attributes_py_core_t8
function_id: merge_entity_attributes_py_core
name: test_un_solo_candidato
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_un_solo_candidato():
      """Un solo candidato -> retorna sus atributos tal cual."""
      a = {"risk_score": 5.0, "verified": True}
      result = merge_entity_attributes([a])
      assert result == a


id: merge_entity_attributes_py_core_t9
function_id: merge_entity_attributes_py_core
name: test_lista_vacia
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_vacia():
      """Lista vacia -> retorna dict vacio."""
      result = merge_entity_attributes([])
      assert result == {}


id: merge_entity_attributes_py_core_t10
function_id: merge_entity_attributes_py_core
name: test_last_seen_max
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_last_seen_max():
      """last_seen conflictivo -> max (mas reciente)."""
      a = {"last_seen": "2023-12-01"}
      b = {"last_seen": "2024-03-15"}
      result = merge_entity_attributes([a, b])
      assert result["last_seen"] == "2024-03-15"


id: merge_entity_attributes_py_core_t11
function_id: merge_entity_attributes_py_core
name: test_un_valor_non_null
file_path: python/functions/core/merge_entity_attributes_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_un_valor_non_null():
      """Un candidato tiene null, otro tiene valor -> usar el valor."""
      a = {"cvss": None}
      b = {"cvss": 8.5}
      result = merge_entity_attributes([a, b])
      assert result["cvss"] == 8.5


id: next_cron_time_py_core_t0
function_id: next_cron_time_py_core
name: test_0_star_desde_30_retorna_proxima_hora_en_punto
file_path: python/functions/core/next_cron_time_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_0_star_desde_30_retorna_proxima_hora_en_punto():
      """0 * * * * desde :30 retorna la proxima hora en punto"""
      sched = parse_cron_expr("0 * * * *")
      after = datetime(2024, 1, 15, 14, 30)
      nxt = next_cron_time(sched, after)
      assert nxt == datetime(2024, 1, 15, 15, 0)


id: next_cron_time_py_core_t1
function_id: next_cron_time_py_core
name: test_0_9_weekdays_desde_viernes_retorna_proximo_lunes
file_path: python/functions/core/next_cron_time_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_0_9_weekdays_desde_viernes_retorna_proximo_lunes():
      """0 9 * * 1-5 desde viernes retorna proximo lunes a las 9"""
      sched = parse_cron_expr("0 9 * * 1-5")
      # 2024-01-19 es viernes
      friday = datetime(2024, 1, 19, 10, 0)
      nxt = next_cron_time(sched, friday)
      # 2024-01-22 es lunes
      assert nxt == datetime(2024, 1, 22, 9, 0)


id: next_cron_time_py_core_t2
function_id: next_cron_time_py_core
name: test_schedule_imposible_retorna_none
file_path: python/functions/core/next_cron_time_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_schedule_imposible_retorna_none():
      """schedule imposible retorna None"""
      # 31 de febrero no existe — schedule nunca se cumple en 366 dias
      sched = parse_cron_expr("0 0 31 2 *")
      after = datetime(2024, 1, 1)
      nxt = next_cron_time(sched, after)
      assert nxt is None


id: next_cron_time_py_core_t3
function_id: next_cron_time_py_core
name: test_paso_15_desde_05_retorna_15
file_path: python/functions/core/next_cron_time_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_paso_15_desde_05_retorna_15():
      """paso */15 desde :05 retorna :15"""
      sched = parse_cron_expr("*/15 * * * *")
      after = datetime(2024, 3, 10, 8, 5)
      nxt = next_cron_time(sched, after)
      assert nxt == datetime(2024, 3, 10, 8, 15)


id: normalize_entity_name_py_core_t0
function_id: normalize_entity_name_py_core
name: test_person_strip_whitespace
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_person_strip_whitespace():
      assert normalize_entity_name("  John Smith  ", "person") == "John Smith"


id: normalize_entity_name_py_core_t1
function_id: normalize_entity_name_py_core
name: test_person_apellido_nombre
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_person_apellido_nombre():
      assert normalize_entity_name("Smith, John", "person") == "John Smith"


id: normalize_entity_name_py_core_t3
function_id: normalize_entity_name_py_core
name: test_organization_llc
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_organization_llc():
      assert normalize_entity_name("Google LLC", "organization") == "Google"


id: normalize_entity_name_py_core_t4
function_id: normalize_entity_name_py_core
name: test_organization_corp
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_organization_corp():
      assert normalize_entity_name("ACME Corp.", "organization") == "Acme"


id: normalize_entity_name_py_core_t6
function_id: normalize_entity_name_py_core
name: test_email_lowercase
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_email_lowercase():
      assert normalize_entity_name("user@GMAIL.com", "email") == "user@gmail.com"


id: normalize_entity_name_py_core_t7
function_id: normalize_entity_name_py_core
name: test_domain_www_y_trailing_dot
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_domain_www_y_trailing_dot():
      assert normalize_entity_name("www.example.com.", "domain") == "example.com"


id: normalize_entity_name_py_core_t8
function_id: normalize_entity_name_py_core
name: test_phone_formato
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_phone_formato():
      assert normalize_entity_name("+1 (555) 123-4567", "phone") == "+15551234567"


id: normalize_entity_name_py_core_t11
function_id: normalize_entity_name_py_core
name: test_crypto_wallet_case_sensitive
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_crypto_wallet_case_sensitive():
      wallet = "1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf"
      assert normalize_entity_name(wallet, "crypto_wallet") == wallet


id: normalize_entity_name_py_core_t12
function_id: normalize_entity_name_py_core
name: test_domain_solo_trailing_dot
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_domain_solo_trailing_dot():
      assert normalize_entity_name("example.com.", "domain") == "example.com"


id: normalize_entity_name_py_core_t13
function_id: normalize_entity_name_py_core
name: test_person_prof_titulo
file_path: python/functions/core/normalize_entity_name_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_person_prof_titulo():
      assert normalize_entity_name("Prof. Alan Turing", "person") == "Alan Turing"


  if __name__ == "__main__":
      import pytest
      pytest.main([__file__, "-v"])


id: parse_code_ast_py_core_t0
function_id: parse_code_ast_py_core
name: test_lenguaje_no_soportado_lanza_value_error
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lenguaje_no_soportado_lanza_value_error():
      """Lenguaje no soportado lanza ValueError con mensaje claro."""
      with pytest.raises(ValueError, match="no soportado"):
          parse_code_ast("x = 1", "cobol")


id: parse_code_ast_py_core_t1
function_id: parse_code_ast_py_core
name: test_lenguaje_no_soportado_lanza_value_error_vacio
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lenguaje_no_soportado_lanza_value_error_vacio():
      """Lenguaje vacio lanza ValueError."""
      with pytest.raises(ValueError):
          parse_code_ast("x = 1", "")


id: parse_code_ast_py_core_t2
function_id: parse_code_ast_py_core
name: test_codigo_vacio_retorna_lista_vacia
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_codigo_vacio_retorna_lista_vacia():
      """Codigo vacio retorna lista vacia sin invocar tree-sitter."""
      result = parse_code_ast("", "python")
      assert result == []


id: parse_code_ast_py_core_t3
function_id: parse_code_ast_py_core
name: test_codigo_solo_espacios_retorna_lista_vacia
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_codigo_solo_espacios_retorna_lista_vacia():
      """Codigo con solo espacios/newlines retorna lista vacia."""
      result = parse_code_ast("   \n\n\t  \n", "python")
      assert result == []


id: parse_code_ast_py_core_t4
function_id: parse_code_ast_py_core
name: test_alias_py_funciona
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_alias_py_funciona():
      """Alias 'py' es aceptado como 'python'."""
      # Solo verificamos que no lanza ValueError por alias
      # Si tree-sitter no esta instalado, lanza ImportError (no ValueError)
      try:
          parse_code_ast("def f(): pass", "py")
      except ImportError:
          pass  # esperado si tree-sitter no instalado
      except ValueError:
          pytest.fail("'py' deberia ser alias valido de 'python'")


id: parse_code_ast_py_core_t5
function_id: parse_code_ast_py_core
name: test_alias_ts_funciona
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_alias_ts_funciona():
      """Alias 'ts' es aceptado como 'typescript'."""
      try:
          parse_code_ast("const x = 1;", "ts")
      except ImportError:
          pass
      except ValueError:
          pytest.fail("'ts' deberia ser alias valido de 'typescript'")


id: parse_code_ast_py_core_t6
function_id: parse_code_ast_py_core
name: test_lenguajes_soportados_incluyen_go_y_python
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lenguajes_soportados_incluyen_go_y_python():
      """SUPPORTED_LANGUAGES contiene los lenguajes principales."""
      assert "python" in SUPPORTED_LANGUAGES
      assert "go" in SUPPORTED_LANGUAGES
      assert "typescript" in SUPPORTED_LANGUAGES
      assert "rust" in SUPPORTED_LANGUAGES


  # ---------------------------------------------------------------------------
  # Tests que requieren tree-sitter
  # ---------------------------------------------------------------------------

  pytestmark_ts = pytest.mark.skipif(
      not HAS_TREE_SITTER,
      reason="tree-sitter no instalado"
  )


  @pytest.mark.skipif(not HAS_TREE_SITTER, reason="tree-sitter no instalado")


id: parse_code_ast_py_core_t7
function_id: parse_code_ast_py_core
name: test_python_extrae_funcion_simple
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_python_extrae_funcion_simple():
      """Codigo Python con funcion simple retorna una entidad function."""
      source = '''\
  def greet(name: str) -> str:
      """Saluda al usuario."""
      return f"Hello, {name}!"
  '''
      try:
          import tree_sitter_python  # noqa: F401
      except ImportError:
          pytest.skip("tree-sitter-python no instalado")

      result = parse_code_ast(source, "python")
      assert len(result) == 1
      entity = result[0]
      assert entity.kind == "function"
      assert entity.name == "greet"
      assert entity.start_line == 1
      assert entity.language == "python"
      assert "greet" in entity.signature


  @pytest.mark.skipif(not HAS_TREE_SITTER, reason="tree-sitter no instalado")


id: parse_code_ast_py_core_t8
function_id: parse_code_ast_py_core
name: test_python_extrae_clase_con_metodos
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_python_extrae_clase_con_metodos():
      """Codigo Python con clase extrae clase y sus metodos como children."""
      source = '''\
  class Animal:
      """Clase base de animales."""

      def __init__(self, name: str) -> None:
          self.name = name

      def speak(self) -> str:
          return ""
  '''
      try:
          import tree_sitter_python  # noqa: F401
      except ImportError:
          pytest.skip("tree-sitter-python no instalado")

      result = parse_code_ast(source, "python")
      classes = [e for e in result if e.kind == "class"]
      assert len(classes) == 1
      cls = classes[0]
      assert cls.name == "Animal"
      assert len(cls.children) >= 1
      method_names = [c.name for c in cls.children]
      assert "speak" in method_names or "__init__" in method_names


  @pytest.mark.skipif(not HAS_TREE_SITTER, reason="tree-sitter no instalado")


id: parse_code_ast_py_core_t9
function_id: parse_code_ast_py_core
name: test_python_extrae_docstring
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_python_extrae_docstring():
      """La entidad Python con docstring lo captura correctamente."""
      source = '''\
  def documented():
      """Esta es la documentacion."""
      pass
  '''
      try:
          import tree_sitter_python  # noqa: F401
      except ImportError:
          pytest.skip("tree-sitter-python no instalado")

      result = parse_code_ast(source, "python")
      assert len(result) == 1
      assert result[0].docstring is not None
      assert "documentacion" in result[0].docstring


  @pytest.mark.skipif(not HAS_TREE_SITTER, reason="tree-sitter no instalado")


id: parse_code_ast_py_core_t10
function_id: parse_code_ast_py_core
name: test_go_extrae_funciones_y_tipos
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_go_extrae_funciones_y_tipos():
      """Codigo Go extrae funciones y tipos."""
      source = '''\
  package main

  // Add suma dos enteros.
  func Add(a, b int) int {
      return a + b
  }

  type Point struct {
      X float64
      Y float64
  }
  '''
      try:
          import tree_sitter_go  # noqa: F401
      except ImportError:
          pytest.skip("tree-sitter-go no instalado")

      result = parse_code_ast(source, "go")
      names = [e.name for e in result]
      assert "Add" in names
      assert "Point" in names


  @pytest.mark.skipif(not HAS_TREE_SITTER, reason="tree-sitter no instalado")


id: parse_code_ast_py_core_t11
function_id: parse_code_ast_py_core
name: test_typescript_extrae_interfaces
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_typescript_extrae_interfaces():
      """Codigo TypeScript extrae interfaces y funciones."""
      source = '''\
  interface User {
      id: number;
      name: string;
  }

  function greetUser(user: User): string {
      return `Hello, ${user.name}`;
  }
  '''
      try:
          import tree_sitter_typescript  # noqa: F401
      except ImportError:
          pytest.skip("tree-sitter-typescript no instalado")

      result = parse_code_ast(source, "typescript")
      kinds = [e.kind for e in result]
      assert "interface" in kinds or "function" in kinds


  @pytest.mark.skipif(not HAS_TREE_SITTER, reason="tree-sitter no instalado")


id: parse_code_ast_py_core_t12
function_id: parse_code_ast_py_core
name: test_codigo_con_errores_de_sintaxis_es_tolerante
file_path: python/functions/core/parse_code_ast_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_codigo_con_errores_de_sintaxis_es_tolerante():
      """tree-sitter es tolerante con errores de sintaxis: retorna lo que puede."""
      source = '''\
  def valid_function():
      pass

  def broken_function(
      # error: parentesis no cerrado
  '''
      try:
          import tree_sitter_python  # noqa: F401
      except ImportError:
          pytest.skip("tree-sitter-python no instalado")

      # No debe lanzar excepcion — tree-sitter es tolerante a errores
      result = parse_code_ast(source, "python")
      # Al menos la funcion valida deberia extraerse
      names = [e.name for e in result]
      assert "valid_function" in names


id: parse_cron_expr_py_core_t0
function_id: parse_cron_expr_py_core
name: test_expande_minutos_a_0_15_30_45
file_path: python/functions/core/parse_cron_expr_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_expande_minutos_a_0_15_30_45():
      """*/15 expande minutos a [0 15 30 45]"""
      sched = parse_cron_expr("*/15 * * * *")
      assert sched["minute"] == [0, 15, 30, 45]
      assert sched["hour"] == list(range(24))


id: parse_cron_expr_py_core_t1
function_id: parse_cron_expr_py_core
name: test_daily_resuelve_a_minuto_0_hora_0
file_path: python/functions/core/parse_cron_expr_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_daily_resuelve_a_minuto_0_hora_0():
      """@daily resuelve a minuto 0 hora 0"""
      sched = parse_cron_expr("@daily")
      assert sched["minute"] == [0]
      assert sched["hour"] == [0]
      assert sched["day_of_month"] == list(range(1, 32))


id: parse_cron_expr_py_core_t2
function_id: parse_cron_expr_py_core
name: test_0_9_1_15_expande_dias_a_1_15
file_path: python/functions/core/parse_cron_expr_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_0_9_1_15_expande_dias_a_1_15():
      """0 9 1,15 * * expande dias a [1 15]"""
      sched = parse_cron_expr("0 9 1,15 * *")
      assert sched["minute"] == [0]
      assert sched["hour"] == [9]
      assert sched["day_of_month"] == [1, 15]


id: parse_cron_expr_py_core_t3
function_id: parse_cron_expr_py_core
name: test_0_9_1_5_expande_dia_semana_a_1_2_3_4_5
file_path: python/functions/core/parse_cron_expr_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_0_9_1_5_expande_dia_semana_a_1_2_3_4_5():
      """0 9 * * 1-5 expande dia de semana a [1 2 3 4 5]"""
      sched = parse_cron_expr("0 9 * * 1-5")
      assert sched["day_of_week"] == [1, 2, 3, 4, 5]


id: parse_cron_expr_py_core_t4
function_id: parse_cron_expr_py_core
name: test_expresion_con_4_campos_lanza_valueerror
file_path: python/functions/core/parse_cron_expr_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_expresion_con_4_campos_lanza_valueerror():
      """expresion con 4 campos lanza ValueError"""
      with pytest.raises(ValueError):
          parse_cron_expr("0 9 * *")


id: parse_cron_expr_py_core_t5
function_id: parse_cron_expr_py_core
name: test_minuto_fuera_de_rango_lanza_valueerror
file_path: python/functions/core/parse_cron_expr_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_minuto_fuera_de_rango_lanza_valueerror():
      """minuto fuera de rango lanza ValueError"""
      with pytest.raises(ValueError):
          parse_cron_expr("60 * * * *")


id: parse_iso_datetime_py_core_t0
function_id: parse_iso_datetime_py_core
name: test_iso_normal_sin_fraccion
file_path: python/functions/core/parse_iso_datetime_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_iso_normal_sin_fraccion():
      result = parse_iso_datetime("2026-02-21T13:20:23+08:00")
      assert result.year == 2026
      assert result.month == 2
      assert result.day == 21
      assert result.hour == 13
      assert result.minute == 20
      assert result.second == 23
      assert result.tzinfo is not None


id: parse_iso_datetime_py_core_t1
function_id: parse_iso_datetime_py_core
name: test_iso_con_z_como_utc
file_path: python/functions/core/parse_iso_datetime_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_iso_con_z_como_utc():
      result = parse_iso_datetime("2026-02-21T13:20:23Z")
      assert result.tzinfo is not None
      assert result.utcoffset() == timedelta(0)


id: parse_iso_datetime_py_core_t2
function_id: parse_iso_datetime_py_core
name: test_iso_con_fraccion_de_mas_de_6_digitos
file_path: python/functions/core/parse_iso_datetime_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_iso_con_fraccion_de_mas_de_6_digitos():
      # Windows produce fracciones con 7 digitos: .1470042
      result = parse_iso_datetime("2026-02-21T13:20:23.1470042+08:00")
      assert result.year == 2026
      assert result.microsecond == 147004  # truncado a 6 digitos: 147004
      assert result.tzinfo is not None


id: parse_iso_datetime_py_core_t3
function_id: parse_iso_datetime_py_core
name: test_iso_con_timezone_offset
file_path: python/functions/core/parse_iso_datetime_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_iso_con_timezone_offset():
      result = parse_iso_datetime("2026-06-15T09:00:00.000000-05:00")
      assert result.hour == 9
      expected_offset = timedelta(hours=-5)
      assert result.utcoffset() == expected_offset


id: parse_llm_json_py_core_t0
function_id: parse_llm_json_py_core
name: test_JSON_limpio_es_parseado_correctamente
file_path: python/functions/core/parse_llm_json_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_JSON_limpio_es_parseado_correctamente():
      result = parse_llm_json('{"status": "ok", "value": 42}')
      assert result == {"status": "ok", "value": 42}


id: parse_llm_json_py_core_t1
function_id: parse_llm_json_py_core
name: test_JSON_con_think_tags_y_codeblock_es_parseado_correctamente
file_path: python/functions/core/parse_llm_json_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_JSON_con_think_tags_y_codeblock_es_parseado_correctamente():
      raw = '<think>I need to return JSON.</think>\n```json\n{"status": "ok"}\n```'
      result = parse_llm_json(raw)
      assert result == {"status": "ok"}


id: parse_llm_json_py_core_t2
function_id: parse_llm_json_py_core
name: test_JSON_invalido_lanza_ValueError
file_path: python/functions/core/parse_llm_json_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_JSON_invalido_lanza_ValueError():
      with pytest.raises(ValueError, match="parse_llm_json"):
          parse_llm_json("not json at all")


id: parser_registry_py_core_t0
function_id: parser_registry_py_core
name: test_registrar_parser_custom
file_path: python/functions/core/parser_registry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_registrar_parser_custom():
      """registrar parser custom"""
      registry = ParserRegistry()
      stub = StubParser([".foo"])
      registry.register("foo", stub)

      assert "foo" in registry.list_parsers()
      assert ".foo" in registry.list_supported_extensions()
      assert registry.get_parser("foo") is stub


id: parser_registry_py_core_t1
function_id: parser_registry_py_core
name: test_resolver_por_extension
file_path: python/functions/core/parser_registry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_resolver_por_extension():
      """resolver parser por extension de archivo"""
      registry = ParserRegistry()
      md_parser = StubParser([".md", ".markdown"], "markdown")
      registry.register("markdown", md_parser)

      result = registry.get_parser_for_file("documento.md")
      assert result is md_parser

      result2 = registry.get_parser_for_file("readme.markdown")
      assert result2 is md_parser


id: parser_registry_py_core_t2
function_id: parser_registry_py_core
name: test_parse_file
file_path: python/functions/core/parser_registry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_file():
      """parsear archivo existente con extension registrada"""
      registry = ParserRegistry()
      txt_parser = StubParser([".txt"], "text")
      registry.register("text", txt_parser)

      # Crear archivo temporal .txt
      with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as f:
          f.write(b"hola mundo")
          tmp_path = f.name

      try:
          result = _run(registry.parse(tmp_path))
          assert result.parser_name == "text"
      finally:
          os.unlink(tmp_path)


id: parser_registry_py_core_t3
function_id: parser_registry_py_core
name: test_parse_string
file_path: python/functions/core/parser_registry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_parse_string():
      """parsear string de contenido sin path"""
      registry = ParserRegistry()
      text_parser = StubParser([".txt"], "text")
      registry.register("text", text_parser)

      # Contenido con newlines → se trata como contenido directo
      content = "linea 1\nlinea 2\nlinea 3"
      result = _run(registry.parse(content))
      assert result.parser_name == "text"


id: parser_registry_py_core_t4
function_id: parser_registry_py_core
name: test_unregister
file_path: python/functions/core/parser_registry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_unregister():
      """desregistrar parser elimina nombre y extensiones"""
      registry = ParserRegistry()
      stub = StubParser([".bar"])
      registry.register("bar", stub)
      assert "bar" in registry.list_parsers()

      registry.unregister("bar")
      assert "bar" not in registry.list_parsers()
      assert ".bar" not in registry.list_supported_extensions()
      assert registry.get_parser("bar") is None


id: parser_registry_py_core_t5
function_id: parser_registry_py_core
name: test_extension_desconocida_fallback_text
file_path: python/functions/core/parser_registry_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_extension_desconocida_fallback_text():
      """extension desconocida hace fallback al parser text"""
      registry = ParserRegistry()
      text_parser = StubParser([".txt"], "text")
      registry.register("text", text_parser)

      # Crear archivo con extension no registrada
      with tempfile.NamedTemporaryFile(suffix=".xyz", delete=False) as f:
          f.write(b"contenido")
          tmp_path = f.name

      try:
          result = _run(registry.parse(tmp_path))
          assert result.parser_name == "text"
      finally:
          os.unlink(tmp_path)


  if __name__ == "__main__":
      tests = [
          test_registrar_parser_custom,
          test_resolver_por_extension,
          test_parse_file,
          test_parse_string,
          test_unregister,
          test_extension_desconocida_fallback_text,
      ]

      passed = 0
      failed = 0
      for t in tests:
          try:
              t()
              print(f"PASS: {t.__doc__}")
              passed += 1
          except Exception as e:
              print(f"FAIL: {t.__doc__} — {e}")
              failed += 1

      print(f"---\nResults: {passed} passed, {failed} failed")
      if failed:
          sys.exit(1)


id: preprocess_text_py_core_t0
function_id: preprocess_text_py_core
name: test_texto_con_saltos_de_linea_windows_crlf
file_path: python/functions/core/preprocess_text_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_saltos_de_linea_windows_crlf():
      text = "hello\r\nworld\r\nend"
      result = preprocess_text(text)
      assert result == "hello\nworld\nend"


id: preprocess_text_py_core_t1
function_id: preprocess_text_py_core
name: test_texto_con_multiples_newlines_consecutivos
file_path: python/functions/core/preprocess_text_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_multiples_newlines_consecutivos():
      text = "first\n\n\n\n\nsecond"
      result = preprocess_text(text)
      assert result == "first\n\nsecond"


id: preprocess_text_py_core_t2
function_id: preprocess_text_py_core
name: test_texto_con_espacios_leading_y_trailing_en_cada_linea
file_path: python/functions/core/preprocess_text_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_espacios_leading_y_trailing_en_cada_linea():
      text = "  hello  \n  world  \n  end  "
      result = preprocess_text(text)
      assert result == "hello\nworld\nend"


id: react_loop_py_core_t0
function_id: react_loop_py_core
name: test_agente_que_usa_1_tool_y_da_respuesta_final
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_agente_que_usa_1_tool_y_da_respuesta_final():
      called_with = {}

      def search(query: str) -> str:
          called_with["query"] = query
          return "Paris"

      llm_responses = [
          "Thought: I need to search for the capital.\nAction: search\nAction Input: {\"query\": \"capital of France\"}\n",
          "Thought: I have the answer.\nFinal Answer: The capital of France is Paris.",
      ]

      result = react_loop(
          llm_chat=_make_llm(llm_responses),
          tools={"search": search},
          system_prompt="You are a helpful assistant.",
          user_prompt="What is the capital of France?",
      )

      assert result == "The capital of France is Paris."
      assert called_with.get("query") == "capital of France"


id: react_loop_py_core_t1
function_id: react_loop_py_core
name: test_agente_que_usa_multiples_tools_iterativamente
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_agente_que_usa_multiples_tools_iterativamente():
      call_log = []

      def lookup(entity: str) -> str:
          call_log.append(("lookup", entity))
          return f"Info about {entity}"

      def summarize(text: str) -> str:
          call_log.append(("summarize", text))
          return "Summary: " + text[:20]

      llm_responses = [
          "Thought: Lookup first.\nAction: lookup\nAction Input: {\"entity\": \"Python\"}",
          "Thought: Now summarize.\nAction: summarize\nAction Input: {\"text\": \"Info about Python\"}",
          "Final Answer: Done.",
      ]

      result = react_loop(
          llm_chat=_make_llm(llm_responses),
          tools={"lookup": lookup, "summarize": summarize},
          system_prompt="You are an assistant.",
          user_prompt="Tell me about Python.",
          max_iterations=5,
      )

      assert result == "Done."
      assert len(call_log) == 2
      assert call_log[0] == ("lookup", "Python")
      assert call_log[1] == ("summarize", "Info about Python")


id: react_loop_py_core_t2
function_id: react_loop_py_core
name: test_agente_que_da_respuesta_directa_sin_tools
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_agente_que_da_respuesta_directa_sin_tools():
      llm_responses = [
          "Final Answer: 42 is the answer.",
      ]

      result = react_loop(
          llm_chat=_make_llm(llm_responses),
          tools={},
          system_prompt="You are a calculator.",
          user_prompt="What is 6*7?",
      )

      assert result == "42 is the answer."


id: react_loop_py_core_t3
function_id: react_loop_py_core
name: test_max_iterations_alcanzado_fallback
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_max_iterations_alcanzado_fallback():
      call_count = {"n": 0}

      def noop(q: str) -> str:
          return "nothing"

      def llm_chat(messages: list[dict]) -> str:
          call_count["n"] += 1
          return "Thought: Thinking...\nAction: noop\nAction Input: {\"q\": \"x\"}"

      result = react_loop(
          llm_chat=llm_chat,
          tools={"noop": noop},
          system_prompt=".",
          user_prompt="Loop forever.",
          max_iterations=3,
      )

      assert call_count["n"] == 3
      assert "Thought: Thinking" in result


id: react_loop_py_core_t4
function_id: react_loop_py_core
name: test_tool_que_falla_error_handling
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tool_que_falla_error_handling():
      observations = []

      def broken_tool(x: str) -> str:
          raise ValueError("tool exploded")

      llm_responses = [
          "Action: broken_tool\nAction Input: {\"x\": \"test\"}",
          "Final Answer: Tool failed gracefully.",
      ]

      result = react_loop(
          llm_chat=_make_llm(llm_responses),
          tools={"broken_tool": broken_tool},
          system_prompt=".",
          user_prompt="Use broken tool.",
          on_observation=lambda obs: observations.append(obs),
      )

      assert result == "Tool failed gracefully."
      assert any("Error" in obs for obs in observations)


id: render_template_py_core_t0
function_id: render_template_py_core
name: test_sustitucion_simple
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sustitucion_simple():
      result = render_template("Hola {{nombre}}!", {"nombre": "Ana"})
      assert result == "Hola Ana!"


id: render_template_py_core_t1
function_id: render_template_py_core
name: test_dot_path_en_nested_dicts
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dot_path_en_nested_dicts():
      ctx = {"user": {"name": "Bob", "address": {"city": "Madrid"}}}
      result = render_template("{{user.name}} vive en {{user.address.city}}", ctx)
      assert result == "Bob vive en Madrid"


id: render_template_py_core_t2
function_id: render_template_py_core
name: test_for_loop_con_lista_de_strings
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_for_loop_con_lista_de_strings():
      ctx = {"items": ["a", "b", "c"]}
      result = render_template("{% for x in items %}{{x}},{% endfor %}", ctx)
      assert result == "a,b,c,"


id: render_template_py_core_t3
function_id: render_template_py_core
name: test_for_loop_con_lista_de_dicts
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_for_loop_con_lista_de_dicts():
      ctx = {"rows": [{"name": "Ana"}, {"name": "Bob"}]}
      result = render_template("{% for row in rows %}{{row.name}} {% endfor %}", ctx)
      assert result == "Ana Bob "


id: render_template_py_core_t4
function_id: render_template_py_core
name: test_if_endif_condicional
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_if_endif_condicional():
      result = render_template("{% if show %}visible{% endif %}", {"show": True})
      assert result == "visible"

      result = render_template("{% if show %}visible{% endif %}", {"show": False})
      assert result == ""


id: render_template_py_core_t5
function_id: render_template_py_core
name: test_variable_missing_retorna_string_vacio
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_variable_missing_retorna_string_vacio():
      result = render_template("{{inexistente}}", {})
      assert result == ""


id: render_template_py_core_t6
function_id: render_template_py_core
name: test_variable_missing_configurable
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_variable_missing_configurable():
      result = render_template("{{inexistente}}", {}, missing="N/A")
      assert result == "N/A"


id: render_template_py_core_t7
function_id: render_template_py_core
name: test_html_escaping_por_defecto
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_html_escaping_por_defecto():
      ctx = {"code": "<script>alert('xss')</script>"}
      result = render_template("{{code}}", ctx)
      assert "<script>" not in result
      assert "&lt;script&gt;" in result


id: render_template_py_core_t8
function_id: render_template_py_core
name: test_triple_braces_sin_escape
file_path: python/functions/core/render_template_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_triple_braces_sin_escape():
      ctx = {"html": "<b>bold</b>"}
      result = render_template("{{{html}}}", ctx)
      assert result == "<b>bold</b>"


id: retry_with_backoff_py_core_t0
function_id: retry_with_backoff_py_core
name: test_funcion_que_falla_dos_veces_y_luego_exito
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_funcion_que_falla_dos_veces_y_luego_exito():
      """Debe tener exito en el tercer intento tras 2 fallos."""
      calls = []

      @retry_with_backoff(max_retries=3, initial_delay=0.0, jitter=False)
      def flaky():
          calls.append(1)
          if len(calls) < 3:
              raise ConnectionError("transient")
          return "ok"

      result = flaky()
      assert result == "ok"
      assert len(calls) == 3


id: retry_with_backoff_py_core_t1
function_id: retry_with_backoff_py_core
name: test_funcion_que_siempre_falla_agota_retries_y_lanza
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_funcion_que_siempre_falla_agota_retries_y_lanza():
      """Debe lanzar la excepcion tras agotar todos los reintentos."""
      @retry_with_backoff(max_retries=2, initial_delay=0.0, jitter=False)
      def always_fails():
          raise ValueError("permanent")

      with pytest.raises(ValueError, match="permanent"):
          always_fails()


id: retry_with_backoff_py_core_t2
function_id: retry_with_backoff_py_core
name: test_on_retry_callback_se_llama
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_on_retry_callback_se_llama():
      """El callback on_retry debe invocarse una vez por reintento."""
      retries_seen: list[tuple[Exception, int]] = []

      def on_retry(exc: Exception, attempt: int) -> None:
          retries_seen.append((exc, attempt))

      @retry_with_backoff(max_retries=2, initial_delay=0.0, jitter=False, on_retry=on_retry)
      def flaky():
          raise RuntimeError("boom")

      with pytest.raises(RuntimeError):
          flaky()

      assert len(retries_seen) == 2
      assert all(isinstance(e, RuntimeError) for e, _ in retries_seen)
      assert [a for _, a in retries_seen] == [0, 1]


id: retry_with_backoff_py_core_t3
function_id: retry_with_backoff_py_core
name: test_jitter_produce_delays_variables
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_jitter_produce_delays_variables():
      """Con jitter=True los delays deben variar entre llamadas."""
      delays: list[float] = []
      original_sleep = __import__("time").sleep

      import time

      def capture_sleep(d: float) -> None:
          delays.append(d)

      import unittest.mock as mock

      @retry_with_backoff(max_retries=3, initial_delay=1.0, max_delay=30.0, jitter=True)
      def always_fails():
          raise RuntimeError("x")

      with mock.patch("time.sleep", side_effect=capture_sleep):
          with pytest.raises(RuntimeError):
              always_fails()

      # All delays should be > 0
      assert all(d > 0 for d in delays)
      # With jitter they are unlikely to be all identical (probabilistic; seed not fixed)
      # At minimum we verify the count matches max_retries
      assert len(delays) == 3


id: retry_with_backoff_py_core_t4
function_id: retry_with_backoff_py_core
name: test_solo_reintenta_excepciones_especificadas
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_solo_reintenta_excepciones_especificadas():
      """Solo debe reintentar las excepciones incluidas en el tuple."""
      calls = []

      @retry_with_backoff(
          max_retries=3,
          initial_delay=0.0,
          jitter=False,
          exceptions=(ConnectionError,),
      )
      def raises_value_error():
          calls.append(1)
          raise ValueError("not retryable")

      with pytest.raises(ValueError):
          raises_value_error()

      # Should have been called exactly once — no retries for ValueError
      assert len(calls) == 1


  # ---------------------------------------------------------------------------
  # retry_with_backoff_async
  # ---------------------------------------------------------------------------


id: retry_with_backoff_py_core_t5
function_id: retry_with_backoff_py_core
name: test_async_funcion_que_falla_dos_veces_y_luego_exito
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_async_funcion_que_falla_dos_veces_y_luego_exito():
      """Async: debe tener exito en el tercer intento tras 2 fallos."""
      calls = []

      @retry_with_backoff_async(max_retries=3, initial_delay=0.0, jitter=False)
      async def flaky():
          calls.append(1)
          if len(calls) < 3:
              raise ConnectionError("transient")
          return "ok"

      result = asyncio.run(flaky())
      assert result == "ok"
      assert len(calls) == 3


id: retry_with_backoff_py_core_t6
function_id: retry_with_backoff_py_core
name: test_async_funcion_que_siempre_falla_agota_retries_y_lanza
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_async_funcion_que_siempre_falla_agota_retries_y_lanza():
      """Async: debe lanzar la excepcion tras agotar todos los reintentos."""
      @retry_with_backoff_async(max_retries=2, initial_delay=0.0, jitter=False)
      async def always_fails():
          raise ValueError("permanent")

      with pytest.raises(ValueError, match="permanent"):
          asyncio.run(always_fails())


id: retry_with_backoff_py_core_t7
function_id: retry_with_backoff_py_core
name: test_async_on_retry_callback_se_llama
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_async_on_retry_callback_se_llama():
      """Async: el callback on_retry debe invocarse una vez por reintento."""
      retries_seen: list[tuple[Exception, int]] = []

      def on_retry(exc: Exception, attempt: int) -> None:
          retries_seen.append((exc, attempt))

      @retry_with_backoff_async(max_retries=2, initial_delay=0.0, jitter=False, on_retry=on_retry)
      async def flaky():
          raise RuntimeError("boom")

      with pytest.raises(RuntimeError):
          asyncio.run(flaky())

      assert len(retries_seen) == 2
      assert [a for _, a in retries_seen] == [0, 1]


id: retry_with_backoff_py_core_t8
function_id: retry_with_backoff_py_core
name: test_async_jitter_produce_delays_variables
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_async_jitter_produce_delays_variables():
      """Async: con jitter=True los delays registrados deben ser mayores que cero."""
      import unittest.mock as mock

      delays: list[float] = []

      async def capture_sleep(d: float) -> None:
          delays.append(d)

      @retry_with_backoff_async(max_retries=3, initial_delay=1.0, max_delay=30.0, jitter=True)
      async def always_fails():
          raise RuntimeError("x")

      with mock.patch("asyncio.sleep", side_effect=capture_sleep):
          with pytest.raises(RuntimeError):
              asyncio.run(always_fails())

      assert all(d > 0 for d in delays)
      assert len(delays) == 3


id: retry_with_backoff_py_core_t9
function_id: retry_with_backoff_py_core
name: test_async_solo_reintenta_excepciones_especificadas
file_path: python/functions/core/retry_with_backoff_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_async_solo_reintenta_excepciones_especificadas():
      """Async: solo debe reintentar las excepciones del tuple especificado."""
      calls = []

      @retry_with_backoff_async(
          max_retries=3,
          initial_delay=0.0,
          jitter=False,
          exceptions=(ConnectionError,),
      )
      async def raises_value_error():
          calls.append(1)
          raise ValueError("not retryable")

      with pytest.raises(ValueError):
          asyncio.run(raises_value_error())

      assert len(calls) == 1


id: split_text_into_chunks_py_core_t0
function_id: split_text_into_chunks_py_core
name: test_texto_corto_cabe_en_1_chunk
file_path: python/functions/core/split_text_into_chunks_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_corto_cabe_en_1_chunk():
      text = "Hello world. This is a short sentence."
      result = split_text_into_chunks(text, chunk_size=500)
      assert result == [text], f"Expected [text], got {result}"


id: split_text_into_chunks_py_core_t1
function_id: split_text_into_chunks_py_core
name: test_texto_largo_con_oraciones_corta_en_punto
file_path: python/functions/core/split_text_into_chunks_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_largo_con_oraciones_corta_en_punto():
      # Dos oraciones claras que juntas superan chunk_size
      s1 = "This is the first sentence which ends with a period. "
      s2 = "This is the second sentence which also ends with a period."
      text = s1 + s2
      result = split_text_into_chunks(text, chunk_size=60, overlap=10)
      assert len(result) >= 2, f"Expected at least 2 chunks, got {result}"
      # El primer chunk debe terminar alrededor del punto de la primera oración
      assert result[0].endswith("period."), f"First chunk should end at sentence boundary: {result[0]!r}"


id: split_text_into_chunks_py_core_t2
function_id: split_text_into_chunks_py_core
name: test_texto_cjk_con_separador_ideografico
file_path: python/functions/core/split_text_into_chunks_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_cjk_con_separador_ideografico():
      # Texto CJK con separador 。
      s1 = "これは最初の文です。"
      s2 = "これは二番目の文です。"
      text = s1 + s2
      # chunk_size pequeño para forzar el corte
      result = split_text_into_chunks(text, chunk_size=len(s1) + 2, overlap=2)
      assert len(result) >= 2, f"Expected at least 2 chunks for CJK text, got {result}"
      assert result[0].endswith("。"), f"First chunk should end with CJK period: {result[0]!r}"


id: split_text_into_chunks_py_core_t3
function_id: split_text_into_chunks_py_core
name: test_texto_sin_separadores_corta_en_chunk_size_exacto
file_path: python/functions/core/split_text_into_chunks_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_sin_separadores_corta_en_chunk_size_exacto():
      # Texto sin ningún separador de oración
      text = "a" * 200
      result = split_text_into_chunks(text, chunk_size=50, overlap=0)
      # Cada chunk debe tener exactamente 50 chars (excepto el último)
      for chunk in result[:-1]:
          assert len(chunk) == 50, f"Chunk should be 50 chars, got {len(chunk)}: {chunk!r}"
      # Último puede ser menor
      assert len(result[-1]) <= 50


id: split_text_into_chunks_py_core_t4
function_id: split_text_into_chunks_py_core
name: test_overlap_funciona
file_path: python/functions/core/split_text_into_chunks_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_overlap_funciona():
      # Verificar que el inicio del chunk N+1 solapa con el final del chunk N
      text = "a" * 300
      chunk_size = 100
      overlap = 20
      result = split_text_into_chunks(text, chunk_size=chunk_size, overlap=overlap)
      assert len(result) >= 2, "Need at least 2 chunks to test overlap"
      # La suma total de chars sin overlap debería ser < len(text) (hay solapamiento)
      total_chars = sum(len(c) for c in result)
      assert total_chars > len(text), f"With overlap, total chars ({total_chars}) should exceed text length ({len(text)})"


id: split_text_into_chunks_py_core_t5
function_id: split_text_into_chunks_py_core
name: test_texto_vacio_retorna_lista_vacia
file_path: python/functions/core/split_text_into_chunks_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_vacio_retorna_lista_vacia():
      assert split_text_into_chunks("") == []
      assert split_text_into_chunks("   ") == []


id: strip_markdown_codeblock_py_core_t0
function_id: strip_markdown_codeblock_py_core
name: test_JSON_sin_codeblock_no_cambia
file_path: python/functions/core/strip_markdown_codeblock_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_JSON_sin_codeblock_no_cambia():
      result = strip_markdown_codeblock('{"key": "value"}')
      assert result == '{"key": "value"}'


id: strip_markdown_codeblock_py_core_t1
function_id: strip_markdown_codeblock_py_core
name: test_JSON_con_wrapper_json_es_desenvuelto
file_path: python/functions/core/strip_markdown_codeblock_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_JSON_con_wrapper_json_es_desenvuelto():
      result = strip_markdown_codeblock('```json\n{"key": "value"}\n```')
      assert result == '{"key": "value"}'


id: strip_markdown_codeblock_py_core_t2
function_id: strip_markdown_codeblock_py_core
name: test_JSON_con_wrapper_solo_es_desenvuelto
file_path: python/functions/core/strip_markdown_codeblock_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_JSON_con_wrapper_solo_es_desenvuelto():
      result = strip_markdown_codeblock('```\n{"key": "value"}\n```')
      assert result == '{"key": "value"}'


id: strip_markdown_codeblock_py_core_t3
function_id: strip_markdown_codeblock_py_core
name: test_texto_que_no_es_JSON_pasa_sin_cambio
file_path: python/functions/core/strip_markdown_codeblock_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_que_no_es_JSON_pasa_sin_cambio():
      result = strip_markdown_codeblock("hello world")
      assert result == "hello world"


id: strip_think_tags_py_core_t0
function_id: strip_think_tags_py_core
name: test_texto_sin_tags_no_cambia
file_path: python/functions/core/strip_think_tags_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_sin_tags_no_cambia():
      result = strip_think_tags("La respuesta es 42.")
      assert result == "La respuesta es 42."


id: strip_think_tags_py_core_t1
function_id: strip_think_tags_py_core
name: test_texto_con_think_tags_los_remueve
file_path: python/functions/core/strip_think_tags_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_think_tags_los_remueve():
      result = strip_think_tags("<think>razonamiento interno</think>La respuesta es 42.")
      assert result == "La respuesta es 42."


id: strip_think_tags_py_core_t2
function_id: strip_think_tags_py_core
name: test_tags_multilinea_son_removidos
file_path: python/functions/core/strip_think_tags_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tags_multilinea_son_removidos():
      text = "<think>\nPrimero analizo...\nLuego concluyo...\n</think>Respuesta final."
      result = strip_think_tags(text)
      assert result == "Respuesta final."


id: strip_think_tags_py_core_t3
function_id: strip_think_tags_py_core
name: test_multiples_tags_son_removidos
file_path: python/functions/core/strip_think_tags_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_multiples_tags_son_removidos():
      text = "<think>paso 1</think>Parte A<think>paso 2</think>Parte B"
      result = strip_think_tags(text)
      assert result == "Parte AParte B"


id: t_py_core_t0
function_id: t_py_core
name: test_key_existente_retorna_traduccion
file_path: python/functions/core/t_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_existente_retorna_traduccion():
      setup()
      result = t("report.done", locale="en")
      assert result == "Done", f"Expected 'Done', got '{result}'"


id: t_py_core_t1
function_id: t_py_core
name: test_key_inexistente_retorna_la_key
file_path: python/functions/core/t_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_inexistente_retorna_la_key():
      setup()
      result = t("nonexistent.key", locale="en")
      assert result == "nonexistent.key", f"Expected 'nonexistent.key', got '{result}'"


id: t_py_core_t2
function_id: t_py_core
name: test_interpolacion_de_variables
file_path: python/functions/core/t_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_interpolacion_de_variables():
      setup()
      result = t("report.sectionStart", locale="en", title="Introduction")
      assert result == "Generating section: Introduction", f"Got '{result}'"


id: t_py_core_t3
function_id: t_py_core
name: test_dot_path_profundo
file_path: python/functions/core/t_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dot_path_profundo():
      setup()
      result = t("report.deep.nested.value", locale="en")
      assert result == "Deep value", f"Expected 'Deep value', got '{result}'"


id: t_py_core_t4
function_id: t_py_core
name: test_fallback_a_locale_default
file_path: python/functions/core/t_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_fallback_a_locale_default():
      setup()
      # "report.sectionStart" no existe en "es", debe hacer fallback a "en"
      result = t("report.sectionStart", locale="es", title="Intro")
      assert result == "Generating section: Intro", f"Got '{result}'"


  if __name__ == "__main__":
      test_key_existente_retorna_traduccion()
      print("PASS: key existente retorna traduccion")

      test_key_inexistente_retorna_la_key()
      print("PASS: key inexistente retorna la key")

      test_interpolacion_de_variables()
      print("PASS: interpolacion de variables")

      test_dot_path_profundo()
      print("PASS: dot-path profundo")

      test_fallback_a_locale_default()
      print("PASS: fallback a locale default")

      print("---")
      print("All tests passed.")


id: to_csv_py_core_t0
function_id: to_csv_py_core
name: test_lista_simple_csv_con_header
file_path: python/functions/core/to_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_simple_csv_con_header():
      rows = [{"nombre": "Ana", "edad": 30}, {"nombre": "Bob", "edad": 25}]
      result = to_csv(rows)
      lines = result.split("\r\n")
      assert lines[0] == "nombre,edad"
      assert lines[1] == "Ana,30"
      assert lines[2] == "Bob,25"


id: to_csv_py_core_t1
function_id: to_csv_py_core
name: test_campos_con_comas_comillas_newlines
file_path: python/functions/core/to_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campos_con_comas_comillas_newlines():
      rows = [{"a": 'dijo "hola"', "b": "uno,dos", "c": "linea1\nlinea2"}]
      result = to_csv(rows)
      lines = result.split("\r\n")
      assert '"dijo ""hola"""' in lines[1]
      assert '"uno,dos"' in lines[1]
      assert '"linea1\nlinea2"' in lines[1]


id: to_csv_py_core_t2
function_id: to_csv_py_core
name: test_columns_explicitas_reordena_y_filtra
file_path: python/functions/core/to_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_columns_explicitas_reordena_y_filtra():
      rows = [{"a": 1, "b": 2, "c": 3}]
      result = to_csv(rows, columns=["c", "a"])
      lines = result.split("\r\n")
      assert lines[0] == "c,a"
      assert lines[1] == "3,1"


id: to_csv_py_core_t3
function_id: to_csv_py_core
name: test_include_header_false
file_path: python/functions/core/to_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_include_header_false():
      rows = [{"x": "foo", "y": "bar"}]
      result = to_csv(rows, include_header=False)
      lines = result.split("\r\n")
      assert len(lines) == 1
      assert lines[0] == "foo,bar"


id: to_csv_py_core_t5
function_id: to_csv_py_core
name: test_valores_none_campo_vacio
file_path: python/functions/core/to_csv_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_valores_none_campo_vacio():
      rows = [{"a": "ok", "b": None}]
      result = to_csv(rows)
      lines = result.split("\r\n")
      assert lines[1] == "ok,"


id: to_jsonl_py_core_t0
function_id: to_jsonl_py_core
name: test_lista_de_dicts_jsonl
file_path: python/functions/core/to_jsonl_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_de_dicts_jsonl():
      rows = [{"a": 1}, {"b": 2}]
      result = to_jsonl(rows)
      lines = result.split("\n")
      assert len(lines) == 2
      assert json.loads(lines[0]) == {"a": 1}
      assert json.loads(lines[1]) == {"b": 2}


id: to_jsonl_py_core_t1
function_id: to_jsonl_py_core
name: test_valores_unicode_none_nested
file_path: python/functions/core/to_jsonl_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_valores_unicode_none_nested():
      rows = [{"emoji": "🔥", "nulo": None, "nested": {"x": 1}}]
      result = to_jsonl(rows)
      parsed = json.loads(result)
      assert parsed["emoji"] == "🔥"
      assert parsed["nulo"] is None
      assert parsed["nested"] == {"x": 1}


id: to_jsonl_py_core_t3
function_id: to_jsonl_py_core
name: test_cada_linea_es_json_parseable
file_path: python/functions/core/to_jsonl_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_cada_linea_es_json_parseable():
      rows = [{"k": i} for i in range(5)]
      result = to_jsonl(rows)
      for i, line in enumerate(result.split("\n")):
          assert json.loads(line) == {"k": i}


id: validate_json_schema_py_core_t0
function_id: validate_json_schema_py_core
name: test_schema_simple_con_types_y_required
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_schema_simple_con_types_y_required():
      schema = {
          "type": "object",
          "required": ["name", "age"],
          "properties": {
              "name": {"type": "string"},
              "age": {"type": "integer"},
          },
      }
      valid, errors = validate_json_schema({"name": "Alice", "age": 30}, schema)
      assert valid is True
      assert errors == []


id: validate_json_schema_py_core_t1
function_id: validate_json_schema_py_core
name: test_nested_object_validation
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_nested_object_validation():
      schema = {
          "type": "object",
          "properties": {
              "address": {
                  "type": "object",
                  "required": ["zip"],
                  "properties": {
                      "zip": {"type": "string"},
                      "street": {"type": "string"},
                  },
              }
          },
      }
      valid, errors = validate_json_schema(
          {"address": {"zip": 12345, "street": "Main St"}}, schema
      )
      assert valid is False
      assert any("$.address.zip" in e for e in errors)


id: validate_json_schema_py_core_t2
function_id: validate_json_schema_py_core
name: test_array_con_items_schema
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_array_con_items_schema():
      schema = {
          "type": "array",
          "items": {"type": "string"},
          "minItems": 1,
          "maxItems": 3,
      }
      valid, errors = validate_json_schema(["a", "b"], schema)
      assert valid is True

      valid2, errors2 = validate_json_schema(["a", 2, "c"], schema)
      assert valid2 is False
      assert any("[1]" in e for e in errors2)


id: validate_json_schema_py_core_t3
function_id: validate_json_schema_py_core
name: test_pattern_validation_en_strings
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pattern_validation_en_strings():
      schema = {
          "type": "object",
          "properties": {
              "email": {"type": "string", "pattern": r"^[^@]+@[^@]+\.[^@]+$"},
          },
      }
      valid, errors = validate_json_schema({"email": "user@example.com"}, schema)
      assert valid is True

      valid2, errors2 = validate_json_schema({"email": "not-an-email"}, schema)
      assert valid2 is False
      assert any("pattern" in e for e in errors2)


id: validate_json_schema_py_core_t4
function_id: validate_json_schema_py_core
name: test_numeric_ranges
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_numeric_ranges():
      schema = {
          "type": "integer",
          "minimum": 0,
          "maximum": 100,
      }
      valid, _ = validate_json_schema(50, schema)
      assert valid is True

      valid2, errors2 = validate_json_schema(150, schema)
      assert valid2 is False
      assert any("maximum" in e for e in errors2)


id: validate_json_schema_py_core_t5
function_id: validate_json_schema_py_core
name: test_multiples_errores_acumulados_con_paths_correctos
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_multiples_errores_acumulados_con_paths_correctos():
      schema = {
          "type": "object",
          "required": ["name", "age", "email"],
          "properties": {
              "name": {"type": "string"},
              "age": {"type": "integer", "minimum": 0, "maximum": 120},
              "email": {"type": "string", "pattern": r"^[^@]+@[^@]+$"},
          },
      }
      data = {"name": 123, "age": -5, "email": "bad"}
      valid, errors = validate_json_schema(data, schema)
      assert valid is False
      # name falla type, age falla minimum, email falla pattern
      assert len(errors) >= 2
      paths = [e.split(":")[0] for e in errors]
      assert "$.name" in paths or any("$.name" in e for e in errors)


id: validate_json_schema_py_core_t6
function_id: validate_json_schema_py_core
name: test_schema_vacio_acepta_todo
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_schema_vacio_acepta_todo():
      valid, errors = validate_json_schema({"any": "thing"}, {})
      assert valid is True
      assert errors == []

      valid2, errors2 = validate_json_schema([1, 2, 3], {})
      assert valid2 is True
      assert errors2 == []


id: validate_json_schema_py_core_t7
function_id: validate_json_schema_py_core
name: test_data_none_contra_required_field
file_path: python/functions/core/validate_json_schema_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_data_none_contra_required_field():
      schema = {
          "type": "object",
          "required": ["name"],
          "properties": {
              "name": {"type": "string"},
          },
      }
      valid, errors = validate_json_schema({"name": None}, schema)
      assert valid is False
      assert any("name" in e for e in errors)


id: envelope_decrypt_py_cybersecurity_t0
function_id: envelope_decrypt_py_cybersecurity
name: test_encrypt_decrypt_roundtrip
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_encrypt_decrypt_roundtrip():
      master_key = secrets.token_bytes(32)
      plaintext = b"datos de prueba para envelope encryption"
      ciphertext = envelope_encrypt(plaintext, master_key)
      result = envelope_decrypt(ciphertext, master_key)
      assert result == plaintext


id: envelope_decrypt_py_cybersecurity_t1
function_id: envelope_decrypt_py_cybersecurity
name: test_datos_vacios
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_datos_vacios():
      master_key = secrets.token_bytes(32)
      ciphertext = envelope_encrypt(b"", master_key)
      result = envelope_decrypt(ciphertext, master_key)
      assert result == b""


id: envelope_decrypt_py_cybersecurity_t2
function_id: envelope_decrypt_py_cybersecurity
name: test_datos_grandes
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_datos_grandes():
      master_key = secrets.token_bytes(32)
      plaintext = secrets.token_bytes(1024 * 1024)  # 1 MB
      ciphertext = envelope_encrypt(plaintext, master_key)
      result = envelope_decrypt(ciphertext, master_key)
      assert result == plaintext


id: envelope_decrypt_py_cybersecurity_t3
function_id: envelope_decrypt_py_cybersecurity
name: test_decrypt_datos_no_cifrados_passthrough
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_decrypt_datos_no_cifrados_passthrough():
      master_key = secrets.token_bytes(32)
      plain = b"archivo no cifrado, sin magic bytes"
      result = envelope_decrypt(plain, master_key)
      assert result == plain


id: envelope_decrypt_py_cybersecurity_t4
function_id: envelope_decrypt_py_cybersecurity
name: test_key_incorrecta
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_incorrecta():
      master_key = secrets.token_bytes(32)
      wrong_key = secrets.token_bytes(32)
      ciphertext = envelope_encrypt(b"secreto", master_key)
      try:
          envelope_decrypt(ciphertext, wrong_key)
          assert False, "deberia haber lanzado excepcion"
      except Exception:
          pass  # esperado: InvalidTag de cryptography


id: envelope_decrypt_py_cybersecurity_t5
function_id: envelope_decrypt_py_cybersecurity
name: test_envelope_truncado
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_envelope_truncado():
      master_key = secrets.token_bytes(32)
      ciphertext = envelope_encrypt(b"datos", master_key)
      truncated = ciphertext[:6]  # header incompleto
      try:
          envelope_decrypt(truncated, master_key)
          assert False, "deberia haber lanzado ValueError"
      except ValueError:
          pass


id: envelope_decrypt_py_cybersecurity_t6
function_id: envelope_decrypt_py_cybersecurity
name: test_magic_invalido
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_magic_invalido():
      master_key = secrets.token_bytes(32)
      # Construir datos con magic valido para pasar el check del passthrough
      # pero con header corrupto
      bad_envelope = b"OVE1" + b"\x00" * 20  # magic correcto pero header invalido
      try:
          envelope_decrypt(bad_envelope, master_key)
          assert False, "deberia haber lanzado excepcion"
      except Exception:
          pass


id: envelope_decrypt_py_cybersecurity_t7
function_id: envelope_decrypt_py_cybersecurity
name: test_ciphertext_tiene_magic_correcto
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ciphertext_tiene_magic_correcto():
      master_key = secrets.token_bytes(32)
      ciphertext = envelope_encrypt(b"test", master_key)
      assert ciphertext[:4] == b"OVE1"


id: envelope_decrypt_py_cybersecurity_t8
function_id: envelope_decrypt_py_cybersecurity
name: test_ciphertext_es_distinto_cada_vez
file_path: python/functions/cybersecurity/envelope_encrypt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ciphertext_es_distinto_cada_vez():
      master_key = secrets.token_bytes(32)
      plaintext = b"mismo mensaje"
      ct1 = envelope_encrypt(plaintext, master_key)
      ct2 = envelope_encrypt(plaintext, master_key)
      # IVs aleatorios garantizan ciphertexts distintos
      assert ct1 != ct2


  if __name__ == "__main__":
      test_encrypt_decrypt_roundtrip()
      test_datos_vacios()
      test_datos_grandes()
      test_decrypt_datos_no_cifrados_passthrough()
      test_key_incorrecta()
      test_envelope_truncado()
      test_magic_invalido()
      test_ciphertext_tiene_magic_correcto()
      test_ciphertext_es_distinto_cada_vez()
      print("Todos los tests pasaron.")


id: aggregate_by_group_py_datascience_t0
function_id: aggregate_by_group_py_datascience
name: test_group_by_una_columna_con_sum
file_path: python/functions/datascience/aggregate_by_group_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_group_by_una_columna_con_sum():
      """Group by una columna con sum."""
      rows = [
          {"dept": "eng", "salary": 100},
          {"dept": "eng", "salary": 120},
          {"dept": "sales", "salary": 80},
      ]
      result = aggregate_by_group(rows, group_by=["dept"], aggs={"salary": "sum"})
      assert len(result) == 2
      eng = next(r for r in result if r["dept"] == "eng")
      sales = next(r for r in result if r["dept"] == "sales")
      assert eng["salary"] == 220
      assert sales["salary"] == 80


id: aggregate_by_group_py_datascience_t1
function_id: aggregate_by_group_py_datascience
name: test_group_by_multiples_columnas
file_path: python/functions/datascience/aggregate_by_group_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_group_by_multiples_columnas():
      """Group by multiples columnas."""
      rows = [
          {"dept": "eng", "level": "senior", "salary": 150},
          {"dept": "eng", "level": "junior", "salary": 80},
          {"dept": "eng", "level": "senior", "salary": 160},
          {"dept": "sales", "level": "senior", "salary": 120},
      ]
      result = aggregate_by_group(rows, group_by=["dept", "level"], aggs={"salary": "sum"})
      assert len(result) == 3
      eng_senior = next(r for r in result if r["dept"] == "eng" and r["level"] == "senior")
      assert eng_senior["salary"] == 310


id: aggregate_by_group_py_datascience_t2
function_id: aggregate_by_group_py_datascience
name: test_agregacion_mean_count_min_max
file_path: python/functions/datascience/aggregate_by_group_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_agregacion_mean_count_min_max():
      """Agregacion mean count min max."""
      rows = [
          {"cat": "A", "val": 10},
          {"cat": "A", "val": 20},
          {"cat": "A", "val": 30},
      ]
      result_mean = aggregate_by_group(rows, group_by=["cat"], aggs={"val": "mean"})
      assert result_mean[0]["val"] == 20.0

      result_count = aggregate_by_group(rows, group_by=["cat"], aggs={"val": "count"})
      assert result_count[0]["val"] == 3

      result_min = aggregate_by_group(rows, group_by=["cat"], aggs={"val": "min"})
      assert result_min[0]["val"] == 10

      result_max = aggregate_by_group(rows, group_by=["cat"], aggs={"val": "max"})
      assert result_max[0]["val"] == 30


id: aggregate_by_group_py_datascience_t3
function_id: aggregate_by_group_py_datascience
name: test_collect_acumula_en_lista
file_path: python/functions/datascience/aggregate_by_group_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_collect_acumula_en_lista():
      """collect acumula en lista."""
      rows = [
          {"dept": "eng", "name": "Alice"},
          {"dept": "eng", "name": "Bob"},
          {"dept": "sales", "name": "Carol"},
      ]
      result = aggregate_by_group(rows, group_by=["dept"], aggs={"name": "collect"})
      eng = next(r for r in result if r["dept"] == "eng")
      assert sorted(eng["name"]) == ["Alice", "Bob"]


id: aggregate_by_group_py_datascience_t4
function_id: aggregate_by_group_py_datascience
name: test_grupo_con_una_sola_fila
file_path: python/functions/datascience/aggregate_by_group_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_grupo_con_una_sola_fila():
      """Grupo con una sola fila."""
      rows = [{"dept": "eng", "salary": 100}]
      result = aggregate_by_group(rows, group_by=["dept"], aggs={"salary": "sum"})
      assert len(result) == 1
      assert result[0]["salary"] == 100


id: aggregate_by_group_py_datascience_t5
function_id: aggregate_by_group_py_datascience
name: test_campo_con_none_se_ignora_en_agregaciones_numericas
file_path: python/functions/datascience/aggregate_by_group_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campo_con_none_se_ignora_en_agregaciones_numericas():
      """Campo con None se ignora en agregaciones numericas."""
      rows = [
          {"dept": "eng", "salary": 100},
          {"dept": "eng", "salary": None},
          {"dept": "eng", "salary": 200},
      ]
      result = aggregate_by_group(rows, group_by=["dept"], aggs={"salary": "sum"})
      assert result[0]["salary"] == 300

      result_mean = aggregate_by_group(rows, group_by=["dept"], aggs={"salary": "mean"})
      assert result_mean[0]["salary"] == 150.0


id: build_entity_schema_prompt_py_datascience_t0
function_id: build_entity_schema_prompt_py_datascience
name: test_lista_con_varios_presets
file_path: python/functions/datascience/build_entity_schema_prompt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_con_varios_presets():
      presets = [
          {
              "type_ref": "osint_person_go_cybersecurity",
              "label": "Person",
              "metadata_fields": ["full_name", "alias", "nationality", "dob", "risk_score"],
          },
          {
              "type_ref": "osint_organization_go_cybersecurity",
              "label": "Organization",
              "metadata_fields": ["legal_name", "country", "sector", "founded", "risk_score"],
          },
      ]
      result = build_entity_schema_prompt(presets)
      assert "Entity types available for extraction:" in result
      assert "1. Person (type_ref: osint_person_go_cybersecurity)" in result
      assert "   Attributes: full_name, alias, nationality, dob, risk_score" in result
      assert "2. Organization (type_ref: osint_organization_go_cybersecurity)" in result
      assert "   Attributes: legal_name, country, sector, founded, risk_score" in result


id: build_entity_schema_prompt_py_datascience_t1
function_id: build_entity_schema_prompt_py_datascience
name: test_lista_vacia_retorna_string_vacio
file_path: python/functions/datascience/build_entity_schema_prompt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_vacia_retorna_string_vacio():
      result = build_entity_schema_prompt([])
      assert result == ""


id: build_entity_schema_prompt_py_datascience_t2
function_id: build_entity_schema_prompt_py_datascience
name: test_preset_sin_metadata_fields
file_path: python/functions/datascience/build_entity_schema_prompt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_preset_sin_metadata_fields():
      presets = [
          {
              "type_ref": "osint_person_go_cybersecurity",
              "label": "Person",
          }
      ]
      result = build_entity_schema_prompt(presets)
      assert "1. Person (type_ref: osint_person_go_cybersecurity)" in result
      assert "Attributes:" not in result


id: build_relation_schema_prompt_py_datascience_t0
function_id: build_relation_schema_prompt_py_datascience
name: test_lista_normal
file_path: python/functions/datascience/build_relation_schema_prompt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_normal():
      relation_types = ["funds", "employs", "communicates_with", "owns", "operates"]
      result = build_relation_schema_prompt(relation_types)
      assert result.startswith("Allowed relation types:")
      assert "funds" in result
      assert "employs" in result
      assert "communicates_with" in result
      assert "owns" in result
      assert "operates" in result


id: build_relation_schema_prompt_py_datascience_t1
function_id: build_relation_schema_prompt_py_datascience
name: test_lista_vacia_retorna_string_vacio
file_path: python/functions/datascience/build_relation_schema_prompt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_vacia_retorna_string_vacio():
      result = build_relation_schema_prompt([])
      assert result == ""


id: deduplicate_entities_py_datascience_t0
function_id: deduplicate_entities_py_datascience
name: test_john_smith_y_smith_john_merge
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_john_smith_y_smith_john_merge():
      """John Smith y Smith, John se mergean."""
      a = _make("John Smith", type_ref="person")
      b = _make("Smith, John", type_ref="person")
      result = deduplicate_entities([a, b])
      assert result.total_before == 2
      assert result.total_after == 1
      assert len(result.entities) == 1
      assert len(result.merge_log) == 1


id: deduplicate_entities_py_datascience_t1
function_id: deduplicate_entities_py_datascience
name: test_google_y_google_llc_merge
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_google_y_google_llc_merge():
      """Google y Google LLC se mergean."""
      a = _make("Google", type_ref="organization")
      b = _make("Google LLC", type_ref="organization")
      result = deduplicate_entities([a, b])
      assert result.total_after == 1
      assert len(result.entities) == 1


id: deduplicate_entities_py_datascience_t2
function_id: deduplicate_entities_py_datascience
name: test_ip_matching_exacto
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ip_matching_exacto():
      """192.168.1.1 y 192.168.1.1 se mergean por matching exacto."""
      a = _make("192.168.1.1", type_ref="ip", confidence=0.8)
      b = _make("192.168.1.1", type_ref="ip", confidence=0.9)
      result = deduplicate_entities([a, b])
      assert result.total_after == 1


id: deduplicate_entities_py_datascience_t3
function_id: deduplicate_entities_py_datascience
name: test_same_name_different_type_no_merge
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_same_name_different_type_no_merge():
      """John Smith (person) y John Smith (organization) NO se mergean."""
      a = _make("John Smith", type_ref="person")
      b = _make("John Smith", type_ref="organization")
      result = deduplicate_entities([a, b], same_type_only=True)
      assert result.total_after == 2


id: deduplicate_entities_py_datascience_t4
function_id: deduplicate_entities_py_datascience
name: test_clusters_transitivos
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_clusters_transitivos():
      """Clusters transitivos: A~B, B~C -> {A, B, C} en un solo cluster."""
      a = _make("Alice Johnson", type_ref="person")
      b = _make("Alice Johnso", type_ref="person")   # muy similar a A
      c = _make("Alice Johns", type_ref="person")    # muy similar a B
      result = deduplicate_entities([a, b, c], name_threshold=0.80)
      assert result.total_after == 1


id: deduplicate_entities_py_datascience_t5
function_id: deduplicate_entities_py_datascience
name: test_sin_duplicados_sin_cambios
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_sin_duplicados_sin_cambios():
      """Entidades sin duplicados pasan sin modificacion."""
      a = _make("Alice Smith", type_ref="person")
      b = _make("Bob Jones", type_ref="person")
      c = _make("Charlie Brown", type_ref="person")
      result = deduplicate_entities([a, b, c])
      assert result.total_before == 3
      assert result.total_after == 3
      assert len(result.merge_log) == 0


id: deduplicate_entities_py_datascience_t6
function_id: deduplicate_entities_py_datascience
name: test_confidence_y_atributos_merge_correctos
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_confidence_y_atributos_merge_correctos():
      """Confidence toma el max del cluster; atributos se fusionan."""
      a = _make("John Smith", type_ref="person", confidence=0.7, role="CEO")
      b = _make("Smith, John", type_ref="person", confidence=0.95, company="Acme")
      result = deduplicate_entities([a, b])
      assert result.total_after == 1
      entity = result.entities[0]
      # confidence = max(0.7, 0.95)
      assert entity.confidence == 0.95
      # atributos de ambos candidatos presentes
      assert "role" in entity.attributes
      assert "company" in entity.attributes


id: deduplicate_entities_py_datascience_t7
function_id: deduplicate_entities_py_datascience
name: test_lista_vacia
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_vacia():
      """Lista vacia retorna resultado vacio."""
      result = deduplicate_entities([])
      assert result.total_before == 0
      assert result.total_after == 0
      assert result.entities == []
      assert result.merge_log == []


id: deduplicate_entities_py_datascience_t8
function_id: deduplicate_entities_py_datascience
name: test_name_to_id_resolucion
file_path: python/functions/datascience/deduplicate_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_name_to_id_resolucion():
      """name_to_id contiene todos los nombres originales del cluster."""
      a = _make("John Smith", type_ref="person")
      b = _make("Smith, John", type_ref="person")
      result = deduplicate_entities([a, b])
      # Ambos nombres deben apuntar al mismo ID
      ids = list(result.entity_id_map.values())
      assert len(ids) == 1
      ent_id = ids[0]
      # name_to_id debe tener entradas para los nombres originales
      assert any(v == ent_id for v in result.name_to_id.values())
      assert len(result.name_to_id) >= 2


id: deduplicate_relations_py_datascience_t0
function_id: deduplicate_relations_py_datascience
name: test_dos_relaciones_identicas_se_colapsan_en_una
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dos_relaciones_identicas_se_colapsan_en_una():
      """2 relaciones identicas (from, to, type) → 1."""
      rels = [
          _make_rel("John Smith", "Acme Corp", description="John es CEO", confidence=0.9),
          _make_rel("John Smith", "Acme Corp", description="John es CEO", confidence=0.7),
      ]
      result = deduplicate_relations(rels, _ENTITY_MAP)
      assert len(result) == 1
      assert result[0].from_id == "entity_001"
      assert result[0].to_id == "entity_002"
      assert result[0].confidence == 0.9  # max


id: deduplicate_relations_py_datascience_t1
function_id: deduplicate_relations_py_datascience
name: test_relacion_con_nombre_mergeado_se_resuelve_al_id_correcto
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_relacion_con_nombre_mergeado_se_resuelve_al_id_correcto():
      """Relacion con nombre mergeado → se resuelve al ID correcto."""
      # entity_id_map incluye "smith, john" como alias de entity_001
      merged_map = {**_ENTITY_MAP, "smith, john": "entity_001"}
      rels = [_make_rel("Smith, John", "Acme Corp")]
      result = deduplicate_relations(rels, merged_map)
      assert len(result) == 1
      assert result[0].from_id == "entity_001"
      assert result[0].to_id == "entity_002"


id: deduplicate_relations_py_datascience_t2
function_id: deduplicate_relations_py_datascience
name: test_self_loop_se_descarta
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_self_loop_se_descarta():
      """Self-loop (from_id == to_id) → descartado."""
      rels = [_make_rel("John Smith", "John Smith", relation_type="knows")]
      result = deduplicate_relations(rels, _ENTITY_MAP)
      assert len(result) == 0


id: deduplicate_relations_py_datascience_t3
function_id: deduplicate_relations_py_datascience
name: test_nombre_no_mapeado_sin_fuzzy_match_se_descarta
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_nombre_no_mapeado_sin_fuzzy_match_se_descarta():
      """Relacion con nombre no mapeado y sin fuzzy match → descartada."""
      rels = [_make_rel("Unknown Entity XYZ", "Acme Corp")]
      result = deduplicate_relations(rels, _ENTITY_MAP)
      assert len(result) == 0


id: deduplicate_relations_py_datascience_t4
function_id: deduplicate_relations_py_datascience
name: test_relaciones_distintas_se_mantienen
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_relaciones_distintas_se_mantienen():
      """Relaciones con (from, to, type) distintos → todas se mantienen."""
      rels = [
          _make_rel("John Smith", "Acme Corp", relation_type="works_at"),
          _make_rel("Jane Doe", "Acme Corp", relation_type="works_at"),
          _make_rel("John Smith", "Google", relation_type="invested_in"),
      ]
      result = deduplicate_relations(rels, _ENTITY_MAP)
      assert len(result) == 3


id: deduplicate_relations_py_datascience_t5
function_id: deduplicate_relations_py_datascience
name: test_merge_descripcion_concatena_unicas
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_merge_descripcion_concatena_unicas():
      """Merge de relaciones: descripciones unicas se concatenan."""
      rels = [
          _make_rel("John Smith", "Acme Corp", description="John es CEO", confidence=0.9),
          _make_rel("John Smith", "Acme Corp", description="Acme fue fundada por John", confidence=0.7),
          _make_rel("John Smith", "Acme Corp", description="John es CEO", confidence=0.6),
      ]
      result = deduplicate_relations(rels, _ENTITY_MAP)
      assert len(result) == 1
      assert "John es CEO" in result[0].description
      assert "Acme fue fundada por John" in result[0].description
      # La descripcion duplicada ("John es CEO") no aparece dos veces
      assert result[0].description.count("John es CEO") == 1
      assert result[0].confidence == 0.9


id: deduplicate_relations_py_datascience_t6
function_id: deduplicate_relations_py_datascience
name: test_lista_vacia_retorna_lista_vacia
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_vacia_retorna_lista_vacia():
      """Lista vacia de relaciones → lista vacia."""
      result = deduplicate_relations([], _ENTITY_MAP)
      assert result == []


id: deduplicate_relations_py_datascience_t7
function_id: deduplicate_relations_py_datascience
name: test_fuzzy_match_resuelve_nombre_cercano
file_path: python/functions/datascience/deduplicate_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_fuzzy_match_resuelve_nombre_cercano():
      """Nombre con typo pequeño → fuzzy match lo resuelve."""
      # "john smit" tiene distancia 1 de "john smith"
      rels = [_make_rel("John Smit", "Acme Corp")]
      result = deduplicate_relations(rels, _ENTITY_MAP)
      assert len(result) == 1
      assert result[0].from_id == "entity_001"


id: detect_drift_py_datascience_t0
function_id: detect_drift_py_datascience
name: test_campo_con_drift_claro_z_mayor_threshold
file_path: python/functions/datascience/detect_drift_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campo_con_drift_claro_z_mayor_threshold():
      history = [
          {"records_out": 100},
          {"records_out": 105},
          {"records_out": 98},
      ]
      current = {"records_out": 50}
      results = detect_drift(history, current, ["records_out"])
      assert len(results) == 1
      r = results[0]
      assert r["field"] == "records_out"
      assert r["current"] == 50.0
      assert r["drifted"] is True
      assert r["z_score"] < -2.0  # muy lejos de la media


id: detect_drift_py_datascience_t1
function_id: detect_drift_py_datascience
name: test_campo_estable_z_menor_threshold
file_path: python/functions/datascience/detect_drift_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_campo_estable_z_menor_threshold():
      history = [
          {"val": 100.0},
          {"val": 102.0},
          {"val": 98.0},
          {"val": 101.0},
      ]
      current = {"val": 100.5}  # dentro del rango normal
      results = detect_drift(history, current, ["val"])
      assert len(results) == 1
      r = results[0]
      assert r["drifted"] is False
      assert abs(r["z_score"]) < 2.0


id: detect_drift_py_datascience_t2
function_id: detect_drift_py_datascience
name: test_historial_con_un_solo_punto_std_0_drifted_False_con_nota
file_path: python/functions/datascience/detect_drift_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_historial_con_un_solo_punto_std_0_drifted_False_con_nota():
      history = [{"val": 100.0}]
      current = {"val": 999.0}
      results = detect_drift(history, current, ["val"])
      assert len(results) == 1
      r = results[0]
      assert r["std"] == 0.0
      assert r["z_score"] == 0.0
      assert r["drifted"] is False
      assert r["mean"] == 100.0


id: detect_drift_py_datascience_t3
function_id: detect_drift_py_datascience
name: test_historial_vacio_todos_drifted_False
file_path: python/functions/datascience/detect_drift_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_historial_vacio_todos_drifted_False():
      history = []
      current = {"records_out": 50, "duration_ms": 2000}
      results = detect_drift(history, current, ["records_out", "duration_ms"])
      assert len(results) == 2
      for r in results:
          assert r["drifted"] is False
          assert r["z_score"] == 0.0
          assert r["mean"] == 0.0


id: detect_drift_py_datascience_t4
function_id: detect_drift_py_datascience
name: test_threshold_custom
file_path: python/functions/datascience/detect_drift_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_threshold_custom():
      history = [
          {"val": 100.0},
          {"val": 100.0},
          {"val": 110.0},
          {"val": 90.0},
      ]
      # std ~ 7.07, mean = 100
      current = {"val": 115.0}  # z ~ 2.12

      # threshold default 2.0 -> drifted
      results = detect_drift(history, current, ["val"], threshold=2.0)
      assert results[0]["drifted"] is True

      # threshold 3.0 -> no drifted
      results2 = detect_drift(history, current, ["val"], threshold=3.0)
      assert results2[0]["drifted"] is False


  if __name__ == "__main__":
      test_campo_con_drift_claro_z_mayor_threshold()
      test_campo_estable_z_menor_threshold()
      test_historial_con_un_solo_punto_std_0_drifted_False_con_nota()
      test_historial_vacio_todos_drifted_False()
      test_threshold_custom()
      print("All tests passed.")


id: diff_entities_py_datascience_t0
function_id: diff_entities_py_datascience
name: test_entity_anadida
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_entity_anadida():
      before = [{"id": "1", "name": "Alice"}]
      after = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      result = diff_entities(before, after)
      assert len(result["added"]) == 1
      assert result["added"][0]["id"] == "2"
      assert result["removed"] == []
      assert result["modified"] == []
      assert result["unchanged"] == 1
      assert "1 added" in result["summary"]


id: diff_entities_py_datascience_t1
function_id: diff_entities_py_datascience
name: test_entity_eliminada
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_entity_eliminada():
      before = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      after = [{"id": "1", "name": "Alice"}]
      result = diff_entities(before, after)
      assert result["added"] == []
      assert len(result["removed"]) == 1
      assert result["removed"][0]["id"] == "2"
      assert result["unchanged"] == 1
      assert "1 removed" in result["summary"]


id: diff_entities_py_datascience_t2
function_id: diff_entities_py_datascience
name: test_entity_modificada_con_detalle_de_campos
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_entity_modificada_con_detalle_de_campos():
      before = [{"id": "1", "name": "Alice", "status": "active"}]
      after = [{"id": "1", "name": "Alice", "status": "inactive"}]
      result = diff_entities(before, after)
      assert result["added"] == []
      assert result["removed"] == []
      assert len(result["modified"]) == 1
      mod = result["modified"][0]
      assert mod["key"] == "1"
      assert "status" in mod["changes"]
      assert mod["changes"]["status"]["old"] == "active"
      assert mod["changes"]["status"]["new"] == "inactive"
      assert result["unchanged"] == 0


id: diff_entities_py_datascience_t3
function_id: diff_entities_py_datascience
name: test_entities_identicas_unchanged
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_entities_identicas_unchanged():
      before = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      after = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      result = diff_entities(before, after)
      assert result["added"] == []
      assert result["removed"] == []
      assert result["modified"] == []
      assert result["unchanged"] == 2
      assert "2 unchanged" in result["summary"]


id: diff_entities_py_datascience_t4
function_id: diff_entities_py_datascience
name: test_ignore_fields_funciona
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ignore_fields_funciona():
      before = [{"id": "1", "name": "Alice", "updated_at": "2024-01-01", "created_at": "2023-01-01"}]
      after = [{"id": "1", "name": "Alice", "updated_at": "2024-06-01", "created_at": "2023-01-01"}]
      result = diff_entities(before, after)
      # updated_at se ignora por defecto -> unchanged
      assert result["unchanged"] == 1
      assert result["modified"] == []

      # Si no ignoramos updated_at, debe detectar el cambio
      result2 = diff_entities(before, after, ignore_fields=[])
      assert len(result2["modified"]) == 1
      assert "updated_at" in result2["modified"][0]["changes"]


id: diff_entities_py_datascience_t5
function_id: diff_entities_py_datascience
name: test_compare_fields_filtra_correctamente
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_compare_fields_filtra_correctamente():
      before = [{"id": "1", "name": "Alice", "status": "active", "score": 10}]
      after = [{"id": "1", "name": "Bob", "status": "inactive", "score": 10}]
      # Solo comparar score -> no hay cambio en score, unchanged
      result = diff_entities(before, after, compare_fields=["score"])
      assert result["unchanged"] == 1
      assert result["modified"] == []

      # Solo comparar name -> detecta cambio
      result2 = diff_entities(before, after, compare_fields=["name"])
      assert len(result2["modified"]) == 1
      assert "name" in result2["modified"][0]["changes"]
      assert "status" not in result2["modified"][0]["changes"]


id: diff_entities_py_datascience_t6
function_id: diff_entities_py_datascience
name: test_lista_vacia_vs_lista_con_datos
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_lista_vacia_vs_lista_con_datos():
      before = []
      after = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      result = diff_entities(before, after)
      assert len(result["added"]) == 2
      assert result["removed"] == []
      assert result["unchanged"] == 0

      # Invertido
      result2 = diff_entities(after, before)
      assert result2["added"] == []
      assert len(result2["removed"]) == 2
      assert result2["unchanged"] == 0


  if __name__ == "__main__":
      test_entity_anadida()
      test_entity_eliminada()
      test_entity_modificada_con_detalle_de_campos()
      test_entities_identicas_unchanged()
      test_ignore_fields_funciona()
      test_compare_fields_filtra_correctamente()
      test_lista_vacia_vs_lista_con_datos()
      print("All tests passed.")


id: diff_relations_py_datascience_t0
function_id: diff_relations_py_datascience
name: test_relacion_anadida
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_relacion_anadida():
      before = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0}]
      after = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "C", "target_id": "D", "relation_type": "owns", "weight": 0.5},
      ]
      result = diff_relations(before, after)
      assert len(result["added"]) == 1
      assert result["added"][0]["source_id"] == "C"
      assert result["removed"] == []
      assert result["unchanged"] == 1
      assert "1 added" in result["summary"]


id: diff_relations_py_datascience_t1
function_id: diff_relations_py_datascience
name: test_relacion_eliminada
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_relacion_eliminada():
      before = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "C", "target_id": "D", "relation_type": "owns", "weight": 0.5},
      ]
      after = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0}]
      result = diff_relations(before, after)
      assert result["added"] == []
      assert len(result["removed"]) == 1
      assert result["removed"][0]["source_id"] == "C"
      assert result["unchanged"] == 1
      assert "1 removed" in result["summary"]


id: diff_relations_py_datascience_t2
function_id: diff_relations_py_datascience
name: test_relacion_con_metadata_modificada_mismo_source_target_type_distinto_weight
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_relacion_con_metadata_modificada_mismo_source_target_type_distinto_weight():
      before = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0}]
      after = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 5.0}]
      result = diff_relations(before, after)
      assert result["added"] == []
      assert result["removed"] == []
      assert len(result["modified"]) == 1
      mod = result["modified"][0]
      assert mod["key"] == "A|B|knows"
      assert "weight" in mod["changes"]
      assert mod["changes"]["weight"]["old"] == 1.0
      assert mod["changes"]["weight"]["new"] == 5.0
      assert result["unchanged"] == 0


id: diff_relations_py_datascience_t3
function_id: diff_relations_py_datascience
name: test_key_compuesta_funciona_correctamente
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_compuesta_funciona_correctamente():
      # Misma pareja A->B pero diferente tipo de relacion -> dos relaciones distintas
      before = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "A", "target_id": "B", "relation_type": "owns", "weight": 0.5},
      ]
      after = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "A", "target_id": "B", "relation_type": "trusts", "weight": 0.8},
      ]
      result = diff_relations(before, after)
      # owns eliminada, trusts añadida, knows sin cambios
      assert len(result["added"]) == 1
      assert result["added"][0]["relation_type"] == "trusts"
      assert len(result["removed"]) == 1
      assert result["removed"][0]["relation_type"] == "owns"
      assert result["unchanged"] == 1
      assert result["modified"] == []


  if __name__ == "__main__":
      test_relacion_anadida()
      test_relacion_eliminada()
      test_relacion_con_metadata_modificada_mismo_source_target_type_distinto_weight()
      test_key_compuesta_funciona_correctamente()
      print("All tests passed.")


id: extract_entities_llm_py_datascience_t0
function_id: extract_entities_llm_py_datascience
name: test_texto_con_entidades_claras_retorna_entity_candidate
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_entidades_claras_retorna_entity_candidate():
      """texto con entidades claras retorna EntityCandidate"""
      llm = make_llm({
          "entities": [
              {
                  "name": "John Smith",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {"full_name": "John Smith", "nationality": "US"},
                  "confidence": 0.95,
              },
              {
                  "name": "evil-corp.com",
                  "type_ref": "osint_domain_go_cybersecurity",
                  "attributes": {"fqdn": "evil-corp.com"},
                  "confidence": 0.88,
              },
          ]
      })

      result = extract_entities_llm(
          "John Smith, US citizen, linked to evil-corp.com.", SCHEMA, llm
      )

      assert len(result) == 2

      person = next(e for e in result if e.name == "John Smith")
      assert person.type_ref == "osint_person_go_cybersecurity"
      assert person.type_label == "Person"
      assert person.attributes["full_name"] == "John Smith"
      assert person.confidence == 0.95

      domain = next(e for e in result if e.name == "evil-corp.com")
      assert domain.type_ref == "osint_domain_go_cybersecurity"
      assert domain.type_label == "Domain"
      assert domain.attributes["fqdn"] == "evil-corp.com"
      assert domain.confidence == 0.88


id: extract_entities_llm_py_datascience_t1
function_id: extract_entities_llm_py_datascience
name: test_texto_sin_entidades_retorna_lista_vacia
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_sin_entidades_retorna_lista_vacia():
      """texto sin entidades retorna lista vacia"""
      llm = make_llm({"entities": []})

      result = extract_entities_llm(
          "The sky is blue and the grass is green.", SCHEMA, llm
      )

      assert result == []


id: extract_entities_llm_py_datascience_t2
function_id: extract_entities_llm_py_datascience
name: test_llm_retorna_json_mal_formado_retorna_lista_vacia_con_warning
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_llm_retorna_json_mal_formado_retorna_lista_vacia_con_warning():
      """llm retorna json mal formado retorna lista vacia con warning"""
      def bad_llm(messages: list[dict]) -> dict:
          raise ValueError("JSON decode error")

      with warnings.catch_warnings(record=True) as caught:
          warnings.simplefilter("always")
          result = extract_entities_llm("Some text with entities.", SCHEMA, bad_llm)

      assert result == []
      assert len(caught) == 1
      assert "error llamando al LLM" in str(caught[0].message)


id: extract_entities_llm_py_datascience_t3
function_id: extract_entities_llm_py_datascience
name: test_type_ref_invalido_en_respuesta_se_descarta_con_warning
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_type_ref_invalido_en_respuesta_se_descarta_con_warning():
      """type_ref invalido en respuesta se descarta con warning"""
      llm = make_llm({
          "entities": [
              {
                  "name": "Valid Person",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {},
                  "confidence": 0.9,
              },
              {
                  "name": "Unknown Thing",
                  "type_ref": "nonexistent_type_ref",
                  "attributes": {},
                  "confidence": 0.8,
              },
          ]
      })

      with warnings.catch_warnings(record=True) as caught:
          warnings.simplefilter("always")
          result = extract_entities_llm("Text with entities.", SCHEMA, llm)

      assert len(result) == 1
      assert result[0].name == "Valid Person"
      assert any("nonexistent_type_ref" in str(w.message) for w in caught)


id: extract_entities_llm_py_datascience_t4
function_id: extract_entities_llm_py_datascience
name: test_confidence_se_propaga_correctamente
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_confidence_se_propaga_correctamente():
      """confidence se propaga correctamente"""
      llm = make_llm({
          "entities": [
              {
                  "name": "Implied Person",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {},
                  "confidence": 0.7,
              },
              {
                  "name": "Weakly Implied Domain",
                  "type_ref": "osint_domain_go_cybersecurity",
                  "attributes": {},
                  "confidence": 0.5,
              },
              {
                  "name": "Explicit Entity",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {},
                  "confidence": 1.0,
              },
          ]
      })

      result = extract_entities_llm("Some text.", SCHEMA, llm)

      assert len(result) == 3
      confidences = {e.name: e.confidence for e in result}
      assert confidences["Implied Person"] == 0.7
      assert confidences["Weakly Implied Domain"] == 0.5
      assert confidences["Explicit Entity"] == 1.0


id: extract_entities_llm_py_datascience_t5
function_id: extract_entities_llm_py_datascience
name: test_schema_vacio_lanza_value_error
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_schema_vacio_lanza_value_error():
      """schema vacio lanza ValueError"""
      llm = make_llm({"entities": []})

      with pytest.raises(ValueError, match="entity_schema no puede estar vacio"):
          extract_entities_llm("Some text.", [], llm)


id: extract_relations_llm_py_datascience_t0
function_id: extract_relations_llm_py_datascience
name: test_texto_con_dos_entidades_relacionadas
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_dos_entidades_relacionadas():
      entities = [_make_entity("Acme Corp", "Organization"), _make_entity("John Smith", "Person")]
      relation_types = ["employs", "funds", "related_to"]

      llm_response = {
          "relations": [
              {
                  "from_name": "Acme Corp",
                  "to_name": "John Smith",
                  "relation_type": "employs",
                  "description": "Acme Corp employs John Smith as CEO",
                  "confidence": 0.9,
              }
          ]
      }

      result = extract_relations_llm(
          text="Acme Corp employs John Smith as CEO.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      assert len(result) == 1
      rel = result[0]
      assert rel.from_name == "Acme Corp"
      assert rel.to_name == "John Smith"
      assert rel.relation_type == "employs"
      assert rel.confidence == 0.9
      assert "CEO" in rel.description


id: extract_relations_llm_py_datascience_t1
function_id: extract_relations_llm_py_datascience
name: test_texto_con_entidades_pero_sin_relacion
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_texto_con_entidades_pero_sin_relacion():
      entities = [_make_entity("Alice", "Person"), _make_entity("Bob", "Person")]
      relation_types = ["funds", "employs"]

      llm_response = {"relations": []}

      result = extract_relations_llm(
          text="Alice and Bob both attended the conference.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      assert result == []


id: extract_relations_llm_py_datascience_t2
function_id: extract_relations_llm_py_datascience
name: test_menos_de_dos_entidades_retorna_lista_vacia
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_menos_de_dos_entidades_retorna_lista_vacia():
      entities = [_make_entity("Solo Corp", "Organization")]
      relation_types = ["employs", "funds"]

      # El LLM nunca deberia ser llamado, pero si lo fuera retornaria relaciones
      llm_response = {
          "relations": [
              {"from_name": "Solo Corp", "to_name": "Nobody", "relation_type": "employs", "confidence": 0.9}
          ]
      }

      result = extract_relations_llm(
          text="Solo Corp is a company.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      assert result == []


id: extract_relations_llm_py_datascience_t3
function_id: extract_relations_llm_py_datascience
name: test_llm_inventa_entidad_que_no_existe_se_descarta
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_llm_inventa_entidad_que_no_existe_se_descarta():
      entities = [_make_entity("Alice", "Person"), _make_entity("Bob", "Person")]
      relation_types = ["funds", "employs", "related_to"]

      llm_response = {
          "relations": [
              # Valida — Alice y Bob existen
              {
                  "from_name": "Alice",
                  "to_name": "Bob",
                  "relation_type": "funds",
                  "description": "Alice funds Bob",
                  "confidence": 0.8,
              },
              # Invalida — "Charlie" no esta en entities
              {
                  "from_name": "Alice",
                  "to_name": "Charlie",
                  "relation_type": "employs",
                  "description": "Alice employs Charlie",
                  "confidence": 0.7,
              },
              # Invalida — "Unknown Corp" no esta en entities
              {
                  "from_name": "Unknown Corp",
                  "to_name": "Bob",
                  "relation_type": "related_to",
                  "description": "...",
                  "confidence": 0.6,
              },
          ]
      }

      result = extract_relations_llm(
          text="Alice funds Bob. Alice also employs Charlie from Unknown Corp.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      # Solo la primera relacion es valida
      assert len(result) == 1
      assert result[0].from_name == "Alice"
      assert result[0].to_name == "Bob"
      assert result[0].relation_type == "funds"


id: hotness_score_py_datascience_t0
function_id: hotness_score_py_datascience
name: test_active_count_zero_updated_at_reciente
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_active_count_zero_updated_at_reciente():
      """active_count=0, updated_at reciente."""
      updated_at = NOW - timedelta(hours=1)
      score = hotness_score(0, updated_at, now=NOW)
      # freq = sigmoid(log1p(0)) = sigmoid(0) = 0.5
      # recency ~ 1.0 (casi nuevo)
      assert 0.45 < score < 0.55, f"Expected ~0.5, got {score}"


id: hotness_score_py_datascience_t1
function_id: hotness_score_py_datascience
name: test_active_count_alto_updated_at_reciente
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_active_count_alto_updated_at_reciente():
      """active_count=100, updated_at reciente (score alto)."""
      updated_at = NOW - timedelta(hours=1)
      score = hotness_score(100, updated_at, now=NOW)
      # freq = sigmoid(log1p(100)) = sigmoid(4.615) ~ 0.99
      # recency ~ 1.0
      assert score > 0.95, f"Expected > 0.95, got {score}"


id: hotness_score_py_datascience_t2
function_id: hotness_score_py_datascience
name: test_active_count_alto_updated_at_hace_30_dias
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_active_count_alto_updated_at_hace_30_dias():
      """active_count=100, updated_at hace 30 dias (score bajo)."""
      updated_at = NOW - timedelta(days=30)
      score = hotness_score(100, updated_at, now=NOW)
      # recency = exp(-ln2/7 * 30) = exp(-2.97) ~ 0.051
      # score ~ 0.99 * 0.051 ~ 0.05
      assert score < 0.1, f"Expected < 0.1, got {score}"


id: hotness_score_py_datascience_t3
function_id: hotness_score_py_datascience
name: test_updated_at_none_retorna_cero
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_updated_at_none_retorna_cero():
      """updated_at=None (retorna 0.0)."""
      score = hotness_score(100, None, now=NOW)
      assert score == 0.0, f"Expected 0.0, got {score}"


id: hotness_score_py_datascience_t4
function_id: hotness_score_py_datascience
name: test_now_explicito
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_now_explicito():
      """now explicito (determinista para tests)."""
      updated_at = NOW - timedelta(days=7)
      score = hotness_score(50, updated_at, now=NOW)
      # recency = exp(-ln2/7 * 7) = 0.5
      # freq = sigmoid(log1p(50)) ~ sigmoid(3.93) ~ 0.981
      expected = (1.0 / (1.0 + math.exp(-math.log1p(50)))) * 0.5
      assert abs(score - expected) < 1e-9, f"Expected {expected}, got {score}"


id: hotness_score_py_datascience_t5
function_id: hotness_score_py_datascience
name: test_half_life_days_custom
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_half_life_days_custom():
      """half_life_days custom."""
      updated_at = NOW - timedelta(days=1)
      # Con half_life=1 dia, despues de 1 dia recency = 0.5
      score = hotness_score(50, updated_at, now=NOW, half_life_days=1.0)
      freq = 1.0 / (1.0 + math.exp(-math.log1p(50)))
      expected = freq * 0.5
      assert abs(score - expected) < 1e-6, f"Expected {expected}, got {score}"


id: melt_py_datascience_t0
function_id: melt_py_datascience
name: test_melt_basico
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_melt_basico():
      """Melt basico."""
      rows = [{"region": "US", "q1": 10, "q2": 20}]
      result = melt(rows, id_vars=["region"], value_vars=["q1", "q2"])
      assert len(result) == 2
      assert result[0] == {"region": "US", "variable": "q1", "value": 10}
      assert result[1] == {"region": "US", "variable": "q2", "value": 20}


id: melt_py_datascience_t1
function_id: melt_py_datascience
name: test_melt_multiples_id_vars
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_melt_multiples_id_vars():
      """Multiples id_vars."""
      rows = [{"region": "US", "year": 2023, "q1": 10, "q2": 20}]
      result = melt(rows, id_vars=["region", "year"], value_vars=["q1", "q2"])
      assert len(result) == 2
      assert result[0]["region"] == "US"
      assert result[0]["year"] == 2023
      assert result[0]["variable"] == "q1"
      assert result[0]["value"] == 10
      assert result[1]["variable"] == "q2"
      assert result[1]["value"] == 20


id: melt_py_datascience_t2
function_id: melt_py_datascience
name: test_melt_value_vars_none_derrite_todas_las_columnas_no_id
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_melt_value_vars_none_derrite_todas_las_columnas_no_id():
      """value_vars None derrite todas las columnas no-id."""
      rows = [{"id": 1, "a": 10, "b": 20, "c": 30}]
      result = melt(rows, id_vars=["id"])
      assert len(result) == 3
      vars_found = {r["variable"] for r in result}
      assert vars_found == {"a", "b", "c"}
      values_found = {r["value"] for r in result}
      assert values_found == {10, 20, 30}


id: melt_py_datascience_t3
function_id: melt_py_datascience
name: test_melt_fila_con_campo_faltante_en_value_vars
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_melt_fila_con_campo_faltante_en_value_vars():
      """Fila con campo faltante en value_vars."""
      rows = [{"region": "US", "q1": 10}]  # q2 no existe
      result = melt(rows, id_vars=["region"], value_vars=["q1", "q2"])
      assert len(result) == 2
      q2_row = next(r for r in result if r["variable"] == "q2")
      assert q2_row["value"] is None


id: merge_graphs_py_datascience_t0
function_id: merge_graphs_py_datascience
name: test_dos_grafos_con_entity_duplicada_merge
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_dos_grafos_con_entity_duplicada_merge():
      g1 = {
          "entities": [{"id": "1", "name": "Alice Corp", "type": "company"}],
          "relations": [],
      }
      g2 = {
          "entities": [{"id": "2", "name": "Alice Corp", "type": "company", "country": "US"}],
          "relations": [],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.95)
      # Nombres identicos -> similitud 1.0 -> deben mergearse
      assert len(result["entities"]) == 1
      assert len(result["merge_log"]) == 1
      merged = result["entities"][0]
      # El merge debe preservar "country" aunque el canonical no lo tuviera
      assert merged.get("country") == "US" or merged.get("name") == "Alice Corp"


id: merge_graphs_py_datascience_t1
function_id: merge_graphs_py_datascience
name: test_entities_similares_pero_bajo_threshold_no_merge
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_entities_similares_pero_bajo_threshold_no_merge():
      g1 = {
          "entities": [{"id": "1", "name": "Alice"}],
          "relations": [],
      }
      g2 = {
          "entities": [{"id": "2", "name": "Bob"}],
          "relations": [],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.85)
      # Alice y Bob son muy distintos -> no merge
      assert len(result["entities"]) == 2
      assert len(result["merge_log"]) == 0


id: merge_graphs_py_datascience_t2
function_id: merge_graphs_py_datascience
name: test_relaciones_re_apuntadas_correctamente
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_relaciones_re_apuntadas_correctamente():
      g1 = {
          "entities": [
              {"id": "1", "name": "Alice Corp"},
              {"id": "2", "name": "Bob"},
          ],
          "relations": [
              {"source_id": "2", "target_id": "1", "relation_type": "works_at"},
          ],
      }
      g2 = {
          "entities": [
              {"id": "3", "name": "Alice Corp"},  # duplicada de id=1
          ],
          "relations": [
              {"source_id": "3", "target_id": "2", "relation_type": "knows"},
          ],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.95)
      # Entity 3 mergeada en 1 -> relacion source_id=3 debe apuntar al canonical de 1
      assert len(result["entities"]) == 2  # Alice Corp + Bob
      # Verificar que las relaciones tienen IDs canonicos (no "3")
      for rel in result["relations"]:
          assert rel["source_id"] != "3"
          assert rel["target_id"] != "3"


id: merge_graphs_py_datascience_t3
function_id: merge_graphs_py_datascience
name: test_merge_log_registra_cada_merge
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_merge_log_registra_cada_merge():
      g1 = {
          "entities": [{"id": "1", "name": "OpenAI"}],
          "relations": [],
      }
      g2 = {
          "entities": [{"id": "2", "name": "OpenAI"}],
          "relations": [],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.9)
      assert len(result["merge_log"]) == 1
      log = result["merge_log"][0]
      assert "merged" in log
      assert "into" in log
      assert "similarity" in log
      assert log["similarity"] == 1.0


id: merge_graphs_py_datascience_t4
function_id: merge_graphs_py_datascience
name: test_tres_grafos_merge_transitivo
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_tres_grafos_merge_transitivo():
      # A~B y B~C -> A, B, C deben mergearse en uno
      g1 = {"entities": [{"id": "1", "name": "Acme Corp"}], "relations": []}
      g2 = {"entities": [{"id": "2", "name": "Acme Corp"}], "relations": []}
      g3 = {"entities": [{"id": "3", "name": "Acme Corp"}], "relations": []}
      result = merge_graphs([g1, g2, g3], similarity_threshold=0.9)
      assert len(result["entities"]) == 1


id: merge_graphs_py_datascience_t5
function_id: merge_graphs_py_datascience
name: test_grafos_sin_overlap_concatenacion_simple
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_grafos_sin_overlap_concatenacion_simple():
      g1 = {
          "entities": [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}],
          "relations": [{"source_id": "1", "target_id": "2", "relation_type": "knows"}],
      }
      g2 = {
          "entities": [{"id": "3", "name": "Carol"}, {"id": "4", "name": "Dave"}],
          "relations": [{"source_id": "3", "target_id": "4", "relation_type": "knows"}],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.85)
      # Ninguna entity similar -> concatenacion directa
      assert len(result["entities"]) == 4
      assert len(result["relations"]) == 2
      assert len(result["merge_log"]) == 0


  if __name__ == "__main__":
      test_dos_grafos_con_entity_duplicada_merge()
      test_entities_similares_pero_bajo_threshold_no_merge()
      test_relaciones_re_apuntadas_correctamente()
      test_merge_log_registra_cada_merge()
      test_tres_grafos_merge_transitivo()
      test_grafos_sin_overlap_concatenacion_simple()
      print("All tests passed.")


id: pivot_py_datascience_t0
function_id: pivot_py_datascience
name: test_pivot_basico_con_sum
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pivot_basico_con_sum():
      """Pivot basico con sum."""
      rows = [
          {"region": "US", "product": "A", "sales": 10},
          {"region": "US", "product": "B", "sales": 20},
          {"region": "EU", "product": "A", "sales": 15},
      ]
      result = pivot(rows, index="region", columns="product", values="sales")
      assert len(result) == 2
      us = next(r for r in result if r["region"] == "US")
      eu = next(r for r in result if r["region"] == "EU")
      assert us["A"] == 10
      assert us["B"] == 20
      assert eu["A"] == 15
      assert eu["B"] == 0


id: pivot_py_datascience_t1
function_id: pivot_py_datascience
name: test_pivot_con_count_y_mean
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pivot_con_count_y_mean():
      """Pivot con count y mean."""
      rows = [
          {"region": "US", "product": "A", "sales": 10},
          {"region": "US", "product": "A", "sales": 20},
          {"region": "EU", "product": "A", "sales": 15},
      ]
      result_count = pivot(rows, index="region", columns="product", values="sales", agg="count")
      us_count = next(r for r in result_count if r["region"] == "US")
      assert us_count["A"] == 2

      result_mean = pivot(rows, index="region", columns="product", values="sales", agg="mean")
      us_mean = next(r for r in result_mean if r["region"] == "US")
      assert us_mean["A"] == 15.0


id: pivot_py_datascience_t2
function_id: pivot_py_datascience
name: test_pivot_valores_faltantes_rellenados_con_0
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pivot_valores_faltantes_rellenados_con_0():
      """Valores faltantes rellenados con 0."""
      rows = [
          {"region": "US", "product": "A", "sales": 5},
          {"region": "EU", "product": "B", "sales": 8},
      ]
      result = pivot(rows, index="region", columns="product", values="sales")
      us = next(r for r in result if r["region"] == "US")
      eu = next(r for r in result if r["region"] == "EU")
      assert us["B"] == 0
      assert eu["A"] == 0


id: pivot_py_datascience_t3
function_id: pivot_py_datascience
name: test_pivot_una_sola_fila
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pivot_una_sola_fila():
      """Una sola fila."""
      rows = [{"region": "US", "product": "A", "sales": 42}]
      result = pivot(rows, index="region", columns="product", values="sales")
      assert len(result) == 1
      assert result[0]["region"] == "US"
      assert result[0]["A"] == 42


id: pivot_py_datascience_t4
function_id: pivot_py_datascience
name: test_pivot_multiples_valores_por_celda_requieren_agregacion
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_pivot_multiples_valores_por_celda_requieren_agregacion():
      """Multiples valores por celda requieren agregacion."""
      rows = [
          {"region": "US", "product": "A", "sales": 10},
          {"region": "US", "product": "A", "sales": 30},
      ]
      result_sum = pivot(rows, index="region", columns="product", values="sales", agg="sum")
      assert result_sum[0]["A"] == 40

      result_min = pivot(rows, index="region", columns="product", values="sales", agg="min")
      assert result_min[0]["A"] == 10

      result_max = pivot(rows, index="region", columns="product", values="sales", agg="max")
      assert result_max[0]["A"] == 30


id: cache_to_file_py_infra_t0
function_id: cache_to_file_py_infra
name: test_set_y_get_basico
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_set_y_get_basico(store):
      store.set("hello", {"x": 42})
      assert store.get("hello") == {"x": 42}


id: cache_to_file_py_infra_t1
function_id: cache_to_file_py_infra
name: test_ttl_expirado_retorna_none
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ttl_expirado_retorna_none(store):
      store.set("temp", "val", ttl=0.05)
      time.sleep(0.1)
      assert store.get("temp") is None


id: cache_to_file_py_infra_t2
function_id: cache_to_file_py_infra
name: test_archivo_meta_con_metadata_correcta
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_meta_con_metadata_correcta(tmp_path):
      s = cache_to_file(str(tmp_path), "ns")
      s.set("mykey", "myval", ttl=60)
      ns_dir = os.path.join(str(tmp_path), "ns")
      meta_files = [f for f in os.listdir(ns_dir) if f.endswith(".meta")]
      assert len(meta_files) == 1
      with open(os.path.join(ns_dir, meta_files[0])) as f:
          meta = json.load(f)
      assert meta["original_key"] == "mykey"
      assert meta["expires_at"] is not None
      assert meta["created_at"] > 0


id: cache_to_file_py_infra_t3
function_id: cache_to_file_py_infra
name: test_clear_elimina_directorio_del_namespace
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_clear_elimina_directorio_del_namespace(tmp_path):
      s = cache_to_file(str(tmp_path), "mynamespace")
      s.set("a", 1)
      s.set("b", 2)
      removed = s.clear()
      assert removed == 2
      assert s.get("a") is None
      assert s.get("b") is None


id: cache_to_file_py_infra_t4
function_id: cache_to_file_py_infra
name: test_key_con_caracteres_especiales_hash_seguro
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_key_con_caracteres_especiales_hash_seguro(store):
      key = "https://example.com/path?q=1&r=2 <special>#hash"
      store.set(key, "safe")
      assert store.get(key) == "safe"


id: cache_to_sqlite_py_infra_t0
function_id: cache_to_sqlite_py_infra
name: test_set_y_get_basico
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_set_y_get_basico(store):
      store.set("foo", {"x": 1})
      assert store.get("foo") == {"x": 1}


id: cache_to_sqlite_py_infra_t1
function_id: cache_to_sqlite_py_infra
name: test_ttl_expirado_retorna_none
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ttl_expirado_retorna_none(store):
      store.set("expiring", "hello", ttl=0.05)
      time.sleep(0.1)
      assert store.get("expiring") is None


id: cache_to_sqlite_py_infra_t2
function_id: cache_to_sqlite_py_infra
name: test_ttl_cero_nunca_expira
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_ttl_cero_nunca_expira(store):
      store.set("forever", 42, ttl=0)
      time.sleep(0.05)
      assert store.get("forever") == 42


id: cache_to_sqlite_py_infra_t3
function_id: cache_to_sqlite_py_infra
name: test_get_or_set_factory_solo_se_llama_en_miss
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_get_or_set_factory_solo_se_llama_en_miss(store):
      calls = []

      def factory():
          calls.append(1)
          return "computed"

      result1 = store.get_or_set("key", factory, ttl=10)
      result2 = store.get_or_set("key", factory, ttl=10)
      assert result1 == "computed"
      assert result2 == "computed"
      assert len(calls) == 1


id: cache_to_sqlite_py_infra_t4
function_id: cache_to_sqlite_py_infra
name: test_namespaces_independientes
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_namespaces_independientes(store_and_other):
      s1, s2 = store_and_other
      s1.set("k", "from_ns1")
      assert s2.get("k") is None
      s2.set("k", "from_ns2")
      assert s1.get("k") == "from_ns1"
      assert s2.get("k") == "from_ns2"


id: cache_to_sqlite_py_infra_t5
function_id: cache_to_sqlite_py_infra
name: test_clear_elimina_solo_el_namespace
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_clear_elimina_solo_el_namespace(store_and_other):
      s1, s2 = store_and_other
      s1.set("a", 1)
      s2.set("b", 2)
      removed = s1.clear()
      assert removed == 1
      assert s1.get("a") is None
      assert s2.get("b") == 2


id: cache_to_sqlite_py_infra_t6
function_id: cache_to_sqlite_py_infra
name: test_stats_contadores_correctos
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_stats_contadores_correctos(store):
      store.set("x", 10)
      store.get("x")   # hit
      store.get("x")   # hit
      store.get("z")   # miss
      s = store.stats()
      assert s["hits"] == 2
      assert s["misses"] == 1
      assert s["size"] == 1


id: cache_to_sqlite_py_infra_t7
function_id: cache_to_sqlite_py_infra
name: test_concurrencia
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_concurrencia(tmp_path):
      db = str(tmp_path / "concurrent.db")
      s = cache_to_sqlite(db, "parallel")
      errors = []

      def worker(i):
          try:
              s.set(f"key_{i}", i)
              val = s.get(f"key_{i}")
              assert val == i
          except Exception as e:
              errors.append(e)

      threads = [threading.Thread(target=worker, args=(i,)) for i in range(20)]
      for t in threads:
          t.start()
      for t in threads:
          t.join()

      assert errors == [], f"Errors in threads: {errors}"


id: get_logger_py_infra_t0
function_id: get_logger_py_infra
name: test_logger_tiene_dos_handlers
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_logger_tiene_dos_handlers():
      with tempfile.TemporaryDirectory() as log_dir:
          logger = setup_logger(name="test_two_handlers", log_dir=log_dir)
          assert len(logger.handlers) == 2
          # limpiar para no contaminar otros tests
          logger.handlers.clear()


id: get_logger_py_infra_t1
function_id: get_logger_py_infra
name: test_segundo_call_no_duplica_handlers
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_segundo_call_no_duplica_handlers():
      with tempfile.TemporaryDirectory() as log_dir:
          logger1 = setup_logger(name="test_idempotent", log_dir=log_dir)
          handler_count_after_first = len(logger1.handlers)
          logger2 = setup_logger(name="test_idempotent", log_dir=log_dir)
          assert logger1 is logger2
          assert len(logger2.handlers) == handler_count_after_first
          logger1.handlers.clear()


id: get_logger_py_infra_t2
function_id: get_logger_py_infra
name: test_archivo_se_crea_en_log_dir
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_se_crea_en_log_dir():
      with tempfile.TemporaryDirectory() as log_dir:
          logger = setup_logger(name="test_file_created", log_dir=log_dir)
          log_files = [f for f in os.listdir(log_dir) if f.endswith(".log")]
          assert len(log_files) == 1
          logger.handlers.clear()


id: get_logger_py_infra_t3
function_id: get_logger_py_infra
name: test_get_logger_retorna_logger_configurado
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_get_logger_retorna_logger_configurado():
      with tempfile.TemporaryDirectory() as log_dir:
          # Primero configurar para que get_logger encuentre handlers
          setup_logger(name="test_get_logger", log_dir=log_dir)
          logger = get_logger(name="test_get_logger")
          assert len(logger.handlers) == 2
          logger.handlers.clear()


id: get_logger_py_infra_t4
function_id: get_logger_py_infra
name: test_logger_level_es_debug
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_logger_level_es_debug():
      with tempfile.TemporaryDirectory() as log_dir:
          logger = setup_logger(name="test_level_debug", log_dir=log_dir, level=logging.DEBUG)
          assert logger.level == logging.DEBUG
          logger.handlers.clear()


id: normalize_zip_filenames_py_infra_t0
function_id: normalize_zip_filenames_py_infra
name: test_zip_normal
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_zip_normal():
      """ZIP normal extrae correctamente dentro del destino."""
      zip_path = _make_zip({"hello.txt": b"hello world", "subdir/file.py": b"# code"})
      try:
          with tempfile.TemporaryDirectory() as dest:
              safe_extract_zip(zip_path, dest)
              assert os.path.isfile(os.path.join(dest, "hello.txt"))
              assert os.path.isfile(os.path.join(dest, "subdir", "file.py"))
              with open(os.path.join(dest, "hello.txt"), "rb") as f:
                  assert f.read() == b"hello world"
      finally:
          os.unlink(zip_path)


id: normalize_zip_filenames_py_infra_t1
function_id: normalize_zip_filenames_py_infra
name: test_zip_con_path_traversal
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_zip_con_path_traversal():
      """ZIP con path traversal lanza ValueError."""
      zip_path = _make_zip_with_traversal("../../etc/passwd")
      try:
          with tempfile.TemporaryDirectory() as dest:
              raised = False
              try:
                  safe_extract_zip(zip_path, dest)
              except ValueError as e:
                  raised = True
                  assert "Zip Slip" in str(e)
              assert raised, "Expected ValueError for path traversal"
      finally:
          os.unlink(zip_path)


id: normalize_zip_filenames_py_infra_t2
function_id: normalize_zip_filenames_py_infra
name: test_zip_con_paths_absolutos
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_zip_con_paths_absolutos():
      """ZIP con paths absolutos lanza ValueError."""
      zip_path = _make_zip_with_traversal("/etc/passwd")
      try:
          with tempfile.TemporaryDirectory() as dest:
              raised = False
              try:
                  safe_extract_zip(zip_path, dest)
              except ValueError as e:
                  raised = True
                  assert "Zip Slip" in str(e)
              assert raised, "Expected ValueError for absolute path"
      finally:
          os.unlink(zip_path)


id: normalize_zip_filenames_py_infra_t3
function_id: normalize_zip_filenames_py_infra
name: test_normalize_utf8_correctos_no_cambian
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_normalize_utf8_correctos_no_cambian():
      """ZIP con nombres UTF-8 correctos (flag 0x800) no se modifican."""
      buf = io.BytesIO()
      with zipfile.ZipFile(buf, "w") as zipf:
          info = zipfile.ZipInfo("archivo_normal.txt")
          info.flag_bits |= 0x800  # marcar como UTF-8
          zipf.writestr(info, b"content")
      buf.seek(0)
      with zipfile.ZipFile(buf, "r") as zipf:
          original_name = zipf.infolist()[0].filename
          normalize_zip_filenames(zipf)
          assert zipf.infolist()[0].filename == original_name


id: normalize_zip_filenames_py_infra_t4
function_id: normalize_zip_filenames_py_infra
name: test_normalize_cjk_mojibake_repara
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_normalize_cjk_mojibake_repara():
      """ZIP con nombres CJK en mojibake (UTF-8 bytes leidos como CP437) se reparan.

      Simula un ZIP donde los bytes del filename son UTF-8 valido de un nombre CJK,
      pero el flag 0x800 no esta seteado, asi que zipfile los decodifica como CP437
      produciendo mojibake. normalize_zip_filenames debe detectarlo y repararlo.
      """
      cjk_name = "\u6587\u4ef6.txt"  # 文件.txt

      # Construir ZIP con bytes UTF-8 crudos en el campo filename, sin flag 0x800.
      # Python no permite esto via ZipInfo (fuerza 0x800 para non-ASCII), por eso
      # construimos el ZIP manualmente con _make_zip_with_raw_filename.
      utf8_bytes = cjk_name.encode("utf-8")
      zip_bytes = _make_zip_with_raw_filename(utf8_bytes, b"cjk content")

      with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zipf:
          member = zipf.infolist()[0]
          # Sin el flag, zipfile lee los bytes como CP437: debe ser mojibake
          assert not (member.flag_bits & 0x800), "Flag 0x800 no deberia estar seteado"
          assert member.filename != cjk_name, "El nombre aun no debe estar reparado"

          normalize_zip_filenames(zipf)
          repaired = zipf.infolist()[0].filename
          has_cjk = any(
              "\u4e00" <= c <= "\u9fff" or "\u3400" <= c <= "\u4dbf" for c in repaired
          )
          assert has_cjk, f"Esperaba CJK en nombre reparado, got: {repaired!r}"


  if __name__ == "__main__":
      test_zip_normal()
      print("PASS: ZIP normal extrae correctamente dentro del destino")

      test_zip_con_path_traversal()
      print("PASS: ZIP con path traversal lanza ValueError")

      test_zip_con_paths_absolutos()
      print("PASS: ZIP con paths absolutos lanza ValueError")

      test_normalize_utf8_correctos_no_cambian()
      print("PASS: ZIP con nombres UTF-8 correctos no se modifican")

      test_normalize_cjk_mojibake_repara()
      print("PASS: ZIP con nombres CJK mojibake se reparan")

      print("\nAll tests passed.")


id: read_file_with_encoding_py_infra_t0
function_id: read_file_with_encoding_py_infra
name: test_archivo_utf8
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_utf8():
      texto = "Hola mundo con acentos: áéíóú"
      path = _write_bytes(texto.encode("utf-8"))
      try:
          result = read_file_with_encoding(path)
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t1
function_id: read_file_with_encoding_py_infra
name: test_archivo_utf8_con_bom
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_utf8_con_bom():
      texto = "Contenido con BOM"
      path = _write_bytes(texto.encode("utf-8-sig"))
      try:
          # Usando utf-8-sig explicitamente para que el BOM sea eliminado
          result = read_file_with_encoding(path, encodings=["utf-8-sig"])
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t2
function_id: read_file_with_encoding_py_infra
name: test_archivo_latin1
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_latin1():
      texto = "Texto en Latin-1: café"
      path = _write_bytes(texto.encode("latin-1"))
      try:
          result = read_file_with_encoding(path)
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t3
function_id: read_file_with_encoding_py_infra
name: test_archivo_binario_falla
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_binario_falla():
      # Bytes que no son validos en ningun encoding de texto comun
      path = _write_bytes(bytes([0x80, 0x81, 0x82, 0x83, 0xFF, 0xFE, 0x00, 0x01]))
      try:
          with pytest.raises(ValueError, match="Unable to decode file"):
              # Forzar solo encodings estrictos para que falle con binario puro
              read_file_with_encoding(path, encodings=["utf-8", "utf-8-sig"])
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t4
function_id: read_file_with_encoding_py_infra
name: test_encodings_personalizados
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_encodings_personalizados():
      texto = "Windows text: Ñoño"
      path = _write_bytes(texto.encode("cp1252"))
      try:
          result = read_file_with_encoding(path, encodings=["cp1252"])
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t5
function_id: read_file_with_encoding_py_infra
name: test_archivo_no_existe
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_no_existe():
      with pytest.raises(FileNotFoundError):
          read_file_with_encoding("/tmp/archivo_que_no_existe_12345.txt")


id: scan_directory_py_infra_t0
function_id: scan_directory_py_infra
name: test_directorio_con_mezcla_de_archivos
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_directorio_con_mezcla_de_archivos():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "report.pdf": "pdf content",
              "notes.md": "# Notes",
              "image.png": "png content",
              "data.csv": "a,b,c",
          })

          result = scan_directory(str(root), supported_extensions={".pdf", ".md"})

          rel_paths = [f.rel_path for f in result.processable]
          assert "notes.md" in rel_paths, f"notes.md no en processable: {rel_paths}"
          assert "report.pdf" in rel_paths, f"report.pdf no en processable: {rel_paths}"

          unsup_paths = [f.rel_path for f in result.unsupported]
          assert "image.png" in unsup_paths, f"image.png no en unsupported: {unsup_paths}"
          assert "data.csv" in unsup_paths, f"data.csv no en unsupported: {unsup_paths}"

          assert all(f.classification == "processable" for f in result.processable)
          assert all(f.classification == "unsupported" for f in result.unsupported)


  # ---------------------------------------------------------------------------
  # Test: directorio con dot files
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t1
function_id: scan_directory_py_infra
name: test_directorio_con_dot_files
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_directorio_con_dot_files():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "visible.txt": "content",
              ".hidden": "hidden content",
              ".env": "SECRET=x",
          })

          result = scan_directory(str(root))

          all_paths = [f.rel_path for f in result.processable + result.unsupported]
          assert ".hidden" not in all_paths, f".hidden no deberia aparecer: {all_paths}"
          assert ".env" not in all_paths, f".env no deberia aparecer: {all_paths}"
          assert "visible.txt" in all_paths, f"visible.txt deberia aparecer: {all_paths}"

          skipped_paths = " ".join(result.skipped)
          assert ".hidden" in skipped_paths or ".env" in skipped_paths


  # ---------------------------------------------------------------------------
  # Test: directorio con subdirs ignorados
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t2
function_id: scan_directory_py_infra
name: test_directorio_con_subdirs_ignorados
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_directorio_con_subdirs_ignorados():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "main.py": "print('hello')",
              "__pycache__/module.pyc": "bytecode",
              "node_modules/lib/index.js": "// js",
              ".git/config": "[core]",
              "src/utils.py": "def f(): pass",
          })

          result = scan_directory(str(root))

          all_rels = [f.rel_path for f in result.processable + result.unsupported]

          # Archivos dentro de dirs ignorados no deben aparecer
          assert not any("__pycache__" in r for r in all_rels), \
              f"__pycache__ no deberia estar en resultados: {all_rels}"
          assert not any("node_modules" in r for r in all_rels), \
              f"node_modules no deberia estar en resultados: {all_rels}"
          assert not any(".git" in r for r in all_rels), \
              f".git no deberia estar en resultados: {all_rels}"

          # Archivos fuera de dirs ignorados si deben aparecer
          assert "main.py" in all_rels, f"main.py deberia estar: {all_rels}"
          assert "src/utils.py" in all_rels, f"src/utils.py deberia estar: {all_rels}"


  # ---------------------------------------------------------------------------
  # Test: filtros include/exclude
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t3
function_id: scan_directory_py_infra
name: test_filtros_include_exclude
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_filtros_include_exclude():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "report.pdf": "content",
              "notes.md": "notes",
              "image.png": "image",
              "drafts/draft.md": "draft",
              "temp.tmp": "tmp",
          })

          # Solo incluir .pdf y .md
          result = scan_directory(str(root), include="*.pdf,*.md")
          all_rels = [f.rel_path for f in result.processable + result.unsupported]
          assert "image.png" not in all_rels, f"image.png no deberia incluirse: {all_rels}"
          assert "temp.tmp" not in all_rels, f"temp.tmp no deberia incluirse: {all_rels}"
          assert "report.pdf" in all_rels
          assert "notes.md" in all_rels

          # Excluir path prefix drafts/ y extension .tmp
          result2 = scan_directory(str(root), exclude="drafts/,*.tmp")
          all_rels2 = [f.rel_path for f in result2.processable + result2.unsupported]
          assert "drafts/draft.md" not in all_rels2, \
              f"drafts/draft.md no deberia incluirse: {all_rels2}"
          assert "temp.tmp" not in all_rels2, f"temp.tmp no deberia incluirse: {all_rels2}"
          assert "report.pdf" in all_rels2


  # ---------------------------------------------------------------------------
  # Test: modo strict
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t4
function_id: scan_directory_py_infra
name: test_modo_strict
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_modo_strict():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "doc.pdf": "content",
              "image.png": "image",
          })

          # strict=False no lanza error aunque haya unsupported
          result = scan_directory(str(root), supported_extensions={".pdf"}, strict=False)
          assert len(result.unsupported) == 1

          # strict=True lanza ValueError
          raised = False
          try:
              scan_directory(str(root), supported_extensions={".pdf"}, strict=True)
          except ValueError:
              raised = True
          assert raised, "strict=True deberia lanzar ValueError cuando hay unsupported"


  if __name__ == "__main__":
      test_directorio_con_mezcla_de_archivos()
      print("PASS: directorio con mezcla de archivos")

      test_directorio_con_dot_files()
      print("PASS: directorio con dot files")

      test_directorio_con_subdirs_ignorados()
      print("PASS: directorio con subdirs ignorados")

      test_filtros_include_exclude()
      print("PASS: filtros include/exclude")

      test_modo_strict()
      print("PASS: modo strict")

      print("\nAll tests passed.")


id: extraction_pipeline_py_pipelines_t0
function_id: extraction_pipeline_py_pipelines
name: test_documento_con_entidades_y_relaciones
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_documento_con_entidades_y_relaciones():
      """documento con entidades y relaciones retorna ExtractionResult completo"""
      text = (
          "John Smith, a US national, operates the domain evil-corp.com. "
          "He was identified as the main administrator of the infrastructure."
      )
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_with_entities,
              chunk_size=500,
              chunk_overlap=50,
              confidence_threshold=0.5,
              dedup_threshold=0.85,
          )
          assert result is not None
          assert len(result.entities) >= 1
          assert result.stats.total_chunks >= 1
          assert result.stats.total_chars > 0
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t1
function_id: extraction_pipeline_py_pipelines
name: test_documento_vacio
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_documento_vacio():
      """documento vacio retorna ExtractionResult con listas vacias"""
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write("")
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_empty,
          )
          assert result is not None
          assert result.entities == []
          assert result.relations == []
          assert result.stats.total_chunks == 0
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t2
function_id: extraction_pipeline_py_pipelines
name: test_documento_sin_entidades_detectables
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_documento_sin_entidades_detectables():
      """documento sin entidades detectables retorna listas vacias"""
      text = "The weather is nice today. The sun shines brightly over the mountains."
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_empty,
              confidence_threshold=0.5,
          )
          assert result is not None
          assert result.entities == []
          assert result.relations == []
          assert result.stats.raw_entities_count == 0
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t3
function_id: extraction_pipeline_py_pipelines
name: test_archivo_no_encontrado_lanza_filenotfounderror
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_archivo_no_encontrado_lanza_filenotfounderror():
      """archivo no encontrado lanza FileNotFoundError"""
      import pytest
      with pytest.raises(FileNotFoundError):
          extraction_pipeline(
              file_path="/tmp/no_existe_para_test_extraccion_pipeline.txt",
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_empty,
          )


id: extraction_pipeline_py_pipelines_t4
function_id: extraction_pipeline_py_pipelines
name: test_entity_presets_vacio_lanza_valueerror
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_entity_presets_vacio_lanza_valueerror():
      """entity presets vacio lanza ValueError"""
      import pytest
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write("some text")
          tmp_path = f.name

      try:
          with pytest.raises(ValueError):
              extraction_pipeline(
                  file_path=tmp_path,
                  entity_presets=[],
                  relation_types=RELATION_TYPES,
                  llm_chat_json=_llm_empty,
              )
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t5
function_id: extraction_pipeline_py_pipelines
name: test_progress_callback_se_invoca
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_progress_callback_se_invoca():
      """progress callback se invoca durante la ejecucion"""
      calls: list[tuple[str, float]] = []

      def _on_progress(msg: str, pct: float) -> None:
          calls.append((msg, pct))

      text = "John Smith operates evil-corp.com."
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_with_entities,
              on_progress=_on_progress,
          )
          assert len(calls) > 0
          messages = [c[0] for c in calls]
          assert any("Extracting" in m or "Done" in m or "Dedup" in m for m in messages)
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t6
function_id: extraction_pipeline_py_pipelines
name: test_stats_se_rellenan_correctamente
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-05T16:44:26Z
updated_at: 2026-04-05T16:44:26Z
code:
  def test_stats_se_rellenan_correctamente():
      """stats se rellenan correctamente con conteos y tiempo"""
      text = "John Smith, a US national, operates the domain evil-corp.com."
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_with_entities,
          )
          assert result.stats.total_chars > 0
          assert result.stats.total_chunks >= 1
          assert result.stats.processing_time_seconds >= 0.0
      finally:
          os.unlink(tmp_path)


