# 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: registry_dashboard_cpp_tui
name: registry_dashboard
lang: cpp
domain: tui
tags: ["dashboard","imgui","visualization","registry"]
uses_types: []
framework: imgui
entry_point: main.cpp
dir_path: apps/registry_dashboard
created_at: 2026-04-07T22:21:40Z
updated_at: 2026-04-07T22:42:49Z
content_hash: dd7a67cfb8cce2e8b37ce22f375c86cf227ca8534f36cd820a49fb26b7327141
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/registry_dashboard
description:
  Dashboard ImGui para visualizar el estado del fn_registry: KPIs, charts, tablas, desglose por lenguaje/dominio/pureza

uses_functions:
  ["kpi_card_cpp_viz","bar_chart_cpp_viz","pie_chart_cpp_viz","table_view_cpp_viz","sparkline_cpp_viz","dashboard_panel_cpp_core","dashboard_grid_cpp_core","fps_overlay_cpp_core","fullscreen_window_cpp_core"]

documentation:
  ## Arquitectura

  Dashboard C++ que lee `registry.db` directamente via SQLite C API y visualiza el estado completo del fn_registry.

  **Data layer** (`data.cpp`): Carga todas las stats, desgloses y listas en un solo paso desde SQLite. Soporte para reload en caliente.

  **Views** (`views.cpp`): Compone funciones del registry C++ para renderizar:
  - 6 KPI cards: total functions, types, apps, analysis, tested%, pure%
  - Bar charts: funciones por lenguaje, por dominio, actividad ultimos 30 dias
  - Pie charts: pureza (pure/impure), kind (function/pipeline/component), tested/untested
  - Tablas: ultimas 20 funciones, apps, analysis, tipos

  **Entrada**: Paths a `registry.db` como argumentos con fallback encadenado.

  ## Build

  ```bash
  # Linux
  cd cpp && cmake -B build/linux -S . && cmake --build build/linux --target registry_dashboard -j$(nproc)

  # Windows (cross-compile)
  cd cpp && cmake -B build/windows -S . -DCMAKE_TOOLCHAIN_FILE=toolchains/mingw-w64.cmake && cmake --build build/windows --target registry_dashboard -j$(nproc)
  ```

  ## Ejecucion

  ```bash
  # Linux
  ./cpp/build/linux/apps/registry_dashboard/registry_dashboard /home/lucas/fn_registry/registry.db

  # Windows (PowerShell) — intenta WSL mount, luego local
  .\registry_dashboard.ps1

  # Otra maquina — copiar .exe + .db al mismo dir
  registry_dashboard.exe .\registry.db
  ```

  ## Roadmap

  - [ ] Integrar Go runtime (CGo) para ejecutar comandos `fn` desde el GUI
  - [ ] Filtros interactivos por lenguaje/dominio en sidebar
  - [ ] Busqueda FTS5 integrada
  - [ ] Detalles de funcion al hacer click en tabla

notes:
  SQLite compilado estaticamente en Windows via amalgamation vendoreada. En Linux usa libsqlite3 del sistema.


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`


id: agents_and_robots_go_agents
name: agents_and_robots
lang: go
domain: agents
tags: ["agents","matrix","bots","llm","element","e2ee","tools","service"]
uses_functions: []
uses_types: []
framework: mautrix
entry_point: cmd/launcher/main.go
dir_path: projects/element_agents/apps/agents_and_robots
created_at: 2026-04-12T18:17:19Z
updated_at: 2026-04-12T18:17:19Z
content_hash: 39e82e8341564fda8c4e07f376242350b7ac42177370be54417e7a2ad8cf2557
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/agents_and_robots.git
project_id: element_agents
description:
  Plataforma Go de bots autonomos Matrix con arquitectura pure core / impure shell. Launcher, agentctl, dashboard TUI y register.

documentation:
  ## Deploy

  - **VPS:** organic-machine.com (SSH alias: organic-machine.com)
  - **Remote path:** /home/ubuntu/CodeProyects/agents_and_robots
  - **Build:** `go build -tags goolm -ldflags="-s -w" -o bin/launcher ./cmd/launcher`
  - **Run:** `./bin/launcher --log-level info`
  - **Binarios:** launcher, agentctl, register, dashboard, verify (en bin/)
  - **Proceso:** launcher corre como proceso directo (no systemd)
  - **Config:** agents/*/config.yaml + .env con tokens Matrix, API keys LLM, pickle keys E2EE

notes:
  4 binarios principales:
  - **launcher** — Inicia agentes como goroutines, descubre configs, sync Matrix
  - **agentctl** — CLI: list, start, stop, remove agentes
  - **register** — Registra usuarios bot en Synapse via admin API
  - **dashboard** — TUI interactiva (bubbletea) para gestion de agentes

  Agentes desplegados: assistant-bot, asistente-2, meteorologo, wikipedia-bot, exchange-bot, reminder-bot, test-bot, father-bot (orchestrator).

  Dependencias clave: mautrix-go, go-openai, mcp-go, cobra, bubbletea, modernc/sqlite, zerolog.
  No requiere CGO (usa pure Go SQLite via modernc).


id: element_matrix_chat_bash_infra
name: element_matrix_chat
lang: bash
domain: infra
tags: ["matrix","element","synapse","docker","selfhosted","chat","livekit","service"]
uses_functions: []
uses_types: []
framework: docker-compose
entry_point: docker-compose.yml
dir_path: projects/element_agents/apps/element_matrix_chat
created_at: 2026-04-12T18:17:19Z
updated_at: 2026-04-12T18:17:19Z
content_hash: dd799b22b5a743628a1ddb6320938522049afdc127388b0458777042095e5b51
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/egutierrez/element_matrix_chat.git
project_id: element_agents
description:
  Stack completo Matrix autohosteado con Docker Compose: Synapse, Element Web, MAS, LiveKit, PostgreSQL, Synapse Admin.

documentation:
  ## Deploy

  - **VPS:** organic-machine.com (SSH alias: organic-machine.com)
  - **Remote path:** /home/ubuntu/CodeProyects/element_matrix_chat
  - **Deploy:** `docker-compose up -d` (+ `docker-compose -f docker-compose.livekit.yml up -d`)
  - **Setup:** `./scripts/setup.sh`
  - **Puertos:** Element 8081, Synapse Admin 8082, MAS 8083, LiveKit 7880-7882
  - **Config:** .env + configs/homeserver.yaml.template + element-config.json + mas/config.yaml

notes:
  Stack de 10+ containers Docker:
  - **Synapse** — Homeserver Matrix (Python)
  - **Element Web** — Cliente web React
  - **MAS** — Matrix Authentication Service (OIDC/OAuth)
  - **LiveKit** — SFU para videollamadas (Element Call)
  - **PostgreSQL x2** — BDs para Synapse y MAS
  - **Synapse Admin** — Panel de administracion
  - **Nginx** — Reverse proxy para .well-known
  - **LiveKit JWT** — Generador de tokens para RTC

  Red Docker: matrix_net (10.10.10.0/24), gestionada externamente.
  Coolify maneja el proxy principal (puertos 80/443).


id: dag_engine_go_infra
name: dag_engine
lang: go
domain: infra
tags: ["service","dag","workflow","scheduler","web","cron"]
framework: net/http + vite + react
entry_point: main.go
dir_path: apps/dag_engine
created_at: 2026-04-12T11:03:56Z
updated_at: 2026-04-12T11:03:56Z
content_hash: 31758115e7d9239e0da2f31aa80ed24f33545e09b8fd6193fede1952b50b06ac
description:
  Motor de ejecucion de DAGs con CLI y interfaz web. Reemplaza Dagu con implementacion propia compatible con el formato YAML existente. Almacena historial de ejecuciones en SQLite.

uses_functions:
  ["dag_parse_go_core","dag_validate_go_core","dag_topo_sort_go_core","dag_resolve_env_go_core","parse_cron_expr_go_core","next_cron_time_go_core","cron_ticker_go_infra","cron_match_go_core","process_spawn_go_infra","process_wait_go_infra","process_kill_go_infra"]

uses_types:
  ["dag_definition_go_core","dag_step_go_core","dag_validation_result_go_core","cron_schedule_go_core","process_handle_go_infra","process_result_go_infra","DagRun_go_infra","DagStepResult_go_infra"]

documentation:
  ## Arquitectura

  CLI + servidor web en un unico binario:

  ```
  dag-engine run <path.yaml>      # ejecuta un DAG desde terminal
  dag-engine list [dir]            # lista DAGs con schedule y estado
  dag-engine status [dag_name]     # historial de ejecuciones
  dag-engine validate <path.yaml>  # valida sin ejecutar
  dag-engine server                # arranca HTTP + frontend web
  ```

  ### Backend (Go)

  - `net/http` con `ServeMux` (Go 1.22+ pattern routing)
  - SQLite via `go-sqlite3` para historial de runs
  - Executor: parse -> validate -> topo_sort -> spawn/wait por nivel -> store
  - Scheduler: cron_ticker por cada DAG con schedule

  ### Frontend (Vite + React + Mantine)

  - DagList: tabla de DAGs con schedule, tags, ultimo status
  - DagDetail: metadata + "Run Now" + historial
  - RunDetail: timeline de steps con stdout/stderr expandible

  ### Storage

  SQLite `dag_engine.db`:
  - `dag_runs`: id, dag_name, status, trigger, started_at, finished_at, error
  - `dag_step_results`: id, run_id, step_name, status, exit_code, stdout, stderr, duration_ms

  ### Build

  ```bash
  cd frontend && pnpm install && pnpm build
  cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
  ```

  ### Uso

  ```bash
  # CLI
  ./dag-engine run ~/dagu/dags/example.yaml
  ./dag-engine list ~/dagu/dags/

  # Servidor web
  ./dag-engine server --port 8090 --dags-dir ~/dagu/dags/ --scheduler
  # Browser: http://localhost:8090
  ```

notes:
  Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
  Puerto por defecto 8090 (mismo que Dagu).


id: sqlite_api_go_infra
name: sqlite_api
lang: go
domain: infra
tags: ["service","api","sqlite","http","registry","fts5"]
uses_functions: []
uses_types: []
framework: net/http
entry_point: main.go
dir_path: projects/fn_monitoring/apps/sqlite_api
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
content_hash: 301fd71f8ef62accb5797fa24be76aa4791fb9c28c333e7211990df0372ec64c
project_id: fn_monitoring
description:
  API REST HTTP read-only sobre registry.db y operations.db de cada app. Permite consultas SQL (solo SELECT/PRAGMA), busqueda FTS5, exploracion de tablas y schema. Bind por defecto a localhost:8484.

documentation:
  ## Uso

  ```bash
  # Arrancar (default: 127.0.0.1:8484)
  cd apps/sqlite_api && go run -tags fts5 .

  # Bind personalizado
  go run -tags fts5 . --bind 0.0.0.0:8484
  ```

  ## Endpoints

  | Metodo | Path | Descripcion |
  |--------|------|-------------|
  | GET | `/health` | Health check |
  | GET | `/api/databases` | Lista DBs disponibles (registry + ops:*) |
  | GET | `/api/databases/:db/tables` | Tablas y vistas de una DB |
  | GET | `/api/databases/:db/schema` | Schema SQL completo |
  | POST | `/api/databases/:db/query` | Ejecuta query SQL read-only |
  | GET | `/api/databases/:db/fts?q=...&table=...` | Busqueda FTS5 directa |

  ## Seguridad

  - Solo queries SELECT, PRAGMA, WITH y EXPLAIN
  - SQLite abierto con `?mode=ro` (read-only a nivel driver)
  - Timeout de 5 segundos por query
  - Bind a localhost por defecto
  - CORS habilitado para acceso desde frontends

  ## Bases de datos

  - `registry` — registry.db de la raiz
  - `ops:{app}` — operations.db de apps/{app}/ y projects/*/apps/{app}/

  Auto-descubre operations.db al arrancar escaneando apps/ y projects/*/apps/.

  ## Health check

  ```bash
  curl http://localhost:8484/health
  # {"status":"ok"}
  ```

  ## Puerto

  8484 (no colisiona con Metabase 3000, Jupyter 8888, deploy_server 9090).


# 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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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: cron_match_go_core_t0
function_id: cron_match_go_core
name: TestCronMatch
file_path: functions/core/cron_match_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestCronMatch(t *testing.T) {
  	t.Run("9:00 AM coincide con 0 9 * * *", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 * * *")
  		if err != nil {
  			t.Fatalf("ParseCronExpr: %v", err)
  		}
  		ts := time.Date(2026, 4, 11, 9, 0, 0, 0, time.UTC)
  		if !CronMatch(sched, ts) {
  			t.Errorf("expected match for 9:00 AM with '0 9 * * *'")
  		}
  	})

  	t.Run("9:15 AM NO coincide con 0 9 * * *", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 * * *")
  		if err != nil {
  			t.Fatalf("ParseCronExpr: %v", err)
  		}
  		ts := time.Date(2026, 4, 11, 9, 15, 0, 0, time.UTC)
  		if CronMatch(sched, ts) {
  			t.Errorf("expected no match for 9:15 AM with '0 9 * * *'")
  		}
  	})

  	t.Run("lunes a las 9 coincide con 0 9 * * 1", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 * * 1")
  		if err != nil {
  			t.Fatalf("ParseCronExpr: %v", err)
  		}
  		// 2026-04-13 is a Monday
  		ts := time.Date(2026, 4, 13, 9, 0, 0, 0, time.UTC)
  		if !CronMatch(sched, ts) {
  			t.Errorf("expected match for Monday 9:00 with '0 9 * * 1'")
  		}
  	})

  	t.Run("domingo a las 9 NO coincide con 0 9 * * 1", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 9 * * 1")
  		if err != nil {
  			t.Fatalf("ParseCronExpr: %v", err)
  		}
  		// 2026-04-12 is a Sunday (weekday 0)
  		ts := time.Date(2026, 4, 12, 9, 0, 0, 0, time.UTC)
  		if CronMatch(sched, ts) {
  			t.Errorf("expected no match for Sunday 9:00 with '0 9 * * 1'")
  		}
  	})

  	t.Run("wildcard * coincide con cualquier valor", func(t *testing.T) {
  		sched, err := ParseCronExpr("*/15 * * * *")
  		if err != nil {
  			t.Fatalf("ParseCronExpr: %v", err)
  		}
  		// 9:30 should match */15 (0,15,30,45)
  		ts := time.Date(2026, 4, 11, 9, 30, 0, 0, time.UTC)
  		if !CronMatch(sched, ts) {
  			t.Errorf("expected match for 9:30 with '*/15 * * * *'")
  		}
  		// 9:07 should NOT match */15
  		ts2 := time.Date(2026, 4, 11, 9, 7, 0, 0, time.UTC)
  		if CronMatch(sched, ts2) {
  			t.Errorf("expected no match for 9:07 with '*/15 * * * *'")
  		}
  	})

  	t.Run("specific month", func(t *testing.T) {
  		sched, err := ParseCronExpr("0 0 1 4 *")
  		if err != nil {
  			t.Fatalf("ParseCronExpr: %v", err)
  		}
  		// April 1st matches
  		ts := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
  		if !CronMatch(sched, ts) {
  			t.Errorf("expected match for April 1 with '0 0 1 4 *'")
  		}
  		// March 1st does NOT match
  		ts2 := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
  		if CronMatch(sched, ts2) {
  			t.Errorf("expected no match for March 1 with '0 0 1 4 *'")
  		}
  	})
  }


id: dag_parse_go_core_t0
function_id: dag_parse_go_core
name: TestDagParse
file_path: functions/core/dag_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestDagParse(t *testing.T) {
  	t.Run("parsea DAG simple con steps y depends", func(t *testing.T) {
  		data := []byte(`
  name: example
  description: Example workflow
  steps:
    - name: hello
      command: echo "Hello!"
    - name: list_files
      command: ls -la /tmp
      depends:
        - hello
  `)
  		dag, err := DagParse(data)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if dag.Name != "example" {
  			t.Errorf("Name: got %q, want %q", dag.Name, "example")
  		}
  		if len(dag.Steps) != 2 {
  			t.Fatalf("Steps: got %d, want 2", len(dag.Steps))
  		}
  		if dag.Steps[0].Name != "hello" {
  			t.Errorf("Steps[0].Name: got %q, want %q", dag.Steps[0].Name, "hello")
  		}
  		if dag.Steps[1].Name != "list_files" {
  			t.Errorf("Steps[1].Name: got %q, want %q", dag.Steps[1].Name, "list_files")
  		}
  		if len(dag.Steps[1].Depends) != 1 || dag.Steps[1].Depends[0] != "hello" {
  			t.Errorf("Steps[1].Depends: got %v, want [hello]", dag.Steps[1].Depends)
  		}
  	})

  	t.Run("parsea schedule como string y como lista", func(t *testing.T) {
  		// Schedule as string.
  		dataStr := []byte(`
  name: dag-string-schedule
  schedule: "0 9 * * 5"
  steps:
    - name: step1
      command: echo ok
  `)
  		dagStr, err := DagParse(dataStr)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(dagStr.Schedule) != 1 || dagStr.Schedule[0] != "0 9 * * 5" {
  			t.Errorf("Schedule (string): got %v, want [\"0 9 * * 5\"]", dagStr.Schedule)
  		}

  		// Schedule as list.
  		dataList := []byte(`
  name: dag-list-schedule
  schedule:
    - "0 9 * * *"
    - "0 18 * * *"
  steps:
    - name: step1
      command: echo ok
  `)
  		dagList, err := DagParse(dataList)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(dagList.Schedule) != 2 {
  			t.Fatalf("Schedule (list): got %d items, want 2", len(dagList.Schedule))
  		}
  		if dagList.Schedule[0] != "0 9 * * *" {
  			t.Errorf("Schedule[0]: got %q, want %q", dagList.Schedule[0], "0 9 * * *")
  		}
  		if dagList.Schedule[1] != "0 18 * * *" {
  			t.Errorf("Schedule[1]: got %q, want %q", dagList.Schedule[1], "0 18 * * *")
  		}
  	})

  	t.Run("parsea env en formato lista de maps", func(t *testing.T) {
  		data := []byte(`
  name: dag-env
  env:
    - PROJECT_DIR: /home/lucas/analysis
    - PYTHON: /home/lucas/.venv/bin/python
  steps:
    - name: step1
      command: echo ${PROJECT_DIR}
  `)
  		dag, err := DagParse(data)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if dag.Env["PROJECT_DIR"] != "/home/lucas/analysis" {
  			t.Errorf("Env[PROJECT_DIR]: got %q, want %q", dag.Env["PROJECT_DIR"], "/home/lucas/analysis")
  		}
  		if dag.Env["PYTHON"] != "/home/lucas/.venv/bin/python" {
  			t.Errorf("Env[PYTHON]: got %q, want %q", dag.Env["PYTHON"], "/home/lucas/.venv/bin/python")
  		}
  	})

  	t.Run("parsea handler_on y handlers como alias", func(t *testing.T) {
  		// handler_on with single step object.
  		dataHandlerOn := []byte(`
  name: dag-handler-on
  handler_on:
    failure:
      command: echo "FALLO"
  steps:
    - name: step1
      command: echo hello
  `)
  		dagHO, err := DagParse(dataHandlerOn)
  		if err != nil {
  			t.Fatalf("unexpected error (handler_on): %v", err)
  		}
  		if len(dagHO.HandlerOn.Failure) != 1 {
  			t.Fatalf("HandlerOn.Failure: got %d steps, want 1", len(dagHO.HandlerOn.Failure))
  		}
  		if dagHO.HandlerOn.Failure[0].Command != `echo "FALLO"` {
  			t.Errorf("HandlerOn.Failure[0].Command: got %q", dagHO.HandlerOn.Failure[0].Command)
  		}

  		// handlers alias with list of steps.
  		dataHandlers := []byte(`
  name: dag-handlers
  handlers:
    failure:
      - name: mark_as_failed
        command: echo "FAILED"
    success:
      - name: notify
        command: echo "OK"
  steps:
    - name: step1
      command: echo hello
  `)
  		dagH, err := DagParse(dataHandlers)
  		if err != nil {
  			t.Fatalf("unexpected error (handlers): %v", err)
  		}
  		if len(dagH.HandlerOn.Failure) != 1 || dagH.HandlerOn.Failure[0].Name != "mark_as_failed" {
  			t.Errorf("HandlerOn.Failure: got %v", dagH.HandlerOn.Failure)
  		}
  		if len(dagH.HandlerOn.Success) != 1 || dagH.HandlerOn.Success[0].Name != "notify" {
  			t.Errorf("HandlerOn.Success: got %v", dagH.HandlerOn.Success)
  		}
  	})

  	t.Run("parsea continue_on y working_dir a nivel step", func(t *testing.T) {
  		data := []byte(`
  name: dag-step-options
  steps:
    - id: ingest
      description: Procesar archivos
      working_dir: /home/lucas/project
      command: ./bin/ingest
      continue_on:
        failure: true
    - id: informe
      command: python /tmp/script.py
      depends: [ingest]
  `)
  		dag, err := DagParse(data)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(dag.Steps) != 2 {
  			t.Fatalf("Steps: got %d, want 2", len(dag.Steps))
  		}
  		step0 := dag.Steps[0]
  		if step0.ID != "ingest" {
  			t.Errorf("Steps[0].ID: got %q, want %q", step0.ID, "ingest")
  		}
  		if step0.Dir != "/home/lucas/project" {
  			t.Errorf("Steps[0].Dir: got %q, want %q", step0.Dir, "/home/lucas/project")
  		}
  		if !step0.ContinueOn.Failure {
  			t.Errorf("Steps[0].ContinueOn.Failure: got false, want true")
  		}
  		step1 := dag.Steps[1]
  		if len(step1.Depends) != 1 || step1.Depends[0] != "ingest" {
  			t.Errorf("Steps[1].Depends: got %v, want [ingest]", step1.Depends)
  		}
  	})

  	t.Run("parsea type graph", func(t *testing.T) {
  		data := []byte(`
  name: dag-graph
  type: graph
  tags: [finanzas, semanal]
  steps:
    - name: step1
      command: echo a
    - name: step2
      command: echo b
      depends: [step1]
  `)
  		dag, err := DagParse(data)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if dag.Type != "graph" {
  			t.Errorf("Type: got %q, want %q", dag.Type, "graph")
  		}
  		if len(dag.Tags) != 2 {
  			t.Errorf("Tags: got %v, want [finanzas semanal]", dag.Tags)
  		}
  	})
  }


id: dag_topo_sort_go_core_t0
function_id: dag_topo_sort_go_core
name: TestDagTopoSort
file_path: functions/core/dag_topo_sort_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestDagTopoSort(t *testing.T) {
  	t.Run("cadena lineal A->B->C produce tres niveles", func(t *testing.T) {
  		steps := []DagStep{
  			{Name: "a"},
  			{Name: "b", Depends: []string{"a"}},
  			{Name: "c", Depends: []string{"b"}},
  		}
  		levels, err := DagTopoSort(steps)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(levels) != 3 {
  			t.Fatalf("levels: got %d, want 3", len(levels))
  		}
  		if len(levels[0]) != 1 || levels[0][0].Name != "a" {
  			t.Errorf("levels[0]: got %v, want [a]", stepNames(levels[0]))
  		}
  		if len(levels[1]) != 1 || levels[1][0].Name != "b" {
  			t.Errorf("levels[1]: got %v, want [b]", stepNames(levels[1]))
  		}
  		if len(levels[2]) != 1 || levels[2][0].Name != "c" {
  			t.Errorf("levels[2]: got %v, want [c]", stepNames(levels[2]))
  		}
  	})

  	t.Run("diamond A->B A->C B->D C->D produce cuatro niveles", func(t *testing.T) {
  		steps := []DagStep{
  			{Name: "a"},
  			{Name: "b", Depends: []string{"a"}},
  			{Name: "c", Depends: []string{"a"}},
  			{Name: "d", Depends: []string{"b", "c"}},
  		}
  		levels, err := DagTopoSort(steps)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		// Expected: level0=[a], level1=[b,c], level2=[d]
  		if len(levels) != 3 {
  			t.Fatalf("levels: got %d, want 3", len(levels))
  		}
  		if len(levels[0]) != 1 || levels[0][0].Name != "a" {
  			t.Errorf("levels[0]: got %v, want [a]", stepNames(levels[0]))
  		}
  		if len(levels[1]) != 2 {
  			t.Errorf("levels[1]: got %v, want 2 steps", stepNames(levels[1]))
  		}
  		if len(levels[2]) != 1 || levels[2][0].Name != "d" {
  			t.Errorf("levels[2]: got %v, want [d]", stepNames(levels[2]))
  		}
  	})

  	t.Run("steps paralelos sin depends produce un solo nivel", func(t *testing.T) {
  		steps := []DagStep{
  			{Name: "x"},
  			{Name: "y"},
  			{Name: "z"},
  		}
  		levels, err := DagTopoSort(steps)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(levels) != 1 {
  			t.Fatalf("levels: got %d, want 1", len(levels))
  		}
  		if len(levels[0]) != 3 {
  			t.Errorf("levels[0]: got %d steps, want 3", len(levels[0]))
  		}
  	})

  	t.Run("ciclo A->B->A retorna error", func(t *testing.T) {
  		steps := []DagStep{
  			{Name: "a", Depends: []string{"b"}},
  			{Name: "b", Depends: []string{"a"}},
  		}
  		_, err := DagTopoSort(steps)
  		if err == nil {
  			t.Fatal("expected error for cycle, got nil")
  		}
  	})
  }

  // stepNames extracts step names for readable test output.
  func stepNames(steps []DagStep) []string {
  	names := make([]string, len(steps))
  	for i, s := range steps {
  		names[i] = stepRef(s)
  	}
  	return names
  }


id: dag_validate_go_core_t0
function_id: dag_validate_go_core
name: TestDagValidate
file_path: functions/core/dag_validate_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestDagValidate(t *testing.T) {
  	t.Run("DAG valido retorna Valid true y levels calculados", func(t *testing.T) {
  		dag := DagDefinition{
  			Name: "valid-dag",
  			Steps: []DagStep{
  				{Name: "a", Command: "echo a"},
  				{Name: "b", Command: "echo b", Depends: []string{"a"}},
  				{Name: "c", Command: "echo c", Depends: []string{"a"}},
  				{Name: "d", Command: "echo d", Depends: []string{"b", "c"}},
  			},
  		}
  		res := DagValidate(dag)
  		if !res.Valid {
  			t.Errorf("Valid: got false, want true. Errors: %v", res.Errors)
  		}
  		if len(res.Errors) != 0 {
  			t.Errorf("Errors: got %v, want empty", res.Errors)
  		}
  		// Should have 3 levels: [a], [b,c], [d]
  		if len(res.Levels) != 3 {
  			t.Errorf("Levels: got %d, want 3. Levels: %v", len(res.Levels), res.Levels)
  		}
  		if len(res.Levels[0]) != 1 || res.Levels[0][0] != "a" {
  			t.Errorf("Levels[0]: got %v, want [a]", res.Levels[0])
  		}
  		if len(res.Levels[2]) != 1 || res.Levels[2][0] != "d" {
  			t.Errorf("Levels[2]: got %v, want [d]", res.Levels[2])
  		}
  	})

  	t.Run("step sin nombre retorna error", func(t *testing.T) {
  		dag := DagDefinition{
  			Name: "bad-dag",
  			Steps: []DagStep{
  				{Command: "echo hello"}, // no name, no ID
  			},
  		}
  		res := DagValidate(dag)
  		if res.Valid {
  			t.Error("Valid: got true, want false")
  		}
  		if len(res.Errors) == 0 {
  			t.Error("Errors: got empty, want at least one error")
  		}
  	})

  	t.Run("depends a step inexistente retorna error", func(t *testing.T) {
  		dag := DagDefinition{
  			Name: "bad-deps-dag",
  			Steps: []DagStep{
  				{Name: "step1", Command: "echo ok"},
  				{Name: "step2", Command: "echo ok", Depends: []string{"nonexistent"}},
  			},
  		}
  		res := DagValidate(dag)
  		if res.Valid {
  			t.Error("Valid: got true, want false")
  		}
  		found := false
  		for _, e := range res.Errors {
  			if containsStr(e, "nonexistent") {
  				found = true
  				break
  			}
  		}
  		if !found {
  			t.Errorf("Errors should mention 'nonexistent', got: %v", res.Errors)
  		}
  	})

  	t.Run("ciclo en dependencias retorna error", func(t *testing.T) {
  		dag := DagDefinition{
  			Name: "cyclic-dag",
  			Steps: []DagStep{
  				{Name: "a", Command: "echo a", Depends: []string{"b"}},
  				{Name: "b", Command: "echo b", Depends: []string{"a"}},
  			},
  		}
  		res := DagValidate(dag)
  		if res.Valid {
  			t.Error("Valid: got true, want false")
  		}
  		found := false
  		for _, e := range res.Errors {
  			if containsStr(e, "cycle") {
  				found = true
  				break
  			}
  		}
  		if !found {
  			t.Errorf("Errors should mention 'cycle', got: %v", res.Errors)
  		}
  	})
  }

  // containsStr returns true if s contains substr.
  func containsStr(s, substr string) bool {
  	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
  		func() bool {
  			for i := 0; i <= len(s)-len(substr); i++ {
  				if s[i:i+len(substr)] == substr {
  					return true
  				}
  			}
  			return false
  		}())
  }


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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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: job_cleanup_go_infra_t0
function_id: job_cleanup_go_infra
name: TestJobQueueCreate
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobQueueCreate(t *testing.T) {
  	t.Run("test_queue_create", func(t *testing.T) {
  		db := openTestDB(t)
  		q, err := JobQueueCreate(db, "jobs")
  		if err != nil {
  			t.Fatalf("JobQueueCreate: %v", err)
  		}
  		if q == nil {
  			t.Fatal("expected non-nil JobQueue")
  		}
  		if q.TableName != "jobs" {
  			t.Errorf("TableName = %q, want %q", q.TableName, "jobs")
  		}

  		// Verify table exists by inserting a row directly.
  		_, err = db.Exec(`INSERT INTO jobs (id, type, scheduled_at) VALUES ('x', 'test', ?)`,
  			time.Now().UTC().Format(time.RFC3339))
  		if err != nil {
  			t.Fatalf("table not created: %v", err)
  		}
  	})

  	t.Run("test_queue_create_default_table_name", func(t *testing.T) {
  		db := openTestDB(t)
  		q, err := JobQueueCreate(db, "")
  		if err != nil {
  			t.Fatalf("JobQueueCreate with empty name: %v", err)
  		}
  		if q.TableName != "jobs" {
  			t.Errorf("TableName = %q, want %q", q.TableName, "jobs")
  		}
  	})

  	t.Run("test_queue_create_nil_db", func(t *testing.T) {
  		_, err := JobQueueCreate(nil, "jobs")
  		if err == nil {
  			t.Fatal("expected error for nil db")
  		}
  	})
  }


id: job_cleanup_go_infra_t1
function_id: job_cleanup_go_infra
name: TestJobEnqueueDequeue
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobEnqueueDequeue(t *testing.T) {
  	t.Run("enqueue_dequeue_atomicidad", func(t *testing.T) {
  		db := openTestDB(t)
  		q, err := JobQueueCreate(db, "jobs")
  		if err != nil {
  			t.Fatalf("create: %v", err)
  		}

  		id, err := JobEnqueue(q, "send_email", `{"to":"a@b.com"}`)
  		if err != nil {
  			t.Fatalf("enqueue: %v", err)
  		}
  		if id == "" {
  			t.Fatal("expected non-empty job ID")
  		}

  		job, err := JobDequeue(q, nil)
  		if err != nil {
  			t.Fatalf("dequeue: %v", err)
  		}
  		if job == nil {
  			t.Fatal("expected a job, got nil")
  		}
  		if job.ID != id {
  			t.Errorf("job ID = %q, want %q", job.ID, id)
  		}
  		if job.Type != "send_email" {
  			t.Errorf("job Type = %q, want %q", job.Type, "send_email")
  		}
  		if job.Status != JobStatusRunning {
  			t.Errorf("job Status = %q, want %q", job.Status, JobStatusRunning)
  		}

  		// Second dequeue should return nil (job is now running, not pending).
  		job2, err := JobDequeue(q, nil)
  		if err != nil {
  			t.Fatalf("second dequeue: %v", err)
  		}
  		if job2 != nil {
  			t.Errorf("expected nil second dequeue, got job %q", job2.ID)
  		}
  	})

  	t.Run("dequeue_empty_queue_returns_nil", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		job, err := JobDequeue(q, nil)
  		if err != nil {
  			t.Fatalf("dequeue empty: %v", err)
  		}
  		if job != nil {
  			t.Errorf("expected nil, got job %q", job.ID)
  		}
  	})

  	t.Run("dequeue_priority_order", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		_, _ = JobEnqueue(q, "low", "{}", WithPriority(0))
  		_, _ = JobEnqueue(q, "high", "{}", WithPriority(10))
  		_, _ = JobEnqueue(q, "mid", "{}", WithPriority(5))

  		job, _ := JobDequeue(q, nil)
  		if job == nil || job.Type != "high" {
  			t.Errorf("expected high-priority job first, got %v", job)
  		}
  	})

  	t.Run("dequeue_jobtype_filter", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		_, _ = JobEnqueue(q, "email", "{}")
  		_, _ = JobEnqueue(q, "sms", "{}")

  		// Only dequeue "sms" jobs.
  		job, err := JobDequeue(q, []string{"sms"})
  		if err != nil {
  			t.Fatalf("dequeue with filter: %v", err)
  		}
  		if job == nil || job.Type != "sms" {
  			t.Errorf("expected sms job, got %v", job)
  		}
  	})

  	t.Run("dequeue_scheduled_in_future_waits", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		future := time.Now().Add(1 * time.Hour)
  		_, _ = JobEnqueue(q, "future", "{}", WithScheduledAt(future))

  		job, err := JobDequeue(q, nil)
  		if err != nil {
  			t.Fatalf("dequeue future: %v", err)
  		}
  		if job != nil {
  			t.Errorf("expected nil (future job not yet due), got job %q", job.ID)
  		}
  	})
  }


id: job_cleanup_go_infra_t2
function_id: job_cleanup_go_infra
name: TestJobCompleteAndFail
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobCompleteAndFail(t *testing.T) {
  	t.Run("complete_fail_transitions", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		id, _ := JobEnqueue(q, "work", "{}", WithMaxAttempts(3))
  		job, _ := JobDequeue(q, nil)
  		if job == nil {
  			t.Fatal("expected job")
  		}

  		// Complete it.
  		if err := JobComplete(q, id, `{"ok":true}`); err != nil {
  			t.Fatalf("complete: %v", err)
  		}

  		// Verify status in DB.
  		var status string
  		_ = db.QueryRow(`SELECT status FROM jobs WHERE id = ?`, id).Scan(&status)
  		if status != "completed" {
  			t.Errorf("status = %q, want completed", status)
  		}
  	})

  	t.Run("fail_increments_attempts", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		id, _ := JobEnqueue(q, "work", "{}", WithMaxAttempts(3))
  		_, _ = JobDequeue(q, nil)

  		// First failure → status = failed (attempts=1 < max=3).
  		if err := JobFail(q, id, "oops"); err != nil {
  			t.Fatalf("fail: %v", err)
  		}
  		var status string
  		var attempts int
  		_ = db.QueryRow(`SELECT status, attempts FROM jobs WHERE id = ?`, id).Scan(&status, &attempts)
  		if status != "failed" {
  			t.Errorf("status = %q, want failed", status)
  		}
  		if attempts != 1 {
  			t.Errorf("attempts = %d, want 1", attempts)
  		}
  	})

  	t.Run("fail_transitions_to_dead", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		id, _ := JobEnqueue(q, "work", "{}", WithMaxAttempts(2))

  		// Fail twice → dead.
  		for i := 0; i < 2; i++ {
  			// Re-set to pending so we can dequeue again (simulate retry logic).
  			if i > 0 {
  				_, _ = db.Exec(`UPDATE jobs SET status = 'pending' WHERE id = ?`, id)
  			}
  			_, _ = JobDequeue(q, nil)
  			_ = JobFail(q, id, fmt.Sprintf("error %d", i+1))
  		}

  		var status string
  		_ = db.QueryRow(`SELECT status FROM jobs WHERE id = ?`, id).Scan(&status)
  		if status != "dead" {
  			t.Errorf("status = %q, want dead after max_attempts reached", status)
  		}
  	})
  }


id: job_cleanup_go_infra_t3
function_id: job_cleanup_go_infra
name: TestJobStatusSummaryFn
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobStatusSummaryFn(t *testing.T) {
  	t.Run("job_status_summary_format", func(t *testing.T) {
  		counts := map[string]int{
  			"pending":   5,
  			"running":   2,
  			"completed": 10,
  			"failed":    1,
  			"dead":      0,
  		}
  		got := JobStatusSummary(counts)
  		want := "pending: 5, running: 2, completed: 10, failed: 1, dead: 0"
  		if got != want {
  			t.Errorf("got %q, want %q", got, want)
  		}
  	})

  	t.Run("job_status_summary_empty_map", func(t *testing.T) {
  		got := JobStatusSummary(map[string]int{})
  		want := "pending: 0, running: 0, completed: 0, failed: 0, dead: 0"
  		if got != want {
  			t.Errorf("got %q, want %q", got, want)
  		}
  	})
  }


id: job_cleanup_go_infra_t4
function_id: job_cleanup_go_infra
name: TestJobWorkerGracefulShutdown
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobWorkerGracefulShutdown(t *testing.T) {
  	t.Run("worker_graceful_shutdown", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		ctx, cancel := context.WithCancel(context.Background())

  		done := make(chan struct{})
  		go func() {
  			defer close(done)
  			_ = JobWorker(ctx, q, func(j Job) error {
  				return nil
  			}, WithPollInterval(10*time.Millisecond))
  		}()

  		// Let the worker poll a couple of times.
  		time.Sleep(50 * time.Millisecond)
  		cancel()

  		select {
  		case <-done:
  			// OK — worker exited cleanly.
  		case <-time.After(2 * time.Second):
  			t.Fatal("worker did not stop within timeout after context cancellation")
  		}
  	})

  	t.Run("worker_processes_jobs", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		var processed atomic.Int32
  		for i := 0; i < 3; i++ {
  			_, _ = JobEnqueue(q, "task", `{}`)
  		}

  		ctx, cancel := context.WithCancel(context.Background())
  		go func() {
  			_ = JobWorker(ctx, q, func(j Job) error {
  				processed.Add(1)
  				return nil
  			}, WithPollInterval(10*time.Millisecond))
  		}()

  		// Wait for all 3 to be processed (with timeout).
  		deadline := time.Now().Add(2 * time.Second)
  		for time.Now().Before(deadline) {
  			if processed.Load() >= 3 {
  				break
  			}
  			time.Sleep(10 * time.Millisecond)
  		}
  		cancel()

  		if n := processed.Load(); n < 3 {
  			t.Errorf("processed %d jobs, want 3", n)
  		}
  	})
  }


id: job_cleanup_go_infra_t5
function_id: job_cleanup_go_infra
name: TestJobConcurrency
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobConcurrency(t *testing.T) {
  	t.Run("concurrency_multiples_goroutines_dequeue", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		const numJobs = 50
  		for i := 0; i < numJobs; i++ {
  			_, _ = JobEnqueue(q, "task", fmt.Sprintf(`{"i":%d}`, i))
  		}

  		var processed atomic.Int32
  		var wg sync.WaitGroup

  		ctx, cancel := context.WithCancel(context.Background())
  		const numWorkers = 5
  		for i := 0; i < numWorkers; i++ {
  			wg.Add(1)
  			go func() {
  				defer wg.Done()
  				_ = JobWorker(ctx, q, func(j Job) error {
  					processed.Add(1)
  					return nil
  				}, WithPollInterval(5*time.Millisecond))
  			}()
  		}

  		// Wait until all jobs are processed.
  		deadline := time.Now().Add(5 * time.Second)
  		for time.Now().Before(deadline) {
  			if processed.Load() >= numJobs {
  				break
  			}
  			time.Sleep(10 * time.Millisecond)
  		}
  		cancel()
  		wg.Wait()

  		if n := processed.Load(); n != numJobs {
  			t.Errorf("processed %d, want %d (no double-processing with concurrent workers)", n, numJobs)
  		}
  	})
  }


id: job_cleanup_go_infra_t6
function_id: job_cleanup_go_infra
name: TestJobCleanupFn
file_path: functions/infra/job_queue_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestJobCleanupFn(t *testing.T) {
  	t.Run("job_cleanup_removes_old_terminal_jobs", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		id, _ := JobEnqueue(q, "work", "{}")
  		_, _ = JobDequeue(q, nil)
  		_ = JobComplete(q, id, "")

  		// Make the job look old (set created_at in the past, always UTC to match JobCleanup).
  		past := time.Now().UTC().Add(-25 * time.Hour).Format(time.RFC3339)
  		_, _ = db.Exec(`UPDATE jobs SET created_at = ? WHERE id = ?`, past, id)

  		n, err := JobCleanup(q, 24*time.Hour)
  		if err != nil {
  			t.Fatalf("cleanup: %v", err)
  		}
  		if n != 1 {
  			t.Errorf("deleted %d rows, want 1", n)
  		}

  		// Job should be gone.
  		var count int
  		_ = db.QueryRow(`SELECT COUNT(*) FROM jobs WHERE id = ?`, id).Scan(&count)
  		if count != 0 {
  			t.Errorf("job still in DB after cleanup")
  		}
  	})

  	t.Run("job_cleanup_keeps_recent_jobs", func(t *testing.T) {
  		db := openTestDB(t)
  		q, _ := JobQueueCreate(db, "jobs")

  		id, _ := JobEnqueue(q, "work", "{}")
  		_, _ = JobDequeue(q, nil)
  		_ = JobComplete(q, id, "")

  		// Job was just created — should not be cleaned up.
  		n, err := JobCleanup(q, 24*time.Hour)
  		if err != nil {
  			t.Fatalf("cleanup: %v", err)
  		}
  		if n != 0 {
  			t.Errorf("deleted %d rows, want 0 (job is recent)", n)
  		}
  	})
  }


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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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: process_kill_go_infra_t0
function_id: process_kill_go_infra
name: TestProcessSpawn
file_path: functions/infra/process_spawn_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestProcessSpawn(t *testing.T) {
  	t.Run("spawn and wait echo", func(t *testing.T) {
  		h, err := ProcessSpawn("echo hello", "", nil, "")
  		if err != nil {
  			t.Fatalf("spawn: %v", err)
  		}
  		res, err := ProcessWait(h, 10)
  		if err != nil {
  			t.Fatalf("wait: %v", err)
  		}
  		if res.ExitCode != 0 {
  			t.Errorf("exit code: got %d, want 0", res.ExitCode)
  		}
  		if !strings.Contains(res.Stdout, "hello") {
  			t.Errorf("stdout: got %q, want it to contain 'hello'", res.Stdout)
  		}
  	})

  	t.Run("spawn with timeout kills", func(t *testing.T) {
  		h, err := ProcessSpawn("sleep 60", "", nil, "")
  		if err != nil {
  			t.Fatalf("spawn: %v", err)
  		}
  		res, err := ProcessWait(h, 2)
  		if err != nil {
  			t.Fatalf("wait: %v", err)
  		}
  		if !res.Killed {
  			t.Errorf("killed: got false, want true")
  		}
  		if res.ExitCode == 0 {
  			t.Errorf("exit code: got 0, want != 0 after kill")
  		}
  	})

  	t.Run("spawn with env", func(t *testing.T) {
  		h, err := ProcessSpawn("echo $TEST_VAR", "", []string{"PATH=/usr/bin:/bin", "TEST_VAR=hello123"}, "")
  		if err != nil {
  			t.Fatalf("spawn: %v", err)
  		}
  		res, err := ProcessWait(h, 10)
  		if err != nil {
  			t.Fatalf("wait: %v", err)
  		}
  		if !strings.Contains(res.Stdout, "hello123") {
  			t.Errorf("stdout: got %q, want it to contain 'hello123'", res.Stdout)
  		}
  	})

  	t.Run("spawn script", func(t *testing.T) {
  		script := "#!/bin/sh\necho line1\necho line2"
  		h, err := ProcessSpawn(script, "", nil, "")
  		if err != nil {
  			t.Fatalf("spawn: %v", err)
  		}
  		res, err := ProcessWait(h, 10)
  		if err != nil {
  			t.Fatalf("wait: %v", err)
  		}
  		if !strings.Contains(res.Stdout, "line1") {
  			t.Errorf("stdout: got %q, want it to contain 'line1'", res.Stdout)
  		}
  		if !strings.Contains(res.Stdout, "line2") {
  			t.Errorf("stdout: got %q, want it to contain 'line2'", res.Stdout)
  		}
  	})

  	t.Run("spawn with working dir", func(t *testing.T) {
  		h, err := ProcessSpawn("pwd", "/tmp", nil, "")
  		if err != nil {
  			t.Fatalf("spawn: %v", err)
  		}
  		res, err := ProcessWait(h, 10)
  		if err != nil {
  			t.Fatalf("wait: %v", err)
  		}
  		if !strings.Contains(res.Stdout, "/tmp") {
  			t.Errorf("stdout: got %q, want it to contain '/tmp'", res.Stdout)
  		}
  	})

  	t.Run("kill process", func(t *testing.T) {
  		h, err := ProcessSpawn("sleep 60", "", nil, "")
  		if err != nil {
  			t.Fatalf("spawn: %v", err)
  		}
  		if err := ProcessKill(h, 1); err != nil {
  			t.Fatalf("kill: %v", err)
  		}
  		// After kill, Wait should unblock quickly.
  		_ = h.Cmd.Wait()
  		state := h.Cmd.ProcessState
  		if state == nil {
  			t.Fatal("process state is nil after kill+wait")
  		}
  		if state.ExitCode() == 0 {
  			t.Errorf("exit code: got 0 after kill, want non-zero")
  		}
  	})
  }


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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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_config_add_entry_go_infra_t0
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigParse_MultipleHosts
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigParse_MultipleHosts(t *testing.T) {
  	content := `
  Host prod
      HostName 10.0.0.1
      User admin
      Port 22
      IdentityFile ~/.ssh/id_prod

  Host staging
      HostName 10.0.0.2
      User deploy
      Port 2222
  `
  	entries := SSHConfigParse(content)
  	if len(entries) != 2 {
  		t.Fatalf("expected 2 entries, got %d", len(entries))
  	}
  	if entries[0].Alias != "prod" {
  		t.Errorf("expected alias prod, got %s", entries[0].Alias)
  	}
  	if entries[0].HostName != "10.0.0.1" {
  		t.Errorf("expected hostname 10.0.0.1, got %s", entries[0].HostName)
  	}
  	if entries[0].User != "admin" {
  		t.Errorf("expected user admin, got %s", entries[0].User)
  	}
  	if entries[0].Port != 22 {
  		t.Errorf("expected port 22, got %d", entries[0].Port)
  	}
  	if entries[0].IdentityFile != "~/.ssh/id_prod" {
  		t.Errorf("expected identity file ~/.ssh/id_prod, got %s", entries[0].IdentityFile)
  	}
  	if entries[1].Alias != "staging" {
  		t.Errorf("expected alias staging, got %s", entries[1].Alias)
  	}
  	if entries[1].Port != 2222 {
  		t.Errorf("expected port 2222, got %d", entries[1].Port)
  	}
  }


id: ssh_config_add_entry_go_infra_t1
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigParse_IgnoresCommentsAndBlanks
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigParse_IgnoresCommentsAndBlanks(t *testing.T) {
  	content := `
  # This is a comment
  Host myserver
      HostName 192.168.1.1
      # inline comment
      User root
  `
  	entries := SSHConfigParse(content)
  	if len(entries) != 1 {
  		t.Fatalf("expected 1 entry, got %d", len(entries))
  	}
  	if entries[0].User != "root" {
  		t.Errorf("expected user root, got %s", entries[0].User)
  	}
  }


id: ssh_config_add_entry_go_infra_t2
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigParse_IgnoresWildcards
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigParse_IgnoresWildcards(t *testing.T) {
  	content := `
  Host *
      ServerAliveInterval 60

  Host prod
      HostName 10.0.0.1

  Host 192.168.*
      User local
  `
  	entries := SSHConfigParse(content)
  	if len(entries) != 1 {
  		t.Fatalf("expected 1 entry (wildcards ignored), got %d", len(entries))
  	}
  	if entries[0].Alias != "prod" {
  		t.Errorf("expected alias prod, got %s", entries[0].Alias)
  	}
  }


id: ssh_config_add_entry_go_infra_t3
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigParse_ExtraOptions
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigParse_ExtraOptions(t *testing.T) {
  	content := `Host jump
      HostName bastion.example.com
      User ops
      ForwardAgent yes
      ProxyJump none
  `
  	entries := SSHConfigParse(content)
  	if len(entries) != 1 {
  		t.Fatalf("expected 1 entry, got %d", len(entries))
  	}
  	if entries[0].Options["ForwardAgent"] != "yes" {
  		t.Errorf("expected ForwardAgent=yes, got %s", entries[0].Options["ForwardAgent"])
  	}
  	if entries[0].Options["ProxyJump"] != "none" {
  		t.Errorf("expected ProxyJump=none, got %s", entries[0].Options["ProxyJump"])
  	}
  }


id: ssh_config_add_entry_go_infra_t4
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigRender_FullEntry
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigRender_FullEntry(t *testing.T) {
  	entries := []SSHConfigEntry{{
  		Alias:        "prod",
  		HostName:     "10.0.0.1",
  		User:         "admin",
  		Port:         2222,
  		IdentityFile: "~/.ssh/id_prod",
  	}}
  	result := SSHConfigRender(entries)
  	if !strings.Contains(result, "Host prod") {
  		t.Error("missing Host directive")
  	}
  	if !strings.Contains(result, "HostName 10.0.0.1") {
  		t.Error("missing HostName")
  	}
  	if !strings.Contains(result, "User admin") {
  		t.Error("missing User")
  	}
  	if !strings.Contains(result, "Port 2222") {
  		t.Error("missing Port")
  	}
  	if !strings.Contains(result, "IdentityFile ~/.ssh/id_prod") {
  		t.Error("missing IdentityFile")
  	}
  }


id: ssh_config_add_entry_go_infra_t5
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigRender_MinimalEntry
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigRender_MinimalEntry(t *testing.T) {
  	entries := []SSHConfigEntry{{Alias: "local"}}
  	result := SSHConfigRender(entries)
  	if result != "Host local\n" {
  		t.Errorf("expected minimal render, got %q", result)
  	}
  }


id: ssh_config_add_entry_go_infra_t6
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigRender_MultipleEntries
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigRender_MultipleEntries(t *testing.T) {
  	entries := []SSHConfigEntry{
  		{Alias: "a", HostName: "1.1.1.1"},
  		{Alias: "b", HostName: "2.2.2.2"},
  	}
  	result := SSHConfigRender(entries)
  	parts := strings.Split(result, "\n\n")
  	if len(parts) < 2 {
  		t.Error("expected blocks separated by blank line")
  	}
  }


id: ssh_config_add_entry_go_infra_t7
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigRender_OptionsSorted
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigRender_OptionsSorted(t *testing.T) {
  	entries := []SSHConfigEntry{{
  		Alias:    "test",
  		HostName: "1.1.1.1",
  		Options:  map[string]string{"ProxyJump": "bastion", "ForwardAgent": "yes"},
  	}}
  	result := SSHConfigRender(entries)
  	fwIdx := strings.Index(result, "ForwardAgent")
  	pjIdx := strings.Index(result, "ProxyJump")
  	if fwIdx < 0 || pjIdx < 0 {
  		t.Fatal("missing options in render")
  	}
  	if fwIdx > pjIdx {
  		t.Error("options should be sorted alphabetically")
  	}
  }


id: ssh_config_add_entry_go_infra_t8
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigFind_Exists
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigFind_Exists(t *testing.T) {
  	entries := []SSHConfigEntry{
  		{Alias: "prod", HostName: "10.0.0.1"},
  		{Alias: "staging", HostName: "10.0.0.2"},
  	}
  	entry, ok := SSHConfigFind(entries, "staging")
  	if !ok {
  		t.Fatal("expected to find staging")
  	}
  	if entry.HostName != "10.0.0.2" {
  		t.Errorf("expected hostname 10.0.0.2, got %s", entry.HostName)
  	}
  }


id: ssh_config_add_entry_go_infra_t9
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigFind_NotExists
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigFind_NotExists(t *testing.T) {
  	entries := []SSHConfigEntry{{Alias: "prod"}}
  	_, ok := SSHConfigFind(entries, "nope")
  	if ok {
  		t.Error("expected not found")
  	}
  }


id: ssh_config_add_entry_go_infra_t10
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigAddEntry_ToEmpty
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigAddEntry_ToEmpty(t *testing.T) {
  	entry := SSHConfigEntry{Alias: "new", HostName: "1.1.1.1"}
  	result, err := SSHConfigAddEntry(nil, entry)
  	if err != nil {
  		t.Fatal(err)
  	}
  	if len(result) != 1 || result[0].Alias != "new" {
  		t.Errorf("unexpected result: %+v", result)
  	}
  }


id: ssh_config_add_entry_go_infra_t11
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigAddEntry_ToExisting
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigAddEntry_ToExisting(t *testing.T) {
  	existing := []SSHConfigEntry{{Alias: "old"}}
  	entry := SSHConfigEntry{Alias: "new"}
  	result, err := SSHConfigAddEntry(existing, entry)
  	if err != nil {
  		t.Fatal(err)
  	}
  	if len(result) != 2 {
  		t.Fatalf("expected 2 entries, got %d", len(result))
  	}
  	// Original no mutado
  	if len(existing) != 1 {
  		t.Error("original slice was mutated")
  	}
  }


id: ssh_config_add_entry_go_infra_t12
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigAddEntry_DuplicateAlias
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigAddEntry_DuplicateAlias(t *testing.T) {
  	existing := []SSHConfigEntry{{Alias: "prod"}}
  	entry := SSHConfigEntry{Alias: "prod"}
  	_, err := SSHConfigAddEntry(existing, entry)
  	if err == nil {
  		t.Error("expected error for duplicate alias")
  	}
  }


id: ssh_config_add_entry_go_infra_t13
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigRemoveEntry_Exists
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigRemoveEntry_Exists(t *testing.T) {
  	entries := []SSHConfigEntry{
  		{Alias: "a"},
  		{Alias: "b"},
  		{Alias: "c"},
  	}
  	result, err := SSHConfigRemoveEntry(entries, "b")
  	if err != nil {
  		t.Fatal(err)
  	}
  	if len(result) != 2 {
  		t.Fatalf("expected 2 entries, got %d", len(result))
  	}
  	if result[0].Alias != "a" || result[1].Alias != "c" {
  		t.Errorf("unexpected order: %+v", result)
  	}
  	// Original no mutado
  	if len(entries) != 3 {
  		t.Error("original slice was mutated")
  	}
  }


id: ssh_config_add_entry_go_infra_t14
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigRemoveEntry_NotExists
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigRemoveEntry_NotExists(t *testing.T) {
  	entries := []SSHConfigEntry{{Alias: "prod"}}
  	_, err := SSHConfigRemoveEntry(entries, "nope")
  	if err == nil {
  		t.Error("expected error for missing alias")
  	}
  }


id: ssh_config_add_entry_go_infra_t15
function_id: ssh_config_add_entry_go_infra
name: TestSSHConfigParseRender_Roundtrip
file_path: functions/infra/ssh_config_parse_test.go
lang: go
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  func TestSSHConfigParseRender_Roundtrip(t *testing.T) {
  	original := []SSHConfigEntry{
  		{Alias: "prod", HostName: "10.0.0.1", User: "admin", Port: 22, IdentityFile: "~/.ssh/id_prod"},
  		{Alias: "staging", HostName: "10.0.0.2", User: "deploy", Port: 2222},
  	}
  	text := SSHConfigRender(original)
  	parsed := SSHConfigParse(text)
  	if len(parsed) != len(original) {
  		t.Fatalf("roundtrip: expected %d entries, got %d", len(original), len(parsed))
  	}
  	for i := range original {
  		if parsed[i].Alias != original[i].Alias {
  			t.Errorf("roundtrip[%d]: alias mismatch %s != %s", i, parsed[i].Alias, original[i].Alias)
  		}
  		if parsed[i].HostName != original[i].HostName {
  			t.Errorf("roundtrip[%d]: hostname mismatch", i)
  		}
  		if parsed[i].User != original[i].User {
  			t.Errorf("roundtrip[%d]: user mismatch", i)
  		}
  		if parsed[i].Port != original[i].Port {
  			t.Errorf("roundtrip[%d]: port mismatch", i)
  		}
  	}
  }


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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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: build_guide_prompt_py_core_t0
function_id: build_guide_prompt_py_core
name: test_con_ubicacion_completa_pois_y_query
file_path: python/functions/core/build_guide_prompt_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_con_ubicacion_completa_pois_y_query():
      location = {
          "display_name": "Puerta del Sol, Madrid",
          "street": "Puerta del Sol",
          "neighbourhood": "Sol",
          "city": "Madrid",
          "country": "España",
      }
      pois = [
          {
              "name": "Museo del Prado",
              "category": "museum",
              "score": 1.0,
              "matched_interests": ["arte"],
              "lat": 40.41,
              "lon": -3.70,
              "tags": {},
          },
          {
              "name": "Jardines del Retiro",
              "category": "park",
              "score": 0.8,
              "matched_interests": ["naturaleza"],
              "lat": 40.41,
              "lon": -3.68,
              "tags": {},
          },
      ]
      interests = [
          {"name": "arte", "keywords": ["museo", "pintura"], "weight": 1.0},
          {"name": "naturaleza", "keywords": ["parque", "jardín"], "weight": 0.8},
      ]
      user_query = "¿Qué hay interesante por aquí?"

      messages = build_guide_prompt(location, pois, interests, user_query)

      assert len(messages) == 2
      assert messages[0]["role"] == "system"
      assert messages[1]["role"] == "user"

      system = messages[0]["content"]
      assert "Madrid" in system
      assert "arte" in system
      assert "naturaleza" in system

      user = messages[1]["content"]
      assert "Puerta del Sol, Madrid" in user
      assert "Sol" in user
      assert "Museo del Prado" in user
      assert "museum" in user
      assert "arte" in user
      assert "Jardines del Retiro" in user
      assert "¿Qué hay interesante por aquí?" in user


id: build_guide_prompt_py_core_t1
function_id: build_guide_prompt_py_core
name: test_sin_pois_solo_ubicacion
file_path: python/functions/core/build_guide_prompt_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_sin_pois_solo_ubicacion():
      location = {
          "display_name": "Calle Mayor, Salamanca",
          "street": "Calle Mayor",
          "neighbourhood": "Centro Histórico",
          "city": "Salamanca",
          "country": "España",
      }
      pois = []
      interests = [{"name": "historia", "keywords": ["catedral"], "weight": 1.0}]
      user_query = "¿Algo interesante cerca?"

      messages = build_guide_prompt(location, pois, interests, user_query)

      assert len(messages) == 2
      user = messages[1]["content"]
      assert "Salamanca" in user
      assert "Lugares cercanos" not in user
      assert "¿Algo interesante cerca?" in user


id: build_guide_prompt_py_core_t2
function_id: build_guide_prompt_py_core
name: test_sin_query_usa_default
file_path: python/functions/core/build_guide_prompt_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_sin_query_usa_default():
      location = {
          "display_name": "Plaza Mayor, Sevilla",
          "city": "Sevilla",
          "neighbourhood": "Casco Antiguo",
          "country": "España",
      }
      pois = [
          {
              "name": "Catedral de Sevilla",
              "category": "cathedral",
              "score": 0.95,
              "matched_interests": ["historia", "arquitectura"],
              "lat": 37.38,
              "lon": -5.99,
              "tags": {},
          }
      ]
      interests = [
          {"name": "historia", "keywords": ["catedral"], "weight": 1.0},
          {"name": "arquitectura", "keywords": ["edificio"], "weight": 0.9},
      ]

      messages = build_guide_prompt(location, pois, interests)

      assert len(messages) == 2
      user = messages[1]["content"]
      assert "¿Qué me puedes contar de esta zona?" in user
      assert "Catedral de Sevilla" in user
      assert "historia" in user
      assert "arquitectura" in user


id: build_guide_prompt_py_core_t3
function_id: build_guide_prompt_py_core
name: test_max_5_pois
file_path: python/functions/core/build_guide_prompt_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_max_5_pois():
      location = {"display_name": "Barcelona", "city": "Barcelona"}
      pois = [
          {"name": f"Lugar {i}", "category": "poi", "score": 1.0 - i * 0.1, "matched_interests": ["cultura"], "lat": 0, "lon": 0, "tags": {}}
          for i in range(10)
      ]
      interests = [{"name": "cultura", "keywords": [], "weight": 1.0}]

      messages = build_guide_prompt(location, pois, interests)

      user = messages[1]["content"]
      # Solo los primeros 5 POIs deben aparecer
      for i in range(5):
          assert f"Lugar {i}" in user
      for i in range(5, 10):
          assert f"Lugar {i}" not in user


id: build_guide_prompt_py_core_t4
function_id: build_guide_prompt_py_core
name: test_lang_aparece_en_system
file_path: python/functions/core/build_guide_prompt_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_lang_aparece_en_system():
      location = {"display_name": "Paris", "city": "Paris"}
      pois = []
      interests = [{"name": "art", "keywords": [], "weight": 1.0}]

      messages = build_guide_prompt(location, pois, interests, lang="fr")

      system = messages[0]["content"]
      assert "fr" in system


id: build_guide_prompt_py_core_t5
function_id: build_guide_prompt_py_core
name: test_location_sin_neighbourhood
file_path: python/functions/core/build_guide_prompt_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_location_sin_neighbourhood():
      location = {
          "display_name": "Gran Via, Madrid",
          "city": "Madrid",
      }
      pois = []
      interests = []

      messages = build_guide_prompt(location, pois, interests)

      user = messages[1]["content"]
      assert "Gran Via, Madrid" in user
      # No debe romper si no hay neighbourhood
      assert len(messages) == 2


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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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: generate_pwa_manifest_py_core_t0
function_id: generate_pwa_manifest_py_core
name: test_valores_por_defecto_generan_manifest_valido_con_icons_placeholder
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_valores_por_defecto_generan_manifest_valido_con_icons_placeholder():
      result = generate_pwa_manifest(name="Mi App", short_name="App")

      assert result["name"] == "Mi App"
      assert result["short_name"] == "App"
      assert result["start_url"] == "/"
      assert result["scope"] == "/"
      assert result["display"] == "standalone"
      assert result["orientation"] == "portrait"
      assert result["theme_color"] == "#000000"
      assert result["background_color"] == "#000000"
      assert result["lang"] == "es"
      assert result["description"] == ""
      assert result["categories"] == []

      icons = result["icons"]
      assert len(icons) == 2
      assert icons[0]["src"] == "/icons/icon-192x192.png"
      assert icons[0]["sizes"] == "192x192"
      assert icons[0]["type"] == "image/png"
      assert icons[0]["purpose"] == "any maskable"
      assert icons[1]["src"] == "/icons/icon-512x512.png"
      assert icons[1]["sizes"] == "512x512"


id: generate_pwa_manifest_py_core_t1
function_id: generate_pwa_manifest_py_core
name: test_icons_custom_se_respetan
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_icons_custom_se_respetan():
      custom_icons = [
          {"src": "/my-icon.png", "sizes": "256x256", "type": "image/png", "purpose": "any"},
      ]
      result = generate_pwa_manifest(name="App", short_name="A", icons=custom_icons)

      assert result["icons"] == custom_icons
      assert len(result["icons"]) == 1
      assert result["icons"][0]["src"] == "/my-icon.png"
      assert result["icons"][0]["sizes"] == "256x256"


id: generate_pwa_manifest_py_core_t2
function_id: generate_pwa_manifest_py_core
name: test_todos_los_campos_se_incluyen_correctamente
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_todos_los_campos_se_incluyen_correctamente():
      result = generate_pwa_manifest(
          name="Voice Guide",
          short_name="VoiceGuide",
          description="Guia por voz con POIs",
          start_url="/app",
          display="fullscreen",
          orientation="landscape",
          theme_color="#12b886",
          background_color="#1a1b1e",
          categories=["navigation", "travel"],
          lang="en",
      )

      assert result["name"] == "Voice Guide"
      assert result["short_name"] == "VoiceGuide"
      assert result["description"] == "Guia por voz con POIs"
      assert result["start_url"] == "/app"
      assert result["scope"] == "/"
      assert result["display"] == "fullscreen"
      assert result["orientation"] == "landscape"
      assert result["theme_color"] == "#12b886"
      assert result["background_color"] == "#1a1b1e"
      assert result["categories"] == ["navigation", "travel"]
      assert result["lang"] == "en"
      # icons son placeholders por defecto
      assert len(result["icons"]) == 2


id: generate_pwa_manifest_py_core_t3
function_id: generate_pwa_manifest_py_core
name: test_categories_none_produce_lista_vacia
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_categories_none_produce_lista_vacia():
      result = generate_pwa_manifest(name="App", short_name="A", categories=None)
      assert result["categories"] == []


id: generate_pwa_manifest_py_core_t4
function_id: generate_pwa_manifest_py_core
name: test_json_dumps_compatible
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_json_dumps_compatible():
      import json

      result = generate_pwa_manifest(
          name="Voice Guide",
          short_name="VG",
          theme_color="#12b886",
          background_color="#1a1b1e",
      )
      serialized = json.dumps(result)
      parsed = json.loads(serialized)
      assert parsed["name"] == "Voice Guide"
      assert parsed["theme_color"] == "#12b886"


id: generate_service_worker_py_core_t0
function_id: generate_service_worker_py_core
name: test_default_sw_contains_install_activate_fetch_handlers
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_default_sw_contains_install_activate_fetch_handlers():
      """SW por defecto contiene install/activate/fetch handlers."""
      code = generate_service_worker()
      assert "addEventListener('install'" in code
      assert "addEventListener('activate'" in code
      assert "addEventListener('fetch'" in code


id: generate_service_worker_py_core_t1
function_id: generate_service_worker_py_core
name: test_custom_precache_urls_appear_in_code
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_custom_precache_urls_appear_in_code():
      """Custom precache_urls aparecen en el codigo."""
      urls = ["/", "/index.html", "/manifest.json", "/offline.html"]
      code = generate_service_worker(precache_urls=urls)
      for url in urls:
          assert f"'{url}'" in code


id: generate_service_worker_py_core_t2
function_id: generate_service_worker_py_core
name: test_custom_network_first_patterns_appear_in_fetch_handler
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_custom_network_first_patterns_appear_in_fetch_handler():
      """Custom network_first_patterns aparecen en el fetch handler."""
      patterns = ["/api/", "/graphql"]
      code = generate_service_worker(network_first_patterns=patterns)
      for pattern in patterns:
          assert f"url.includes('{pattern}')" in code


id: generate_service_worker_py_core_t3
function_id: generate_service_worker_py_core
name: test_cache_name_appears_in_code
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_cache_name_appears_in_code():
      """El cache_name personalizado aparece en el codigo generado."""
      code = generate_service_worker(cache_name="voice-guide-v1")
      assert "voice-guide-v1" in code


id: generate_service_worker_py_core_t4
function_id: generate_service_worker_py_core
name: test_default_values_are_used_when_none
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_default_values_are_used_when_none():
      """Los valores por defecto se usan cuando los parametros son None."""
      code = generate_service_worker()
      assert "'/'" in code
      assert "'/index.html'" in code
      assert "url.includes('/api/')" in code


id: generate_service_worker_py_core_t5
function_id: generate_service_worker_py_core
name: test_returns_valid_string
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_returns_valid_string():
      """La funcion retorna un string no vacio."""
      code = generate_service_worker()
      assert isinstance(code, str)
      assert len(code) > 0


id: generate_service_worker_py_core_t6
function_id: generate_service_worker_py_core
name: test_activate_cleans_old_caches
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_activate_cleans_old_caches():
      """El evento activate limpia caches antiguos."""
      code = generate_service_worker(cache_name="my-cache-v2")
      assert "caches.keys()" in code
      assert "caches.delete(name)" in code
      assert "name !== CACHE_NAME" in code


id: generate_service_worker_py_core_t7
function_id: generate_service_worker_py_core
name: test_cache_first_fallback_saves_to_cache
file_path: python/functions/core/generate_service_worker_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_cache_first_fallback_saves_to_cache():
      """La estrategia cache-first guarda respuestas de red en cache."""
      code = generate_service_worker()
      assert "cache.put(event.request" in code


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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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: match_pois_to_interests_py_core_t0
function_id: match_pois_to_interests_py_core
name: test_pois_que_matchean_parcialmente_por_categoria_y_nombre
file_path: python/functions/core/match_pois_to_interests_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_pois_que_matchean_parcialmente_por_categoria_y_nombre():
      """pois que matchean parcialmente por categoria y nombre"""
      interests = [{"name": "arte", "keywords": ["museo", "museum", "arte", "galería"], "weight": 1.0}]
      result = match_pois_to_interests(POIS_SAMPLE, interests)

      ids = [p["id"] for p in result]
      # museo matchea por category "museum" (0.5) + nombre "Museo" (0.3) + tag "museum" (0.2) = 1.0
      assert 1 in ids
      # galería: sin match de categoria exacta (gallery != keywords), pero nombre contiene "Arte" -> check keywords
      # keywords: "museo", "museum", "arte", "galería" — "arte" en "galería de arte moderno" -> 0.3
      assert 3 in ids
      # bar no matchea ninguno
      assert 2 not in ids
      # el museo debe tener score mas alto que la galeria
      museo = next(p for p in result if p["id"] == 1)
      galeria = next(p for p in result if p["id"] == 3)
      assert museo["score"] > galeria["score"]
      assert "arte" in museo["matched_interests"]
      assert "arte" in galeria["matched_interests"]


id: match_pois_to_interests_py_core_t1
function_id: match_pois_to_interests_py_core
name: test_pois_sin_match_retorna_lista_vacia
file_path: python/functions/core/match_pois_to_interests_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_pois_sin_match_retorna_lista_vacia():
      """pois sin match retorna lista vacia"""
      pois = [
          {"id": 10, "lat": 40.0, "lon": -3.0, "name": "Parking Norte", "category": "parking", "tags": {"amenity": "parking"}},
          {"id": 11, "lat": 40.1, "lon": -3.1, "name": "Gasolinera BP", "category": "fuel", "tags": {"amenity": "fuel"}},
      ]
      interests = [{"name": "arte", "keywords": ["museo", "museum", "galería", "pintura"], "weight": 1.0}]
      result = match_pois_to_interests(pois, interests)
      assert result == []


id: match_pois_to_interests_py_core_t2
function_id: match_pois_to_interests_py_core
name: test_multiples_intereses_con_diferentes_weights_acumulan_score
file_path: python/functions/core/match_pois_to_interests_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_multiples_intereses_con_diferentes_weights_acumulan_score():
      """multiples intereses con diferentes weights acumulan score"""
      pois = [
          {
              "id": 20,
              "lat": 40.5,
              "lon": -3.5,
              "name": "Café Museo",
              "category": "cafe",
              "tags": {"amenity": "cafe", "tourism": "museum"},
          }
      ]
      interests = [
          {"name": "arte", "keywords": ["museum", "museo", "arte"], "weight": 1.0},
          {"name": "gastronomia", "keywords": ["cafe", "restaurant", "bar"], "weight": 0.8},
      ]
      result = match_pois_to_interests(pois, interests)
      assert len(result) == 1
      poi = result[0]
      # arte: nombre "Café Museo" contiene "museo" (0.3) + tag "museum" (0.2) = 0.5 * 1.0 = 0.5
      # gastronomia: category "cafe" in keywords (0.5) + nombre "Café" contiene "cafe" (0.3) = 0.8 * 0.8 = 0.64
      # total ~ 1.14
      assert poi["score"] > 1.0
      assert "arte" in poi["matched_interests"]
      assert "gastronomia" in poi["matched_interests"]
      assert len(poi["matched_interests"]) == 2


id: match_pois_to_interests_py_core_t3
function_id: match_pois_to_interests_py_core
name: test_max_results_limita_resultados
file_path: python/functions/core/match_pois_to_interests_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_max_results_limita_resultados():
      pois = [
          {"id": i, "lat": 40.0, "lon": -3.0, "name": f"Museo {i}", "category": "museum", "tags": {}}
          for i in range(20)
      ]
      interests = [{"name": "arte", "keywords": ["museum", "museo"], "weight": 1.0}]
      result = match_pois_to_interests(pois, interests, max_results=5)
      assert len(result) == 5


id: match_pois_to_interests_py_core_t4
function_id: match_pois_to_interests_py_core
name: test_inputs_no_mutados
file_path: python/functions/core/match_pois_to_interests_test.py
lang: py
created_at: 2026-04-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
code:
  def test_inputs_no_mutados():
      pois = [
          {"id": 1, "lat": 40.0, "lon": -3.0, "name": "Museo A", "category": "museum", "tags": {}}
      ]
      interests = [{"name": "arte", "keywords": ["museum"], "weight": 1.0}]
      original_poi_keys = set(pois[0].keys())
      match_pois_to_interests(pois, interests)
      # el poi original no debe tener score ni matched_interests
      assert set(pois[0].keys()) == original_poi_keys


  if __name__ == "__main__":
      test_pois_que_matchean_parcialmente_por_categoria_y_nombre()
      print("PASS: pois que matchean parcialmente por categoria y nombre")

      test_pois_sin_match_retorna_lista_vacia()
      print("PASS: pois sin match retorna lista vacia")

      test_multiples_intereses_con_diferentes_weights_acumulan_score()
      print("PASS: multiples intereses con diferentes weights acumulan score")

      test_max_results_limita_resultados()
      print("PASS: max_results limita resultados")

      test_inputs_no_mutados()
      print("PASS: inputs no mutados")

      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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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-13T00:00:19Z
updated_at: 2026-04-13T00:00:19Z
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


