# types

id: runner_status_py_core
name: runner_status
lang: py
domain: core
version: 1.0.0
algebraic: sum
description: Maquina de estados para un runner de simulacion o pipeline de larga duracion.
tags: ["simulation","runner","status","state-machine","enum"]
uses_types: []
file_path: python/types/core/runner_status.py
created_at: 2026-04-05T11:18:45Z
updated_at: 2026-04-05T11:18:45Z
content_hash: 2a7bd0e9ea9440ffdf57d5329adfb7b864cc78e3cd55ef10510c4e16d5e59ffc
definition:
  class RunnerStatus(str, Enum):
      IDLE = "idle"
      STARTING = "starting"
      RUNNING = "running"
      PAUSED = "paused"
      STOPPING = "stopping"
      STOPPED = "stopped"
      COMPLETED = "completed"
      FAILED = "failed"

notes:
  Tipo suma — exactamente uno de los estados es activo en cada momento. Hereda de `str` para que sea serializable a JSON directamente.

documentation:
  ## Transiciones validas

  ```
  IDLE -> STARTING -> RUNNING
  RUNNING -> PAUSED -> RUNNING   (resume)
  RUNNING -> STOPPING -> STOPPED
  RUNNING -> COMPLETED
  RUNNING -> FAILED
  STARTING -> FAILED
  ```

code:
  """Estado de un runner de simulacion o pipeline de larga duracion."""

  from enum import Enum


  class RunnerStatus(str, Enum):
      """Maquina de estados para un runner de simulacion o pipeline de larga duracion.

      Transiciones validas:
          IDLE -> STARTING -> RUNNING
          RUNNING -> PAUSED -> RUNNING  (resume)
          RUNNING -> STOPPING -> STOPPED
          RUNNING -> COMPLETED
          RUNNING -> FAILED
          STARTING -> FAILED
      """

      IDLE = "idle"
      STARTING = "starting"
      RUNNING = "running"
      PAUSED = "paused"
      STOPPING = "stopping"
      STOPPED = "stopped"
      COMPLETED = "completed"
      FAILED = "failed"


id: task_py_core
name: task
lang: py
domain: core
version: 1.0.0
algebraic: product
tags: ["task","async","background","progress","tracking","polling"]
uses_types: ["task_status_py_core"]
file_path: python/types/core/task.py
created_at: 2026-04-05T11:18:49Z
updated_at: 2026-04-05T11:18:49Z
content_hash: df66c44f51b008ced6984a4a56b0f76f321ac8c3f67da7afd7a105ac0f6c5837
definition:
  @dataclass
  class Task:
      task_id: str
      task_type: str
      status: TaskStatus
      created_at: datetime
      updated_at: datetime
      progress: int = 0
      message: str = ""
      result: dict | None = None
      error: str | None = None
      metadata: dict = field(default_factory=dict)
      progress_detail: dict = field(default_factory=dict)

description:
  Tarea de larga duracion con seguimiento de progreso. Product type que modela el ciclo de vida completo: creacion, progreso incremental, completado exitoso o fallo. Incluye to_dict() para serializacion JSON con timestamps ISO 8601.

notes:
  El campo `progress` es un entero 0-100. El campo `progress_detail` permite informar detalles granulares (etapa actual, items procesados) sin romper la interfaz del tipo.

  El metodo `to_dict()` serializa `status` como string via `.value` y los timestamps como strings ISO 8601, haciendo el resultado directamente serializable con `json.dumps()`.

  El campo `result` almacena el output de la tarea al completar. El campo `error` almacena la descripcion del fallo. Solo uno de los dos deberia estar poblado en un estado terminal.

code:
  """Task — tarea de larga duracion con seguimiento de progreso."""

  from dataclasses import dataclass, field
  from datetime import datetime, timezone

  from task_status import TaskStatus


  @dataclass
  class Task:
      """Tarea de larga duracion con seguimiento de progreso.

      Attributes:
          task_id:         UUID que identifica la tarea de forma unica.
          task_type:       Tipo libre: "graph_build", "simulation", "report_generation", etc.
          status:          Estado actual del ciclo de vida (TaskStatus).
          created_at:      Timestamp de creacion (UTC).
          updated_at:      Timestamp de ultima actualizacion (UTC).
          progress:        Porcentaje de avance 0-100.
          message:         Mensaje de estado legible por el usuario.
          result:          Resultado serializado al completar, None si no ha completado.
          error:           Descripcion del error al fallar, None si no ha fallado.
          metadata:        Datos arbitrarios asociados a la tarea (parametros de entrada, etc.).
          progress_detail: Detalles granulares de progreso (etapa actual, items procesados, etc.).
      """

      task_id: str
      task_type: str
      status: TaskStatus
      created_at: datetime
      updated_at: datetime
      progress: int = 0
      message: str = ""
      result: dict | None = None
      error: str | None = None
      metadata: dict = field(default_factory=dict)
      progress_detail: dict = field(default_factory=dict)

      def to_dict(self) -> dict:
          """Serializa la tarea a dict con timestamps en formato ISO 8601.

          Returns:
              dict con todos los campos de la tarea listos para JSON.
          """
          return {
              "task_id": self.task_id,
              "task_type": self.task_type,
              "status": self.status.value,
              "created_at": self.created_at.isoformat(),
              "updated_at": self.updated_at.isoformat(),
              "progress": self.progress,
              "message": self.message,
              "result": self.result,
              "error": self.error,
              "metadata": self.metadata,
              "progress_detail": self.progress_detail,
          }


id: task_status_py_core
name: task_status
lang: py
domain: core
version: 1.0.0
algebraic: sum
tags: ["task","status","enum","async","background"]
uses_types: []
file_path: python/types/core/task_status.py
created_at: 2026-04-05T11:18:45Z
updated_at: 2026-04-05T11:18:45Z
content_hash: 54df263cd377decbaf2ce32456315548e85ccd359add15a4ce36b857af7c0664
definition:
  class TaskStatus(str, Enum):
      PENDING = "pending"
      PROCESSING = "processing"
      COMPLETED = "completed"
      FAILED = "failed"

description:
  Estado de una tarea de larga duracion (graph building, simulation, report generation). Enum sum type con cuatro variantes: pending, processing, completed, failed.

notes:
  Hereda de `str` para que los valores sean directamente serializables a JSON sin conversion adicional.
  Usado por `Task` para expresar el ciclo de vida de una tarea background.

code:
  """TaskStatus — estado de una tarea de larga duracion."""

  from enum import Enum


  class TaskStatus(str, Enum):
      """Estado de una tarea de larga duracion.

      Valores posibles:
          PENDING    — creada, en cola.
          PROCESSING — en ejecucion activa.
          COMPLETED  — terminada con exito.
          FAILED     — terminada con error.
      """

      PENDING = "pending"
      PROCESSING = "processing"
      COMPLETED = "completed"
      FAILED = "failed"


id: text_part_py_core
name: text_part
lang: py
domain: core
version: 1.0.0
algebraic: product
definition:
  @dataclass
  class TextPart:
  text: str = ""
  type: str = "text"

tags: ["message","part","text","multipart","llm","conversation"]
uses_types: []
file_path: python/types/core/text_part.py
created_at: 2026-04-05T11:20:17Z
updated_at: 2026-04-05T11:20:17Z
content_hash: 991651a24a877b6fa46c110c0de9c99cbcc0fb8edda72cf485a85a41f444bd90
description:
  Parte de texto plano en un mensaje multipart. Contiene el contenido textual y el discriminador literal 'text' para deserializacion por tipo.

notes:
  El campo `type` actua como discriminador literal en el tipo suma `Part`. Siempre vale `"text"`.

  Es el tipo de parte mas basico: un contenedor simple para texto plano dentro de un `Message`.

code:
  """TextPart — parte de texto de un mensaje multipart."""

  from dataclasses import dataclass, field


  @dataclass
  class TextPart:
      """Parte de texto plano en un mensaje multipart.

      Attributes:
          text: Contenido textual de la parte.
          type: Discriminador literal, siempre "text".
      """

      text: str = ""
      type: str = field(default="text", init=True)


id: tool_part_py_core
name: tool_part
lang: py
domain: core
version: 1.0.0
algebraic: product
tags: ["message","part","tool","tool_call","function_call","multipart","llm","tracing","tokens"]
uses_types: []
file_path: python/types/core/tool_part.py
created_at: 2026-04-05T11:20:41Z
updated_at: 2026-04-05T11:20:41Z
content_hash: 7a30e5aee84e87eb1c92b8d47884b670192e02ec4d8aa95f77175f2d1a2f3907
definition:
  @dataclass
  class ToolPart:
      tool_id: str = ""
      tool_name: str = ""
      tool_input: dict | None = None
      tool_output: str = ""
      tool_status: str = "pending"
      duration_ms: float | None = None
      prompt_tokens: int | None = None
      completion_tokens: int | None = None
      type: str = "tool"

description:
  Registro de una llamada a herramienta (tool call) dentro de un mensaje multipart. Captura input, output, estado del ciclo de vida y metricas de uso de tokens para trazabilidad y presupuesto.

notes:
  `tool_status` modela el ciclo de vida de la llamada: "pending" (encolada), "running" (en ejecucion), "completed" (finalizada con exito), "error" (fallida).

  `tool_input` es un dict arbitrario con los argumentos. `tool_output` es siempre string para mantener compatibilidad con la API de herramientas de LLMs (OpenAI, Anthropic).

  `duration_ms`, `prompt_tokens` y `completion_tokens` son opcionales — se poblan cuando la herramienta instrumenta su ejecucion. Utiles para presupuesto de tokens y profiling.

  El campo `type` actua como discriminador literal en el tipo suma `Part`. Siempre vale `"tool"`.

code:
  """ToolPart — registro de una llamada a herramienta en un mensaje multipart."""

  from dataclasses import dataclass, field


  @dataclass
  class ToolPart:
      """Registro de una llamada a herramienta (tool call) dentro de un mensaje multipart.

      Attributes:
          tool_id:           Identificador unico de la invocacion de la herramienta.
          tool_name:         Nombre de la herramienta invocada.
          tool_input:        Argumentos de entrada pasados a la herramienta.
          tool_output:       Resultado devuelto por la herramienta como string.
          tool_status:       Estado de la llamada: "pending" | "running" | "completed" | "error".
          duration_ms:       Duracion de la llamada en milisegundos.
          prompt_tokens:     Tokens de entrada consumidos por la herramienta (si aplica).
          completion_tokens: Tokens de salida generados por la herramienta (si aplica).
          type:              Discriminador literal, siempre "tool".
      """

      tool_id: str = ""
      tool_name: str = ""
      tool_input: dict | None = None
      tool_output: str = ""
      tool_status: str = "pending"
      duration_ms: float | None = None
      prompt_tokens: int | None = None
      completion_tokens: int | None = None
      type: str = field(default="tool", init=True)


id: typed_query_py_core
name: typed_query
lang: py
domain: core
version: 1.0.0
algebraic: product
tags: ["retrieval","query","rag","intent","search","plan"]
uses_types: ["context_type_py_core"]
file_path: python/types/core/typed_query.py
created_at: 2026-04-05T11:21:03Z
updated_at: 2026-04-05T11:21:03Z
content_hash: 5ef9fba530c4f0be0725c75c3fe048b07e57f49f4911feb37afaa5ada783ecf9
definition:
  @dataclass
  class TypedQuery:
      query: str
      context_type: ContextType | None
      intent: str
      priority: int = 3
      target_directories: list[str] = field(default_factory=list)

description:
  Query tipada que resulta del analisis de intenciones. Un plan de busqueda se descompone en multiples TypedQueries, cada una apuntando a un tipo de contexto diferente.

notes:
  `priority` sigue la convencion 1=alta, 5=baja (similar a niveles de issue). Valor por defecto 3 es prioridad media.

  `context_type = None` indica que la query aplica a todos los tipos de contexto sin filtro. Util para queries generales que el retriever distribuye entre todas las categorias.

  `target_directories` vacio significa buscar en todos los directorios disponibles. Con URIs especificos, el retriever limita la busqueda al subconjunto indicado — util para reducir ruido en colecciones grandes.

code:
  """TypedQuery — query tipada resultante del analisis de intenciones."""

  from __future__ import annotations

  from dataclasses import dataclass, field

  from context_type import ContextType


  @dataclass
  class TypedQuery:
      """Query tipada que resulta del analisis de intenciones.

      Un plan de busqueda se descompone en multiples TypedQueries, cada una
      apuntando a un tipo de contexto diferente. Permite estrategias de
      retrieval especificas por categoria.

      Attributes:
          query:              Texto de la query de busqueda.
          context_type:       Tipo de contexto objetivo (MEMORY, RESOURCE, SKILL) o None para todos.
          intent:             Descripcion de la intencion detras de esta query.
          priority:           Prioridad 1-5 (1 es mayor prioridad).
          target_directories: URIs de directorios donde buscar. Lista vacia = buscar en todos.
      """

      query: str
      context_type: ContextType | None
      intent: str
      priority: int = 3
      target_directories: list[str] = field(default_factory=list)


id: deduplication_result_py_datascience
name: deduplication_result
lang: py
domain: datascience
version: 1.0.0
algebraic: product
tags: ["deduplication","entity","merge","knowledge-graph","nlp","datascience"]
uses_types: ["entity_candidate_py_datascience"]
file_path: python/types/datascience/deduplication_result.py
created_at: 2026-04-05T11:22:32Z
updated_at: 2026-04-05T11:22:32Z
content_hash: 7d4de365b887aa83eba1882ffb31655f9258b0ab610370a03bc0ab0ce07b45e5
definition:
  @dataclass
  class DeduplicationResult:
      entities: list[EntityCandidate]
      entity_id_map: dict[str, str]
      name_to_id: dict[str, str]
      merge_log: list[dict] = field(default_factory=list)
      total_before: int = 0
      total_after: int = 0

description:
  Resultado de deduplicacion de entidades. name_to_id mapea todos los nombres originales (incluyendo aliases mergeados) a su ID final para resolver relaciones.

examples:
  ```python
  from python.types.datascience.deduplication_result import DeduplicationResult
  from python.types.datascience.entity_candidate import EntityCandidate

  result = DeduplicationResult(
      entities=[
          EntityCandidate(
              name="John Smith",
              name_normalized="john smith",
              merged_from=["John Smith", "Smith, John"],
          ),
      ],
      entity_id_map={"john smith": "ent_001"},
      name_to_id={
          "John Smith": "ent_001",
          "Smith, John": "ent_001",
          "john smith": "ent_001",
      },
      merge_log=[
          {"merged": ["John Smith", "Smith, John"], "into": "John Smith", "score": 0.92}
      ],
      total_before=2,
      total_after=1,
  )
  ```

notes:
  El `name_to_id` es la clave para la resolucion de relaciones. Cuando una relacion dice `from_name="Smith, John"`, se puede buscar en `name_to_id` y encontrar el ID `ent_001` aunque el nombre canonico sea `"John Smith"`. `entity_id_map` es un subset que usa solo nombres normalizados, util para lookup durante la misma fase de deduplicacion.

documentation:
  ## Campos

  - `entities`: lista final de `EntityCandidate` despues de deduplicacion
  - `entity_id_map`: mapa `nombre_normalizado → entity_id` para lookup rapido
  - `name_to_id`: mapa de TODOS los nombres originales (incluyendo aliases) a su ID final
  - `merge_log`: lista de eventos de merge, cada uno con `{"merged": [...], "into": "...", "score": 0.92}`
  - `total_before`: numero de entidades antes de deduplicacion
  - `total_after`: numero de entidades despues de deduplicacion

code:
  """DeduplicationResult — resultado del proceso de deduplicacion de entidades."""

  from dataclasses import dataclass, field

  from python.types.datascience.entity_candidate import EntityCandidate


  @dataclass
  class DeduplicationResult:
      """Resultado de deduplicacion de entidades.

      El `name_to_id` mapea TODOS los nombres originales (incluyendo los
      mergeados) a su ID final, permitiendo resolver relaciones que usan
      cualquier variante del nombre.
      """

      entities: list[EntityCandidate]
      entity_id_map: dict[str, str]
      name_to_id: dict[str, str]
      merge_log: list[dict] = field(default_factory=list)
      total_before: int = 0
      total_after: int = 0


id: entity_candidate_py_datascience
name: entity_candidate
lang: py
domain: datascience
version: 1.0.0
algebraic: product
tags: ["extraction","entity","llm","knowledge-graph","nlp","datascience"]
uses_types: []
file_path: python/types/datascience/entity_candidate.py
created_at: 2026-04-05T11:22:16Z
updated_at: 2026-04-05T11:22:16Z
content_hash: 3b4a628f28e455edb147fef5e427e2a45cda45ddf1c7fdf9d342525bcd457c0e
definition:
  @dataclass
  class EntityCandidate:
      name: str
      name_normalized: str = ""
      type_ref: str = ""
      type_label: str = ""
      attributes: dict = field(default_factory=dict)
      confidence: float = 0.0
      source_chunk_indices: list[int] = field(default_factory=list)
      merged_from: list[str] = field(default_factory=list)

description:
  Candidato de entidad extraido por el LLM. Puede venir de un solo chunk o ser el resultado de mergear multiples extracciones. merged_from rastrea los nombres originales para debugging.

examples:
  ```python
  from python.types.datascience.entity_candidate import EntityCandidate

  candidate = EntityCandidate(
      name="John Smith",
      name_normalized="john smith",
      type_ref="osint_person_go_cybersecurity",
      type_label="Person",
      attributes={"role": "CEO", "company": "Acme Corp"},
      confidence=0.92,
      source_chunk_indices=[0, 3],
      merged_from=["John Smith", "Smith, John"],
  )

  print(candidate.to_dict())
  ```

notes:
  Tipo inmutable en su intencion pero declarado como dataclass mutable para permitir que las fases de deduplicacion rellenen `name_normalized` y que el pipeline complete los campos opcionales progresivamente. `merged_from` es la fuente de verdad para auditar que entidades se colapsaron juntas.

documentation:
  ## Campos

  - `name`: nombre crudo extraido del texto
  - `name_normalized`: nombre normalizado para deduplicacion (se llena en la fase de dedup)
  - `type_ref`: registry type ID del tipo de entidad (ej: `osint_person_go_cybersecurity`)
  - `type_label`: label humano del tipo (ej: `"Person"`)
  - `attributes`: metadata extraida del texto como diccionario libre
  - `confidence`: score de confianza 0.0-1.0
  - `source_chunk_indices`: indices de los chunks donde aparecio la entidad
  - `merged_from`: nombres originales si el candidato fue creado por merge de varios

code:
  """EntityCandidate — candidato de entidad extraido por el LLM."""

  from dataclasses import dataclass, field


  @dataclass
  class EntityCandidate:
      """Candidato de entidad extraido por el LLM.

      Puede venir de un solo chunk o ser el resultado de mergear multiples
      extracciones. `merged_from` rastrea los nombres originales para debugging.
      """

      name: str
      name_normalized: str = ""
      type_ref: str = ""
      type_label: str = ""
      attributes: dict = field(default_factory=dict)
      confidence: float = 0.0
      source_chunk_indices: list[int] = field(default_factory=list)
      merged_from: list[str] = field(default_factory=list)

      def to_dict(self) -> dict:
          """Serializa el candidato a un diccionario."""
          return {
              "name": self.name,
              "name_normalized": self.name_normalized,
              "type_ref": self.type_ref,
              "type_label": self.type_label,
              "attributes": self.attributes,
              "confidence": self.confidence,
              "source_chunk_indices": self.source_chunk_indices,
              "merged_from": self.merged_from,
          }


id: extraction_result_py_datascience
name: extraction_result
lang: py
domain: datascience
version: 1.0.0
algebraic: product
tags: ["extraction","result","pipeline","knowledge-graph","datascience"]
file_path: python/types/datascience/extraction_result.py
created_at: 2026-04-05T11:22:24Z
updated_at: 2026-04-05T11:22:24Z
content_hash: 648dba0953c99ddea4782c5b004b6ba087fb6972dd6e1a200b7576a6d897b7b6
definition:
  @dataclass
  class ExtractionResult:
      entities: list[EntityCandidate]
      relations: list[RelationCandidate]
      stats: ExtractionStats = field(default_factory=ExtractionStats)

description:
  Resultado final del pipeline de extraccion. Contiene las listas deduplicadas de entidades y relaciones junto con las estadisticas del proceso completo.

uses_types:
  ["entity_candidate_py_datascience","relation_candidate_py_datascience","extraction_stats_py_datascience"]

examples:
  ```python
  from python.types.datascience.entity_candidate import EntityCandidate
  from python.types.datascience.extraction_result import ExtractionResult
  from python.types.datascience.extraction_stats import ExtractionStats
  from python.types.datascience.relation_candidate import RelationCandidate

  result = ExtractionResult(
      entities=[
          EntityCandidate(name="John Smith", type_label="Person", confidence=0.9),
      ],
      relations=[
          RelationCandidate(
              from_name="John Smith",
              to_name="Acme Corp",
              from_id="ent_001",
              to_id="ent_002",
              relation_type="works_at",
          ),
      ],
      stats=ExtractionStats(total_chunks=10, final_entities_count=1, final_relations_count=1),
  )
  ```

notes:
  Tipo de salida del pipeline completo de extraccion. Agrupa todo lo necesario para persistir el resultado en una base de datos de grafos o para pasarlo a la siguiente fase del pipeline (enriquecimiento, visualizacion, etc.).

documentation:
  ## Campos

  - `entities`: lista final de `EntityCandidate` deduplicados
  - `relations`: lista final de `RelationCandidate` deduplicadas con IDs resueltos
  - `stats`: `ExtractionStats` con metricas del proceso completo

code:
  """ExtractionResult — resultado final del pipeline de extraccion."""

  from dataclasses import dataclass, field

  from python.types.datascience.entity_candidate import EntityCandidate
  from python.types.datascience.extraction_stats import ExtractionStats
  from python.types.datascience.relation_candidate import RelationCandidate


  @dataclass
  class ExtractionResult:
      """Resultado final del pipeline de extraccion de entidades y relaciones.

      Contiene las listas deduplicadas de entidades y relaciones junto con
      las estadisticas del proceso completo.
      """

      entities: list[EntityCandidate]
      relations: list[RelationCandidate]
      stats: ExtractionStats = field(default_factory=ExtractionStats)


id: extraction_stats_py_datascience
name: extraction_stats
lang: py
domain: datascience
version: 1.0.0
algebraic: product
tags: ["extraction","stats","reporting","debugging","datascience"]
uses_types: []
file_path: python/types/datascience/extraction_stats.py
created_at: 2026-04-05T11:22:16Z
updated_at: 2026-04-05T11:22:16Z
content_hash: 653c9fd93a92be02e97bd1cbfd55b12487e03725753528c876faa6bd071df0a6
definition:
  @dataclass
  class ExtractionStats:
      total_chunks: int = 0
      total_chars: int = 0
      raw_entities_count: int = 0
      final_entities_count: int = 0
      entities_merged: int = 0
      raw_relations_count: int = 0
      final_relations_count: int = 0
      relations_merged: int = 0
      relations_discarded: int = 0
      entity_types_found: dict[str, int] = field(default_factory=dict)
      relation_types_found: dict[str, int] = field(default_factory=dict)
      processing_time_seconds: float = 0.0

description:
  Estadisticas del proceso de extraccion de entidades y relaciones. Registra conteos antes y despues de deduplicacion, tiempo de procesamiento y distribucion de tipos.

examples:
  ```python
  from python.types.datascience.extraction_stats import ExtractionStats

  stats = ExtractionStats(
      total_chunks=42,
      total_chars=85000,
      raw_entities_count=120,
      final_entities_count=34,
      entities_merged=86,
      raw_relations_count=200,
      final_relations_count=67,
      relations_discarded=5,
      entity_types_found={"Person": 18, "Organization": 16},
      processing_time_seconds=12.4,
  )
  ```

notes:
  Acumulable progresivamente durante el pipeline. Util para detectar documentos con baja densidad de informacion (pocos entities por chunk) o pipelines lentos. El ratio `entities_merged / raw_entities_count` indica que tan redundante fue el texto fuente.

documentation:
  ## Campos

  - `total_chunks`: numero total de chunks procesados
  - `total_chars`: caracteres totales procesados
  - `raw_entities_count`: entidades extraidas antes de deduplicacion
  - `final_entities_count`: entidades restantes despues de deduplicacion
  - `entities_merged`: cuantas entidades se colapsaron en otras
  - `raw_relations_count`: relaciones extraidas antes de deduplicacion
  - `final_relations_count`: relaciones restantes despues de deduplicacion
  - `relations_merged`: relaciones duplicadas que se colapsaron
  - `relations_discarded`: self-loops o relaciones sin match de entidad descartadas
  - `entity_types_found`: mapa `type_ref → count` de entidades por tipo
  - `relation_types_found`: mapa `relation_type → count` de relaciones por tipo
  - `processing_time_seconds`: tiempo total del pipeline en segundos

code:
  """ExtractionStats — estadisticas del proceso de extraccion."""

  from dataclasses import dataclass, field


  @dataclass
  class ExtractionStats:
      """Estadisticas del proceso de extraccion.

      Util para reporting y debugging. Registra conteos antes y despues de
      deduplicacion, tiempo de procesamiento y distribucion de tipos encontrados.
      """

      total_chunks: int = 0
      total_chars: int = 0
      raw_entities_count: int = 0
      final_entities_count: int = 0
      entities_merged: int = 0
      raw_relations_count: int = 0
      final_relations_count: int = 0
      relations_merged: int = 0
      relations_discarded: int = 0
      entity_types_found: dict[str, int] = field(default_factory=dict)
      relation_types_found: dict[str, int] = field(default_factory=dict)
      processing_time_seconds: float = 0.0


id: relation_candidate_py_datascience
name: relation_candidate
lang: py
domain: datascience
version: 1.0.0
algebraic: product
tags: ["extraction","relation","llm","knowledge-graph","nlp","datascience"]
uses_types: []
file_path: python/types/datascience/relation_candidate.py
created_at: 2026-04-05T11:22:16Z
updated_at: 2026-04-05T11:22:16Z
content_hash: 16ae73b1752306e04dae0fea236f4897098fcd347e30be51bdf795430996008f
definition:
  @dataclass
  class RelationCandidate:
      from_name: str
      to_name: str
      from_id: str = ""
      to_id: str = ""
      relation_type: str = ""
      description: str = ""
      confidence: float = 0.0
      source_chunk_index: int = -1

description:
  Candidato de relacion entre dos entidades extraido por el LLM. from_id y to_id se resuelven durante la deduplicacion.

examples:
  ```python
  from python.types.datascience.relation_candidate import RelationCandidate

  rel = RelationCandidate(
      from_name="John Smith",
      to_name="Acme Corp",
      relation_type="works_at",
      description="John Smith es CEO de Acme Corp",
      confidence=0.88,
      source_chunk_index=2,
  )

  print(rel.to_dict())
  ```

notes:
  Los campos `from_id` y `to_id` empiezan vacios y se resuelven contra el `name_to_id` del `DeduplicationResult`. Relaciones donde no se puede resolver alguno de los IDs se descartan y se cuentan en `ExtractionStats.relations_discarded`. Los self-loops (`from_id == to_id`) tambien se descartan.

documentation:
  ## Campos

  - `from_name`: nombre crudo de la entidad origen tal como aparece en el texto
  - `to_name`: nombre crudo de la entidad destino
  - `from_id`: entity ID resuelto (se llena en la fase de deduplicacion)
  - `to_id`: entity ID resuelto del destino
  - `relation_type`: tipo de relacion segun los presets del dominio
  - `description`: descripcion textual libre de la relacion
  - `confidence`: score de confianza 0.0-1.0
  - `source_chunk_index`: indice del chunk del que se extrajo (-1 si es desconocido)

code:
  """RelationCandidate — candidato de relacion extraido por el LLM."""

  from dataclasses import dataclass


  @dataclass
  class RelationCandidate:
      """Candidato de relacion entre dos entidades extraido por el LLM.

      `from_name` y `to_name` contienen los nombres crudos del texto. `from_id`
      y `to_id` se llenan durante la fase de deduplicacion cuando se resuelven
      contra los EntityCandidate finales.
      """

      from_name: str
      to_name: str
      from_id: str = ""
      to_id: str = ""
      relation_type: str = ""
      description: str = ""
      confidence: float = 0.0
      source_chunk_index: int = -1

      def to_dict(self) -> dict:
          """Serializa el candidato a un diccionario."""
          return {
              "from_name": self.from_name,
              "to_name": self.to_name,
              "from_id": self.from_id,
              "to_id": self.to_id,
              "relation_type": self.relation_type,
              "description": self.description,
              "confidence": self.confidence,
              "source_chunk_index": self.source_chunk_index,
          }


id: score_distribution_py_datascience
name: score_distribution
lang: py
domain: datascience
version: 1.0.0
algebraic: product
tags: ["retrieval","score","distribution","statistics","debugging","rag"]
uses_types: []
file_path: python/types/datascience/score_distribution.py
created_at: 2026-04-05T11:21:35Z
updated_at: 2026-04-05T11:21:35Z
content_hash: f3ef867ae93759d697915548bad451d12fb969bde50e461cea0eb959f6efebf1
definition:
  @dataclass
  class ScoreDistribution:
      scores: list[tuple[str, float]]
      min_score: float = 0.0
      max_score: float = 0.0
      mean_score: float = 0.0
      threshold: float = 0.0

description:
  Estadisticas de distribucion de scores para visualizacion y debugging de retrieval. Incluye classmethod from_scores() que calcula stats automaticamente y to_dict() con count y above_threshold.

notes:
  `from_scores(uri_scores, threshold)` es el constructor principal. Ordena los scores de mayor a menor y calcula min, max y mean automaticamente. El parametro `threshold` solo se almacena en el campo del mismo nombre — el filtrado real lo aplica el retriever.

  `to_dict()` incluye `count` (total de items) y `above_threshold` (items con score >= threshold). Estos campos adicionales facilitan el logging y la observabilidad sin necesidad de recalcular en el consumidor.

  Los scores estan en rango 0.0-1.0 tipicamente, aunque el tipo no lo valida — la responsabilidad de normalizacion es del retriever.

code:
  """ScoreDistribution — estadisticas de distribucion de scores para retrieval."""

  from __future__ import annotations

  from dataclasses import dataclass, field


  @dataclass
  class ScoreDistribution:
      """Estadisticas de distribucion de scores para visualizacion y debugging de retrieval.

      Attributes:
          scores:     Lista de tuplas (uri, score) ordenadas de mayor a menor score.
          min_score:  Score minimo en la distribucion.
          max_score:  Score maximo en la distribucion.
          mean_score: Score medio de la distribucion.
          threshold:  Umbral aplicado para filtrar resultados.
      """

      scores: list[tuple[str, float]] = field(default_factory=list)
      min_score: float = 0.0
      max_score: float = 0.0
      mean_score: float = 0.0
      threshold: float = 0.0

      @classmethod
      def from_scores(
          cls,
          uri_scores: list[tuple[str, float]],
          threshold: float = 0.0,
      ) -> ScoreDistribution:
          """Construye ScoreDistribution calculando estadisticas desde una lista de scores.

          Args:
              uri_scores: Lista de tuplas (uri, score), no necesariamente ordenada.
              threshold:  Umbral de inclusion. Solo afecta el campo `threshold` del resultado.

          Returns:
              ScoreDistribution con scores ordenados desc y estadisticas calculadas.
          """
          if not uri_scores:
              return cls(scores=[], threshold=threshold)

          sorted_scores = sorted(uri_scores, key=lambda x: x[1], reverse=True)
          raw_scores = [s for _, s in sorted_scores]

          return cls(
              scores=sorted_scores,
              min_score=min(raw_scores),
              max_score=max(raw_scores),
              mean_score=sum(raw_scores) / len(raw_scores),
              threshold=threshold,
          )

      def to_dict(self) -> dict:
          """Serializa la distribucion a dict incluyendo count y above_threshold.

          Returns:
              dict con scores, stats, count total y count de scores sobre el umbral.
          """
          above = [s for _, s in self.scores if s >= self.threshold]
          return {
              "scores": [{"uri": uri, "score": score} for uri, score in self.scores],
              "min_score": self.min_score,
              "max_score": self.max_score,
              "mean_score": self.mean_score,
              "threshold": self.threshold,
              "count": len(self.scores),
              "above_threshold": len(above),
          }


id: BQClient_py_infra
name: BQClient
lang: py
domain: infra
version: 1.0.0
algebraic: product
definition:
  @dataclass
  class BQClient:
  project_id: str
  _client: bigquery.Client

tags: ["bigquery","gcp","client","google-cloud"]
uses_types: []
file_path: python/types/infra/bq_client.py
created_at: 2026-04-07T09:56:37Z
updated_at: 2026-04-07T09:56:37Z
content_hash: 73b45291163bf58bfe764258322ce6d2cb8e30928e4be59347fb7c8dc013a34e
description:
  Cliente para Google BigQuery. Contiene el project_id y el cliente oficial del SDK. Se obtiene con bq_auth().

examples:
  ```python
  from bigquery import bq_auth, BQClient

  client: BQClient = bq_auth("my-project")
  print(client.project_id)  # "my-project"
  client.close()
  ```

notes:
  Wrapper sobre `google.cloud.bigquery.Client`. El campo `_client` es interno y no serializable.
  Se usa como context manager: `with bq_auth() as client: ...`

code:
  """Tipo BQClient para Google BigQuery."""

  from dataclasses import dataclass, field

  # Nota: la implementacion real esta en python/functions/bigquery/client.py
  # Este archivo existe solo como referencia del tipo para el registry.

  @dataclass
  class BQClient:
      """Cliente para Google BigQuery.

      Attributes:
          project_id: ID del proyecto GCP.
          _client: Cliente oficial de BigQuery (interno, no serializable).
      """
      project_id: str
      _client: object = field(repr=False, default=None)


id: classified_file_py_infra
name: classified_file
lang: py
domain: infra
version: 1.0.0
algebraic: product
definition:
  @dataclass
  class ClassifiedFile:
  path: str
  rel_path: str
  classification: str

tags: ["file","directory","scan","classification","infra"]
uses_types: []
file_path: python/types/infra/classified_file.py
created_at: 2026-04-05T11:19:41Z
updated_at: 2026-04-05T11:19:41Z
content_hash: 8d988b0df4dee410165203fe2f5783825d504adbd41a9c1005d52d7fdf0e55da
description:
  Archivo clasificado durante un escaneo de directorio. Contiene el path absoluto, el path relativo a la raiz del escaneo (con forward slashes), y la clasificacion: 'processable' o 'unsupported'.

examples:
  ```python
  from classified_file import ClassifiedFile

  f = ClassifiedFile(
      path="/home/user/docs/report.pdf",
      rel_path="docs/report.pdf",
      classification="processable",
  )
  ```

notes:
  Tipo producto inmutable usado como elemento de `DirectoryScanResult.processable` y `DirectoryScanResult.unsupported`. El campo `rel_path` siempre usa forward slashes independientemente del OS para garantizar consistencia cross-platform.

documentation:
  ## Campos

  - `path` — path absoluto del archivo en el sistema de archivos.
  - `rel_path` — path relativo a la raiz del escaneo, siempre con forward slashes (`/`).
  - `classification` — `"processable"` si la extension esta entre las soportadas, `"unsupported"` en caso contrario.

code:
  """ClassifiedFile — archivo clasificado durante un escaneo de directorio."""

  from dataclasses import dataclass


  @dataclass
  class ClassifiedFile:
      """Archivo clasificado durante un escaneo de directorio.

      Contiene el path absoluto, el path relativo a la raiz del escaneo,
      y la clasificacion del archivo segun su extension.
      """

      path: str
      rel_path: str
      classification: str  # "processable" | "unsupported"


id: directory_scan_result_py_infra
name: directory_scan_result
lang: py
domain: infra
version: 1.0.0
algebraic: product
tags: ["directory","scan","filesystem","result","infra"]
uses_types: ["classified_file_py_infra"]
file_path: python/types/infra/directory_scan_result.py
created_at: 2026-04-05T11:20:13Z
updated_at: 2026-04-05T11:20:18Z
content_hash: e66fec59793d2c103a8384d05c8e4b1a6ad22c27f07d583830b09e9b1427dea6
definition:
  @dataclass
  class DirectoryScanResult:
      root: str
      processable: list[ClassifiedFile] = field(default_factory=list)
      unsupported: list[ClassifiedFile] = field(default_factory=list)
      skipped: list[str] = field(default_factory=list)
      warnings: list[str] = field(default_factory=list)

description:
  Resultado de un escaneo de arbol de directorios. Agrupa archivos procesables, no soportados, paths saltados (con razon) y warnings generados durante el escaneo.

examples:
  ```python
  from scan_directory import scan_directory

  result = scan_directory("/data/docs", supported_extensions={".pdf", ".md"})
  print(f"Procesables: {len(result.processable)}")
  print(f"No soportados: {len(result.unsupported)}")
  for s in result.skipped:
      print(f"  Saltado: {s}")
  ```

notes:
  Producido exclusivamente por `scan_directory`. Los campos `processable` y `unsupported` estan ordenados por `rel_path`. El campo `skipped` incluye tanto directorios podados como archivos individuales ignorados (dot files, symlinks, archivos vacios).

documentation:
  ## Campos

  - `root` — path absoluto del directorio raiz escaneado.
  - `processable` — lista de `ClassifiedFile` con clasificacion `"processable"`.
  - `unsupported` — lista de `ClassifiedFile` con clasificacion `"unsupported"`.
  - `skipped` — lista de strings con formato `"path (reason)"` para archivos/dirs omitidos.
  - `warnings` — mensajes de advertencia no fatales generados durante el escaneo.

code:
  """DirectoryScanResult — resultado de un escaneo de arbol de directorios."""

  from dataclasses import dataclass, field

  from classified_file import ClassifiedFile


  @dataclass
  class DirectoryScanResult:
      """Resultado de un escaneo de arbol de directorios.

      Agrupa los archivos procesables, no soportados, los paths saltados
      y los warnings generados durante el escaneo.
      """

      root: str
      processable: list[ClassifiedFile] = field(default_factory=list)
      unsupported: list[ClassifiedFile] = field(default_factory=list)
      skipped: list[str] = field(default_factory=list)   # "path (reason)"
      warnings: list[str] = field(default_factory=list)

      def all_processable_files(self) -> list[ClassifiedFile]:
          """Alias de processable para una API mas clara."""
          return self.processable


id: pdf_doc_py_infra
name: pdf_doc
lang: py
domain: infra
version: 1.0.0
algebraic: product
tags: ["pdf","document","builder","fpdf2","infra"]
uses_types: []
file_path: python/types/infra/pdf_doc.py
created_at: 2026-04-13T00:06:12Z
updated_at: 2026-04-13T00:06:12Z
content_hash: 6bc2a49ad60d4315563e1552be255369eca3d04b3788d385b4af7b4eaf3b0030
definition:
  @dataclass
  class PDFDoc:
      fpdf: Any = None
      pages: list = field(default_factory=list)
      title: str = ""
      author: str = ""
      subject: str = ""
      default_font: str = "Helvetica"
      margin_left: float = 20.0
      margin_right: float = 20.0
      margin_top: float = 20.0
      margin_bottom: float = 20.0
      header_text: str = ""
      footer_text: str = ""
      page_numbers: bool = False

description:
  Wrapper de documento PDF generado con fpdf2. Contiene el objeto FPDF interno con el estado mutable del documento y la configuracion de metadatos, margenes, header y footer.

examples:
  ```python
  from pdf_doc import PDFDoc

  # El doc se crea via pdf_create, no directamente
  # doc = pdf_create(title="Mi Reporte", author="Agente")
  # doc.title  =>  "Mi Reporte"
  # doc.fpdf   =>  <fpdf2.FPDF object>
  ```

notes:
  Este tipo es el carrier del estado del builder. Todas las funciones `pdf_add_*` reciben un `PDFDoc` y retornan uno modificado (API fluent). El campo `fpdf` contiene el estado mutable — no es thread-safe. Creado exclusivamente via `pdf_create_py_infra`.

documentation:
  ## Campos

  - `fpdf` — objeto `fpdf2.FPDF` con el estado interno del documento. Se inicializa en `pdf_create`. No serializable directamente — usar `pdf_save` para obtener bytes o archivo.
  - `pages` — lista de dicts con metadata de cada pagina añadida: `{orientation, width, height}`.
  - `title` — titulo del documento (metadata PDF, visible en propiedades del PDF reader).
  - `author` — autor del documento (metadata PDF).
  - `subject` — asunto del documento (metadata PDF).
  - `default_font` — fuente por defecto para texto: `Helvetica`, `Times`, `Courier`. Por defecto `Helvetica`.
  - `margin_left`, `margin_right`, `margin_top`, `margin_bottom` — margenes del documento en milimetros. Por defecto `20.0` mm cada uno.
  - `header_text` — texto a mostrar en el header de cada pagina. Vacio = sin header.
  - `footer_text` — texto del footer. Acepta `{n}` y `{total}` como placeholders. Vacio = sin footer.
  - `page_numbers` — si `True`, incluye numero de pagina en el footer.

code:
  """PDFDoc — wrapper de documento PDF sobre fpdf2."""

  from dataclasses import dataclass, field
  from typing import Any


  @dataclass
  class PDFDoc:
      """Wrapper de documento PDF generado con fpdf2.

      Contiene el objeto FPDF interno con el estado del documento
      y la configuracion de metadatos y margenes del documento.
      La lista `pages` registra las paginas añadidas como metadata.
      """

      fpdf: Any = None            # objeto fpdf2.FPDF — estado mutable del documento
      pages: list = field(default_factory=list)  # lista de dicts con metadata de cada pagina
      title: str = ""
      author: str = ""
      subject: str = ""
      default_font: str = "Helvetica"
      margin_left: float = 20.0    # mm
      margin_right: float = 20.0   # mm
      margin_top: float = 20.0     # mm
      margin_bottom: float = 20.0  # mm
      header_text: str = ""        # texto del header recurrente (vacio = sin header)
      footer_text: str = ""        # texto del footer recurrente (vacio = sin footer)
      page_numbers: bool = False   # mostrar numeracion de pagina en footer


id: pdf_page_py_infra
name: pdf_page
lang: py
domain: infra
version: 1.0.0
algebraic: product
tags: ["pdf","page","layout","infra"]
uses_types: []
file_path: python/types/infra/pdf_page.py
created_at: 2026-04-13T00:06:12Z
updated_at: 2026-04-13T00:06:12Z
content_hash: 74205184764f54038b8a4c1033f0c58f1a9dd69be695de66d84e88662eee6cc2
definition:
  @dataclass
  class PDFPage:
      width: float = 210.0
      height: float = 297.0
      orientation: str = "portrait"

description:
  Configuracion de una pagina dentro de un documento PDF. Define dimensiones y orientacion en milimetros. Por defecto A4 vertical (210 x 297 mm).

examples:
  ```python
  from pdf_page import PDFPage

  # A4 vertical (defecto)
  pagina_a4 = PDFPage()

  # A4 horizontal
  pagina_horizontal = PDFPage(width=297.0, height=210.0, orientation="landscape")

  # Carta americana
  carta = PDFPage(width=215.9, height=279.4)
  ```

notes:
  Tipo producto de solo configuracion. No tiene comportamiento — es metadata de layout. Las dimensiones se pasan a fpdf2 al llamar `add_page()`.

documentation:
  ## Campos

  - `width` — ancho de la pagina en milimetros. A4 = 210.0, Letter = 215.9. Por defecto `210.0`.
  - `height` — alto de la pagina en milimetros. A4 = 297.0, Letter = 279.4. Por defecto `297.0`.
  - `orientation` — orientacion de la pagina: `portrait` (vertical) o `landscape` (horizontal). Por defecto `portrait`.

code:
  """PDFPage — configuracion de pagina para PDF."""

  from dataclasses import dataclass


  @dataclass
  class PDFPage:
      """Configuracion de una pagina dentro de un documento PDF.

      Define dimensiones y orientacion. Las medidas son en milimetros.
      Por defecto A4 vertical (210 x 297 mm).
      """

      width: float = 210.0        # mm — A4 ancho
      height: float = 297.0       # mm — A4 alto
      orientation: str = "portrait"  # portrait, landscape


id: pdf_style_py_infra
name: pdf_style
lang: py
domain: infra
version: 1.0.0
algebraic: product
tags: ["pdf","style","typography","infra"]
uses_types: []
file_path: python/types/infra/pdf_style.py
created_at: 2026-04-13T00:06:12Z
updated_at: 2026-04-13T00:06:12Z
content_hash: 732780620cbec547dc6d23062dff1b78aa5bcfab21c900e8f837b7d52d23780f
definition:
  @dataclass
  class PDFStyle:
      font_family: str = "Helvetica"
      font_size: float = 12.0
      color: str = "#000000"
      alignment: str = "left"
      line_height: float = 1.2
      margin_top: float = 0.0
      margin_bottom: float = 2.0

description:
  Configuracion de estilo de texto para generacion de PDF. Agrupa fuente, tamaño, color, alineacion y margenes verticales para aplicar a bloques de texto y celdas de tabla.

examples:
  ```python
  from pdf_style import PDFStyle

  # Titulo grande centrado
  titulo = PDFStyle(font_family="Helvetica-Bold", font_size=22.0, alignment="center")

  # Texto de pie en gris
  pie = PDFStyle(font_size=9.0, color="#666666")

  # Cuerpo justificado con margen
  cuerpo = PDFStyle(font_size=11.0, alignment="justify", margin_bottom=4.0)
  ```

notes:
  Tipo producto inmutable por convencion. Las fuentes built-in de fpdf2 (`Helvetica`, `Times`, `Courier` y sus variantes `-Bold`, `-Oblique`, `-BoldOblique`) no requieren archivos .ttf externos. Para fuentes custom, registrarlas en fpdf2 antes de usarlas.

documentation:
  ## Campos

  - `font_family` — nombre de fuente fpdf2: `Helvetica`, `Helvetica-Bold`, `Helvetica-Oblique`, `Times`, `Courier`. Por defecto `Helvetica`.
  - `font_size` — tamaño en puntos tipograficos. Por defecto `12.0`.
  - `color` — color hex RGB (ej: `"#000000"` negro, `"#FF0000"` rojo). Por defecto negro.
  - `alignment` — alineacion horizontal del texto: `left`, `center`, `right`, `justify`. Por defecto `left`.
  - `line_height` — multiplicador de interlineado respecto al tamaño de fuente. `1.2` da un interlineado comodo.
  - `margin_top` — espacio vertical en mm antes del bloque de texto. Por defecto `0.0`.
  - `margin_bottom` — espacio vertical en mm despues del bloque de texto. Por defecto `2.0`.

code:
  """PDFStyle — configuracion de estilo de texto para PDF."""

  from dataclasses import dataclass, field


  @dataclass
  class PDFStyle:
      """Configuracion de estilo de texto para generacion de PDF.

      Agrupa fuente, tamaño, color, alineacion y margenes verticales
      para aplicar a bloques de texto y celdas de tabla.
      """

      font_family: str = "Helvetica"
      font_size: float = 12.0
      color: str = "#000000"      # hex color RGB
      alignment: str = "left"     # left, center, right, justify
      line_height: float = 1.2
      margin_top: float = 0.0     # mm de margen antes del bloque
      margin_bottom: float = 2.0  # mm de margen despues del bloque


# proposals

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-15T00:04:14Z
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.


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-15T00:04:14Z
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_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-15T00:04:14Z
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_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-15T00:04:14Z
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_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-15T00:04:14Z
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_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-15T00:04:14Z
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_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-15T00:04:14Z
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_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-15T00:04:14Z
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.


# 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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
content_hash: 83544a964e6fdd49ce628466ffe96492fc5f157bd1d98a08ce4c58d9e08ff92c
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/pipeline_launcher
notes:
  Aplicacion TUI que lista pipelines con tag `launcher` del registry, permite ejecutarlos y muestra historial de ejecuciones desde operations.db.


id: script_navegador_go_infra
name: script_navegador
lang: go
domain: infra
tags: ["cdp","chrome","browser","automation","yaml"]
uses_types: []
entry_point: main.go
dir_path: apps/script_navegador
created_at: 2026-04-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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: registry_dashboard_cpp_tui
name: registry_dashboard
lang: cpp
domain: tui
tags: ["dashboard","imgui","visualization","registry","http"]
uses_types: []
framework: imgui
entry_point: main.cpp
dir_path: projects/fn_monitoring/apps/registry_dashboard
created_at: 2026-04-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
content_hash: 3dedfc411d841a23336740af0b561b4787df99afe40e10ff5abea2d666428c1d
repo_url: https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/registry_dashboard
project_id: fn_monitoring
description:
  Dashboard ImGui para visualizar el estado del fn_registry. Consume datos via sqlite_api HTTP (fallback a SQLite directo). 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++ con dos modos de acceso a datos:

  1. **HTTP API** (primario): Conecta a `sqlite_api` via HTTP para obtener datos de registry.db. No requiere acceso al filesystem.
  2. **SQLite directo** (fallback): Lee registry.db directamente si la API no esta disponible.

  **Data layers:**
  - `data_http.cpp`: Carga datos via HTTP POST a sqlite_api (cpp-httplib + nlohmann/json)
  - `data.cpp`: Carga directa desde SQLite C API

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

  ## 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
  # Via API (default, intenta conectar a localhost:8484)
  ./registry_dashboard

  # API explicita
  ./registry_dashboard --api http://192.168.1.10:8484

  # Con SQLite fallback
  ./registry_dashboard --api http://127.0.0.1:8484 /path/to/registry.db

  # Solo SQLite (sin API)
  ./registry_dashboard /path/to/registry.db

  # Windows (PowerShell)
  .\registry_dashboard.ps1
  ```

  ## Dependencias vendored

  | Libreria | Version | Archivo |
  |----------|---------|---------|
  | cpp-httplib | v0.18.3 | vendor/httplib.h |
  | nlohmann/json | v3.11.3 | vendor/nlohmann/json.hpp |

  ## Roadmap

  - [ ] Filtros interactivos por lenguaje/dominio en sidebar
  - [ ] Busqueda FTS5 integrada via API
  - [ ] Detalles de funcion al hacer click en tabla

notes:
  - Por defecto intenta conectar a sqlite_api en `http://127.0.0.1:8484`. Si falla, usa SQLite directo.
  - SQLite compilado estaticamente en Windows via amalgamation vendoreada. En Linux usa libsqlite3 del sistema.
  - cpp-httplib usa sockets nativos (no OpenSSL) — solo HTTP, no HTTPS.


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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-15T00:01:11Z
updated_at: 2026-04-15T00:01:11Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: assert_contains_all_go_core_t0
function_id: assert_contains_all_go_core
name: TestAssertContainsAll
file_path: functions/core/assert_contains_all_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestAssertContainsAll(t *testing.T) {
  	t.Run("todos presentes retorna true y nil", func(t *testing.T) {
  		ok, missing := AssertContainsAll([]string{"a", "b", "c"}, []string{"a", "b"})
  		if !ok || missing != nil {
  			t.Errorf("got ok=%v missing=%v, want ok=true missing=nil", ok, missing)
  		}
  	})

  	t.Run("un elemento faltante retorna false con el faltante", func(t *testing.T) {
  		ok, missing := AssertContainsAll([]string{"a", "b"}, []string{"a", "c"})
  		if ok {
  			t.Error("expected ok=false")
  		}
  		if len(missing) != 1 || missing[0] != "c" {
  			t.Errorf("got missing=%v, want [c]", missing)
  		}
  	})

  	t.Run("multiples faltantes retornan todos en missing", func(t *testing.T) {
  		ok, missing := AssertContainsAll([]string{"a"}, []string{"b", "c", "d"})
  		if ok {
  			t.Error("expected ok=false")
  		}
  		if len(missing) != 3 {
  			t.Errorf("got missing=%v, want [b c d]", missing)
  		}
  	})

  	t.Run("needles vacio retorna true", func(t *testing.T) {
  		ok, missing := AssertContainsAll([]string{"a", "b"}, []string{})
  		if !ok || missing != nil {
  			t.Errorf("got ok=%v missing=%v, want ok=true missing=nil", ok, missing)
  		}
  	})

  	t.Run("haystack vacio con needles no vacios retorna todos como faltantes", func(t *testing.T) {
  		ok, missing := AssertContainsAll([]string{}, []string{"x", "y"})
  		if ok {
  			t.Error("expected ok=false")
  		}
  		if len(missing) != 2 {
  			t.Errorf("got missing=%v, want [x y]", missing)
  		}
  	})

  	t.Run("duplicados en haystack no afectan el resultado", func(t *testing.T) {
  		ok, missing := AssertContainsAll([]string{"a", "a", "b", "b"}, []string{"a", "b"})
  		if !ok || missing != nil {
  			t.Errorf("got ok=%v missing=%v, want ok=true missing=nil", ok, missing)
  		}
  	})
  }


id: assert_json_equal_go_core_t0
function_id: assert_json_equal_go_core
name: TestAssertJSONEqual
file_path: functions/core/assert_json_equal_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestAssertJSONEqual(t *testing.T) {
  	t.Run("json identico retorna true y diff vacio", func(t *testing.T) {
  		ok, diff := AssertJSONEqual([]byte(`{"a":1}`), []byte(`{"a":1}`))
  		if !ok || diff != "" {
  			t.Errorf("got ok=%v diff=%q, want ok=true diff=\"\"", ok, diff)
  		}
  	})

  	t.Run("objetos con claves en distinto orden son iguales", func(t *testing.T) {
  		ok, diff := AssertJSONEqual(
  			[]byte(`{"a":1,"b":2}`),
  			[]byte(`{"b":2,"a":1}`),
  		)
  		if !ok || diff != "" {
  			t.Errorf("got ok=%v diff=%q, want ok=true diff=\"\"", ok, diff)
  		}
  	})

  	t.Run("valores distintos retorna false con diff", func(t *testing.T) {
  		ok, diff := AssertJSONEqual([]byte(`{"a":1}`), []byte(`{"a":2}`))
  		if ok {
  			t.Error("expected ok=false for different values")
  		}
  		if diff == "" {
  			t.Error("expected non-empty diff")
  		}
  	})

  	t.Run("json invalido en expected retorna false con mensaje de error", func(t *testing.T) {
  		ok, diff := AssertJSONEqual([]byte(`{not json}`), []byte(`{"a":1}`))
  		if ok {
  			t.Error("expected ok=false for invalid expected JSON")
  		}
  		if diff == "" {
  			t.Error("expected error message in diff")
  		}
  	})

  	t.Run("json invalido en actual retorna false con mensaje de error", func(t *testing.T) {
  		ok, diff := AssertJSONEqual([]byte(`{"a":1}`), []byte(`{not json}`))
  		if ok {
  			t.Error("expected ok=false for invalid actual JSON")
  		}
  		if diff == "" {
  			t.Error("expected error message in diff")
  		}
  	})

  	t.Run("arrays con mismo contenido son iguales", func(t *testing.T) {
  		ok, diff := AssertJSONEqual([]byte(`[1,2,3]`), []byte(`[1,2,3]`))
  		if !ok || diff != "" {
  			t.Errorf("got ok=%v diff=%q, want ok=true diff=\"\"", ok, diff)
  		}
  	})

  	t.Run("arrays con distinto orden son distintos", func(t *testing.T) {
  		ok, diff := AssertJSONEqual([]byte(`[1,2,3]`), []byte(`[3,2,1]`))
  		if ok {
  			t.Error("expected ok=false for different array order")
  		}
  		if diff == "" {
  			t.Error("expected non-empty diff for different arrays")
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: test_capture_logs_go_core_t0
function_id: test_capture_logs_go_core
name: TestTestCaptureLogs
file_path: functions/core/test_capture_logs_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestTestCaptureLogs(t *testing.T) {
  	t.Run("log.Print capturado en el buffer", func(t *testing.T) {
  		buf, _ := TestCaptureLogs(t)

  		log.Print("hello from test")

  		if buf.Len() == 0 {
  			t.Error("expected buffer to have content, got empty")
  		}

  		content := buf.String()
  		if content == "" {
  			t.Error("expected non-empty buffer content")
  		}
  	})

  	t.Run("flush retorna lineas separadas", func(t *testing.T) {
  		_, flush := TestCaptureLogs(t)

  		log.Print("line one")
  		log.Print("line two")
  		log.Print("line three")

  		lines := flush()
  		if len(lines) != 3 {
  			t.Errorf("got %d lines, want 3: %v", len(lines), lines)
  		}
  		if lines[0] != "line one" {
  			t.Errorf("line[0] = %q, want \"line one\"", lines[0])
  		}
  		if lines[1] != "line two" {
  			t.Errorf("line[1] = %q, want \"line two\"", lines[1])
  		}
  		if lines[2] != "line three" {
  			t.Errorf("line[2] = %q, want \"line three\"", lines[2])
  		}
  	})

  	t.Run("sin logs flush retorna nil", func(t *testing.T) {
  		_, flush := TestCaptureLogs(t)
  		lines := flush()
  		if lines != nil {
  			t.Errorf("expected nil for no logs, got %v", lines)
  		}
  	})

  	t.Run("log restaurado al original tras flush", func(t *testing.T) {
  		_, flush := TestCaptureLogs(t)
  		log.Print("captured")
  		flush()

  		// Despues de flush el logger esta restaurado
  		// Verificamos que no panics ni errores al loggear normalmente
  		// (el output va al writer original, no al buffer)
  		log.Print("this goes to original output")
  	})
  }


id: test_db_seed_go_core_t0
function_id: test_db_seed_go_core
name: TestTestDBSeed
file_path: functions/core/test_db_seed_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestTestDBSeed(t *testing.T) {
  	schema := `CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL);`

  	t.Run("inserta una fila correctamente", func(t *testing.T) {
  		tdb := TestDBSetup(t, schema)

  		err := TestDBSeed(t, tdb.DB, "products", []map[string]any{
  			{"name": "Widget", "price": 9.99},
  		})
  		if err != nil {
  			t.Fatalf("seed: %v", err)
  		}

  		var count int
  		if err := tdb.DB.QueryRow(`SELECT COUNT(*) FROM products`).Scan(&count); err != nil {
  			t.Fatalf("count: %v", err)
  		}
  		if count != 1 {
  			t.Errorf("got %d rows, want 1", count)
  		}
  	})

  	t.Run("inserta multiples filas en una transaccion", func(t *testing.T) {
  		tdb := TestDBSetup(t, schema)

  		rows := []map[string]any{
  			{"name": "A", "price": 1.0},
  			{"name": "B", "price": 2.0},
  			{"name": "C", "price": 3.0},
  		}
  		err := TestDBSeed(t, tdb.DB, "products", rows)
  		if err != nil {
  			t.Fatalf("seed: %v", err)
  		}

  		var count int
  		if err := tdb.DB.QueryRow(`SELECT COUNT(*) FROM products`).Scan(&count); err != nil {
  			t.Fatalf("count: %v", err)
  		}
  		if count != 3 {
  			t.Errorf("got %d rows, want 3", count)
  		}
  	})

  	t.Run("slice vacio no hace nada y retorna nil", func(t *testing.T) {
  		tdb := TestDBSetup(t, schema)

  		err := TestDBSeed(t, tdb.DB, "products", []map[string]any{})
  		if err != nil {
  			t.Errorf("expected nil for empty rows, got: %v", err)
  		}

  		var count int
  		if err := tdb.DB.QueryRow(`SELECT COUNT(*) FROM products`).Scan(&count); err != nil {
  			t.Fatalf("count: %v", err)
  		}
  		if count != 0 {
  			t.Errorf("got %d rows, want 0", count)
  		}
  	})

  	t.Run("error en tabla inexistente retorna error", func(t *testing.T) {
  		tdb := TestDBSetup(t, schema)

  		err := TestDBSeed(t, tdb.DB, "nonexistent_table", []map[string]any{
  			{"name": "X", "price": 1.0},
  		})
  		if err == nil {
  			t.Error("expected error for nonexistent table, got nil")
  		}
  	})
  }


id: test_db_setup_go_core_t0
function_id: test_db_setup_go_core
name: TestTestDBSetup
file_path: functions/core/test_db_setup_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestTestDBSetup(t *testing.T) {
  	t.Run("DB en memoria creado sin schema esta disponible", func(t *testing.T) {
  		tdb := TestDBSetup(t, "")

  		if tdb.DB == nil {
  			t.Fatal("expected non-nil DB")
  		}
  		if tdb.Path != ":memory:" {
  			t.Errorf("got path %q, want \":memory:\"", tdb.Path)
  		}

  		// Ping para verificar conexion
  		if err := tdb.DB.Ping(); err != nil {
  			t.Errorf("ping failed: %v", err)
  		}
  	})

  	t.Run("schema se ejecuta correctamente al crear el DB", func(t *testing.T) {
  		schema := `CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL);`
  		tdb := TestDBSetup(t, schema)

  		// Insertar y recuperar para verificar que la tabla existe
  		_, err := tdb.DB.Exec(`INSERT INTO items (name) VALUES ('test')`)
  		if err != nil {
  			t.Fatalf("insert: %v", err)
  		}

  		var name string
  		if err := tdb.DB.QueryRow(`SELECT name FROM items WHERE id=1`).Scan(&name); err != nil {
  			t.Fatalf("select: %v", err)
  		}
  		if name != "test" {
  			t.Errorf("got %q, want \"test\"", name)
  		}
  	})

  	t.Run("schema invalido llama t.Fatalf", func(t *testing.T) {
  		// Usamos un sub-test con recover para verificar que t.Fatalf es llamado
  		// En tests reales t.Fatalf detiene la goroutine, aqui verificamos indirectamente
  		// que un schema invalido no devuelve un DB utilizable
  		// Este test documenta el comportamiento esperado; en la practica t.Fatalf
  		// termina el test inmediatamente con error si el schema es invalido.
  		// Se verifica que el DB valido funciona y que el schema invalido habria fallado.
  		tdb := TestDBSetup(t, `CREATE TABLE ok (id INTEGER PRIMARY KEY);`)
  		if tdb.DB == nil {
  			t.Fatal("expected valid DB with valid schema")
  		}
  	})
  }


id: test_env_set_go_core_t0
function_id: test_env_set_go_core
name: TestTestEnvSet
file_path: functions/core/test_env_set_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestTestEnvSet(t *testing.T) {
  	t.Run("variable nueva se setea y se restaura al finalizar", func(t *testing.T) {
  		const key = "TEST_ENV_SET_NEW_VAR_XYZ"
  		os.Unsetenv(key) // asegurarse de que no existe

  		TestEnvSet(t, map[string]string{key: "hello"})

  		if got := os.Getenv(key); got != "hello" {
  			t.Errorf("got %q, want \"hello\"", got)
  		}

  		// Simular que el test termino llamando al restore
  		// (en tests reales t.Cleanup lo hace automaticamente)
  	})

  	t.Run("variable existente se sobreescribe y se restaura al finalizar", func(t *testing.T) {
  		const key = "TEST_ENV_SET_EXISTING_VAR_XYZ"
  		os.Setenv(key, "original")

  		restore := TestEnvSet(t, map[string]string{key: "overridden"})

  		if got := os.Getenv(key); got != "overridden" {
  			t.Errorf("got %q, want \"overridden\"", got)
  		}

  		restore()

  		if got := os.Getenv(key); got != "original" {
  			t.Errorf("after restore got %q, want \"original\"", got)
  		}
  		os.Unsetenv(key)
  	})

  	t.Run("multiples variables se setean y restauran correctamente", func(t *testing.T) {
  		os.Unsetenv("TEST_MULTI_A")
  		os.Unsetenv("TEST_MULTI_B")

  		restore := TestEnvSet(t, map[string]string{
  			"TEST_MULTI_A": "value-a",
  			"TEST_MULTI_B": "value-b",
  		})

  		if got := os.Getenv("TEST_MULTI_A"); got != "value-a" {
  			t.Errorf("A: got %q, want \"value-a\"", got)
  		}
  		if got := os.Getenv("TEST_MULTI_B"); got != "value-b" {
  			t.Errorf("B: got %q, want \"value-b\"", got)
  		}

  		restore()

  		if got, ok := os.LookupEnv("TEST_MULTI_A"); ok {
  			t.Errorf("A should be unset after restore, got %q", got)
  		}
  		if got, ok := os.LookupEnv("TEST_MULTI_B"); ok {
  			t.Errorf("B should be unset after restore, got %q", got)
  		}
  	})

  	t.Run("restore manual funciona antes del cleanup automatico", func(t *testing.T) {
  		const key = "TEST_ENV_MANUAL_RESTORE"
  		os.Setenv(key, "before")

  		restore := TestEnvSet(t, map[string]string{key: "during"})

  		if got := os.Getenv(key); got != "during" {
  			t.Errorf("got %q, want \"during\"", got)
  		}

  		restore() // restaura manualmente

  		if got := os.Getenv(key); got != "before" {
  			t.Errorf("after manual restore got %q, want \"before\"", got)
  		}
  		os.Unsetenv(key)
  	})
  }


id: test_fixture_load_go_core_t0
function_id: test_fixture_load_go_core
name: TestTestFixtureLoad
file_path: functions/core/test_fixture_load_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestTestFixtureLoad(t *testing.T) {
  	// Crear fixture temporal para los tests
  	dir := t.TempDir()

  	// Fixture JSON valido
  	jsonPath := filepath.Join(dir, "test.json")
  	if err := os.WriteFile(jsonPath, []byte(`{"host":"localhost","port":5432}`), 0o644); err != nil {
  		t.Fatalf("write json fixture: %v", err)
  	}

  	// Fixture JSON invalido
  	badJSONPath := filepath.Join(dir, "bad.json")
  	if err := os.WriteFile(badJSONPath, []byte(`{not valid json`), 0o644); err != nil {
  		t.Fatalf("write bad json fixture: %v", err)
  	}

  	t.Run("carga fixture JSON correctamente", func(t *testing.T) {
  		var dst struct {
  			Host string `json:"host"`
  			Port int    `json:"port"`
  		}
  		err := TestFixtureLoad(t, jsonPath, &dst)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if dst.Host != "localhost" {
  			t.Errorf("got host %q, want \"localhost\"", dst.Host)
  		}
  		if dst.Port != 5432 {
  			t.Errorf("got port %d, want 5432", dst.Port)
  		}
  	})

  	t.Run("archivo inexistente retorna error", func(t *testing.T) {
  		var dst map[string]any
  		err := TestFixtureLoad(t, filepath.Join(dir, "nonexistent.json"), &dst)
  		if err == nil {
  			t.Error("expected error for nonexistent file, got nil")
  		}
  	})

  	t.Run("extension no soportada retorna error", func(t *testing.T) {
  		txtPath := filepath.Join(dir, "file.txt")
  		if err := os.WriteFile(txtPath, []byte("hello"), 0o644); err != nil {
  			t.Fatalf("write txt: %v", err)
  		}
  		var dst any
  		err := TestFixtureLoad(t, txtPath, &dst)
  		if err == nil {
  			t.Error("expected error for unsupported extension, got nil")
  		}
  	})

  	t.Run("JSON invalido retorna error de parsing", func(t *testing.T) {
  		var dst map[string]any
  		err := TestFixtureLoad(t, badJSONPath, &dst)
  		if err == nil {
  			t.Error("expected error for invalid JSON, got nil")
  		}
  	})
  }


id: test_http_server_go_core_t0
function_id: test_http_server_go_core
name: TestTestHTTPServer
file_path: functions/core/test_http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestTestHTTPServer(t *testing.T) {
  	t.Run("ruta registrada responde con el handler correcto", func(t *testing.T) {
  		srv := TestHTTPServer(t, map[string]http.HandlerFunc{
  			"/ping": func(w http.ResponseWriter, r *http.Request) {
  				w.WriteHeader(http.StatusOK)
  				_, _ = w.Write([]byte("pong"))
  			},
  		})

  		resp, err := srv.Client.Get(srv.URL + "/ping")
  		if err != nil {
  			t.Fatalf("GET /ping: %v", err)
  		}
  		defer resp.Body.Close()

  		if resp.StatusCode != http.StatusOK {
  			t.Errorf("got status %d, want 200", resp.StatusCode)
  		}

  		body, _ := io.ReadAll(resp.Body)
  		if string(body) != "pong" {
  			t.Errorf("got body %q, want \"pong\"", string(body))
  		}
  	})

  	t.Run("ruta no registrada devuelve 404", func(t *testing.T) {
  		srv := TestHTTPServer(t, map[string]http.HandlerFunc{
  			"/exists": func(w http.ResponseWriter, r *http.Request) {
  				w.WriteHeader(http.StatusOK)
  			},
  		})

  		resp, err := srv.Client.Get(srv.URL + "/notfound")
  		if err != nil {
  			t.Fatalf("GET /notfound: %v", err)
  		}
  		defer resp.Body.Close()

  		if resp.StatusCode != http.StatusNotFound {
  			t.Errorf("got status %d, want 404", resp.StatusCode)
  		}
  	})

  	t.Run("multiples rutas independientes", func(t *testing.T) {
  		srv := TestHTTPServer(t, map[string]http.HandlerFunc{
  			"/a": func(w http.ResponseWriter, r *http.Request) {
  				w.WriteHeader(http.StatusOK)
  				_, _ = w.Write([]byte("route-a"))
  			},
  			"/b": func(w http.ResponseWriter, r *http.Request) {
  				w.WriteHeader(http.StatusCreated)
  				_, _ = w.Write([]byte("route-b"))
  			},
  		})

  		respA, err := srv.Client.Get(srv.URL + "/a")
  		if err != nil {
  			t.Fatalf("GET /a: %v", err)
  		}
  		defer respA.Body.Close()
  		if respA.StatusCode != http.StatusOK {
  			t.Errorf("/a: got status %d, want 200", respA.StatusCode)
  		}

  		respB, err := srv.Client.Get(srv.URL + "/b")
  		if err != nil {
  			t.Fatalf("GET /b: %v", err)
  		}
  		defer respB.Body.Close()
  		if respB.StatusCode != http.StatusCreated {
  			t.Errorf("/b: got status %d, want 201", respB.StatusCode)
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: config_dump_go_infra_t0
function_id: config_dump_go_infra
name: TestConfigDump
file_path: functions/infra/config_dump_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestConfigDump(t *testing.T) {
  	t.Run("campos normales se incluyen como string", func(t *testing.T) {
  		type Cfg struct {
  			Host string
  			Port int
  		}
  		got := ConfigDump(Cfg{Host: "localhost", Port: 8080})
  		if got["Host"] != "localhost" {
  			t.Errorf("expected Host=localhost, got %q", got["Host"])
  		}
  		if got["Port"] != "8080" {
  			t.Errorf("expected Port=8080, got %q", got["Port"])
  		}
  	})

  	t.Run("campos secret aparecen como tres asteriscos", func(t *testing.T) {
  		type Cfg struct {
  			APIKey string `secret:"true"`
  		}
  		got := ConfigDump(Cfg{APIKey: "super-secret-key"})
  		if got["APIKey"] != "***" {
  			t.Errorf("expected ***, got %q", got["APIKey"])
  		}
  	})

  	t.Run("campos no exportados no aparecen", func(t *testing.T) {
  		type Cfg struct {
  			Public  string
  			private string //nolint:structcheck
  		}
  		_ = Cfg{} // ensure it compiles
  		got := ConfigDump(Cfg{Public: "yes"})
  		if _, ok := got["private"]; ok {
  			t.Error("private field should not be in dump")
  		}
  	})

  	t.Run("puntero a struct funciona", func(t *testing.T) {
  		type Cfg struct {
  			Name string
  		}
  		got := ConfigDump(&Cfg{Name: "test"})
  		if got["Name"] != "test" {
  			t.Errorf("expected Name=test, got %q", got["Name"])
  		}
  	})

  	t.Run("struct con bool y float se formatean correctamente", func(t *testing.T) {
  		type Cfg struct {
  			Debug   bool
  			Timeout float64
  		}
  		got := ConfigDump(Cfg{Debug: true, Timeout: 3.14})
  		if got["Debug"] != "true" {
  			t.Errorf("expected Debug=true, got %q", got["Debug"])
  		}
  		if got["Timeout"] != "3.14" {
  			t.Errorf("expected Timeout=3.14, got %q", got["Timeout"])
  		}
  	})
  }


id: config_from_env_go_infra_t0
function_id: config_from_env_go_infra
name: TestConfigFromEnv
file_path: functions/infra/config_from_env_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestConfigFromEnv(t *testing.T) {
  	t.Run("populate string field desde env", func(t *testing.T) {
  		type Cfg struct {
  			Host string `env:"CFE_HOST"`
  		}
  		t.Setenv("CFE_HOST", "myhost")
  		var c Cfg
  		if err := ConfigFromEnv(&c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if c.Host != "myhost" {
  			t.Errorf("expected myhost, got %q", c.Host)
  		}
  	})

  	t.Run("populate int field con conversion", func(t *testing.T) {
  		type Cfg struct {
  			Port int `env:"CFE_PORT"`
  		}
  		t.Setenv("CFE_PORT", "9090")
  		var c Cfg
  		if err := ConfigFromEnv(&c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if c.Port != 9090 {
  			t.Errorf("expected 9090, got %d", c.Port)
  		}
  	})

  	t.Run("populate bool field con conversion", func(t *testing.T) {
  		type Cfg struct {
  			Debug bool `env:"CFE_DEBUG"`
  		}
  		t.Setenv("CFE_DEBUG", "true")
  		var c Cfg
  		if err := ConfigFromEnv(&c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if !c.Debug {
  			t.Error("expected Debug=true")
  		}
  	})

  	t.Run("populate float64 field", func(t *testing.T) {
  		type Cfg struct {
  			Ratio float64 `env:"CFE_RATIO"`
  		}
  		t.Setenv("CFE_RATIO", "3.14")
  		var c Cfg
  		if err := ConfigFromEnv(&c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if c.Ratio < 3.13 || c.Ratio > 3.15 {
  			t.Errorf("expected ~3.14, got %f", c.Ratio)
  		}
  	})

  	t.Run("populate slice string comma-separated", func(t *testing.T) {
  		type Cfg struct {
  			Tags []string `env:"CFE_TAGS"`
  		}
  		t.Setenv("CFE_TAGS", "a,b,c")
  		var c Cfg
  		if err := ConfigFromEnv(&c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(c.Tags) != 3 || c.Tags[0] != "a" || c.Tags[2] != "c" {
  			t.Errorf("unexpected tags: %v", c.Tags)
  		}
  	})

  	t.Run("valor default usado cuando env no seteada", func(t *testing.T) {
  		type Cfg struct {
  			Port int `env:"CFE_PORT_DEFAULT" default:"8080"`
  		}
  		var c Cfg
  		if err := ConfigFromEnv(&c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if c.Port != 8080 {
  			t.Errorf("expected default 8080, got %d", c.Port)
  		}
  	})

  	t.Run("required sin valor retorna error", func(t *testing.T) {
  		type Cfg struct {
  			APIKey string `env:"CFE_API_KEY_REQUIRED" required:"true"`
  		}
  		var c Cfg
  		err := ConfigFromEnv(&c)
  		if err == nil {
  			t.Error("expected error for missing required var")
  		}
  	})

  	t.Run("int invalido retorna error descriptivo", func(t *testing.T) {
  		type Cfg struct {
  			Port int `env:"CFE_PORT_BAD"`
  		}
  		t.Setenv("CFE_PORT_BAD", "not-a-number")
  		var c Cfg
  		err := ConfigFromEnv(&c)
  		if err == nil {
  			t.Error("expected error for invalid int")
  		}
  	})

  	t.Run("puntero nil retorna error", func(t *testing.T) {
  		var p *struct{ X string }
  		err := ConfigFromEnv(p)
  		if err == nil {
  			t.Error("expected error for nil pointer")
  		}
  	})
  }


id: config_from_file_go_infra_t0
function_id: config_from_file_go_infra
name: TestConfigFromFile
file_path: functions/infra/config_from_file_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestConfigFromFile(t *testing.T) {
  	t.Run("carga JSON valido en struct", func(t *testing.T) {
  		type Cfg struct {
  			Host string `json:"host"`
  			Port int    `json:"port"`
  		}
  		content := `{"host":"testhost","port":5432}`
  		dir := t.TempDir()
  		p := filepath.Join(dir, "config.json")
  		if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
  			t.Fatal(err)
  		}

  		var c Cfg
  		if err := ConfigFromFile(p, &c); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if c.Host != "testhost" || c.Port != 5432 {
  			t.Errorf("unexpected: %+v", c)
  		}
  	})

  	t.Run("JSON invalido retorna error", func(t *testing.T) {
  		dir := t.TempDir()
  		p := filepath.Join(dir, "bad.json")
  		os.WriteFile(p, []byte("{not valid json}"), 0o600)

  		var target map[string]any
  		err := ConfigFromFile(p, &target)
  		if err == nil {
  			t.Error("expected error for invalid JSON")
  		}
  	})

  	t.Run("archivo inexistente retorna error", func(t *testing.T) {
  		var target map[string]any
  		err := ConfigFromFile("/nonexistent/config.json", &target)
  		if err == nil {
  			t.Error("expected error for missing file")
  		}
  	})

  	t.Run("extension YAML retorna not implemented", func(t *testing.T) {
  		var target map[string]any
  		err := ConfigFromFile("config.yaml", &target)
  		if err == nil {
  			t.Error("expected not-implemented error for YAML")
  		}
  	})

  	t.Run("extension desconocida retorna error", func(t *testing.T) {
  		var target map[string]any
  		err := ConfigFromFile("config.toml", &target)
  		if err == nil {
  			t.Error("expected error for unsupported extension")
  		}
  	})
  }


id: config_merge_go_infra_t0
function_id: config_merge_go_infra
name: TestConfigMerge
file_path: functions/infra/config_merge_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestConfigMerge(t *testing.T) {
  	t.Run("override gana sobre base en claves comunes", func(t *testing.T) {
  		base := map[string]string{"host": "localhost", "port": "5432"}
  		over := map[string]string{"port": "9999"}
  		got := ConfigMerge(base, over)
  		if got["port"] != "9999" {
  			t.Errorf("expected port=9999, got %q", got["port"])
  		}
  		if got["host"] != "localhost" {
  			t.Errorf("expected host=localhost, got %q", got["host"])
  		}
  	})

  	t.Run("claves solo en base se mantienen", func(t *testing.T) {
  		base := map[string]string{"a": "1", "b": "2"}
  		over := map[string]string{}
  		got := ConfigMerge(base, over)
  		if got["a"] != "1" || got["b"] != "2" {
  			t.Errorf("unexpected result: %v", got)
  		}
  	})

  	t.Run("claves solo en override se agregan", func(t *testing.T) {
  		base := map[string]string{}
  		over := map[string]string{"x": "new"}
  		got := ConfigMerge(base, over)
  		if got["x"] != "new" {
  			t.Errorf("expected x=new, got %q", got["x"])
  		}
  	})

  	t.Run("no muta el map base", func(t *testing.T) {
  		base := map[string]string{"key": "original"}
  		over := map[string]string{"key": "changed"}
  		_ = ConfigMerge(base, over)
  		if base["key"] != "original" {
  			t.Errorf("base was mutated: got %q", base["key"])
  		}
  	})

  	t.Run("merge con maps vacios retorna mapa vacio", func(t *testing.T) {
  		got := ConfigMerge(map[string]string{}, map[string]string{})
  		if len(got) != 0 {
  			t.Errorf("expected empty map, got %v", got)
  		}
  	})
  }


id: config_validate_go_infra_t0
function_id: config_validate_go_infra
name: TestConfigValidate
file_path: functions/infra/config_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestConfigValidate(t *testing.T) {
  	t.Run("struct valido retorna IsValid true", func(t *testing.T) {
  		type Cfg struct {
  			Host string `required:"true"`
  			Port int    `required:"true"`
  		}
  		got := ConfigValidate(Cfg{Host: "localhost", Port: 8080})
  		if !got.IsValid {
  			t.Errorf("expected IsValid=true, errors: %v", got.Errors)
  		}
  		if len(got.Errors) != 0 {
  			t.Errorf("expected no errors, got %v", got.Errors)
  		}
  	})

  	t.Run("campo required vacio acumula error", func(t *testing.T) {
  		type Cfg struct {
  			Host string `required:"true"`
  			Key  string `required:"true"`
  		}
  		got := ConfigValidate(Cfg{})
  		if got.IsValid {
  			t.Error("expected IsValid=false")
  		}
  		if len(got.Errors) != 2 {
  			t.Errorf("expected 2 errors, got %d: %v", len(got.Errors), got.Errors)
  		}
  		for _, e := range got.Errors {
  			if e.Tag != "required" {
  				t.Errorf("expected tag 'required', got %q", e.Tag)
  			}
  		}
  	})

  	t.Run("format email invalido produce error", func(t *testing.T) {
  		type Cfg struct {
  			Email string `format:"email"`
  		}
  		got := ConfigValidate(Cfg{Email: "not-an-email"})
  		if got.IsValid {
  			t.Error("expected IsValid=false")
  		}
  		if len(got.Errors) == 0 || got.Errors[0].Tag != "format" {
  			t.Errorf("expected format error, got %v", got.Errors)
  		}
  	})

  	t.Run("format email valido pasa", func(t *testing.T) {
  		type Cfg struct {
  			Email string `format:"email"`
  		}
  		got := ConfigValidate(Cfg{Email: "user@example.com"})
  		if !got.IsValid {
  			t.Errorf("expected IsValid=true, errors: %v", got.Errors)
  		}
  	})

  	t.Run("format url invalido produce error", func(t *testing.T) {
  		type Cfg struct {
  			BaseURL string `format:"url"`
  		}
  		got := ConfigValidate(Cfg{BaseURL: "ftp://bad.url"})
  		if got.IsValid {
  			t.Error("expected IsValid=false")
  		}
  		if len(got.Errors) == 0 || got.Errors[0].Tag != "format" {
  			t.Errorf("expected format error, got %v", got.Errors)
  		}
  	})

  	t.Run("puntero a struct funciona igual", func(t *testing.T) {
  		type Cfg struct {
  			Name string `required:"true"`
  		}
  		got := ConfigValidate(&Cfg{Name: "test"})
  		if !got.IsValid {
  			t.Errorf("expected IsValid=true, errors: %v", got.Errors)
  		}
  	})

  	t.Run("todos los errores acumulados aunque falle el primero", func(t *testing.T) {
  		type Cfg struct {
  			A string `required:"true"`
  			B string `required:"true"`
  			C string `required:"true"`
  		}
  		got := ConfigValidate(Cfg{})
  		if len(got.Errors) != 3 {
  			t.Errorf("expected 3 errors, got %d", len(got.Errors))
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: crud_create_handler_go_infra_t0
function_id: crud_create_handler_go_infra
name: TestCRUDDefineResource
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDDefineResource(t *testing.T) {
  	t.Run("construye un recurso valido", func(t *testing.T) {
  		res, err := CRUDDefineResource("project", "projects", sampleProjectFields(), false)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if res.Name != "project" || res.Table != "projects" || len(res.Fields) != 4 {
  			t.Errorf("unexpected resource: %+v", res)
  		}
  	})

  	t.Run("rechaza nombre vacio", func(t *testing.T) {
  		_, err := CRUDDefineResource("", "projects", sampleProjectFields(), false)
  		if err == nil {
  			t.Error("expected error for empty name")
  		}
  	})

  	t.Run("rechaza tabla vacia", func(t *testing.T) {
  		_, err := CRUDDefineResource("project", "", sampleProjectFields(), false)
  		if err == nil {
  			t.Error("expected error for empty table")
  		}
  	})

  	t.Run("rechaza lista de campos vacia", func(t *testing.T) {
  		_, err := CRUDDefineResource("project", "projects", nil, false)
  		if err == nil {
  			t.Error("expected error for empty fields")
  		}
  	})

  	t.Run("rechaza tipos invalidos", func(t *testing.T) {
  		_, err := CRUDDefineResource("project", "projects", []CRUDField{{Name: "x", Type: "FOO"}}, false)
  		if err == nil {
  			t.Error("expected error for invalid type")
  		}
  	})

  	t.Run("rechaza nombres reservados", func(t *testing.T) {
  		for _, reserved := range []string{"id", "created_at", "updated_at", "deleted_at"} {
  			_, err := CRUDDefineResource("project", "projects", []CRUDField{{Name: reserved, Type: "TEXT"}}, false)
  			if err == nil {
  				t.Errorf("expected error for reserved field %q", reserved)
  			}
  		}
  	})

  	t.Run("rechaza duplicados", func(t *testing.T) {
  		_, err := CRUDDefineResource("project", "projects", []CRUDField{
  			{Name: "name", Type: "TEXT"},
  			{Name: "name", Type: "TEXT"},
  		}, false)
  		if err == nil {
  			t.Error("expected error for duplicate field")
  		}
  	})
  }

  // --- CRUDGenerateTableSQL ---


id: crud_create_handler_go_infra_t1
function_id: crud_create_handler_go_infra
name: TestCRUDGenerateTableSQL
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDGenerateTableSQL(t *testing.T) {
  	t.Run("genera tabla basica con timestamps", func(t *testing.T) {
  		res, _ := CRUDDefineResource("project", "projects",
  			[]CRUDField{{Name: "name", Type: "TEXT"}}, false)
  		ddl := CRUDGenerateTableSQL(res)
  		for _, want := range []string{
  			"CREATE TABLE IF NOT EXISTS projects",
  			"id TEXT PRIMARY KEY",
  			"name TEXT",
  			"created_at TEXT NOT NULL",
  			"updated_at TEXT NOT NULL",
  		} {
  			if !strings.Contains(ddl, want) {
  				t.Errorf("DDL missing %q:\n%s", want, ddl)
  			}
  		}
  		if strings.Contains(ddl, "deleted_at") {
  			t.Errorf("DDL should not contain deleted_at:\n%s", ddl)
  		}
  	})

  	t.Run("aplica NOT NULL y UNIQUE", func(t *testing.T) {
  		res, _ := CRUDDefineResource("project", "projects",
  			[]CRUDField{{Name: "name", Type: "TEXT", Required: true, Unique: true}}, false)
  		ddl := CRUDGenerateTableSQL(res)
  		if !strings.Contains(ddl, "name TEXT NOT NULL UNIQUE") {
  			t.Errorf("expected NOT NULL UNIQUE:\n%s", ddl)
  		}
  	})

  	t.Run("aplica DEFAULT", func(t *testing.T) {
  		res, _ := CRUDDefineResource("project", "projects",
  			[]CRUDField{{Name: "priority", Type: "INTEGER", Default: "0"}}, false)
  		ddl := CRUDGenerateTableSQL(res)
  		if !strings.Contains(ddl, "priority INTEGER DEFAULT 0") {
  			t.Errorf("expected DEFAULT clause:\n%s", ddl)
  		}
  	})

  	t.Run("anade deleted_at si soft_delete", func(t *testing.T) {
  		res, _ := CRUDDefineResource("project", "projects",
  			[]CRUDField{{Name: "name", Type: "TEXT"}}, true)
  		ddl := CRUDGenerateTableSQL(res)
  		if !strings.Contains(ddl, "deleted_at TEXT") {
  			t.Errorf("expected deleted_at for soft_delete:\n%s", ddl)
  		}
  	})

  	t.Run("el DDL generado es valido en sqlite", func(t *testing.T) {
  		res, _ := CRUDDefineResource("project", "projects", sampleProjectFields(), true)
  		db := openCRUDTestDB(t)
  		if _, err := db.Exec(CRUDGenerateTableSQL(res)); err != nil {
  			t.Fatalf("DDL not valid: %v", err)
  		}
  	})
  }

  // --- CRUDCreateHandler + CRUDGetHandler ---


id: crud_create_handler_go_infra_t2
function_id: crud_create_handler_go_infra
name: TestCRUDCreateAndGet
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDCreateAndGet(t *testing.T) {
  	res, db := buildProjectResource(t, false)
  	mux := http.NewServeMux()
  	CRUDRegisterRoutes(mux, "/api/projects", res, db)

  	t.Run("crea un registro valido y retorna 201", func(t *testing.T) {
  		rec, body := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
  			"name": "demo", "description": "hola", "status": "active", "priority": 5,
  		})
  		if rec.Code != http.StatusCreated {
  			t.Fatalf("got status %d, want 201: body=%s", rec.Code, rec.Body.String())
  		}
  		if body["name"] != "demo" {
  			t.Errorf("got name=%v, want demo", body["name"])
  		}
  		if body["id"] == nil || body["id"] == "" {
  			t.Errorf("expected generated id, got %v", body["id"])
  		}
  		if body["created_at"] == nil {
  			t.Errorf("expected created_at, got nil")
  		}
  	})

  	t.Run("retorna 400 si faltan required", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400", rec.Code)
  		}
  	})

  	t.Run("valida min_length", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": ""})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400 for empty name", rec.Code)
  		}
  	})

  	t.Run("valida max_length", func(t *testing.T) {
  		longName := strings.Repeat("a", 60)
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": longName})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400 for too-long name", rec.Code)
  		}
  	})

  	t.Run("valida enum", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
  			"name": "x", "status": "pirate",
  		})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400 for invalid enum", rec.Code)
  		}
  	})

  	t.Run("valida min y max numericos", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
  			"name": "numeric-min", "priority": -1,
  		})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400 for priority<0", rec.Code)
  		}
  		rec, _ = doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
  			"name": "numeric-max", "priority": 999,
  		})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400 for priority>10", rec.Code)
  		}
  	})

  	t.Run("retorna 409 si se viola UNIQUE", func(t *testing.T) {
  		_, _ = doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "unique-1"})
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "unique-1"})
  		if rec.Code != http.StatusConflict {
  			t.Errorf("got %d, want 409", rec.Code)
  		}
  	})

  	t.Run("GET recupera el registro creado", func(t *testing.T) {
  		rec, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "get-me"})
  		if rec.Code != http.StatusCreated {
  			t.Fatalf("create failed: %d", rec.Code)
  		}
  		id := created["id"].(string)
  		rec, body := doJSONRequest(t, mux, "GET", "/api/projects/"+id, nil)
  		if rec.Code != http.StatusOK {
  			t.Errorf("got %d, want 200", rec.Code)
  		}
  		if body["name"] != "get-me" {
  			t.Errorf("got name=%v, want get-me", body["name"])
  		}
  	})

  	t.Run("GET no existente retorna 404", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "GET", "/api/projects/nonexistent", nil)
  		if rec.Code != http.StatusNotFound {
  			t.Errorf("got %d, want 404", rec.Code)
  		}
  	})
  }

  // --- CRUDListHandler ---


id: crud_create_handler_go_infra_t3
function_id: crud_create_handler_go_infra
name: TestCRUDListHandler
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDListHandler(t *testing.T) {
  	res, db := buildProjectResource(t, false)
  	mux := http.NewServeMux()
  	CRUDRegisterRoutes(mux, "/api/projects", res, db)

  	// Sembrar datos
  	for i, name := range []string{"a", "b", "c", "d", "e"} {
  		status := "active"
  		if i%2 == 1 {
  			status = "archived"
  		}
  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
  			"name": name, "status": status, "priority": i,
  		})
  		if rec.Code != http.StatusCreated {
  			t.Fatalf("seed %s failed: %d", name, rec.Code)
  		}
  	}

  	t.Run("lista todo con paginacion default", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects", nil))
  		if rec.Code != http.StatusOK {
  			t.Fatalf("got %d, want 200", rec.Code)
  		}
  		var got CRUDListResult
  		if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
  			t.Fatalf("parse body: %v", err)
  		}
  		if got.Total != 5 {
  			t.Errorf("got total=%d, want 5", got.Total)
  		}
  		if len(got.Items) != 5 {
  			t.Errorf("got %d items, want 5", len(got.Items))
  		}
  	})

  	t.Run("respeta page y per_page", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?page=1&per_page=2", nil))
  		var got CRUDListResult
  		_ = json.Unmarshal(rec.Body.Bytes(), &got)
  		if len(got.Items) != 2 {
  			t.Errorf("got %d items, want 2", len(got.Items))
  		}
  		if got.TotalPages != 3 {
  			t.Errorf("got total_pages=%d, want 3", got.TotalPages)
  		}
  	})

  	t.Run("filtra por campo con filter_<field>", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?filter_status=archived", nil))
  		var got CRUDListResult
  		_ = json.Unmarshal(rec.Body.Bytes(), &got)
  		if got.Total != 2 {
  			t.Errorf("got total=%d, want 2 archived", got.Total)
  		}
  	})

  	t.Run("ordena con sort_by y sort_dir", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?sort_by=name&sort_dir=asc", nil))
  		var got CRUDListResult
  		_ = json.Unmarshal(rec.Body.Bytes(), &got)
  		if len(got.Items) < 2 {
  			t.Fatalf("not enough items: %d", len(got.Items))
  		}
  		if got.Items[0]["name"] != "a" || got.Items[1]["name"] != "b" {
  			t.Errorf("sort asc failed: first=%v second=%v", got.Items[0]["name"], got.Items[1]["name"])
  		}
  	})

  	t.Run("ignora filtros con campos desconocidos", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects?filter_unknown=xxx", nil))
  		var got CRUDListResult
  		_ = json.Unmarshal(rec.Body.Bytes(), &got)
  		if got.Total != 5 {
  			t.Errorf("got total=%d, want 5 (filter should be ignored)", got.Total)
  		}
  	})
  }

  // --- CRUDUpdateHandler ---


id: crud_create_handler_go_infra_t4
function_id: crud_create_handler_go_infra
name: TestCRUDUpdateHandler
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDUpdateHandler(t *testing.T) {
  	res, db := buildProjectResource(t, false)
  	mux := http.NewServeMux()
  	CRUDRegisterRoutes(mux, "/api/projects", res, db)

  	createRec, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{
  		"name": "original", "description": "first", "priority": 2,
  	})
  	if createRec.Code != http.StatusCreated {
  		t.Fatalf("create failed: %d", createRec.Code)
  	}
  	id := created["id"].(string)
  	originalUpdatedAt := fmt.Sprintf("%v", created["updated_at"])

  	t.Run("actualiza solo los campos enviados", func(t *testing.T) {
  		rec, body := doJSONRequest(t, mux, "PUT", "/api/projects/"+id, map[string]any{
  			"description": "updated",
  		})
  		if rec.Code != http.StatusOK {
  			t.Fatalf("got %d, want 200: %s", rec.Code, rec.Body.String())
  		}
  		if body["description"] != "updated" {
  			t.Errorf("description not updated: %v", body["description"])
  		}
  		if body["name"] != "original" {
  			t.Errorf("name should not change: %v", body["name"])
  		}
  		if fmt.Sprintf("%v", body["updated_at"]) == originalUpdatedAt {
  			t.Errorf("updated_at should change")
  		}
  	})

  	t.Run("retorna 404 si no existe", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "PUT", "/api/projects/nonexistent", map[string]any{"description": "x"})
  		if rec.Code != http.StatusNotFound {
  			t.Errorf("got %d, want 404", rec.Code)
  		}
  	})

  	t.Run("valida los campos enviados", func(t *testing.T) {
  		rec, _ := doJSONRequest(t, mux, "PUT", "/api/projects/"+id, map[string]any{"priority": 999})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400", rec.Code)
  		}
  	})
  }

  // --- CRUDDeleteHandler ---


id: crud_create_handler_go_infra_t5
function_id: crud_create_handler_go_infra
name: TestCRUDDeleteHandler
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDDeleteHandler(t *testing.T) {
  	t.Run("hard delete borra fisicamente", func(t *testing.T) {
  		res, db := buildProjectResource(t, false)
  		mux := http.NewServeMux()
  		CRUDRegisterRoutes(mux, "/api/projects", res, db)

  		_, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "hard"})
  		id := created["id"].(string)

  		rec, _ := doJSONRequest(t, mux, "DELETE", "/api/projects/"+id, nil)
  		if rec.Code != http.StatusNoContent {
  			t.Errorf("got %d, want 204", rec.Code)
  		}
  		if rec.Body.Len() != 0 {
  			t.Errorf("expected empty body, got %s", rec.Body.String())
  		}

  		// Verificar que ya no existe
  		rec, _ = doJSONRequest(t, mux, "GET", "/api/projects/"+id, nil)
  		if rec.Code != http.StatusNotFound {
  			t.Errorf("after delete, GET got %d, want 404", rec.Code)
  		}

  		// Verificar que la fila ya no esta en la tabla
  		var count int
  		if err := db.QueryRow("SELECT COUNT(*) FROM projects WHERE id = ?", id).Scan(&count); err != nil {
  			t.Fatalf("count: %v", err)
  		}
  		if count != 0 {
  			t.Errorf("hard delete should remove row, got count=%d", count)
  		}
  	})

  	t.Run("soft delete actualiza deleted_at", func(t *testing.T) {
  		res, db := buildProjectResource(t, true)
  		mux := http.NewServeMux()
  		CRUDRegisterRoutes(mux, "/api/projects", res, db)

  		_, created := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "soft"})
  		id := created["id"].(string)

  		rec, _ := doJSONRequest(t, mux, "DELETE", "/api/projects/"+id, nil)
  		if rec.Code != http.StatusNoContent {
  			t.Errorf("got %d, want 204", rec.Code)
  		}

  		// GET debe dar 404 (la fila esta oculta por soft delete)
  		rec, _ = doJSONRequest(t, mux, "GET", "/api/projects/"+id, nil)
  		if rec.Code != http.StatusNotFound {
  			t.Errorf("after soft delete, GET got %d, want 404", rec.Code)
  		}

  		// Pero la fila sigue fisica en la tabla con deleted_at no nulo
  		var deletedAt sql.NullString
  		if err := db.QueryRow("SELECT deleted_at FROM projects WHERE id = ?", id).Scan(&deletedAt); err != nil {
  			t.Fatalf("select: %v", err)
  		}
  		if !deletedAt.Valid || deletedAt.String == "" {
  			t.Errorf("expected deleted_at set, got %+v", deletedAt)
  		}
  	})

  	t.Run("retorna 404 si no existe", func(t *testing.T) {
  		res, db := buildProjectResource(t, false)
  		mux := http.NewServeMux()
  		CRUDRegisterRoutes(mux, "/api/projects", res, db)
  		rec, _ := doJSONRequest(t, mux, "DELETE", "/api/projects/nonexistent", nil)
  		if rec.Code != http.StatusNotFound {
  			t.Errorf("got %d, want 404", rec.Code)
  		}
  	})

  	t.Run("soft delete no lista registros ocultos", func(t *testing.T) {
  		res, db := buildProjectResource(t, true)
  		mux := http.NewServeMux()
  		CRUDRegisterRoutes(mux, "/api/projects", res, db)

  		_, _ = doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "keep"})
  		_, del := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "remove"})
  		delID := del["id"].(string)
  		_, _ = doJSONRequest(t, mux, "DELETE", "/api/projects/"+delID, nil)

  		rec := httptest.NewRecorder()
  		mux.ServeHTTP(rec, httptest.NewRequest("GET", "/api/projects", nil))
  		var got CRUDListResult
  		_ = json.Unmarshal(rec.Body.Bytes(), &got)
  		if got.Total != 1 {
  			t.Errorf("got total=%d, want 1 (soft deleted should be hidden)", got.Total)
  		}
  	})
  }

  // --- CRUDGenerateHandlers + CRUDRegisterRoutes integration ---


id: crud_create_handler_go_infra_t6
function_id: crud_create_handler_go_infra
name: TestCRUDGenerateHandlers
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDGenerateHandlers(t *testing.T) {
  	t.Run("retorna las 5 keys esperadas", func(t *testing.T) {
  		res, db := buildProjectResource(t, false)
  		handlers := CRUDGenerateHandlers(res, db)
  		for _, key := range []string{"list", "get", "create", "update", "delete"} {
  			if handlers[key] == nil {
  				t.Errorf("handler %q is nil", key)
  			}
  		}
  		if len(handlers) != 5 {
  			t.Errorf("expected 5 handlers, got %d", len(handlers))
  		}
  	})
  }


id: crud_create_handler_go_infra_t7
function_id: crud_create_handler_go_infra
name: TestCRUDRegisterRoutesIntegration
file_path: functions/infra/crud_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestCRUDRegisterRoutesIntegration(t *testing.T) {
  	t.Run("CRUD completo end-to-end via servidor HTTP", func(t *testing.T) {
  		res, db := buildProjectResource(t, false)
  		mux := http.NewServeMux()
  		CRUDRegisterRoutes(mux, "/api/projects", res, db)
  		srv := httptest.NewServer(mux)
  		t.Cleanup(srv.Close)

  		// Create
  		createBody, _ := json.Marshal(map[string]any{"name": "e2e", "description": "integration", "priority": 3})
  		resp, err := http.Post(srv.URL+"/api/projects", "application/json", bytes.NewReader(createBody))
  		if err != nil {
  			t.Fatalf("POST: %v", err)
  		}
  		defer resp.Body.Close()
  		if resp.StatusCode != http.StatusCreated {
  			t.Fatalf("create: got %d, want 201", resp.StatusCode)
  		}
  		var created map[string]any
  		_ = json.NewDecoder(resp.Body).Decode(&created)
  		id := created["id"].(string)

  		// Get
  		resp, err = http.Get(srv.URL + "/api/projects/" + id)
  		if err != nil {
  			t.Fatalf("GET: %v", err)
  		}
  		resp.Body.Close()
  		if resp.StatusCode != http.StatusOK {
  			t.Errorf("get: got %d, want 200", resp.StatusCode)
  		}

  		// Update
  		updateBody, _ := json.Marshal(map[string]any{"description": "modified"})
  		req, _ := http.NewRequest("PUT", srv.URL+"/api/projects/"+id, bytes.NewReader(updateBody))
  		req.Header.Set("Content-Type", "application/json")
  		resp, err = http.DefaultClient.Do(req)
  		if err != nil {
  			t.Fatalf("PUT: %v", err)
  		}
  		var updated map[string]any
  		_ = json.NewDecoder(resp.Body).Decode(&updated)
  		resp.Body.Close()
  		if resp.StatusCode != http.StatusOK {
  			t.Errorf("update: got %d, want 200", resp.StatusCode)
  		}
  		if updated["description"] != "modified" {
  			t.Errorf("update did not persist: %v", updated["description"])
  		}

  		// List
  		resp, err = http.Get(srv.URL + "/api/projects")
  		if err != nil {
  			t.Fatalf("LIST: %v", err)
  		}
  		var list CRUDListResult
  		_ = json.NewDecoder(resp.Body).Decode(&list)
  		resp.Body.Close()
  		if list.Total != 1 {
  			t.Errorf("list total: got %d, want 1", list.Total)
  		}

  		// Delete
  		req, _ = http.NewRequest("DELETE", srv.URL+"/api/projects/"+id, nil)
  		resp, err = http.DefaultClient.Do(req)
  		if err != nil {
  			t.Fatalf("DELETE: %v", err)
  		}
  		resp.Body.Close()
  		if resp.StatusCode != http.StatusNoContent {
  			t.Errorf("delete: got %d, want 204", resp.StatusCode)
  		}

  		// Get after delete: 404
  		resp, _ = http.Get(srv.URL + "/api/projects/" + id)
  		resp.Body.Close()
  		if resp.StatusCode != http.StatusNotFound {
  			t.Errorf("after delete: got %d, want 404", resp.StatusCode)
  		}
  	})

  	t.Run("multiples recursos en el mismo mux", func(t *testing.T) {
  		projectRes, db := buildProjectResource(t, false)
  		// Segundo recurso sobre la misma DB
  		userFields := []CRUDField{
  			{Name: "email", Type: "TEXT", Required: true, Unique: true},
  		}
  		userRes, err := CRUDDefineResource("user", "users", userFields, false)
  		if err != nil {
  			t.Fatalf("define user: %v", err)
  		}
  		if _, err := db.Exec(CRUDGenerateTableSQL(userRes)); err != nil {
  			t.Fatalf("create users table: %v", err)
  		}

  		mux := http.NewServeMux()
  		CRUDRegisterRoutes(mux, "/api/projects", projectRes, db)
  		CRUDRegisterRoutes(mux, "/api/users", userRes, db)

  		rec, _ := doJSONRequest(t, mux, "POST", "/api/projects", map[string]any{"name": "p1"})
  		if rec.Code != http.StatusCreated {
  			t.Errorf("projects: got %d", rec.Code)
  		}
  		rec, _ = doJSONRequest(t, mux, "POST", "/api/users", map[string]any{"email": "a@b.c"})
  		if rec.Code != http.StatusCreated {
  			t.Errorf("users: got %d", rec.Code)
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: dotenv_load_go_infra_t0
function_id: dotenv_load_go_infra
name: TestDotenvLoad
file_path: functions/infra/dotenv_load_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestDotenvLoad(t *testing.T) {
  	// helper to write a temp .env file
  	writeEnv := func(t *testing.T, content string) string {
  		t.Helper()
  		dir := t.TempDir()
  		p := filepath.Join(dir, ".env")
  		if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
  			t.Fatal(err)
  		}
  		return p
  	}

  	t.Run("parsea KEY=VALUE y setea variable", func(t *testing.T) {
  		p := writeEnv(t, "DOTENV_TEST_FOO=hello\n")
  		t.Setenv("DOTENV_TEST_FOO", "") // ensure unset
  		os.Unsetenv("DOTENV_TEST_FOO")

  		if err := DotenvLoad(p); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got := os.Getenv("DOTENV_TEST_FOO"); got != "hello" {
  			t.Errorf("expected DOTENV_TEST_FOO=hello, got %q", got)
  		}
  	})

  	t.Run("ignora lineas comentario y vacias", func(t *testing.T) {
  		content := "# this is a comment\n\nDOTENV_TEST_BAR=world\n"
  		p := writeEnv(t, content)
  		os.Unsetenv("DOTENV_TEST_BAR")

  		if err := DotenvLoad(p); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got := os.Getenv("DOTENV_TEST_BAR"); got != "world" {
  			t.Errorf("expected world, got %q", got)
  		}
  	})

  	t.Run("no sobreescribe variables ya seteadas", func(t *testing.T) {
  		p := writeEnv(t, "DOTENV_TEST_EXISTING=new\n")
  		t.Setenv("DOTENV_TEST_EXISTING", "original")

  		if err := DotenvLoad(p); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got := os.Getenv("DOTENV_TEST_EXISTING"); got != "original" {
  			t.Errorf("expected original (not overwritten), got %q", got)
  		}
  	})

  	t.Run("valores con comillas dobles se stripean", func(t *testing.T) {
  		p := writeEnv(t, `DOTENV_TEST_QUOTED="quoted value"`)
  		os.Unsetenv("DOTENV_TEST_QUOTED")

  		if err := DotenvLoad(p); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got := os.Getenv("DOTENV_TEST_QUOTED"); got != "quoted value" {
  			t.Errorf("expected 'quoted value', got %q", got)
  		}
  	})

  	t.Run("valores con comillas simples se stripean", func(t *testing.T) {
  		p := writeEnv(t, "DOTENV_TEST_SINGLE='single quoted'")
  		os.Unsetenv("DOTENV_TEST_SINGLE")

  		if err := DotenvLoad(p); err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got := os.Getenv("DOTENV_TEST_SINGLE"); got != "single quoted" {
  			t.Errorf("expected 'single quoted', got %q", got)
  		}
  	})

  	t.Run("archivo inexistente retorna error", func(t *testing.T) {
  		err := DotenvLoad("/nonexistent/.env.does.not.exist")
  		if err == nil {
  			t.Error("expected error for missing file")
  		}
  	})

  	t.Run("linea sin signo igual retorna error", func(t *testing.T) {
  		p := writeEnv(t, "INVALID_LINE\n")
  		err := DotenvLoad(p)
  		if err == nil {
  			t.Error("expected error for line without '='")
  		}
  	})
  }


id: email_build_html_go_infra_t0
function_id: email_build_html_go_infra
name: TestEmailBuildHTML
file_path: functions/infra/email_build_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestEmailBuildHTML(t *testing.T) {
  	t.Run("construye mensaje html con campos basicos", func(t *testing.T) {
  		msg := EmailBuildHTML("alice@example.com", []string{"bob@example.com"}, "Hola", "<b>Hola</b>")
  		if msg.From != "alice@example.com" {
  			t.Errorf("From: got %q, want %q", msg.From, "alice@example.com")
  		}
  		if len(msg.To) != 1 || msg.To[0] != "bob@example.com" {
  			t.Errorf("To: got %v, want [bob@example.com]", msg.To)
  		}
  		if msg.Subject != "Hola" {
  			t.Errorf("Subject: got %q, want %q", msg.Subject, "Hola")
  		}
  		if msg.BodyHTML != "<b>Hola</b>" {
  			t.Errorf("BodyHTML: got %q, want %q", msg.BodyHTML, "<b>Hola</b>")
  		}
  	})

  	t.Run("copia el slice to para evitar aliasing", func(t *testing.T) {
  		to := []string{"a@example.com", "b@example.com"}
  		msg := EmailBuildHTML("x@x.com", to, "s", "h")
  		to[0] = "mutated@example.com"
  		if msg.To[0] == "mutated@example.com" {
  			t.Error("To slice was aliased — mutation affected the message")
  		}
  	})

  	t.Run("campos no usados quedan vacios", func(t *testing.T) {
  		msg := EmailBuildHTML("f@example.com", []string{"t@example.com"}, "s", "h")
  		if msg.BodyText != "" {
  			t.Errorf("BodyText should be empty, got %q", msg.BodyText)
  		}
  		if len(msg.CC) != 0 {
  			t.Errorf("CC should be empty, got %v", msg.CC)
  		}
  		if len(msg.BCC) != 0 {
  			t.Errorf("BCC should be empty, got %v", msg.BCC)
  		}
  		if len(msg.Attachments) != 0 {
  			t.Errorf("Attachments should be empty, got %v", msg.Attachments)
  		}
  	})
  }


id: email_build_html_go_infra_t1
function_id: email_build_html_go_infra
name: TestEmailBuildText
file_path: functions/infra/email_build_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestEmailBuildText(t *testing.T) {
  	t.Run("construye mensaje text con campos basicos", func(t *testing.T) {
  		msg := EmailBuildText("alice@example.com", []string{"bob@example.com"}, "Alerta", "Servidor caido")
  		if msg.BodyText != "Servidor caido" {
  			t.Errorf("BodyText: got %q, want %q", msg.BodyText, "Servidor caido")
  		}
  		if msg.From != "alice@example.com" {
  			t.Errorf("From: got %q, want %q", msg.From, "alice@example.com")
  		}
  		if msg.Subject != "Alerta" {
  			t.Errorf("Subject: got %q, want %q", msg.Subject, "Alerta")
  		}
  	})

  	t.Run("body html queda vacio", func(t *testing.T) {
  		msg := EmailBuildText("f@x.com", []string{"t@x.com"}, "s", "body")
  		if msg.BodyHTML != "" {
  			t.Errorf("BodyHTML should be empty, got %q", msg.BodyHTML)
  		}
  	})
  }


id: email_build_html_go_infra_t2
function_id: email_build_html_go_infra
name: TestEmailWithAttachment
file_path: functions/infra/email_build_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestEmailWithAttachment(t *testing.T) {
  	t.Run("agrega adjunto sin mutar el original", func(t *testing.T) {
  		orig := EmailBuildHTML("a@example.com", []string{"b@example.com"}, "s", "h")
  		att := EmailAttachment{Filename: "f.pdf", ContentType: "application/pdf", Data: []byte("data")}
  		msg2 := EmailWithAttachment(orig, att)

  		if len(orig.Attachments) != 0 {
  			t.Errorf("original was mutated: got %d attachments, want 0", len(orig.Attachments))
  		}
  		if len(msg2.Attachments) != 1 {
  			t.Errorf("got %d attachments, want 1", len(msg2.Attachments))
  		}
  		if msg2.Attachments[0].Filename != "f.pdf" {
  			t.Errorf("Filename: got %q, want %q", msg2.Attachments[0].Filename, "f.pdf")
  		}
  	})

  	t.Run("multiples adjuntos se acumulan", func(t *testing.T) {
  		msg := EmailBuildHTML("a@x.com", []string{"b@x.com"}, "s", "h")
  		msg = EmailWithAttachment(msg, EmailAttachment{Filename: "a1.txt", ContentType: "text/plain", Data: []byte("1")})
  		msg = EmailWithAttachment(msg, EmailAttachment{Filename: "a2.txt", ContentType: "text/plain", Data: []byte("2")})
  		if len(msg.Attachments) != 2 {
  			t.Errorf("got %d attachments, want 2", len(msg.Attachments))
  		}
  	})

  	t.Run("copia todos los campos del mensaje", func(t *testing.T) {
  		orig := EmailMessage{
  			From:     "a@x.com",
  			To:       []string{"b@x.com"},
  			CC:       []string{"c@x.com"},
  			BCC:      []string{"d@x.com"},
  			Subject:  "Test",
  			BodyHTML: "<p>Hi</p>",
  			BodyText: "Hi",
  			Headers:  map[string]string{"X-Test": "value"},
  		}
  		att := EmailAttachment{Filename: "f.png", ContentType: "image/png", Data: []byte("img")}
  		msg2 := EmailWithAttachment(orig, att)

  		if msg2.From != orig.From {
  			t.Errorf("From mismatch: %q vs %q", msg2.From, orig.From)
  		}
  		if msg2.Subject != orig.Subject {
  			t.Errorf("Subject mismatch: %q vs %q", msg2.Subject, orig.Subject)
  		}
  		if len(msg2.CC) != 1 || msg2.CC[0] != "c@x.com" {
  			t.Errorf("CC not copied: %v", msg2.CC)
  		}
  		if msg2.Headers["X-Test"] != "value" {
  			t.Errorf("Headers not copied: %v", msg2.Headers)
  		}
  	})
  }


id: email_template_render_go_infra_t0
function_id: email_template_render_go_infra
name: TestEmailTemplateRender
file_path: functions/infra/email_template_render_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestEmailTemplateRender(t *testing.T) {
  	t.Run("renderiza template simple con datos", func(t *testing.T) {
  		got, err := EmailTemplateRender("Hola {{.Name}}", map[string]any{"Name": "Alice"})
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got != "Hola Alice" {
  			t.Errorf("got %q, want %q", got, "Hola Alice")
  		}
  	})

  	t.Run("sustituye multiples variables", func(t *testing.T) {
  		tmpl := "Pedido {{.OrderID}} para {{.Customer}} listo."
  		got, err := EmailTemplateRender(tmpl, map[string]any{"OrderID": "ORD-42", "Customer": "Bob"})
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if !strings.Contains(got, "ORD-42") || !strings.Contains(got, "Bob") {
  			t.Errorf("got %q, expected OrderID and Customer", got)
  		}
  	})

  	t.Run("error en template invalido", func(t *testing.T) {
  		_, err := EmailTemplateRender("{{.Unclosed", nil)
  		if err == nil {
  			t.Error("expected error for invalid template, got nil")
  		}
  	})

  	t.Run("template vacio retorna string vacio", func(t *testing.T) {
  		got, err := EmailTemplateRender("", nil)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got != "" {
  			t.Errorf("got %q, want empty string", got)
  		}
  	})
  }


id: env_require_go_infra_t0
function_id: env_require_go_infra
name: TestEnvRequire
file_path: functions/infra/env_require_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestEnvRequire(t *testing.T) {
  	t.Run("retorna valor cuando variable esta seteada", func(t *testing.T) {
  		t.Setenv("ENV_REQUIRE_TEST_KEY", "myvalue")
  		got, err := EnvRequire("ENV_REQUIRE_TEST_KEY")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got != "myvalue" {
  			t.Errorf("expected myvalue, got %q", got)
  		}
  	})

  	t.Run("retorna error con mensaje descriptivo si variable no existe", func(t *testing.T) {
  		got, err := EnvRequire("ENV_REQUIRE_DEFINITELY_NOT_SET_XYZ")
  		if err == nil {
  			t.Error("expected error for missing variable")
  		}
  		if got != "" {
  			t.Errorf("expected empty string on error, got %q", got)
  		}
  		if err != nil && len(err.Error()) == 0 {
  			t.Error("error message should not be empty")
  		}
  	})

  	t.Run("variable vacia se trata como no seteada", func(t *testing.T) {
  		t.Setenv("ENV_REQUIRE_EMPTY_KEY", "")
  		_, err := EnvRequire("ENV_REQUIRE_EMPTY_KEY")
  		if err == nil {
  			t.Error("expected error for empty variable")
  		}
  	})
  }


id: env_require_go_infra_t1
function_id: env_require_go_infra
name: TestEnvRequireAll
file_path: functions/infra/env_require_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestEnvRequireAll(t *testing.T) {
  	t.Run("retorna mapa con todos los valores cuando estan seteados", func(t *testing.T) {
  		t.Setenv("REQUIRE_ALL_A", "alpha")
  		t.Setenv("REQUIRE_ALL_B", "beta")
  		got, err := EnvRequireAll([]string{"REQUIRE_ALL_A", "REQUIRE_ALL_B"})
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if got["REQUIRE_ALL_A"] != "alpha" || got["REQUIRE_ALL_B"] != "beta" {
  			t.Errorf("unexpected map: %v", got)
  		}
  	})

  	t.Run("acumula todos los errores de variables faltantes", func(t *testing.T) {
  		_, err := EnvRequireAll([]string{"REQUIRE_ALL_MISSING_1", "REQUIRE_ALL_MISSING_2"})
  		if err == nil {
  			t.Error("expected error")
  		}
  		msg := err.Error()
  		if len(msg) == 0 {
  			t.Error("expected non-empty error message")
  		}
  	})

  	t.Run("lista vacia retorna mapa vacio sin error", func(t *testing.T) {
  		got, err := EnvRequireAll([]string{})
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(got) != 0 {
  			t.Errorf("expected empty map, got %v", got)
  		}
  	})
  }


id: file_delete_go_infra_t0
function_id: file_delete_go_infra
name: TestFileDelete
file_path: functions/infra/file_delete_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestFileDelete(t *testing.T) {
  	t.Run("elimina archivo existente", func(t *testing.T) {
  		dir := t.TempDir()
  		path := filepath.Join(dir, "x.txt")
  		if err := os.WriteFile(path, []byte("hi"), 0o644); err != nil {
  			t.Fatalf("setup: %v", err)
  		}
  		if err := FileDelete(path); err != nil {
  			t.Fatalf("FileDelete err: %v", err)
  		}
  		if _, err := os.Stat(path); !os.IsNotExist(err) {
  			t.Errorf("archivo aun existe: %v", err)
  		}
  	})

  	t.Run("rechaza path con ..", func(t *testing.T) {
  		err := FileDelete("./uploads/../etc/passwd")
  		if err == nil || !strings.Contains(err.Error(), "path traversal") {
  			t.Errorf("got err %v, want path traversal", err)
  		}
  	})

  	t.Run("rechaza path vacio", func(t *testing.T) {
  		err := FileDelete("")
  		if err == nil {
  			t.Error("got nil, want error")
  		}
  	})

  	t.Run("retorna error si no existe", func(t *testing.T) {
  		err := FileDelete(filepath.Join(t.TempDir(), "nope.txt"))
  		if err == nil {
  			t.Error("got nil, want error not found")
  		}
  	})
  }


id: file_save_disk_go_infra_t0
function_id: file_save_disk_go_infra
name: TestFileSaveDisk
file_path: functions/infra/file_save_disk_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestFileSaveDisk(t *testing.T) {
  	t.Run("guarda contenido en baseDir con nombre UUID", func(t *testing.T) {
  		dir := t.TempDir()
  		body := strings.NewReader("hello world")
  		got, err := FileSaveDisk(dir, "saludos.txt", body)
  		if err != nil {
  			t.Fatalf("FileSaveDisk err: %v", err)
  		}
  		if got.Filename != "saludos.txt" {
  			t.Errorf("got Filename %q, want saludos.txt", got.Filename)
  		}
  		if !strings.HasSuffix(got.StoredName, ".txt") {
  			t.Errorf("got StoredName %q, want suffix .txt", got.StoredName)
  		}
  		if !strings.HasPrefix(got.Path, dir) {
  			t.Errorf("got Path %q, want prefix %q", got.Path, dir)
  		}
  		if got.Size != int64(len("hello world")) {
  			t.Errorf("got Size %d, want %d", got.Size, len("hello world"))
  		}

  		data, err := os.ReadFile(got.Path)
  		if err != nil {
  			t.Fatalf("ReadFile: %v", err)
  		}
  		if string(data) != "hello world" {
  			t.Errorf("contenido en disco %q, want %q", data, "hello world")
  		}
  	})

  	t.Run("crea baseDir si no existe", func(t *testing.T) {
  		base := filepath.Join(t.TempDir(), "nested", "uploads")
  		body := strings.NewReader("x")
  		got, err := FileSaveDisk(base, "a.png", body)
  		if err != nil {
  			t.Fatalf("FileSaveDisk err: %v", err)
  		}
  		if _, err := os.Stat(got.Path); err != nil {
  			t.Fatalf("archivo no escrito: %v", err)
  		}
  	})

  	t.Run("infiere ContentType desde la extension", func(t *testing.T) {
  		dir := t.TempDir()
  		got, err := FileSaveDisk(dir, "logo.png", strings.NewReader("x"))
  		if err != nil {
  			t.Fatalf("err: %v", err)
  		}
  		if !strings.HasPrefix(got.ContentType, "image/png") {
  			t.Errorf("got ContentType %q, want image/png prefix", got.ContentType)
  		}
  	})
  }


id: file_serve_go_infra_t0
function_id: file_serve_go_infra
name: TestFileServe
file_path: functions/infra/file_serve_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestFileServe(t *testing.T) {
  	dir := t.TempDir()
  	if err := os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hola mundo"), 0o644); err != nil {
  		t.Fatalf("setup: %v", err)
  	}

  	handler := FileServe(dir, "/files/", 60)

  	t.Run("sirve archivo existente con headers de cache", func(t *testing.T) {
  		req := httptest.NewRequest("GET", "/files/hello.txt", nil)
  		rec := httptest.NewRecorder()
  		handler.ServeHTTP(rec, req)

  		if rec.Code != http.StatusOK {
  			t.Fatalf("got %d, want 200", rec.Code)
  		}
  		if rec.Body.String() != "hola mundo" {
  			t.Errorf("got body %q", rec.Body.String())
  		}
  		if cc := rec.Header().Get("Cache-Control"); cc != "public, max-age=60" {
  			t.Errorf("got Cache-Control %q", cc)
  		}
  	})

  	t.Run("responde 404 para archivo inexistente", func(t *testing.T) {
  		req := httptest.NewRequest("GET", "/files/missing.txt", nil)
  		rec := httptest.NewRecorder()
  		handler.ServeHTTP(rec, req)
  		if rec.Code != http.StatusNotFound {
  			t.Errorf("got %d, want 404", rec.Code)
  		}
  	})

  	t.Run("rechaza path con .. con 400", func(t *testing.T) {
  		req := httptest.NewRequest("GET", "/files/../etc/passwd", nil)
  		rec := httptest.NewRecorder()
  		handler.ServeHTTP(rec, req)
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400", rec.Code)
  		}
  	})
  }


id: file_unique_name_go_infra_t0
function_id: file_unique_name_go_infra
name: TestFileUniqueName
file_path: functions/infra/file_unique_name_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestFileUniqueName(t *testing.T) {
  	t.Run("preserva extension comun como png", func(t *testing.T) {
  		got := FileUniqueName("foto.png")
  		if !strings.HasSuffix(got, ".png") {
  			t.Fatalf("got %q, want suffix .png", got)
  		}
  		if len(got) < 36+4 { // uuid + ".png"
  			t.Fatalf("got %q, want UUID + .png", got)
  		}
  	})

  	t.Run("convierte extension a minusculas", func(t *testing.T) {
  		got := FileUniqueName("VACACIONES.JPEG")
  		if !strings.HasSuffix(got, ".jpeg") {
  			t.Fatalf("got %q, want suffix .jpeg", got)
  		}
  	})

  	t.Run("remueve caracteres especiales en extension", func(t *testing.T) {
  		got := FileUniqueName("malicious.t!x@t#")
  		if !strings.HasSuffix(got, ".txt") {
  			t.Fatalf("got %q, want suffix .txt", got)
  		}
  	})

  	t.Run("genera UUID sin extension si el archivo no tiene", func(t *testing.T) {
  		got := FileUniqueName("contrato_sin_extension")
  		if strings.Contains(got, ".") {
  			t.Fatalf("got %q, want sin punto", got)
  		}
  		if len(got) != 36 {
  			t.Fatalf("got %q (len %d), want UUID len 36", got, len(got))
  		}
  	})

  	t.Run("trunca extensiones extremadamente largas", func(t *testing.T) {
  		got := FileUniqueName("file." + strings.Repeat("a", 100))
  		// Buscar la ultima parte despues del punto
  		idx := strings.LastIndex(got, ".")
  		if idx < 0 {
  			t.Fatalf("got %q, want al menos un punto", got)
  		}
  		ext := got[idx+1:]
  		if len(ext) > 16 {
  			t.Fatalf("got ext len %d, want <= 16", len(ext))
  		}
  	})

  	t.Run("dos llamadas generan IDs distintos", func(t *testing.T) {
  		a := FileUniqueName("x.png")
  		b := FileUniqueName("x.png")
  		if a == b {
  			t.Fatalf("got %q == %q, want distintos", a, b)
  		}
  	})
  }


id: file_validate_type_go_infra_t0
function_id: file_validate_type_go_infra
name: TestFileValidateType
file_path: functions/infra/file_validate_type_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestFileValidateType(t *testing.T) {
  	allowed := []string{"image/png", "image/jpeg", "application/pdf", "image/webp"}

  	t.Run("detecta JPEG por magic bytes", func(t *testing.T) {
  		header := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01}
  		mime, ok := FileValidateType(header, allowed)
  		if mime != "image/jpeg" || !ok {
  			t.Fatalf("got (%q,%v), want (image/jpeg,true)", mime, ok)
  		}
  	})

  	t.Run("detecta PNG por magic bytes", func(t *testing.T) {
  		header := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D}
  		mime, ok := FileValidateType(header, allowed)
  		if mime != "image/png" || !ok {
  			t.Fatalf("got (%q,%v), want (image/png,true)", mime, ok)
  		}
  	})

  	t.Run("detecta PDF", func(t *testing.T) {
  		header := []byte{0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x37}
  		mime, ok := FileValidateType(header, allowed)
  		if mime != "application/pdf" || !ok {
  			t.Fatalf("got (%q,%v), want (application/pdf,true)", mime, ok)
  		}
  	})

  	t.Run("detecta WebP con prefix RIFF y suffix WEBP", func(t *testing.T) {
  		header := []byte{
  			0x52, 0x49, 0x46, 0x46, // RIFF
  			0xAA, 0xBB, 0xCC, 0xDD, // tamano (cualquier valor)
  			0x57, 0x45, 0x42, 0x50, // WEBP
  			0x56, 0x50, 0x38, 0x20,
  		}
  		mime, ok := FileValidateType(header, allowed)
  		if mime != "image/webp" || !ok {
  			t.Fatalf("got (%q,%v), want (image/webp,true)", mime, ok)
  		}
  	})

  	t.Run("rechaza tipo no permitido", func(t *testing.T) {
  		// PDF detectado, pero no en allowedTypes
  		header := []byte{0x25, 0x50, 0x44, 0x46, 0x2D}
  		mime, ok := FileValidateType(header, []string{"image/png"})
  		if mime != "application/pdf" {
  			t.Fatalf("got mime %q, want application/pdf", mime)
  		}
  		if ok {
  			t.Fatalf("got ok=true, want false (PDF no en allowedTypes)")
  		}
  	})

  	t.Run("tipo desconocido retorna vacio", func(t *testing.T) {
  		header := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
  		mime, ok := FileValidateType(header, allowed)
  		if mime != "" || ok {
  			t.Fatalf("got (%q,%v), want (\"\",false)", mime, ok)
  		}
  	})

  	t.Run("header demasiado corto retorna vacio", func(t *testing.T) {
  		header := []byte{0xFF}
  		mime, ok := FileValidateType(header, allowed)
  		if mime != "" || ok {
  			t.Fatalf("got (%q,%v), want (\"\",false)", mime, ok)
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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_cors_middleware_go_infra_t0
function_id: http_cors_middleware_go_infra
name: TestHTTPCORSMiddleware
file_path: functions/infra/http_cors_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPCORSMiddleware(t *testing.T) {
  	origins := []string{"https://example.com"}
  	methods := []string{"GET", "POST"}

  	base := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		w.WriteHeader(http.StatusOK)
  	})

  	t.Run("setea headers CORS en respuesta normal", func(t *testing.T) {
  		mw := HTTPCORSMiddleware(origins, methods)
  		handler := mw(base)

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/api/test", nil)
  		handler.ServeHTTP(rec, req)

  		if rec.Code != http.StatusOK {
  			t.Errorf("got status %d, want 200", rec.Code)
  		}
  		if rec.Header().Get("Access-Control-Allow-Origin") == "" {
  			t.Error("Access-Control-Allow-Origin header not set")
  		}
  	})

  	t.Run("responde 204 a peticion OPTIONS preflight", func(t *testing.T) {
  		mw := HTTPCORSMiddleware(origins, methods)
  		handler := mw(base)

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("OPTIONS", "/api/test", nil)
  		handler.ServeHTTP(rec, req)

  		if rec.Code != http.StatusNoContent {
  			t.Errorf("got status %d, want 204", rec.Code)
  		}
  	})

  	t.Run("headers incluyen los origenes configurados", func(t *testing.T) {
  		mw := HTTPCORSMiddleware([]string{"https://app.example.com"}, methods)
  		handler := mw(base)

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		handler.ServeHTTP(rec, req)

  		got := rec.Header().Get("Access-Control-Allow-Origin")
  		if got != "https://app.example.com" {
  			t.Errorf("got Access-Control-Allow-Origin=%q, want %q", got, "https://app.example.com")
  		}
  	})

  	t.Run("headers incluyen los metodos configurados", func(t *testing.T) {
  		mw := HTTPCORSMiddleware(origins, []string{"GET", "DELETE"})
  		handler := mw(base)

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		handler.ServeHTTP(rec, req)

  		got := rec.Header().Get("Access-Control-Allow-Methods")
  		if got != "GET, DELETE" {
  			t.Errorf("got Access-Control-Allow-Methods=%q, want %q", got, "GET, DELETE")
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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_error_response_go_infra_t0
function_id: http_error_response_go_infra
name: TestHTTPJSONResponse
file_path: functions/infra/http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPJSONResponse(t *testing.T) {
  	t.Run("escribe status code correcto", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		HTTPJSONResponse(rec, http.StatusCreated, map[string]string{"ok": "true"})
  		if rec.Code != http.StatusCreated {
  			t.Errorf("got status %d, want 201", rec.Code)
  		}
  	})

  	t.Run("setea Content-Type application/json", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		HTTPJSONResponse(rec, http.StatusOK, "test")
  		ct := rec.Header().Get("Content-Type")
  		if ct != "application/json" {
  			t.Errorf("got Content-Type=%q, want application/json", ct)
  		}
  	})

  	t.Run("serializa datos correctamente a JSON", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		data := map[string]int{"count": 42}
  		HTTPJSONResponse(rec, http.StatusOK, data)

  		var got map[string]int
  		if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
  			t.Fatalf("failed to parse response JSON: %v", err)
  		}
  		if got["count"] != 42 {
  			t.Errorf("got count=%d, want 42", got["count"])
  		}
  	})
  }

  // --- HTTPErrorResponse ---


id: http_error_response_go_infra_t1
function_id: http_error_response_go_infra
name: TestHTTPErrorResponse
file_path: functions/infra/http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPErrorResponse(t *testing.T) {
  	t.Run("escribe el status code del HTTPError", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		HTTPErrorResponse(rec, HTTPError{Status: http.StatusBadRequest, Code: "bad", Message: "err"})
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got status %d, want 400", rec.Code)
  		}
  	})

  	t.Run("setea Content-Type application/json", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		HTTPErrorResponse(rec, HTTPError{Status: 500, Code: "err", Message: "fail"})
  		ct := rec.Header().Get("Content-Type")
  		if ct != "application/json" {
  			t.Errorf("got Content-Type=%q, want application/json", ct)
  		}
  	})

  	t.Run("serializa Code y Message en el body JSON", func(t *testing.T) {
  		rec := httptest.NewRecorder()
  		HTTPErrorResponse(rec, HTTPError{Status: 422, Code: "validation_error", Message: "campo requerido"})

  		var got HTTPError
  		if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
  			t.Fatalf("failed to parse response JSON: %v", err)
  		}
  		if got.Code != "validation_error" {
  			t.Errorf("got Code=%q, want validation_error", got.Code)
  		}
  		if got.Message != "campo requerido" {
  			t.Errorf("got Message=%q, want 'campo requerido'", got.Message)
  		}
  	})
  }

  // --- HTTPParseBody ---


id: http_error_response_go_infra_t2
function_id: http_error_response_go_infra
name: TestHTTPParseBody
file_path: functions/infra/http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPParseBody(t *testing.T) {
  	t.Run("decodifica JSON valido correctamente", func(t *testing.T) {
  		body := `{"name":"Lucas","age":30}`
  		req := httptest.NewRequest("POST", "/", strings.NewReader(body))
  		req.Header.Set("Content-Type", "application/json")

  		var dst struct {
  			Name string `json:"name"`
  			Age  int    `json:"age"`
  		}
  		err := HTTPParseBody(req, &dst, 1<<20)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if dst.Name != "Lucas" || dst.Age != 30 {
  			t.Errorf("got {%q, %d}, want {Lucas, 30}", dst.Name, dst.Age)
  		}
  	})

  	t.Run("retorna error si el body supera maxBytes", func(t *testing.T) {
  		body := strings.Repeat("x", 100)
  		payload := `{"data":"` + body + `"}`
  		req := httptest.NewRequest("POST", "/", strings.NewReader(payload))

  		var dst struct {
  			Data string `json:"data"`
  		}
  		err := HTTPParseBody(req, &dst, 10) // limite de 10 bytes
  		if err == nil {
  			t.Error("expected error for oversized body, got nil")
  		}
  	})

  	t.Run("retorna error si el JSON es invalido", func(t *testing.T) {
  		req := httptest.NewRequest("POST", "/", strings.NewReader(`{invalid json`))
  		var dst map[string]any
  		err := HTTPParseBody(req, &dst, 1<<20)
  		if err == nil {
  			t.Error("expected error for invalid JSON, got nil")
  		}
  	})

  	t.Run("retorna error si hay campos desconocidos", func(t *testing.T) {
  		req := httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test","unknown":"field"}`))
  		var dst struct {
  			Name string `json:"name"`
  		}
  		err := HTTPParseBody(req, &dst, 1<<20)
  		if err == nil {
  			t.Error("expected error for unknown fields, got nil")
  		}
  	})
  }

  // --- HTTPLoggerMiddleware ---


id: http_error_response_go_infra_t3
function_id: http_error_response_go_infra
name: TestHTTPLoggerMiddleware
file_path: functions/infra/http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPLoggerMiddleware(t *testing.T) {
  	t.Run("loguea metodo y path del request", func(t *testing.T) {
  		var buf bytes.Buffer
  		mw := HTTPLoggerMiddleware(&buf)
  		handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusOK)
  		}))

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/api/test", nil)
  		handler.ServeHTTP(rec, req)

  		log := buf.String()
  		if !strings.Contains(log, "GET") {
  			t.Errorf("log does not contain method GET: %q", log)
  		}
  		if !strings.Contains(log, "/api/test") {
  			t.Errorf("log does not contain path /api/test: %q", log)
  		}
  	})

  	t.Run("loguea el status code de la respuesta", func(t *testing.T) {
  		var buf bytes.Buffer
  		mw := HTTPLoggerMiddleware(&buf)
  		handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusNotFound)
  		}))

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/missing", nil)
  		handler.ServeHTTP(rec, req)

  		log := buf.String()
  		if !strings.Contains(log, "404") {
  			t.Errorf("log does not contain status 404: %q", log)
  		}
  	})

  	t.Run("loguea la duracion en milisegundos", func(t *testing.T) {
  		var buf bytes.Buffer
  		mw := HTTPLoggerMiddleware(&buf)
  		handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusOK)
  		}))

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		handler.ServeHTTP(rec, req)

  		log := buf.String()
  		if !strings.Contains(log, "ms") {
  			t.Errorf("log does not contain duration in ms: %q", log)
  		}
  	})
  }

  // --- HTTPRouter ---


id: http_error_response_go_infra_t4
function_id: http_error_response_go_infra
name: TestHTTPRouter
file_path: functions/infra/http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPRouter(t *testing.T) {
  	t.Run("registra una ruta y responde correctamente", func(t *testing.T) {
  		mux := HTTPRouter([]Route{
  			{
  				Method: "GET",
  				Path:   "/api/ping",
  				Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  					w.WriteHeader(http.StatusOK)
  					w.Write([]byte("pong"))
  				}),
  			},
  		})

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/api/ping", nil)
  		mux.ServeHTTP(rec, req)

  		if rec.Code != http.StatusOK {
  			t.Errorf("got status %d, want 200", rec.Code)
  		}
  		if rec.Body.String() != "pong" {
  			t.Errorf("got body %q, want pong", rec.Body.String())
  		}
  	})

  	t.Run("registra multiples rutas en el mismo mux", func(t *testing.T) {
  		mux := HTTPRouter([]Route{
  			{Method: "GET", Path: "/a", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				w.Write([]byte("a"))
  			})},
  			{Method: "GET", Path: "/b", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				w.Write([]byte("b"))
  			})},
  		})

  		recA := httptest.NewRecorder()
  		mux.ServeHTTP(recA, httptest.NewRequest("GET", "/a", nil))
  		if recA.Body.String() != "a" {
  			t.Errorf("route /a: got %q, want a", recA.Body.String())
  		}

  		recB := httptest.NewRecorder()
  		mux.ServeHTTP(recB, httptest.NewRequest("GET", "/b", nil))
  		if recB.Body.String() != "b" {
  			t.Errorf("route /b: got %q, want b", recB.Body.String())
  		}
  	})

  	t.Run("ruta no registrada retorna 404", func(t *testing.T) {
  		mux := HTTPRouter([]Route{
  			{Method: "GET", Path: "/known", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				w.WriteHeader(http.StatusOK)
  			})},
  		})

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/unknown", nil)
  		mux.ServeHTTP(rec, req)

  		if rec.Code != http.StatusNotFound {
  			t.Errorf("got status %d, want 404", rec.Code)
  		}
  	})
  }

  // --- HTTPServe ---


id: http_error_response_go_infra_t5
function_id: http_error_response_go_infra
name: TestHTTPServe
file_path: functions/infra/http_server_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPServe(t *testing.T) {
  	t.Run("servidor arranca y responde peticiones", func(t *testing.T) {
  		ctx, cancel := context.WithCancel(context.Background())
  		defer cancel()

  		mux := HTTPRouter([]Route{
  			{Method: "GET", Path: "/ping", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				w.WriteHeader(http.StatusOK)
  				w.Write([]byte("pong"))
  			})},
  		})

  		errCh := make(chan error, 1)
  		go func() {
  			errCh <- HTTPServe("127.0.0.1:0", mux, ctx)
  		}()

  		// Cancelar el contexto rapidamente — el servidor deberia cerrarse limpiamente
  		time.Sleep(10 * time.Millisecond)
  		cancel()

  		select {
  		case err := <-errCh:
  			if err != nil {
  				t.Errorf("unexpected error from HTTPServe: %v", err)
  			}
  		case <-time.After(5 * time.Second):
  			t.Error("HTTPServe did not stop after context cancel")
  		}
  	})

  	t.Run("graceful shutdown al cancelar contexto", func(t *testing.T) {
  		ctx, cancel := context.WithCancel(context.Background())

  		handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusOK)
  		})

  		done := make(chan error, 1)
  		go func() {
  			done <- HTTPServe("127.0.0.1:0", handler, ctx)
  		}()

  		time.Sleep(10 * time.Millisecond)
  		cancel()

  		select {
  		case err := <-done:
  			if err != nil {
  				t.Errorf("shutdown error: %v", err)
  			}
  		case <-time.After(5 * time.Second):
  			t.Error("server did not shutdown within 5 seconds")
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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_middleware_chain_go_infra_t0
function_id: http_middleware_chain_go_infra
name: TestHTTPMiddlewareChain
file_path: functions/infra/http_middleware_chain_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestHTTPMiddlewareChain(t *testing.T) {
  	t.Run("sin middlewares pasa al handler final", func(t *testing.T) {
  		chain := HTTPMiddlewareChain()
  		handler := chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusOK)
  			w.Write([]byte("final"))
  		}))

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		handler.ServeHTTP(rec, req)

  		if rec.Code != http.StatusOK {
  			t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
  		}
  		if rec.Body.String() != "final" {
  			t.Errorf("got body %q, want %q", rec.Body.String(), "final")
  		}
  	})

  	t.Run("un middleware se aplica correctamente", func(t *testing.T) {
  		addHeader := func(next http.Handler) http.Handler {
  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				w.Header().Set("X-Test", "applied")
  				next.ServeHTTP(w, r)
  			})
  		}

  		chain := HTTPMiddlewareChain(Middleware(addHeader))
  		handler := chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			w.WriteHeader(http.StatusOK)
  		}))

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		handler.ServeHTTP(rec, req)

  		if rec.Header().Get("X-Test") != "applied" {
  			t.Errorf("got X-Test=%q, want %q", rec.Header().Get("X-Test"), "applied")
  		}
  	})

  	t.Run("dos middlewares se aplican en orden izquierda-derecha", func(t *testing.T) {
  		order := []string{}

  		first := Middleware(func(next http.Handler) http.Handler {
  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				order = append(order, "first")
  				next.ServeHTTP(w, r)
  			})
  		})
  		second := Middleware(func(next http.Handler) http.Handler {
  			return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  				order = append(order, "second")
  				next.ServeHTTP(w, r)
  			})
  		})

  		chain := HTTPMiddlewareChain(first, second)
  		handler := chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			order = append(order, "handler")
  			w.WriteHeader(http.StatusOK)
  		}))

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		handler.ServeHTTP(rec, req)

  		if len(order) != 3 || order[0] != "first" || order[1] != "second" || order[2] != "handler" {
  			t.Errorf("got order %v, want [first second handler]", order)
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: jwt_generate_go_infra_t0
function_id: jwt_generate_go_infra
name: TestJWTGenerate_ReturnsThreeSegments
file_path: functions/infra/jwt_generate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTGenerate_ReturnsThreeSegments(t *testing.T) {
  	claims := JWTClaims{
  		Subject:   "user-1",
  		ExpiresAt: time.Now().Add(time.Hour).Unix(),
  	}
  	token, err := JWTGenerate(claims, "test-secret-0123456789abcdef")
  	if err != nil {
  		t.Fatalf("JWTGenerate error: %v", err)
  	}
  	parts := strings.Split(token, ".")
  	if len(parts) != 3 {
  		t.Fatalf("esperados 3 segmentos, got %d en %q", len(parts), token)
  	}
  	for i, p := range parts {
  		if p == "" {
  			t.Fatalf("segmento %d vacio", i)
  		}
  	}
  }


id: jwt_generate_go_infra_t1
function_id: jwt_generate_go_infra
name: TestJWTGenerate_FillsIssuedAtWhenZero
file_path: functions/infra/jwt_generate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTGenerate_FillsIssuedAtWhenZero(t *testing.T) {
  	claims := JWTClaims{
  		Subject:   "user-1",
  		ExpiresAt: time.Now().Add(time.Hour).Unix(),
  	}
  	before := time.Now().Unix()
  	token, err := JWTGenerate(claims, "s")
  	if err != nil {
  		t.Fatalf("JWTGenerate error: %v", err)
  	}
  	after := time.Now().Unix()
  	parsed, err := JWTValidate(token, "s")
  	if err != nil {
  		t.Fatalf("JWTValidate error: %v", err)
  	}
  	if parsed.IssuedAt < before || parsed.IssuedAt > after {
  		t.Fatalf("IssuedAt %d fuera del rango [%d,%d]", parsed.IssuedAt, before, after)
  	}
  }


id: jwt_generate_go_infra_t2
function_id: jwt_generate_go_infra
name: TestJWTGenerate_ErrorsOnEmptySecret
file_path: functions/infra/jwt_generate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTGenerate_ErrorsOnEmptySecret(t *testing.T) {
  	_, err := JWTGenerate(JWTClaims{Subject: "x"}, "")
  	if err == nil {
  		t.Fatal("esperaba error con secret vacio")
  	}
  }


id: jwt_middleware_go_infra_t0
function_id: jwt_middleware_go_infra
name: TestJWTMiddleware_ValidToken
file_path: functions/infra/jwt_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTMiddleware_ValidToken(t *testing.T) {
  	secret := "test-sec"
  	token := makeTokenFor(t, "alice", secret)

  	var gotSubject string
  	handler := JWTMiddleware(secret)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		claims, ok := JWTClaimsFromContext(r.Context())
  		if !ok {
  			t.Error("JWTClaimsFromContext no encontro claims")
  		}
  		gotSubject = claims.Subject
  		w.WriteHeader(200)
  	}))

  	req := httptest.NewRequest(http.MethodGet, "/", nil)
  	req.Header.Set("Authorization", "Bearer "+token)
  	rec := httptest.NewRecorder()

  	handler.ServeHTTP(rec, req)
  	if rec.Code != 200 {
  		t.Errorf("status = %d", rec.Code)
  	}
  	if gotSubject != "alice" {
  		t.Errorf("subject = %q", gotSubject)
  	}
  }


id: jwt_middleware_go_infra_t1
function_id: jwt_middleware_go_infra
name: TestJWTMiddleware_MissingAuthHeader
file_path: functions/infra/jwt_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTMiddleware_MissingAuthHeader(t *testing.T) {
  	handler := JWTMiddleware("s")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		t.Error("no deberia ejecutarse")
  	}))
  	req := httptest.NewRequest(http.MethodGet, "/", nil)
  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, req)
  	if rec.Code != 401 {
  		t.Errorf("status = %d", rec.Code)
  	}
  }


id: jwt_middleware_go_infra_t2
function_id: jwt_middleware_go_infra
name: TestJWTMiddleware_WrongFormat
file_path: functions/infra/jwt_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTMiddleware_WrongFormat(t *testing.T) {
  	handler := JWTMiddleware("s")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		t.Error("no deberia ejecutarse")
  	}))
  	req := httptest.NewRequest(http.MethodGet, "/", nil)
  	req.Header.Set("Authorization", "Basic abcdef")
  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, req)
  	if rec.Code != 401 {
  		t.Errorf("status = %d", rec.Code)
  	}
  }


id: jwt_middleware_go_infra_t3
function_id: jwt_middleware_go_infra
name: TestJWTMiddleware_InvalidToken
file_path: functions/infra/jwt_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTMiddleware_InvalidToken(t *testing.T) {
  	handler := JWTMiddleware("secret-a")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		t.Error("no deberia ejecutarse")
  	}))
  	tok := makeTokenFor(t, "x", "secret-b") // firmado con otro secret
  	req := httptest.NewRequest(http.MethodGet, "/", nil)
  	req.Header.Set("Authorization", "Bearer "+tok)
  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, req)
  	if rec.Code != 401 {
  		t.Errorf("status = %d", rec.Code)
  	}
  }


id: jwt_middleware_go_infra_t4
function_id: jwt_middleware_go_infra
name: TestJWTClaimsFromContext_NotPresent
file_path: functions/infra/jwt_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTClaimsFromContext_NotPresent(t *testing.T) {
  	req := httptest.NewRequest(http.MethodGet, "/", nil)
  	_, ok := JWTClaimsFromContext(req.Context())
  	if ok {
  		t.Fatal("no deberia haber claims en un context nuevo")
  	}
  }


id: jwt_validate_go_infra_t0
function_id: jwt_validate_go_infra
name: TestJWTValidate_ValidatesGeneratedToken
file_path: functions/infra/jwt_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTValidate_ValidatesGeneratedToken(t *testing.T) {
  	claims := JWTClaims{
  		Subject:   "user-42",
  		Issuer:    "tester",
  		ExpiresAt: time.Now().Add(time.Hour).Unix(),
  		Custom:    map[string]any{"role": "admin"},
  	}
  	token, err := JWTGenerate(claims, "super-secret")
  	if err != nil {
  		t.Fatalf("JWTGenerate error: %v", err)
  	}
  	got, err := JWTValidate(token, "super-secret")
  	if err != nil {
  		t.Fatalf("JWTValidate error: %v", err)
  	}
  	if got.Subject != "user-42" {
  		t.Errorf("Subject = %q, esperado user-42", got.Subject)
  	}
  	if got.Issuer != "tester" {
  		t.Errorf("Issuer = %q, esperado tester", got.Issuer)
  	}
  	if got.Custom["role"] != "admin" {
  		t.Errorf("Custom[role] = %v, esperado admin", got.Custom["role"])
  	}
  }


id: jwt_validate_go_infra_t1
function_id: jwt_validate_go_infra
name: TestJWTValidate_RejectsInvalidSignature
file_path: functions/infra/jwt_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTValidate_RejectsInvalidSignature(t *testing.T) {
  	token, err := JWTGenerate(JWTClaims{Subject: "x", ExpiresAt: time.Now().Add(time.Hour).Unix()}, "secret-a")
  	if err != nil {
  		t.Fatalf("JWTGenerate error: %v", err)
  	}
  	if _, err := JWTValidate(token, "secret-b"); err == nil {
  		t.Fatal("esperaba error con secret distinto")
  	}
  }


id: jwt_validate_go_infra_t2
function_id: jwt_validate_go_infra
name: TestJWTValidate_RejectsExpiredToken
file_path: functions/infra/jwt_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTValidate_RejectsExpiredToken(t *testing.T) {
  	token, err := JWTGenerate(JWTClaims{
  		Subject:   "x",
  		ExpiresAt: time.Now().Add(-1 * time.Hour).Unix(),
  	}, "s")
  	if err != nil {
  		t.Fatalf("JWTGenerate error: %v", err)
  	}
  	if _, err := JWTValidate(token, "s"); err == nil {
  		t.Fatal("esperaba error con token expirado")
  	}
  }


id: jwt_validate_go_infra_t3
function_id: jwt_validate_go_infra
name: TestJWTValidate_RejectsMalformedToken
file_path: functions/infra/jwt_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTValidate_RejectsMalformedToken(t *testing.T) {
  	cases := []string{
  		"",
  		"not-a-token",
  		"one.two",
  		"one.two.three.four",
  	}
  	for _, tok := range cases {
  		if _, err := JWTValidate(tok, "s"); err == nil {
  			t.Errorf("esperaba error con token %q", tok)
  		}
  	}
  }


id: jwt_validate_go_infra_t4
function_id: jwt_validate_go_infra
name: TestJWTValidate_RejectsOtherAlgorithms
file_path: functions/infra/jwt_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestJWTValidate_RejectsOtherAlgorithms(t *testing.T) {
  	// Token con alg=none no es aceptado aunque la firma sea vacia
  	// Construimos manualmente: header con alg=none
  	// "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" = {"alg":"none","typ":"JWT"}
  	token := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ4In0."
  	if _, err := JWTValidate(token, "s"); err == nil {
  		t.Fatal("esperaba error con alg=none")
  	}
  	if !strings.Contains(token, ".") {
  		t.Fatal("token debe tener puntos")
  	}
  }


id: log_debug_go_infra_t0
function_id: log_debug_go_infra
name: TestLoggerNew
file_path: functions/infra/logger_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestLoggerNew(t *testing.T) {
  	t.Run("crea logger JSON valido", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, err := LoggerNew(LogLevelInfo, buf, "json")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if logger == nil {
  			t.Fatal("expected non-nil logger")
  		}
  		if logger.Format != "json" {
  			t.Errorf("got format=%q, want json", logger.Format)
  		}
  		if logger.Level != LogLevelInfo {
  			t.Errorf("got level=%d, want LogLevelInfo", logger.Level)
  		}
  	})

  	t.Run("crea logger text valido", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, err := LoggerNew(LogLevelDebug, buf, "text")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if logger.Format != "text" {
  			t.Errorf("got format=%q, want text", logger.Format)
  		}
  	})

  	t.Run("rechaza formato invalido", func(t *testing.T) {
  		_, err := LoggerNew(LogLevelInfo, &bytes.Buffer{}, "xml")
  		if err == nil {
  			t.Fatal("expected error for invalid format")
  		}
  	})

  	t.Run("output nil cae en os.Stderr sin panic", func(t *testing.T) {
  		logger, err := LoggerNew(LogLevelError, nil, "json")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if logger.Output == nil {
  			t.Error("expected Output to default to os.Stderr, got nil")
  		}
  	})

  	t.Run("emite JSON valido al escribir", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelInfo, buf, "json")
  		LogInfo(logger, "hello")

  		var parsed map[string]any
  		if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed); err != nil {
  			t.Fatalf("output no es JSON valido: %v\noutput: %s", err, buf.String())
  		}
  		if parsed["msg"] != "hello" {
  			t.Errorf("got msg=%v, want hello", parsed["msg"])
  		}
  		if parsed["level"] != "INFO" {
  			t.Errorf("got level=%v, want INFO", parsed["level"])
  		}
  	})

  	t.Run("filtra mensajes debajo del nivel configurado", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelWarn, buf, "json")
  		LogDebug(logger, "debug msg")
  		LogInfo(logger, "info msg")
  		LogWarn(logger, "warn msg")
  		LogError(logger, "error msg")

  		output := buf.String()
  		if strings.Contains(output, "debug msg") {
  			t.Error("debug msg no deberia aparecer con LogLevelWarn")
  		}
  		if strings.Contains(output, "info msg") {
  			t.Error("info msg no deberia aparecer con LogLevelWarn")
  		}
  		if !strings.Contains(output, "warn msg") {
  			t.Error("warn msg deberia aparecer con LogLevelWarn")
  		}
  		if !strings.Contains(output, "error msg") {
  			t.Error("error msg deberia aparecer con LogLevelWarn")
  		}
  	})
  }

  // --- LoggerWith ---


id: log_debug_go_infra_t1
function_id: log_debug_go_infra
name: TestLoggerWith
file_path: functions/infra/logger_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestLoggerWith(t *testing.T) {
  	t.Run("anade campos al logger", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		base, _ := LoggerNew(LogLevelInfo, buf, "json")
  		appLog := LoggerWith(base, map[string]any{"app": "test", "version": "1.0"})

  		LogInfo(appLog, "evento")

  		var parsed map[string]any
  		if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed); err != nil {
  			t.Fatalf("JSON invalido: %v", err)
  		}
  		if parsed["app"] != "test" {
  			t.Errorf("got app=%v, want test", parsed["app"])
  		}
  		if parsed["version"] != "1.0" {
  			t.Errorf("got version=%v, want 1.0", parsed["version"])
  		}
  	})

  	t.Run("no muta el logger original", func(t *testing.T) {
  		base, _ := LoggerNew(LogLevelInfo, &bytes.Buffer{}, "json")
  		if len(base.Fields) != 0 {
  			t.Fatalf("base logger deberia tener 0 fields iniciales, got %d", len(base.Fields))
  		}
  		_ = LoggerWith(base, map[string]any{"a": 1})
  		if len(base.Fields) != 0 {
  			t.Errorf("base logger no deberia haber mutado, got %d fields", len(base.Fields))
  		}
  	})

  	t.Run("apila fields sobre un logger ya contextualizado", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		base, _ := LoggerNew(LogLevelInfo, buf, "json")
  		appLog := LoggerWith(base, map[string]any{"app": "api"})
  		reqLog := LoggerWith(appLog, map[string]any{"request_id": "abc"})

  		LogInfo(reqLog, "inicio")

  		var parsed map[string]any
  		if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed); err != nil {
  			t.Fatalf("JSON invalido: %v", err)
  		}
  		if parsed["app"] != "api" {
  			t.Errorf("falta campo app heredado del padre, got %v", parsed["app"])
  		}
  		if parsed["request_id"] != "abc" {
  			t.Errorf("falta campo request_id nuevo, got %v", parsed["request_id"])
  		}
  	})

  	t.Run("retorna nil si recibe nil", func(t *testing.T) {
  		got := LoggerWith(nil, map[string]any{"k": "v"})
  		if got != nil {
  			t.Errorf("expected nil, got %v", got)
  		}
  	})
  }

  // --- Log niveles ---


id: log_debug_go_infra_t2
function_id: log_debug_go_infra
name: TestLogLevels
file_path: functions/infra/logger_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestLogLevels(t *testing.T) {
  	t.Run("LogInfo emite nivel INFO", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelDebug, buf, "json")
  		LogInfo(logger, "m")

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["level"] != "INFO" {
  			t.Errorf("got level=%v, want INFO", parsed["level"])
  		}
  	})

  	t.Run("LogWarn emite nivel WARN", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelDebug, buf, "json")
  		LogWarn(logger, "m")

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["level"] != "WARN" {
  			t.Errorf("got level=%v, want WARN", parsed["level"])
  		}
  	})

  	t.Run("LogError emite nivel ERROR", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelDebug, buf, "json")
  		LogError(logger, "m")

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["level"] != "ERROR" {
  			t.Errorf("got level=%v, want ERROR", parsed["level"])
  		}
  	})

  	t.Run("LogDebug emite nivel DEBUG", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelDebug, buf, "json")
  		LogDebug(logger, "m")

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["level"] != "DEBUG" {
  			t.Errorf("got level=%v, want DEBUG", parsed["level"])
  		}
  	})

  	t.Run("campos inline en la llamada aparecen en el JSON", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelInfo, buf, "json")
  		LogInfo(logger, "evento", "port", 8080, "user", "lucas")

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["port"] != float64(8080) {
  			t.Errorf("got port=%v, want 8080", parsed["port"])
  		}
  		if parsed["user"] != "lucas" {
  			t.Errorf("got user=%v, want lucas", parsed["user"])
  		}
  	})

  	t.Run("logger nil no hace panic en las funciones de log", func(t *testing.T) {
  		defer func() {
  			if r := recover(); r != nil {
  				t.Errorf("panic inesperado con logger nil: %v", r)
  			}
  		}()
  		LogDebug(nil, "msg")
  		LogInfo(nil, "msg")
  		LogWarn(nil, "msg")
  		LogError(nil, "msg")
  	})
  }

  // --- LoggerMiddleware ---


id: log_debug_go_infra_t3
function_id: log_debug_go_infra
name: TestLoggerMiddleware
file_path: functions/infra/logger_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestLoggerMiddleware(t *testing.T) {
  	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		w.WriteHeader(http.StatusCreated)
  		_, _ = w.Write([]byte("ok"))
  	})

  	t.Run("loguea method, path, status y duration_ms", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelInfo, buf, "json")
  		mw := LoggerMiddleware(logger)

  		req := httptest.NewRequest(http.MethodPost, "/api/users", nil)
  		rec := httptest.NewRecorder()
  		mw(handler).ServeHTTP(rec, req)

  		var parsed map[string]any
  		if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed); err != nil {
  			t.Fatalf("JSON invalido: %v\noutput: %s", err, buf.String())
  		}
  		if parsed["method"] != "POST" {
  			t.Errorf("got method=%v, want POST", parsed["method"])
  		}
  		if parsed["path"] != "/api/users" {
  			t.Errorf("got path=%v, want /api/users", parsed["path"])
  		}
  		if parsed["status"] != float64(http.StatusCreated) {
  			t.Errorf("got status=%v, want 201", parsed["status"])
  		}
  		if _, ok := parsed["duration_ms"]; !ok {
  			t.Error("falta campo duration_ms en el log")
  		}
  	})

  	t.Run("usa status 200 si el handler no llama WriteHeader", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		logger, _ := LoggerNew(LogLevelInfo, buf, "json")
  		mw := LoggerMiddleware(logger)

  		silentHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			_, _ = w.Write([]byte("hi"))
  		})

  		req := httptest.NewRequest(http.MethodGet, "/health", nil)
  		rec := httptest.NewRecorder()
  		mw(silentHandler).ServeHTTP(rec, req)

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["status"] != float64(http.StatusOK) {
  			t.Errorf("got status=%v, want 200", parsed["status"])
  		}
  	})

  	t.Run("preserva los campos contextuales del logger", func(t *testing.T) {
  		buf := &bytes.Buffer{}
  		base, _ := LoggerNew(LogLevelInfo, buf, "json")
  		appLog := LoggerWith(base, map[string]any{"app": "sqlite_api"})
  		mw := LoggerMiddleware(appLog)

  		req := httptest.NewRequest(http.MethodGet, "/", nil)
  		rec := httptest.NewRecorder()
  		mw(handler).ServeHTTP(rec, req)

  		var parsed map[string]any
  		_ = json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &parsed)
  		if parsed["app"] != "sqlite_api" {
  			t.Errorf("falta campo contextual app=sqlite_api, got %v", parsed["app"])
  		}
  	})
  }


id: migration_create_go_infra_t0
function_id: migration_create_go_infra
name: TestMigrationCreate
file_path: functions/infra/migration_create_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestMigrationCreate(t *testing.T) {
  	t.Run("directorio vacio crea archivo con version 001", func(t *testing.T) {
  		dir := t.TempDir()
  		path, err := MigrationCreate(dir, "create_users")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}

  		base := filepath.Base(path)
  		if base != "001_create_users.sql" {
  			t.Errorf("filename: got %q, want %q", base, "001_create_users.sql")
  		}

  		content, err := os.ReadFile(path)
  		if err != nil {
  			t.Fatalf("cannot read created file: %v", err)
  		}
  		if !strings.Contains(string(content), "-- +up") {
  			t.Errorf("file missing -- +up marker")
  		}
  		if !strings.Contains(string(content), "-- +down") {
  			t.Errorf("file missing -- +down marker")
  		}
  	})

  	t.Run("directorio con migraciones existentes calcula siguiente version", func(t *testing.T) {
  		dir := t.TempDir()

  		// Create existing files
  		os.WriteFile(filepath.Join(dir, "001_create_users.sql"), []byte("-- +up\nCREATE TABLE users (id TEXT);\n-- +down\nDROP TABLE users;\n"), 0o644)
  		os.WriteFile(filepath.Join(dir, "002_add_email.sql"), []byte("-- +up\nALTER TABLE users ADD COLUMN email TEXT;\n-- +down\n\n"), 0o644)

  		path, err := MigrationCreate(dir, "add_index")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}

  		base := filepath.Base(path)
  		if base != "003_add_index.sql" {
  			t.Errorf("filename: got %q, want %q", base, "003_add_index.sql")
  		}
  	})

  	t.Run("nombre invalido retorna error", func(t *testing.T) {
  		dir := t.TempDir()
  		_, err := MigrationCreate(dir, "123invalid")
  		if err == nil {
  			t.Fatal("expected error for invalid name, got nil")
  		}
  	})

  	t.Run("directorio inexistente se crea automaticamente", func(t *testing.T) {
  		base := t.TempDir()
  		dir := filepath.Join(base, "nested", "migrations")

  		path, err := MigrationCreate(dir, "init")
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}

  		if _, err := os.Stat(dir); os.IsNotExist(err) {
  			t.Errorf("directory was not created: %s", dir)
  		}

  		if filepath.Dir(path) != dir {
  			t.Errorf("file not in expected dir: %s", path)
  		}
  	})
  }


id: migration_down_go_infra_t0
function_id: migration_down_go_infra
name: TestMigrationDown
file_path: functions/infra/migration_down_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestMigrationDown(t *testing.T) {
  	t.Run("revertir ultima migracion elimina registro y ejecuta down_sql", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_create_roles.sql",
  			"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")

  		if _, err := MigrationUp(db, dir); err != nil {
  			t.Fatalf("up failed: %v", err)
  		}

  		reverted, err := MigrationDown(db, 1)
  		if err != nil {
  			t.Fatalf("down failed: %v", err)
  		}
  		if len(reverted) != 1 {
  			t.Errorf("expected 1 reverted, got %d", len(reverted))
  		}
  		if reverted[0].Version != 2 {
  			t.Errorf("expected version 2 reverted, got %d", reverted[0].Version)
  		}

  		// roles table should be gone
  		if err := db.QueryRow("SELECT COUNT(*) FROM roles").Err(); err == nil {
  			t.Error("roles table should not exist after down")
  		}

  		// users table should still exist
  		var count int
  		if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
  			t.Errorf("users table should still exist: %v", err)
  		}
  	})

  	t.Run("revertir n migraciones revierte en orden descendente", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_create_roles.sql",
  			"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")
  		writeMigrationFile(t, dir, "003_create_logs.sql",
  			"-- +up\nCREATE TABLE logs (id INTEGER PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS logs;\n")

  		if _, err := MigrationUp(db, dir); err != nil {
  			t.Fatalf("up failed: %v", err)
  		}

  		reverted, err := MigrationDown(db, 2)
  		if err != nil {
  			t.Fatalf("down 2 failed: %v", err)
  		}
  		if len(reverted) != 2 {
  			t.Errorf("expected 2 reverted, got %d", len(reverted))
  		}
  		// Should be reverted in descending order: 3, 2
  		if reverted[0].Version != 3 || reverted[1].Version != 2 {
  			t.Errorf("reverted order wrong: got %d, %d", reverted[0].Version, reverted[1].Version)
  		}

  		// users table should still exist (migration 1 not reverted)
  		var count int
  		if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
  			t.Errorf("users table should still exist: %v", err)
  		}
  	})

  	t.Run("n cero no revierte nada", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")

  		if _, err := MigrationUp(db, dir); err != nil {
  			t.Fatalf("up failed: %v", err)
  		}

  		reverted, err := MigrationDown(db, 0)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(reverted) != 0 {
  			t.Errorf("expected 0 reverted for n=0, got %d", len(reverted))
  		}
  	})

  	t.Run("base de datos sin migraciones retorna slice vacio", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		// Apply to create _migrations table
  		if _, err := MigrationUp(db, dir); err != nil {
  			t.Fatalf("up failed: %v", err)
  		}

  		reverted, err := MigrationDown(db, 5)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(reverted) != 0 {
  			t.Errorf("expected 0 reverted from empty DB, got %d", len(reverted))
  		}
  	})
  }


id: migration_parse_go_infra_t0
function_id: migration_parse_go_infra
name: TestMigrationParse
file_path: functions/infra/migration_parse_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestMigrationParse(t *testing.T) {
  	t.Run("archivo valido con up y down retorna Migration correcta", func(t *testing.T) {
  		content := "\n-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n\n-- +down\nDROP TABLE IF EXISTS users;\n"
  		m, err := MigrationParse("001_create_users.sql", content)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if m.Version != 1 {
  			t.Errorf("Version: got %d, want 1", m.Version)
  		}
  		if m.Name != "create_users" {
  			t.Errorf("Name: got %q, want %q", m.Name, "create_users")
  		}
  		if !strings.Contains(m.UpSQL, "CREATE TABLE users") {
  			t.Errorf("UpSQL missing CREATE TABLE: %q", m.UpSQL)
  		}
  		if !strings.Contains(m.DownSQL, "DROP TABLE") {
  			t.Errorf("DownSQL missing DROP TABLE: %q", m.DownSQL)
  		}
  	})

  	t.Run("archivo sin bloque down retorna DownSQL vacio sin error", func(t *testing.T) {
  		content := "-- +up\nCREATE TABLE logs (id INTEGER PRIMARY KEY);\n"
  		m, err := MigrationParse("002_create_logs.sql", content)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if m.Version != 2 {
  			t.Errorf("Version: got %d, want 2", m.Version)
  		}
  		if m.Name != "create_logs" {
  			t.Errorf("Name: got %q, want %q", m.Name, "create_logs")
  		}
  		if m.DownSQL != "" {
  			t.Errorf("DownSQL: got %q, want empty", m.DownSQL)
  		}
  	})

  	t.Run("filename sin separador underscore retorna error", func(t *testing.T) {
  		_, err := MigrationParse("001.sql", "-- +up\nCREATE TABLE x (id TEXT);\n")
  		if err == nil {
  			t.Fatal("expected error, got nil")
  		}
  	})

  	t.Run("version no numerica retorna error", func(t *testing.T) {
  		_, err := MigrationParse("abc_create_users.sql", "-- +up\nCREATE TABLE x (id TEXT);\n")
  		if err == nil {
  			t.Fatal("expected error, got nil")
  		}
  	})

  	t.Run("bloque up vacio retorna error", func(t *testing.T) {
  		content := "-- +up\n\n-- +down\nDROP TABLE users;\n"
  		_, err := MigrationParse("001_create_users.sql", content)
  		if err == nil {
  			t.Fatal("expected error for empty up block, got nil")
  		}
  	})

  	t.Run("version cero retorna error", func(t *testing.T) {
  		_, err := MigrationParse("000_something.sql", "-- +up\nCREATE TABLE x (id TEXT);\n")
  		if err == nil {
  			t.Fatal("expected error for version 0, got nil")
  		}
  	})

  	t.Run("nombre descriptivo vacio retorna error", func(t *testing.T) {
  		_, err := MigrationParse("001_.sql", "-- +up\nCREATE TABLE x (id TEXT);\n")
  		if err == nil {
  			t.Fatal("expected error for empty descriptive name, got nil")
  		}
  	})

  	t.Run("filename con path completo extrae nombre base", func(t *testing.T) {
  		content := "-- +up\nCREATE TABLE x (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE x;\n"
  		m, err := MigrationParse("apps/my_app/migrations/003_add_index.sql", content)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if m.Version != 3 {
  			t.Errorf("Version: got %d, want 3", m.Version)
  		}
  		if m.Name != "add_index" {
  			t.Errorf("Name: got %q, want %q", m.Name, "add_index")
  		}
  	})
  }


id: migration_status_go_infra_t0
function_id: migration_status_go_infra
name: TestMigrationStatus
file_path: functions/infra/migration_status_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestMigrationStatus(t *testing.T) {
  	t.Run("migraciones en disco pero no en BD aparecen como pending", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_create_roles.sql",
  			"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")

  		statuses, err := MigrationGetStatus(db, dir)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(statuses) != 2 {
  			t.Fatalf("expected 2 statuses, got %d", len(statuses))
  		}
  		for _, s := range statuses {
  			if s.Applied {
  				t.Errorf("version %d should be pending, got applied", s.Version)
  			}
  		}
  	})

  	t.Run("migraciones aplicadas aparecen con Applied=true y AppliedAt", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_create_roles.sql",
  			"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")

  		if _, err := MigrationUp(db, dir); err != nil {
  			t.Fatalf("up failed: %v", err)
  		}

  		statuses, err := MigrationGetStatus(db, dir)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(statuses) != 2 {
  			t.Fatalf("expected 2 statuses, got %d", len(statuses))
  		}
  		for _, s := range statuses {
  			if !s.Applied {
  				t.Errorf("version %d should be applied, got pending", s.Version)
  			}
  			if s.AppliedAt.IsZero() {
  				t.Errorf("version %d AppliedAt should not be zero", s.Version)
  			}
  		}
  	})

  	t.Run("migraciones aplicadas sin archivo en disco aparecen como orphaned", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")

  		if _, err := MigrationUp(db, dir); err != nil {
  			t.Fatalf("up failed: %v", err)
  		}

  		// Remove file from disk but migration is applied in DB
  		// Now check status with an empty dir
  		emptyDir := t.TempDir()
  		statuses, err := MigrationGetStatus(db, emptyDir)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(statuses) != 1 {
  			t.Fatalf("expected 1 status (orphaned), got %d", len(statuses))
  		}
  		s := statuses[0]
  		if !s.Applied {
  			t.Errorf("orphaned migration should have Applied=true")
  		}
  		if !containsStr(s.Name, "orphaned") {
  			t.Errorf("orphaned migration name should contain 'orphaned', got %q", s.Name)
  		}
  	})

  	t.Run("base de datos sin tabla _migrations retorna todas como pending", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")

  		// Do NOT call MigrationUp — _migrations table doesn't exist
  		statuses, err := MigrationGetStatus(db, dir)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(statuses) != 1 {
  			t.Fatalf("expected 1 status, got %d", len(statuses))
  		}
  		if statuses[0].Applied {
  			t.Errorf("migration should be pending when _migrations does not exist")
  		}
  	})
  }


id: migration_up_go_infra_t0
function_id: migration_up_go_infra
name: TestMigrationUp
file_path: functions/infra/migration_up_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestMigrationUp(t *testing.T) {
  	t.Run("base de datos vacia aplica todas las migraciones", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_create_roles.sql",
  			"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")

  		applied, err := MigrationUp(db, dir)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(applied) != 2 {
  			t.Errorf("applied count: got %d, want 2", len(applied))
  		}
  		if applied[0].Version != 1 || applied[1].Version != 2 {
  			t.Errorf("applied versions: got %v", []int{applied[0].Version, applied[1].Version})
  		}

  		// Verify tables were created
  		var count int
  		if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
  			t.Errorf("users table not created: %v", err)
  		}
  		if err := db.QueryRow("SELECT COUNT(*) FROM roles").Scan(&count); err != nil {
  			t.Errorf("roles table not created: %v", err)
  		}
  	})

  	t.Run("migraciones ya aplicadas se omiten", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_create_roles.sql",
  			"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")

  		// Apply all
  		_, err := MigrationUp(db, dir)
  		if err != nil {
  			t.Fatalf("first up failed: %v", err)
  		}

  		// Apply again — should apply nothing
  		applied, err := MigrationUp(db, dir)
  		if err != nil {
  			t.Fatalf("second up failed: %v", err)
  		}
  		if len(applied) != 0 {
  			t.Errorf("expected 0 applied on second run, got %d", len(applied))
  		}
  	})

  	t.Run("migracion con SQL invalido retorna error y deja las anteriores aplicadas", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		writeMigrationFile(t, dir, "001_create_users.sql",
  			"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
  		writeMigrationFile(t, dir, "002_bad_sql.sql",
  			"-- +up\nTHIS IS NOT VALID SQL!!!;\n-- +down\n\n")

  		applied, err := MigrationUp(db, dir)
  		if err == nil {
  			t.Fatal("expected error for invalid SQL, got nil")
  		}
  		// Version 1 should be applied
  		if len(applied) != 1 || applied[0].Version != 1 {
  			t.Errorf("expected [1] applied before failure, got versions: %v", applied)
  		}

  		// users table should still exist (migration 1 committed)
  		var count int
  		if err2 := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err2 != nil {
  			t.Errorf("users table not present after partial apply: %v", err2)
  		}
  	})

  	t.Run("directorio sin archivos sql no aplica nada", func(t *testing.T) {
  		db := openMigrationTestDB(t)
  		dir := t.TempDir()

  		applied, err := MigrationUp(db, dir)
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		if len(applied) != 0 {
  			t.Errorf("expected 0 applied for empty dir, got %d", len(applied))
  		}
  	})
  }


id: migration_validate_go_infra_t0
function_id: migration_validate_go_infra
name: TestMigrationValidate
file_path: functions/infra/migration_validate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestMigrationValidate(t *testing.T) {
  	t.Run("secuencia valida retorna sin errores", func(t *testing.T) {
  		migrations := []Migration{
  			makeMig(1, "create_users", "CREATE TABLE users (id TEXT PRIMARY KEY);"),
  			makeMig(2, "add_email", "ALTER TABLE users ADD COLUMN email TEXT;"),
  			makeMig(3, "create_roles", "CREATE TABLE roles (id TEXT PRIMARY KEY);"),
  		}
  		errs := MigrationValidate(migrations)
  		if len(errs) != 0 {
  			t.Errorf("expected no errors, got: %v", errs)
  		}
  	})

  	t.Run("secuencia vacia retorna sin errores", func(t *testing.T) {
  		errs := MigrationValidate([]Migration{})
  		if len(errs) != 0 {
  			t.Errorf("expected no errors for empty slice, got: %v", errs)
  		}
  	})

  	t.Run("version duplicada reporta error", func(t *testing.T) {
  		migrations := []Migration{
  			makeMig(1, "create_users", "CREATE TABLE users (id TEXT PRIMARY KEY);"),
  			makeMig(1, "create_users_dup", "CREATE TABLE users2 (id TEXT PRIMARY KEY);"),
  			makeMig(2, "add_email", "ALTER TABLE users ADD COLUMN email TEXT;"),
  		}
  		errs := MigrationValidate(migrations)
  		if len(errs) == 0 {
  			t.Fatal("expected error for duplicate version, got none")
  		}
  		found := false
  		for _, e := range errs {
  			if containsStr(e, "duplicate") {
  				found = true
  			}
  		}
  		if !found {
  			t.Errorf("expected 'duplicate' in errors, got: %v", errs)
  		}
  	})

  	t.Run("hueco en versiones reporta version faltante", func(t *testing.T) {
  		migrations := []Migration{
  			makeMig(1, "create_users", "CREATE TABLE users (id TEXT PRIMARY KEY);"),
  			makeMig(3, "create_roles", "CREATE TABLE roles (id TEXT PRIMARY KEY);"),
  		}
  		errs := MigrationValidate(migrations)
  		if len(errs) == 0 {
  			t.Fatal("expected error for gap in versions, got none")
  		}
  		found := false
  		for _, e := range errs {
  			if containsStr(e, "gap") || containsStr(e, "missing 2") {
  				found = true
  			}
  		}
  		if !found {
  			t.Errorf("expected gap error in errors, got: %v", errs)
  		}
  	})

  	t.Run("up_sql vacio reporta error", func(t *testing.T) {
  		migrations := []Migration{
  			{Version: 1, Name: "create_users", UpSQL: "", DownSQL: "DROP TABLE users;"},
  		}
  		errs := MigrationValidate(migrations)
  		if len(errs) == 0 {
  			t.Fatal("expected error for empty up_sql, got none")
  		}
  		found := false
  		for _, e := range errs {
  			if containsStr(e, "empty up_sql") {
  				found = true
  			}
  		}
  		if !found {
  			t.Errorf("expected 'empty up_sql' in errors, got: %v", errs)
  		}
  	})

  	t.Run("nombre vacio reporta error", func(t *testing.T) {
  		migrations := []Migration{
  			{Version: 1, Name: "", UpSQL: "CREATE TABLE users (id TEXT PRIMARY KEY);"},
  		}
  		errs := MigrationValidate(migrations)
  		if len(errs) == 0 {
  			t.Fatal("expected error for empty name, got none")
  		}
  		found := false
  		for _, e := range errs {
  			if containsStr(e, "empty name") {
  				found = true
  			}
  		}
  		if !found {
  			t.Errorf("expected 'empty name' in errors, got: %v", errs)
  		}
  	})

  	t.Run("versiones que no empiezan en 1 reportan error", func(t *testing.T) {
  		migrations := []Migration{
  			makeMig(2, "create_users", "CREATE TABLE users (id TEXT PRIMARY KEY);"),
  			makeMig(3, "add_email", "ALTER TABLE users ADD COLUMN email TEXT;"),
  		}
  		errs := MigrationValidate(migrations)
  		if len(errs) == 0 {
  			t.Fatal("expected error for versions not starting at 1, got none")
  		}
  		found := false
  		for _, e := range errs {
  			if containsStr(e, "start at 1") {
  				found = true
  			}
  		}
  		if !found {
  			t.Errorf("expected 'start at 1' error, got: %v", errs)
  		}
  	})

  	t.Run("multiple errores se reportan todos", func(t *testing.T) {
  		migrations := []Migration{
  			{Version: 2, Name: "", UpSQL: ""},
  			{Version: 4, Name: "something", UpSQL: "CREATE TABLE x (id TEXT);"},
  		}
  		errs := MigrationValidate(migrations)
  		// Expect: not starting at 1, gap between 2 and 4, empty name for v2, empty up_sql for v2
  		if len(errs) < 3 {
  			t.Errorf("expected at least 3 errors, got %d: %v", len(errs), errs)
  		}
  	})
  }

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


id: oauth2_auth_url_go_infra_t0
function_id: oauth2_auth_url_go_infra
name: TestOauth2AuthURL_BuildsBasicParams
file_path: functions/infra/oauth2_auth_url_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2AuthURL_BuildsBasicParams(t *testing.T) {
  	cfg := OAuthConfig{
  		ClientID:    "abc-client",
  		AuthURL:     "https://example.com/authorize",
  		RedirectURL: "http://localhost/callback",
  		Scopes:      []string{"openid", "email"},
  	}
  	got := Oauth2AuthURL(cfg, "state-xyz")
  	u, err := url.Parse(got)
  	if err != nil {
  		t.Fatalf("parse: %v", err)
  	}
  	if u.Scheme != "https" || u.Host != "example.com" || u.Path != "/authorize" {
  		t.Fatalf("base URL incorrecta: %s", got)
  	}
  	q := u.Query()
  	if q.Get("client_id") != "abc-client" {
  		t.Errorf("client_id = %q", q.Get("client_id"))
  	}
  	if q.Get("redirect_uri") != "http://localhost/callback" {
  		t.Errorf("redirect_uri = %q", q.Get("redirect_uri"))
  	}
  	if q.Get("response_type") != "code" {
  		t.Errorf("response_type = %q", q.Get("response_type"))
  	}
  	if q.Get("scope") != "openid email" {
  		t.Errorf("scope = %q", q.Get("scope"))
  	}
  	if q.Get("state") != "state-xyz" {
  		t.Errorf("state = %q", q.Get("state"))
  	}
  }


id: oauth2_auth_url_go_infra_t1
function_id: oauth2_auth_url_go_infra
name: TestOauth2AuthURL_OmitsEmptyState
file_path: functions/infra/oauth2_auth_url_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2AuthURL_OmitsEmptyState(t *testing.T) {
  	cfg := OAuthConfig{ClientID: "c", AuthURL: "https://x.test/a", RedirectURL: "http://r"}
  	got := Oauth2AuthURL(cfg, "")
  	if strings.Contains(got, "state=") {
  		t.Errorf("state deberia estar ausente: %s", got)
  	}
  }


id: oauth2_auth_url_go_infra_t2
function_id: oauth2_auth_url_go_infra
name: TestOauth2AuthURL_HandlesExistingQueryString
file_path: functions/infra/oauth2_auth_url_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2AuthURL_HandlesExistingQueryString(t *testing.T) {
  	cfg := OAuthConfig{
  		ClientID:    "c",
  		AuthURL:     "https://example.com/authorize?hd=domain.com",
  		RedirectURL: "http://r",
  	}
  	got := Oauth2AuthURL(cfg, "s")
  	u, err := url.Parse(got)
  	if err != nil {
  		t.Fatalf("parse: %v", err)
  	}
  	q := u.Query()
  	if q.Get("hd") != "domain.com" {
  		t.Errorf("param pre-existente se perdio")
  	}
  	if q.Get("client_id") != "c" {
  		t.Errorf("client_id no agregado")
  	}
  }


id: oauth2_exchange_go_infra_t0
function_id: oauth2_exchange_go_infra
name: TestOauth2Exchange_Success
file_path: functions/infra/oauth2_exchange_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2Exchange_Success(t *testing.T) {
  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		if r.Method != http.MethodPost {
  			t.Errorf("metodo = %s", r.Method)
  		}
  		if err := r.ParseForm(); err != nil {
  			t.Fatalf("ParseForm: %v", err)
  		}
  		if got := r.PostForm.Get("grant_type"); got != "authorization_code" {
  			t.Errorf("grant_type = %q", got)
  		}
  		if got := r.PostForm.Get("code"); got != "abc-code" {
  			t.Errorf("code = %q", got)
  		}
  		w.Header().Set("Content-Type", "application/json")
  		json.NewEncoder(w).Encode(map[string]any{
  			"access_token":  "at-123",
  			"refresh_token": "rt-456",
  			"token_type":    "Bearer",
  			"expires_in":    3600,
  		})
  	}))
  	defer srv.Close()

  	cfg := OAuthConfig{
  		ClientID:     "c",
  		ClientSecret: "s",
  		TokenURL:     srv.URL,
  		RedirectURL:  "http://r",
  	}
  	tokens, err := Oauth2Exchange(cfg, "abc-code")
  	if err != nil {
  		t.Fatalf("Oauth2Exchange: %v", err)
  	}
  	if tokens.AccessToken != "at-123" || tokens.RefreshToken != "rt-456" || tokens.TokenType != "Bearer" {
  		t.Errorf("tokens = %+v", tokens)
  	}
  	if tokens.ExpiresAt == 0 {
  		t.Error("ExpiresAt no deberia ser 0")
  	}
  }


id: oauth2_exchange_go_infra_t1
function_id: oauth2_exchange_go_infra
name: TestOauth2Exchange_EmptyCode
file_path: functions/infra/oauth2_exchange_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2Exchange_EmptyCode(t *testing.T) {
  	cfg := OAuthConfig{TokenURL: "http://x", ClientID: "c"}
  	if _, err := Oauth2Exchange(cfg, ""); err == nil {
  		t.Fatal("esperaba error con code vacio")
  	}
  }


id: oauth2_exchange_go_infra_t2
function_id: oauth2_exchange_go_infra
name: TestOauth2Exchange_ProviderError
file_path: functions/infra/oauth2_exchange_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2Exchange_ProviderError(t *testing.T) {
  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		w.WriteHeader(400)
  		w.Header().Set("Content-Type", "application/json")
  		json.NewEncoder(w).Encode(map[string]string{
  			"error":             "invalid_grant",
  			"error_description": "code expired",
  		})
  	}))
  	defer srv.Close()

  	cfg := OAuthConfig{TokenURL: srv.URL, ClientID: "c"}
  	if _, err := Oauth2Exchange(cfg, "code"); err == nil {
  		t.Fatal("esperaba error del proveedor")
  	}
  }


id: oauth2_refresh_go_infra_t0
function_id: oauth2_refresh_go_infra
name: TestOauth2Refresh_Success
file_path: functions/infra/oauth2_refresh_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2Refresh_Success(t *testing.T) {
  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		_ = r.ParseForm()
  		if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
  			t.Errorf("grant_type = %q", got)
  		}
  		if got := r.PostForm.Get("refresh_token"); got != "rt-old" {
  			t.Errorf("refresh_token = %q", got)
  		}
  		w.Header().Set("Content-Type", "application/json")
  		json.NewEncoder(w).Encode(map[string]any{
  			"access_token":  "at-new",
  			"refresh_token": "rt-new",
  			"token_type":    "Bearer",
  			"expires_in":    1800,
  		})
  	}))
  	defer srv.Close()

  	cfg := OAuthConfig{TokenURL: srv.URL, ClientID: "c", ClientSecret: "s"}
  	tokens, err := Oauth2Refresh(cfg, "rt-old")
  	if err != nil {
  		t.Fatalf("Oauth2Refresh: %v", err)
  	}
  	if tokens.AccessToken != "at-new" {
  		t.Errorf("AccessToken = %q", tokens.AccessToken)
  	}
  	if tokens.RefreshToken != "rt-new" {
  		t.Errorf("RefreshToken = %q", tokens.RefreshToken)
  	}
  }


id: oauth2_refresh_go_infra_t1
function_id: oauth2_refresh_go_infra
name: TestOauth2Refresh_PreservesRefreshTokenIfProviderOmits
file_path: functions/infra/oauth2_refresh_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2Refresh_PreservesRefreshTokenIfProviderOmits(t *testing.T) {
  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		w.Header().Set("Content-Type", "application/json")
  		json.NewEncoder(w).Encode(map[string]any{
  			"access_token": "at-new",
  			"token_type":   "Bearer",
  			"expires_in":   1800,
  			// no refresh_token
  		})
  	}))
  	defer srv.Close()

  	cfg := OAuthConfig{TokenURL: srv.URL}
  	tokens, err := Oauth2Refresh(cfg, "rt-keep")
  	if err != nil {
  		t.Fatalf("Oauth2Refresh: %v", err)
  	}
  	if tokens.RefreshToken != "rt-keep" {
  		t.Errorf("esperaba conservar rt-keep, got %q", tokens.RefreshToken)
  	}
  }


id: oauth2_refresh_go_infra_t2
function_id: oauth2_refresh_go_infra
name: TestOauth2Refresh_EmptyToken
file_path: functions/infra/oauth2_refresh_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestOauth2Refresh_EmptyToken(t *testing.T) {
  	cfg := OAuthConfig{TokenURL: "http://x"}
  	if _, err := Oauth2Refresh(cfg, ""); err == nil {
  		t.Fatal("esperaba error con refresh vacio")
  	}
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: password_hash_go_infra_t0
function_id: password_hash_go_infra
name: TestPasswordHash_DefaultCost
file_path: functions/infra/password_hash_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestPasswordHash_DefaultCost(t *testing.T) {
  	hash, err := PasswordHash("hunter2", 0)
  	if err != nil {
  		t.Fatalf("PasswordHash error: %v", err)
  	}
  	if !strings.HasPrefix(hash, "$2") {
  		t.Errorf("hash no tiene prefijo bcrypt: %q", hash)
  	}
  }


id: password_hash_go_infra_t1
function_id: password_hash_go_infra
name: TestPasswordHash_CustomCost
file_path: functions/infra/password_hash_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestPasswordHash_CustomCost(t *testing.T) {
  	hash, err := PasswordHash("password", 4) // 4 = minimum, rapido para tests
  	if err != nil {
  		t.Fatalf("PasswordHash error: %v", err)
  	}
  	if hash == "" {
  		t.Fatal("hash vacio")
  	}
  }


id: password_hash_go_infra_t2
function_id: password_hash_go_infra
name: TestPasswordHash_DifferentSalts
file_path: functions/infra/password_hash_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestPasswordHash_DifferentSalts(t *testing.T) {
  	h1, _ := PasswordHash("same-password", 4)
  	h2, _ := PasswordHash("same-password", 4)
  	if h1 == h2 {
  		t.Fatal("los hashes deben diferir por salt distinto")
  	}
  }


id: password_verify_go_infra_t0
function_id: password_verify_go_infra
name: TestPasswordVerify_CorrectPassword
file_path: functions/infra/password_verify_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestPasswordVerify_CorrectPassword(t *testing.T) {
  	hash, _ := PasswordHash("correct-horse-battery-staple", 4)
  	if err := PasswordVerify("correct-horse-battery-staple", hash); err != nil {
  		t.Fatalf("esperaba nil, got %v", err)
  	}
  }


id: password_verify_go_infra_t1
function_id: password_verify_go_infra
name: TestPasswordVerify_WrongPassword
file_path: functions/infra/password_verify_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestPasswordVerify_WrongPassword(t *testing.T) {
  	hash, _ := PasswordHash("secret", 4)
  	if err := PasswordVerify("wrong", hash); err == nil {
  		t.Fatal("esperaba error con password incorrecto")
  	}
  }


id: password_verify_go_infra_t2
function_id: password_verify_go_infra
name: TestPasswordVerify_InvalidHash
file_path: functions/infra/password_verify_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestPasswordVerify_InvalidHash(t *testing.T) {
  	if err := PasswordVerify("x", "not-a-bcrypt-hash"); err == nil {
  		t.Fatal("esperaba error con hash invalido")
  	}
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: rate_limit_headers_go_infra_t0
function_id: rate_limit_headers_go_infra
name: TestRateLimitHeaders
file_path: functions/infra/rate_limit_headers_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimitHeaders(t *testing.T) {
  	now := time.Now()

  	t.Run("setea X-RateLimit-Limit con el burst", func(t *testing.T) {
  		result := RateLimitResult{Allowed: true, Remaining: 5, ResetAt: now}
  		h := RateLimitHeaders(result, 20)
  		if h.Get("X-RateLimit-Limit") != "20" {
  			t.Errorf("X-RateLimit-Limit=%q, want 20", h.Get("X-RateLimit-Limit"))
  		}
  	})

  	t.Run("setea X-RateLimit-Remaining con result.Remaining", func(t *testing.T) {
  		result := RateLimitResult{Allowed: true, Remaining: 7, ResetAt: now}
  		h := RateLimitHeaders(result, 10)
  		if h.Get("X-RateLimit-Remaining") != "7" {
  			t.Errorf("X-RateLimit-Remaining=%q, want 7", h.Get("X-RateLimit-Remaining"))
  		}
  	})

  	t.Run("setea X-RateLimit-Reset como timestamp unix", func(t *testing.T) {
  		future := now.Add(30 * time.Second)
  		result := RateLimitResult{Allowed: true, Remaining: 1, ResetAt: future}
  		h := RateLimitHeaders(result, 10)
  		got := h.Get("X-RateLimit-Reset")
  		want := strconv.FormatInt(future.Unix(), 10)
  		if got != want {
  			t.Errorf("X-RateLimit-Reset=%q, want %q", got, want)
  		}
  	})

  	t.Run("incluye Retry-After cuando no esta permitido", func(t *testing.T) {
  		result := RateLimitResult{Allowed: false, Remaining: 0, ResetAt: now, RetryAfter: 2.3}
  		h := RateLimitHeaders(result, 10)
  		if h.Get("Retry-After") == "" {
  			t.Fatal("Retry-After deberia estar seteado")
  		}
  		// Debe redondear hacia arriba
  		ra, _ := strconv.Atoi(h.Get("Retry-After"))
  		if ra != 3 {
  			t.Errorf("Retry-After=%d, want 3 (ceil de 2.3)", ra)
  		}
  	})

  	t.Run("Retry-After minimo es 1", func(t *testing.T) {
  		result := RateLimitResult{Allowed: false, Remaining: 0, ResetAt: now, RetryAfter: 0.1}
  		h := RateLimitHeaders(result, 10)
  		if h.Get("Retry-After") != "1" {
  			t.Errorf("Retry-After=%q, want 1 (minimo)", h.Get("Retry-After"))
  		}
  	})

  	t.Run("no incluye Retry-After cuando esta permitido", func(t *testing.T) {
  		result := RateLimitResult{Allowed: true, Remaining: 5, ResetAt: now}
  		h := RateLimitHeaders(result, 10)
  		if h.Get("Retry-After") != "" {
  			t.Errorf("Retry-After=%q, want vacio", h.Get("Retry-After"))
  		}
  	})
  }


id: rate_limit_middleware_go_infra_t0
function_id: rate_limit_middleware_go_infra
name: TestRateLimitMiddleware
file_path: functions/infra/rate_limit_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimitMiddleware(t *testing.T) {
  	base := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		w.WriteHeader(http.StatusOK)
  		w.Write([]byte("ok"))
  	})

  	t.Run("permite request bajo el limite", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		mw := RateLimitMiddleware(rl)
  		handler := mw(base)

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/api", nil)
  		req.RemoteAddr = "192.0.2.1:1234"
  		handler.ServeHTTP(rec, req)

  		if rec.Code != http.StatusOK {
  			t.Errorf("status=%d, want 200", rec.Code)
  		}
  		if rec.Header().Get("X-RateLimit-Limit") != "20" {
  			t.Errorf("X-RateLimit-Limit=%q, want 20", rec.Header().Get("X-RateLimit-Limit"))
  		}
  	})

  	t.Run("responde 429 cuando se excede el limite", func(t *testing.T) {
  		rl := RateLimiterNew(1, 2)
  		mw := RateLimitMiddleware(rl)
  		handler := mw(base)

  		// Consumir burst
  		for i := 0; i < 2; i++ {
  			rec := httptest.NewRecorder()
  			req := httptest.NewRequest("GET", "/api", nil)
  			req.RemoteAddr = "192.0.2.2:1234"
  			handler.ServeHTTP(rec, req)
  		}
  		// Tercer request rechazado
  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/api", nil)
  		req.RemoteAddr = "192.0.2.2:1234"
  		handler.ServeHTTP(rec, req)

  		if rec.Code != http.StatusTooManyRequests {
  			t.Errorf("status=%d, want 429", rec.Code)
  		}
  		if rec.Header().Get("Retry-After") == "" {
  			t.Error("Retry-After header no seteado en 429")
  		}
  	})

  	t.Run("IPs distintas tienen buckets independientes", func(t *testing.T) {
  		rl := RateLimiterNew(1, 1)
  		mw := RateLimitMiddleware(rl)
  		handler := mw(base)

  		// Agotar IP A
  		recA := httptest.NewRecorder()
  		reqA := httptest.NewRequest("GET", "/api", nil)
  		reqA.RemoteAddr = "10.0.0.1:1000"
  		handler.ServeHTTP(recA, reqA)

  		recA2 := httptest.NewRecorder()
  		reqA2 := httptest.NewRequest("GET", "/api", nil)
  		reqA2.RemoteAddr = "10.0.0.1:1000"
  		handler.ServeHTTP(recA2, reqA2)
  		if recA2.Code != http.StatusTooManyRequests {
  			t.Fatal("IP A deberia estar limitada")
  		}

  		// IP B intacta
  		recB := httptest.NewRecorder()
  		reqB := httptest.NewRequest("GET", "/api", nil)
  		reqB.RemoteAddr = "10.0.0.2:1000"
  		handler.ServeHTTP(recB, reqB)
  		if recB.Code != http.StatusOK {
  			t.Errorf("IP B status=%d, want 200", recB.Code)
  		}
  	})

  	t.Run("X-Forwarded-For tiene prioridad sobre RemoteAddr", func(t *testing.T) {
  		rl := RateLimiterNew(1, 1)
  		mw := RateLimitMiddleware(rl)
  		handler := mw(base)

  		// Request 1 con XFF=A, RemoteAddr=R1
  		rec1 := httptest.NewRecorder()
  		req1 := httptest.NewRequest("GET", "/api", nil)
  		req1.Header.Set("X-Forwarded-For", "203.0.113.1")
  		req1.RemoteAddr = "192.0.2.99:5000"
  		handler.ServeHTTP(rec1, req1)
  		if rec1.Code != http.StatusOK {
  			t.Fatal("primer request deberia pasar")
  		}

  		// Request 2 con XFF=A pero RemoteAddr distinto -> mismo bucket A -> rechazado
  		rec2 := httptest.NewRecorder()
  		req2 := httptest.NewRequest("GET", "/api", nil)
  		req2.Header.Set("X-Forwarded-For", "203.0.113.1")
  		req2.RemoteAddr = "192.0.2.100:5000"
  		handler.ServeHTTP(rec2, req2)
  		if rec2.Code != http.StatusTooManyRequests {
  			t.Errorf("XFF deberia identificar misma IP, status=%d", rec2.Code)
  		}
  	})

  	t.Run("responde con Retry-After header en 429", func(t *testing.T) {
  		rl := RateLimiterNew(1, 1)
  		mw := RateLimitMiddleware(rl)
  		handler := mw(base)

  		// Agotar
  		req := httptest.NewRequest("GET", "/api", nil)
  		req.RemoteAddr = "172.16.0.1:1000"
  		handler.ServeHTTP(httptest.NewRecorder(), req)

  		// Rechazado
  		rec := httptest.NewRecorder()
  		handler.ServeHTTP(rec, req)
  		if rec.Code != http.StatusTooManyRequests {
  			t.Fatalf("status=%d, want 429", rec.Code)
  		}
  		if rec.Header().Get("Retry-After") == "" {
  			t.Error("Retry-After vacio en 429")
  		}
  		if rec.Header().Get("Content-Type") != "application/json" {
  			t.Errorf("Content-Type=%q, want application/json", rec.Header().Get("Content-Type"))
  		}
  	})
  }


id: rate_limit_middleware_go_infra_t1
function_id: rate_limit_middleware_go_infra
name: TestRateLimitClientIP
file_path: functions/infra/rate_limit_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimitClientIP(t *testing.T) {
  	t.Run("X-Forwarded-For first hop", func(t *testing.T) {
  		req := httptest.NewRequest("GET", "/", nil)
  		req.Header.Set("X-Forwarded-For", "203.0.113.1, 198.51.100.1, 10.0.0.1")
  		req.RemoteAddr = "192.0.2.1:1234"
  		got := rateLimitClientIP(req)
  		if got != "203.0.113.1" {
  			t.Errorf("got %q, want 203.0.113.1", got)
  		}
  	})

  	t.Run("X-Real-IP fallback", func(t *testing.T) {
  		req := httptest.NewRequest("GET", "/", nil)
  		req.Header.Set("X-Real-IP", "203.0.113.5")
  		req.RemoteAddr = "192.0.2.1:1234"
  		got := rateLimitClientIP(req)
  		if got != "203.0.113.5" {
  			t.Errorf("got %q, want 203.0.113.5", got)
  		}
  	})

  	t.Run("RemoteAddr cuando no hay headers", func(t *testing.T) {
  		req := httptest.NewRequest("GET", "/", nil)
  		req.RemoteAddr = "192.0.2.1:1234"
  		got := rateLimitClientIP(req)
  		if got != "192.0.2.1" {
  			t.Errorf("got %q, want 192.0.2.1", got)
  		}
  	})
  }


id: rate_limiter_by_key_go_infra_t0
function_id: rate_limiter_by_key_go_infra
name: TestRateLimiterByKey
file_path: functions/infra/rate_limiter_by_key_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimiterByKey(t *testing.T) {
  	base := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		w.WriteHeader(http.StatusOK)
  	})
  	keyFunc := func(r *http.Request) string {
  		return r.Header.Get("X-API-Key")
  	}

  	t.Run("aplica limit por la clave devuelta por keyFunc", func(t *testing.T) {
  		rl := RateLimiterNew(1, 2)
  		mw := RateLimiterByKey(rl, keyFunc)
  		handler := mw(base)

  		// Agotar tokens de api-key-A
  		for i := 0; i < 2; i++ {
  			req := httptest.NewRequest("GET", "/", nil)
  			req.Header.Set("X-API-Key", "key-A")
  			handler.ServeHTTP(httptest.NewRecorder(), req)
  		}
  		// Tercer rechazado
  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		req.Header.Set("X-API-Key", "key-A")
  		handler.ServeHTTP(rec, req)
  		if rec.Code != http.StatusTooManyRequests {
  			t.Errorf("status=%d, want 429", rec.Code)
  		}

  		// key-B intacta
  		recB := httptest.NewRecorder()
  		reqB := httptest.NewRequest("GET", "/", nil)
  		reqB.Header.Set("X-API-Key", "key-B")
  		handler.ServeHTTP(recB, reqB)
  		if recB.Code != http.StatusOK {
  			t.Errorf("key-B status=%d, want 200", recB.Code)
  		}
  	})

  	t.Run("key vacia salta el limit", func(t *testing.T) {
  		rl := RateLimiterNew(1, 1)
  		mw := RateLimiterByKey(rl, keyFunc)
  		handler := mw(base)

  		// Sin X-API-Key, hagamos muchos requests
  		for i := 0; i < 10; i++ {
  			rec := httptest.NewRecorder()
  			req := httptest.NewRequest("GET", "/", nil)
  			handler.ServeHTTP(rec, req)
  			if rec.Code != http.StatusOK {
  				t.Errorf("request %d status=%d, want 200 (sin key, sin limit)", i, rec.Code)
  			}
  		}
  	})

  	t.Run("responde 429 con body JSON al exceder", func(t *testing.T) {
  		rl := RateLimiterNew(1, 1)
  		mw := RateLimiterByKey(rl, keyFunc)
  		handler := mw(base)

  		// Agotar
  		req := httptest.NewRequest("GET", "/", nil)
  		req.Header.Set("X-API-Key", "key-X")
  		handler.ServeHTTP(httptest.NewRecorder(), req)

  		// Rechazado
  		rec := httptest.NewRecorder()
  		handler.ServeHTTP(rec, req)
  		if rec.Code != http.StatusTooManyRequests {
  			t.Fatalf("status=%d, want 429", rec.Code)
  		}
  		var body map[string]any
  		if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
  			t.Fatalf("body no es JSON: %v", err)
  		}
  		// HTTPError se serializa con campos PascalCase (sin tags JSON)
  		if body["Code"] != "rate_limited" {
  			t.Errorf("Code=%v, want rate_limited", body["Code"])
  		}
  	})

  	t.Run("headers X-RateLimit-* siempre presentes en respuesta", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		mw := RateLimiterByKey(rl, keyFunc)
  		handler := mw(base)

  		rec := httptest.NewRecorder()
  		req := httptest.NewRequest("GET", "/", nil)
  		req.Header.Set("X-API-Key", "key-Y")
  		handler.ServeHTTP(rec, req)

  		if rec.Header().Get("X-RateLimit-Limit") == "" {
  			t.Error("X-RateLimit-Limit ausente")
  		}
  		if rec.Header().Get("X-RateLimit-Remaining") == "" {
  			t.Error("X-RateLimit-Remaining ausente")
  		}
  		if rec.Header().Get("X-RateLimit-Reset") == "" {
  			t.Error("X-RateLimit-Reset ausente")
  		}
  	})
  }


id: rate_limiter_check_go_infra_t0
function_id: rate_limiter_check_go_infra
name: TestRateLimiterCheck
file_path: functions/infra/rate_limiter_check_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimiterCheck(t *testing.T) {
  	t.Run("primer request siempre allowed con burst-1 remaining", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		result := RateLimiterCheck(rl, "client-1")
  		if !result.Allowed {
  			t.Error("primer request rechazado")
  		}
  		if result.Remaining != 19 {
  			t.Errorf("Remaining=%d, want 19", result.Remaining)
  		}
  		if result.RetryAfter != 0 {
  			t.Errorf("RetryAfter=%v, want 0", result.RetryAfter)
  		}
  	})

  	t.Run("consumir todos los tokens bloquea siguiente request", func(t *testing.T) {
  		rl := RateLimiterNew(1, 5)
  		// Consumir los 5 tokens del burst
  		for i := 0; i < 5; i++ {
  			r := RateLimiterCheck(rl, "client-2")
  			if !r.Allowed {
  				t.Fatalf("request %d rechazado, esperaba allowed", i)
  			}
  		}
  		// El sexto deberia ser rechazado
  		r := RateLimiterCheck(rl, "client-2")
  		if r.Allowed {
  			t.Error("request post-burst permitido, esperaba rechazo")
  		}
  		if r.Remaining != 0 {
  			t.Errorf("Remaining=%d, want 0", r.Remaining)
  		}
  	})

  	t.Run("los tokens se recargan con el paso del tiempo", func(t *testing.T) {
  		rl := RateLimiterNew(100, 2) // 100 tokens/seg = 1 token cada 10ms
  		// Consumir burst
  		RateLimiterCheck(rl, "client-3")
  		RateLimiterCheck(rl, "client-3")
  		// Tercer request rechazado
  		r := RateLimiterCheck(rl, "client-3")
  		if r.Allowed {
  			t.Fatal("tercer request inmediato deberia estar rechazado")
  		}
  		// Esperar suficiente para recargar al menos 1 token
  		time.Sleep(50 * time.Millisecond)
  		r2 := RateLimiterCheck(rl, "client-3")
  		if !r2.Allowed {
  			t.Error("request tras recarga deberia estar permitido")
  		}
  	})

  	t.Run("retryAfter es positivo cuando se rechaza", func(t *testing.T) {
  		rl := RateLimiterNew(1, 1) // 1 token/seg, burst 1
  		RateLimiterCheck(rl, "client-4")
  		r := RateLimiterCheck(rl, "client-4")
  		if r.Allowed {
  			t.Fatal("segundo request inmediato deberia rechazarse")
  		}
  		if r.RetryAfter <= 0 {
  			t.Errorf("RetryAfter=%v, want > 0", r.RetryAfter)
  		}
  		if r.RetryAfter > 1.5 {
  			t.Errorf("RetryAfter=%v, esperado <= 1s aprox", r.RetryAfter)
  		}
  	})

  	t.Run("keys distintas tienen buckets independientes", func(t *testing.T) {
  		rl := RateLimiterNew(1, 2)
  		// Consumir todo de A
  		RateLimiterCheck(rl, "A")
  		RateLimiterCheck(rl, "A")
  		ra := RateLimiterCheck(rl, "A")
  		if ra.Allowed {
  			t.Fatal("A deberia estar agotado")
  		}
  		// B sigue intacto
  		rb := RateLimiterCheck(rl, "B")
  		if !rb.Allowed {
  			t.Error("B deberia tener tokens propios")
  		}
  	})
  }


id: rate_limiter_cleanup_go_infra_t0
function_id: rate_limiter_cleanup_go_infra
name: TestRateLimiterCleanup
file_path: functions/infra/rate_limiter_cleanup_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimiterCleanup(t *testing.T) {
  	t.Run("purga entries con lastSeen mas antiguo que maxAge", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		// Inyectar manualmente una entry vieja
  		rl.mu.Lock()
  		rl.clients["old"] = &rateLimiterClient{tokens: 5, lastSeen: time.Now().Add(-time.Hour)}
  		rl.clients["new"] = &rateLimiterClient{tokens: 5, lastSeen: time.Now()}
  		rl.mu.Unlock()

  		stop := RateLimiterCleanup(rl, 10*time.Minute, 30*time.Millisecond)
  		defer stop()

  		// Esperar al menos un tick
  		time.Sleep(100 * time.Millisecond)

  		rl.mu.Lock()
  		_, oldExists := rl.clients["old"]
  		_, newExists := rl.clients["new"]
  		rl.mu.Unlock()

  		if oldExists {
  			t.Error("entry 'old' no fue purgada")
  		}
  		if !newExists {
  			t.Error("entry 'new' fue purgada incorrectamente")
  		}
  	})

  	t.Run("no purga entries recientes", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		RateLimiterCheck(rl, "fresh")

  		stop := RateLimiterCleanup(rl, 10*time.Minute, 30*time.Millisecond)
  		defer stop()

  		time.Sleep(100 * time.Millisecond)

  		rl.mu.Lock()
  		_, exists := rl.clients["fresh"]
  		rl.mu.Unlock()
  		if !exists {
  			t.Error("entry reciente fue purgada incorrectamente")
  		}
  	})

  	t.Run("stop detiene la goroutine sin panic", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		stop := RateLimiterCleanup(rl, time.Second, 10*time.Millisecond)
  		stop()
  		// Si esto no panica, pasa
  		time.Sleep(50 * time.Millisecond)
  	})

  	t.Run("stop es idempotente", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		stop := RateLimiterCleanup(rl, time.Second, 10*time.Millisecond)
  		stop()
  		stop() // segunda llamada no debe panic
  		stop() // tercera tampoco
  	})
  }


id: rate_limiter_new_go_infra_t0
function_id: rate_limiter_new_go_infra
name: TestRateLimiterNew
file_path: functions/infra/rate_limiter_new_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRateLimiterNew(t *testing.T) {
  	t.Run("crea limiter con rate y burst configurados", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		if rl == nil {
  			t.Fatal("RateLimiterNew retorno nil")
  		}
  		if rl.rate != 10 {
  			t.Errorf("rate=%v, want 10", rl.rate)
  		}
  		if rl.burst != 20 {
  			t.Errorf("burst=%d, want 20", rl.burst)
  		}
  	})

  	t.Run("valores cero se sustituyen por 1", func(t *testing.T) {
  		rl := RateLimiterNew(0, 0)
  		if rl.rate != 1 {
  			t.Errorf("rate=%v, want 1 (default)", rl.rate)
  		}
  		if rl.burst != 1 {
  			t.Errorf("burst=%d, want 1 (default)", rl.burst)
  		}

  		rl2 := RateLimiterNew(-5, -10)
  		if rl2.rate != 1 || rl2.burst != 1 {
  			t.Errorf("valores negativos no se normalizaron a 1: rate=%v burst=%d", rl2.rate, rl2.burst)
  		}
  	})

  	t.Run("el mapa de clientes empieza vacio", func(t *testing.T) {
  		rl := RateLimiterNew(10, 20)
  		if rl.clients == nil {
  			t.Error("clients map es nil, deberia ser inicializado")
  		}
  		if len(rl.clients) != 0 {
  			t.Errorf("clients len=%d, want 0", len(rl.clients))
  		}
  	})
  }


id: rbac_check_go_infra_t0
function_id: rbac_check_go_infra
name: TestRBACCheck_Granted
file_path: functions/infra/rbac_check_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACCheck_Granted(t *testing.T) {
  	roles := makeRoles()
  	if !RBACCheck(roles, "admin", Permission{Resource: "users", Action: "delete"}) {
  		t.Fatal("admin deberia tener users/delete")
  	}
  	if !RBACCheck(roles, "viewer", Permission{Resource: "users", Action: "read"}) {
  		t.Fatal("viewer deberia tener users/read")
  	}
  }


id: rbac_check_go_infra_t1
function_id: rbac_check_go_infra
name: TestRBACCheck_Denied
file_path: functions/infra/rbac_check_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACCheck_Denied(t *testing.T) {
  	roles := makeRoles()
  	if RBACCheck(roles, "viewer", Permission{Resource: "users", Action: "delete"}) {
  		t.Fatal("viewer NO deberia tener users/delete")
  	}
  }


id: rbac_check_go_infra_t2
function_id: rbac_check_go_infra
name: TestRBACCheck_UnknownRole
file_path: functions/infra/rbac_check_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACCheck_UnknownRole(t *testing.T) {
  	roles := makeRoles()
  	if RBACCheck(roles, "ghost", Permission{Resource: "users", Action: "read"}) {
  		t.Fatal("rol inexistente no deberia tener permisos")
  	}
  }


id: rbac_check_go_infra_t3
function_id: rbac_check_go_infra
name: TestRBACCheck_ExactMatch
file_path: functions/infra/rbac_check_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACCheck_ExactMatch(t *testing.T) {
  	roles := makeRoles()
  	// Resource distinto
  	if RBACCheck(roles, "admin", Permission{Resource: "billing", Action: "read"}) {
  		t.Fatal("admin no tiene billing/read")
  	}
  	// Action distinta
  	if RBACCheck(roles, "viewer", Permission{Resource: "users", Action: "write"}) {
  		t.Fatal("viewer no tiene users/write")
  	}
  }


id: rbac_middleware_go_infra_t0
function_id: rbac_middleware_go_infra
name: TestRBACMiddleware_PermissionGranted
file_path: functions/infra/rbac_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACMiddleware_PermissionGranted(t *testing.T) {
  	secret := "s"
  	chain := HTTPMiddlewareChain(
  		JWTMiddleware(secret),
  		RBACMiddleware(buildRBACRoles(), Permission{Resource: "users", Action: "delete"}),
  	)
  	var called bool
  	handler := chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		called = true
  		w.WriteHeader(200)
  	}))

  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, buildAuthedRequest(t, "admin", secret))
  	if rec.Code != 200 || !called {
  		t.Errorf("admin deberia pasar: status=%d called=%v", rec.Code, called)
  	}
  }


id: rbac_middleware_go_infra_t1
function_id: rbac_middleware_go_infra
name: TestRBACMiddleware_PermissionDenied
file_path: functions/infra/rbac_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACMiddleware_PermissionDenied(t *testing.T) {
  	secret := "s"
  	chain := HTTPMiddlewareChain(
  		JWTMiddleware(secret),
  		RBACMiddleware(buildRBACRoles(), Permission{Resource: "users", Action: "delete"}),
  	)
  	handler := chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		t.Error("viewer no deberia pasar")
  	}))

  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, buildAuthedRequest(t, "viewer", secret))
  	if rec.Code != 403 {
  		t.Errorf("status = %d, esperaba 403", rec.Code)
  	}
  }


id: rbac_middleware_go_infra_t2
function_id: rbac_middleware_go_infra
name: TestRBACMiddleware_UnknownRole
file_path: functions/infra/rbac_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACMiddleware_UnknownRole(t *testing.T) {
  	secret := "s"
  	chain := HTTPMiddlewareChain(
  		JWTMiddleware(secret),
  		RBACMiddleware(buildRBACRoles(), Permission{Resource: "users", Action: "read"}),
  	)
  	handler := chain(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		t.Error("rol ghost no deberia pasar")
  	}))

  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, buildAuthedRequest(t, "ghost", secret))
  	if rec.Code != 403 {
  		t.Errorf("status = %d, esperaba 403", rec.Code)
  	}
  }


id: rbac_middleware_go_infra_t3
function_id: rbac_middleware_go_infra
name: TestRBACMiddleware_NoClaims
file_path: functions/infra/rbac_middleware_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestRBACMiddleware_NoClaims(t *testing.T) {
  	// Sin JWTMiddleware delante → no hay claims en context
  	mw := RBACMiddleware(buildRBACRoles(), Permission{Resource: "users", Action: "read"})
  	handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		t.Error("no deberia ejecutarse")
  	}))
  	req := httptest.NewRequest(http.MethodGet, "/", nil)
  	rec := httptest.NewRecorder()
  	handler.ServeHTTP(rec, req)
  	if rec.Code != 401 {
  		t.Errorf("status = %d, esperaba 401", rec.Code)
  	}
  }


id: session_cleanup_go_infra_t0
function_id: session_cleanup_go_infra
name: TestSessionCreate_PersistsSession
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionCreate_PersistsSession(t *testing.T) {
  	db := openSessionTestDB(t)
  	s, err := SessionCreate(db, "user-1", time.Hour, map[string]any{"role": "admin"})
  	if err != nil {
  		t.Fatalf("SessionCreate: %v", err)
  	}
  	if s.Token == "" || len(s.Token) != 64 {
  		t.Errorf("token de largo incorrecto: len=%d", len(s.Token))
  	}
  	if s.UserID != "user-1" {
  		t.Errorf("UserID = %q", s.UserID)
  	}
  	if s.ExpiresAt <= s.CreatedAt {
  		t.Errorf("ExpiresAt %d <= CreatedAt %d", s.ExpiresAt, s.CreatedAt)
  	}

  	// Verificar persistencia
  	var count int
  	if err := db.QueryRow("SELECT COUNT(*) FROM sessions").Scan(&count); err != nil {
  		t.Fatalf("query: %v", err)
  	}
  	if count != 1 {
  		t.Errorf("esperaba 1 fila, got %d", count)
  	}
  }


id: session_cleanup_go_infra_t1
function_id: session_cleanup_go_infra
name: TestSessionCreate_GeneratesDistinctTokens
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionCreate_GeneratesDistinctTokens(t *testing.T) {
  	db := openSessionTestDB(t)
  	s1, _ := SessionCreate(db, "u", time.Hour, nil)
  	s2, _ := SessionCreate(db, "u", time.Hour, nil)
  	if s1.Token == s2.Token {
  		t.Fatal("dos sesiones no pueden tener el mismo token")
  	}
  }


id: session_cleanup_go_infra_t2
function_id: session_cleanup_go_infra
name: TestSessionCreate_RejectsEmptyUserID
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionCreate_RejectsEmptyUserID(t *testing.T) {
  	db := openSessionTestDB(t)
  	if _, err := SessionCreate(db, "", time.Hour, nil); err == nil {
  		t.Fatal("esperaba error con user_id vacio")
  	}
  }


id: session_cleanup_go_infra_t3
function_id: session_cleanup_go_infra
name: TestSessionValidate_ValidSession
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionValidate_ValidSession(t *testing.T) {
  	db := openSessionTestDB(t)
  	s, _ := SessionCreate(db, "user-7", time.Hour, map[string]any{"k": "v"})

  	got, err := SessionValidate(db, s.Token)
  	if err != nil {
  		t.Fatalf("SessionValidate: %v", err)
  	}
  	if got.UserID != "user-7" {
  		t.Errorf("UserID = %q", got.UserID)
  	}
  	if got.Metadata["k"] != "v" {
  		t.Errorf("metadata[k] = %v", got.Metadata["k"])
  	}
  }


id: session_cleanup_go_infra_t4
function_id: session_cleanup_go_infra
name: TestSessionValidate_MissingToken
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionValidate_MissingToken(t *testing.T) {
  	db := openSessionTestDB(t)
  	_ = sessionEnsureTable(db)
  	if _, err := SessionValidate(db, "nope"); err == nil {
  		t.Fatal("esperaba error con token inexistente")
  	}
  }


id: session_cleanup_go_infra_t5
function_id: session_cleanup_go_infra
name: TestSessionValidate_ExpiredSession
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionValidate_ExpiredSession(t *testing.T) {
  	db := openSessionTestDB(t)
  	_ = sessionEnsureTable(db)
  	// Insertar manualmente una sesion ya expirada
  	past := time.Now().Unix() - 60
  	_, err := db.Exec(
  		"INSERT INTO sessions (token, user_id, expires_at, created_at, metadata) VALUES (?, ?, ?, ?, ?)",
  		"expired-tok", "u", past, past-100, "{}",
  	)
  	if err != nil {
  		t.Fatalf("insert: %v", err)
  	}
  	if _, err := SessionValidate(db, "expired-tok"); err == nil {
  		t.Fatal("esperaba error con sesion expirada")
  	}
  }


id: session_cleanup_go_infra_t6
function_id: session_cleanup_go_infra
name: TestSessionCleanup_RemovesOnlyExpired
file_path: functions/infra/session_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSessionCleanup_RemovesOnlyExpired(t *testing.T) {
  	db := openSessionTestDB(t)
  	active, _ := SessionCreate(db, "active-user", time.Hour, nil)
  	_ = sessionEnsureTable(db)
  	past := time.Now().Unix() - 60
  	_, _ = db.Exec(
  		"INSERT INTO sessions (token, user_id, expires_at, created_at, metadata) VALUES (?, ?, ?, ?, ?)",
  		"expired", "u", past, past, "{}",
  	)

  	n, err := SessionCleanup(db)
  	if err != nil {
  		t.Fatalf("SessionCleanup: %v", err)
  	}
  	if n != 1 {
  		t.Errorf("esperaba 1 eliminada, got %d", n)
  	}

  	// La activa sigue ahi
  	if _, err := SessionValidate(db, active.Token); err != nil {
  		t.Errorf("sesion activa no deberia haberse borrado: %v", err)
  	}
  }


id: smtp_connect_go_infra_t0
function_id: smtp_connect_go_infra
name: TestSMTPConnect
file_path: functions/infra/smtp_connect_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSMTPConnect(t *testing.T) {
  	t.Run("conecta sin cifrado a servidor mock", func(t *testing.T) {
  		addr, stop := mockSMTPServer(t)
  		defer stop()

  		host, portStr, _ := net.SplitHostPort(addr)
  		port := 0
  		fmt.Sscanf(portStr, "%d", &port)

  		cfg := SMTPConfig{
  			Host:    host,
  			Port:    port,
  			TLSMode: "", // no encryption
  		}
  		client, err := SMTPConnect(cfg)
  		if err != nil {
  			t.Fatalf("SMTPConnect: %v", err)
  		}
  		client.Quit()
  	})

  	t.Run("error si el servidor no existe", func(t *testing.T) {
  		// Use a port that is listening but immediately closes (RST) — we start
  		// and immediately close a listener so the port is known-closed.
  		ln2, err := net.Listen("tcp", "127.0.0.1:0")
  		if err != nil {
  			t.Fatalf("setup: %v", err)
  		}
  		h2, p2str, _ := net.SplitHostPort(ln2.Addr().String())
  		ln2.Close() // now nothing listens — connections get refused
  		p2 := 0
  		fmt.Sscanf(p2str, "%d", &p2)

  		cfg := SMTPConfig{
  			Host:    h2,
  			Port:    p2,
  			TLSMode: "",
  		}
  		_, connErr := SMTPConnect(cfg)
  		if connErr == nil {
  			t.Error("expected error for refused connection, got nil")
  		}
  	})
  }


id: smtp_send_go_infra_t0
function_id: smtp_send_go_infra
name: TestSMTPSend
file_path: functions/infra/smtp_send_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSMTPSend(t *testing.T) {
  	t.Run("envia mensaje texto plano via mock smtp", func(t *testing.T) {
  		host, port, stop := startMockSMTPForSend(t)
  		defer stop()

  		cfg := SMTPConfig{Host: host, Port: port, TLSMode: ""}
  		client, err := SMTPConnect(cfg)
  		if err != nil {
  			t.Fatalf("SMTPConnect: %v", err)
  		}
  		defer client.Quit()

  		msg := EmailBuildText("alice@example.com", []string{"bob@example.com"}, "Test", "Hello Bob")
  		if err := SMTPSend(client, msg); err != nil {
  			t.Fatalf("SMTPSend: %v", err)
  		}
  	})

  	t.Run("envia mensaje html via mock smtp", func(t *testing.T) {
  		host, port, stop := startMockSMTPForSend(t)
  		defer stop()

  		cfg := SMTPConfig{Host: host, Port: port, TLSMode: ""}
  		client, err := SMTPConnect(cfg)
  		if err != nil {
  			t.Fatalf("SMTPConnect: %v", err)
  		}
  		defer client.Quit()

  		msg := EmailBuildHTML("alice@example.com", []string{"bob@example.com"}, "HTML Test", "<b>Hello</b>")
  		if err := SMTPSend(client, msg); err != nil {
  			t.Fatalf("SMTPSend: %v", err)
  		}
  	})

  	t.Run("envia con adjunto via mock smtp", func(t *testing.T) {
  		host, port, stop := startMockSMTPForSend(t)
  		defer stop()

  		cfg := SMTPConfig{Host: host, Port: port, TLSMode: ""}
  		client, err := SMTPConnect(cfg)
  		if err != nil {
  			t.Fatalf("SMTPConnect: %v", err)
  		}
  		defer client.Quit()

  		msg := EmailBuildText("alice@example.com", []string{"bob@example.com"}, "Att Test", "See attachment")
  		att := EmailAttachment{Filename: "data.txt", ContentType: "text/plain", Data: []byte("file content")}
  		msg = EmailWithAttachment(msg, att)

  		if err := SMTPSend(client, msg); err != nil {
  			t.Fatalf("SMTPSend with attachment: %v", err)
  		}
  	})
  }

  // TestExtractAddr tests the internal extractAddr helper.


id: smtp_send_go_infra_t1
function_id: smtp_send_go_infra
name: TestExtractAddr
file_path: functions/infra/smtp_send_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestExtractAddr(t *testing.T) {
  	cases := []struct {
  		input string
  		want  string
  	}{
  		{"Alice <alice@example.com>", "alice@example.com"},
  		{"bob@example.com", "bob@example.com"},
  		{"  carol@example.com  ", "carol@example.com"},
  	}
  	for _, c := range cases {
  		got := extractAddr(c.input)
  		if got != c.want {
  			t.Errorf("extractAddr(%q) = %q, want %q", c.input, got, c.want)
  		}
  	}
  }

  // TestBuildMIME tests the MIME builder with different body combinations.


id: smtp_send_go_infra_t2
function_id: smtp_send_go_infra
name: TestBuildMIME
file_path: functions/infra/smtp_send_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestBuildMIME(t *testing.T) {
  	t.Run("solo texto plano", func(t *testing.T) {
  		msg := EmailBuildText("a@a.com", []string{"b@b.com"}, "Subj", "Hello plain")
  		body, err := buildMIME(msg)
  		if err != nil {
  			t.Fatalf("buildMIME: %v", err)
  		}
  		s := string(body)
  		if !strings.Contains(s, "text/plain") {
  			t.Errorf("expected text/plain in MIME, got:\n%s", s)
  		}
  		if !strings.Contains(s, "Hello plain") {
  			t.Errorf("expected body text in MIME, got:\n%s", s)
  		}
  	})

  	t.Run("solo html", func(t *testing.T) {
  		msg := EmailBuildHTML("a@a.com", []string{"b@b.com"}, "Subj", "<b>Bold</b>")
  		body, err := buildMIME(msg)
  		if err != nil {
  			t.Fatalf("buildMIME: %v", err)
  		}
  		s := string(body)
  		if !strings.Contains(s, "text/html") {
  			t.Errorf("expected text/html in MIME, got:\n%s", s)
  		}
  	})

  	t.Run("multipart con adjunto", func(t *testing.T) {
  		msg := EmailBuildText("a@a.com", []string{"b@b.com"}, "Subj", "body")
  		att := EmailAttachment{Filename: "f.pdf", ContentType: "application/pdf", Data: []byte("pdf")}
  		msg = EmailWithAttachment(msg, att)
  		body, err := buildMIME(msg)
  		if err != nil {
  			t.Fatalf("buildMIME: %v", err)
  		}
  		s := string(body)
  		if !strings.Contains(s, "multipart/mixed") {
  			t.Errorf("expected multipart/mixed in MIME, got:\n%s", s)
  		}
  		if !strings.Contains(s, "f.pdf") {
  			t.Errorf("expected attachment filename in MIME, got:\n%s", s)
  		}
  	})
  }


id: sse_handler_go_infra_t0
function_id: sse_handler_go_infra
name: TestSSESend
file_path: functions/infra/sse_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSSESend(t *testing.T) {
  	t.Run("serializa data simple sin event ni id", func(t *testing.T) {
  		rec := newFlushRecorder()
  		err := SSESend(rec, SSEEvent{Data: "hola"})
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		body := rec.Body.String()
  		want := "data: hola\n\n"
  		if body != want {
  			t.Errorf("got body %q, want %q", body, want)
  		}
  	})

  	t.Run("incluye event cuando esta presente", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{Event: "tick", Data: "1"})
  		body := rec.Body.String()
  		if !strings.HasPrefix(body, "event: tick\n") {
  			t.Errorf("body should start with 'event: tick\\n', got %q", body)
  		}
  	})

  	t.Run("incluye id cuando esta presente", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{ID: "42", Data: "x"})
  		if !strings.Contains(rec.Body.String(), "id: 42\n") {
  			t.Errorf("body should contain 'id: 42\\n', got %q", rec.Body.String())
  		}
  	})

  	t.Run("incluye retry cuando es positivo", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{Retry: 5000, Data: "x"})
  		if !strings.Contains(rec.Body.String(), "retry: 5000\n") {
  			t.Errorf("body should contain 'retry: 5000\\n', got %q", rec.Body.String())
  		}
  	})

  	t.Run("omite retry cuando es 0", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{Retry: 0, Data: "x"})
  		if strings.Contains(rec.Body.String(), "retry:") {
  			t.Errorf("body should NOT contain 'retry:', got %q", rec.Body.String())
  		}
  	})

  	t.Run("data multilinea genera multiples lineas data:", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{Data: "linea1\nlinea2\nlinea3"})
  		body := rec.Body.String()
  		for _, want := range []string{"data: linea1\n", "data: linea2\n", "data: linea3\n"} {
  			if !strings.Contains(body, want) {
  				t.Errorf("body should contain %q, got %q", want, body)
  			}
  		}
  	})

  	t.Run("hace flush si el writer es Flusher", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{Data: "x"})
  		if rec.FlushCount() != 1 {
  			t.Errorf("expected 1 flush, got %d", rec.FlushCount())
  		}
  	})

  	t.Run("termina cada evento con doble salto de linea", func(t *testing.T) {
  		rec := newFlushRecorder()
  		_ = SSESend(rec, SSEEvent{Event: "x", Data: "y"})
  		body := rec.Body.String()
  		if !strings.HasSuffix(body, "\n\n") {
  			t.Errorf("body should end with '\\n\\n', got %q", body)
  		}
  	})
  }

  // --- SSEKeepalive ---


id: sse_handler_go_infra_t1
function_id: sse_handler_go_infra
name: TestSSEKeepalive
file_path: functions/infra/sse_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSSEKeepalive(t *testing.T) {
  	t.Run("escribe comentario keepalive periodicamente", func(t *testing.T) {
  		rec := newFlushRecorder()
  		done := make(chan struct{})

  		go SSEKeepalive(rec, 10*time.Millisecond, done)
  		time.Sleep(50 * time.Millisecond)
  		close(done)
  		time.Sleep(20 * time.Millisecond)

  		body := rec.Body.String()
  		if !strings.Contains(body, ": keepalive\n\n") {
  			t.Errorf("body should contain ': keepalive\\n\\n', got %q", body)
  		}
  		count := strings.Count(body, ": keepalive\n\n")
  		if count < 2 {
  			t.Errorf("expected at least 2 keepalives, got %d", count)
  		}
  	})

  	t.Run("termina cuando done se cierra", func(t *testing.T) {
  		rec := newFlushRecorder()
  		done := make(chan struct{})
  		finished := make(chan struct{})

  		go func() {
  			SSEKeepalive(rec, 5*time.Millisecond, done)
  			close(finished)
  		}()

  		close(done)
  		select {
  		case <-finished:
  			// ok
  		case <-time.After(100 * time.Millisecond):
  			t.Error("SSEKeepalive did not return after done was closed")
  		}
  	})
  }

  // --- SSEHandler ---


id: sse_handler_go_infra_t2
function_id: sse_handler_go_infra
name: TestSSEHandler
file_path: functions/infra/sse_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestSSEHandler(t *testing.T) {
  	t.Run("setea headers SSE correctos", func(t *testing.T) {
  		events := make(chan SSEEvent)
  		handler := SSEHandler(events)

  		// Lanzar handler en goroutine y cerrar canal para que retorne
  		go func() {
  			time.Sleep(20 * time.Millisecond)
  			close(events)
  		}()

  		ts := httptest.NewServer(handler)
  		defer ts.Close()

  		resp, err := http.Get(ts.URL)
  		if err != nil {
  			t.Fatalf("get failed: %v", err)
  		}
  		defer resp.Body.Close()

  		if got := resp.Header.Get("Content-Type"); got != "text/event-stream" {
  			t.Errorf("Content-Type = %q, want text/event-stream", got)
  		}
  		if got := resp.Header.Get("Cache-Control"); got != "no-cache" {
  			t.Errorf("Cache-Control = %q, want no-cache", got)
  		}
  	})

  	t.Run("envia eventos del canal al writer", func(t *testing.T) {
  		events := make(chan SSEEvent, 2)
  		events <- SSEEvent{Event: "first", Data: "1"}
  		events <- SSEEvent{Event: "second", Data: "2"}
  		close(events)

  		handler := SSEHandler(events)
  		ts := httptest.NewServer(handler)
  		defer ts.Close()

  		resp, err := http.Get(ts.URL)
  		if err != nil {
  			t.Fatalf("get failed: %v", err)
  		}
  		defer resp.Body.Close()

  		buf := make([]byte, 4096)
  		n, _ := resp.Body.Read(buf)
  		body := string(buf[:n])

  		for _, want := range []string{"event: first", "event: second", "data: 1", "data: 2"} {
  			if !strings.Contains(body, want) {
  				t.Errorf("body should contain %q, got %q", want, body)
  			}
  		}
  	})

  	t.Run("termina si el contexto del request se cancela", func(t *testing.T) {
  		events := make(chan SSEEvent)
  		handler := SSEHandler(events)

  		ts := httptest.NewServer(handler)
  		defer ts.Close()

  		ctx, cancel := context.WithCancel(context.Background())
  		req, _ := http.NewRequestWithContext(ctx, "GET", ts.URL, nil)

  		done := make(chan struct{})
  		go func() {
  			resp, err := http.DefaultClient.Do(req)
  			if err == nil {
  				resp.Body.Close()
  			}
  			close(done)
  		}()

  		time.Sleep(20 * time.Millisecond)
  		cancel()

  		select {
  		case <-done:
  			// handler debe haber retornado tras la cancelacion del cliente
  		case <-time.After(2 * time.Second):
  			t.Error("handler did not return after context cancel")
  		}
  	})

  	t.Run("retorna 500 si el writer no es Flusher", func(t *testing.T) {
  		// nonFlushWriter no implementa Flusher
  		events := make(chan SSEEvent)
  		handler := SSEHandler(events)

  		rec := &nonFlushWriter{header: http.Header{}}
  		req := httptest.NewRequest("GET", "/", nil)
  		handler(rec, req)

  		if rec.status != http.StatusInternalServerError {
  			t.Errorf("got status %d, want 500", rec.status)
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: thumbnail_generate_go_infra_t0
function_id: thumbnail_generate_go_infra
name: TestThumbnailGenerate
file_path: functions/infra/thumbnail_generate_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestThumbnailGenerate(t *testing.T) {
  	t.Run("genera thumbnail PNG mas pequeno que el original", func(t *testing.T) {
  		dir := t.TempDir()
  		src := filepath.Join(dir, "src.png")
  		dst := filepath.Join(dir, "thumb.png")
  		genPNG(t, src, 400)

  		if err := ThumbnailGenerate(src, dst, 100, 100); err != nil {
  			t.Fatalf("ThumbnailGenerate: %v", err)
  		}

  		f, err := os.Open(dst)
  		if err != nil {
  			t.Fatalf("open dst: %v", err)
  		}
  		defer f.Close()
  		img, _, err := image.Decode(f)
  		if err != nil {
  			t.Fatalf("decode dst: %v", err)
  		}
  		b := img.Bounds()
  		if b.Dx() > 100 || b.Dy() > 100 {
  			t.Errorf("thumbnail %dx%d, want <= 100x100", b.Dx(), b.Dy())
  		}
  	})

  	t.Run("preserva aspect ratio", func(t *testing.T) {
  		dir := t.TempDir()
  		src := filepath.Join(dir, "src.png")
  		dst := filepath.Join(dir, "thumb.png")

  		// Imagen 400x200 (aspect 2:1)
  		img := image.NewRGBA(image.Rect(0, 0, 400, 200))
  		f, _ := os.Create(src)
  		png.Encode(f, img)
  		f.Close()

  		if err := ThumbnailGenerate(src, dst, 100, 100); err != nil {
  			t.Fatalf("err: %v", err)
  		}
  		f2, _ := os.Open(dst)
  		defer f2.Close()
  		thumb, _, _ := image.Decode(f2)
  		b := thumb.Bounds()
  		// Aspect 2:1 mantenido: 100x50
  		if b.Dx() != 100 || b.Dy() != 50 {
  			t.Errorf("thumbnail %dx%d, want 100x50 (aspect 2:1)", b.Dx(), b.Dy())
  		}
  	})

  	t.Run("rechaza extension de salida no soportada", func(t *testing.T) {
  		dir := t.TempDir()
  		src := filepath.Join(dir, "src.png")
  		dst := filepath.Join(dir, "thumb.webp")
  		genPNG(t, src, 200)
  		err := ThumbnailGenerate(src, dst, 100, 100)
  		if err == nil {
  			t.Error("got nil err, want extension no soportada")
  		}
  	})

  	t.Run("no agranda imagenes ya pequenas", func(t *testing.T) {
  		dir := t.TempDir()
  		src := filepath.Join(dir, "src.png")
  		dst := filepath.Join(dir, "thumb.png")
  		genPNG(t, src, 50)

  		if err := ThumbnailGenerate(src, dst, 200, 200); err != nil {
  			t.Fatalf("err: %v", err)
  		}
  		f, _ := os.Open(dst)
  		defer f.Close()
  		thumb, _, _ := image.Decode(f)
  		b := thumb.Bounds()
  		if b.Dx() != 50 || b.Dy() != 50 {
  			t.Errorf("thumbnail %dx%d, want 50x50 (sin agrandar)", b.Dx(), b.Dy())
  		}
  	})
  }


id: upload_handler_go_infra_t0
function_id: upload_handler_go_infra
name: TestUploadHandler
file_path: functions/infra/upload_handler_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestUploadHandler(t *testing.T) {
  	t.Run("acepta upload con imagen y responde JSON", func(t *testing.T) {
  		dir := t.TempDir()
  		cfg := StorageConfig{
  			BaseDir:      dir,
  			MaxFileSize:  1 << 20,
  			AllowedTypes: []string{"image/png"},
  		}
  		ct, body := buildPNGMultipart(t, "foto.png", "data extra")

  		req := httptest.NewRequest("POST", "/upload", body)
  		req.Header.Set("Content-Type", ct)
  		rec := httptest.NewRecorder()

  		UploadHandler(cfg).ServeHTTP(rec, req)

  		if rec.Code != http.StatusOK {
  			t.Fatalf("got %d, body=%s", rec.Code, rec.Body.String())
  		}
  		var resp struct {
  			Files []UploadedFile `json:"files"`
  		}
  		if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
  			t.Fatalf("unmarshal: %v", err)
  		}
  		if len(resp.Files) != 1 {
  			t.Fatalf("got %d files, want 1", len(resp.Files))
  		}
  		if resp.Files[0].ContentType != "image/png" {
  			t.Errorf("got ContentType %q", resp.Files[0].ContentType)
  		}
  	})

  	t.Run("rechaza tipo no permitido con 415", func(t *testing.T) {
  		dir := t.TempDir()
  		cfg := StorageConfig{
  			BaseDir:      dir,
  			MaxFileSize:  1 << 20,
  			AllowedTypes: []string{"application/pdf"}, // PNG no en lista
  		}
  		ct, body := buildPNGMultipart(t, "foto.png", "x")

  		req := httptest.NewRequest("POST", "/upload", body)
  		req.Header.Set("Content-Type", ct)
  		rec := httptest.NewRecorder()

  		UploadHandler(cfg).ServeHTTP(rec, req)

  		if rec.Code != http.StatusUnsupportedMediaType {
  			t.Errorf("got %d, want 415", rec.Code)
  		}
  	})

  	t.Run("rechaza body que excede MaxFileSize con 400", func(t *testing.T) {
  		dir := t.TempDir()
  		cfg := StorageConfig{
  			BaseDir:      dir,
  			MaxFileSize:  10, // muy pequeno
  			AllowedTypes: []string{"image/png"},
  		}
  		ct, body := buildPNGMultipart(t, "foto.png", "muchos bytes extra para superar 10")

  		req := httptest.NewRequest("POST", "/upload", body)
  		req.Header.Set("Content-Type", ct)
  		rec := httptest.NewRecorder()

  		UploadHandler(cfg).ServeHTTP(rec, req)
  		if rec.Code != http.StatusBadRequest {
  			t.Errorf("got %d, want 400", rec.Code)
  		}
  	})
  }


id: upload_parse_go_infra_t0
function_id: upload_parse_go_infra
name: TestUploadParse
file_path: functions/infra/upload_parse_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestUploadParse(t *testing.T) {
  	t.Run("extrae un archivo del multipart", func(t *testing.T) {
  		ct, body := buildMultipart(t, []struct{ field, filename, content string }{
  			{"file", "a.txt", "hello"},
  		})
  		req := newMultipartReq(t, ct, body)

  		got, err := UploadParse(req, 1<<20)
  		if err != nil {
  			t.Fatalf("UploadParse: %v", err)
  		}
  		if len(got) != 1 {
  			t.Fatalf("got %d files, want 1", len(got))
  		}
  		if got[0].Filename != "a.txt" {
  			t.Errorf("got Filename %q", got[0].Filename)
  		}
  		if got[0].Size != 5 {
  			t.Errorf("got Size %d, want 5", got[0].Size)
  		}
  		buf, _ := io.ReadAll(got[0].Content)
  		if string(buf) != "hello" {
  			t.Errorf("got content %q, want hello", buf)
  		}
  	})

  	t.Run("extrae multiples archivos", func(t *testing.T) {
  		ct, body := buildMultipart(t, []struct{ field, filename, content string }{
  			{"a", "1.txt", "uno"},
  			{"b", "2.txt", "dos"},
  		})
  		req := newMultipartReq(t, ct, body)

  		got, err := UploadParse(req, 1<<20)
  		if err != nil {
  			t.Fatalf("UploadParse: %v", err)
  		}
  		if len(got) != 2 {
  			t.Fatalf("got %d files, want 2", len(got))
  		}
  	})

  	t.Run("rechaza content-type no multipart", func(t *testing.T) {
  		req := httptest.NewRequest("POST", "/upload", strings.NewReader("x"))
  		req.Header.Set("Content-Type", "application/json")
  		_, err := UploadParse(req, 1<<20)
  		if err == nil || !strings.Contains(err.Error(), "no es multipart") {
  			t.Errorf("got %v, want error de content-type", err)
  		}
  	})

  	t.Run("respeta maxSize", func(t *testing.T) {
  		ct, body := buildMultipart(t, []struct{ field, filename, content string }{
  			{"file", "big.bin", strings.Repeat("x", 1024)},
  		})
  		req := newMultipartReq(t, ct, body)
  		_, err := UploadParse(req, 100) // demasiado pequeno
  		if err == nil {
  			t.Errorf("got nil err, want max bytes error")
  		}
  	})
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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: ws_broadcast_go_infra_t0
function_id: ws_broadcast_go_infra
name: TestWSHub
file_path: functions/infra/ws_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestWSHub(t *testing.T) {
  	t.Run("registra y desregistra clientes", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		client := &WSClient{Hub: hub, Send: make(chan []byte, 4), ID: "c1"}
  		hub.Register <- client

  		// Esperar a que el hub procese
  		time.Sleep(20 * time.Millisecond)

  		hub.Unregister <- client
  		time.Sleep(20 * time.Millisecond)

  		// El canal Send del cliente debe estar cerrado tras unregister
  		select {
  		case _, ok := <-client.Send:
  			if ok {
  				t.Error("client.Send should be closed after unregister")
  			}
  		default:
  			t.Error("client.Send should be closed (zero-value receive)")
  		}
  	})

  	t.Run("Stop cierra todos los clientes", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()

  		c1 := &WSClient{Hub: hub, Send: make(chan []byte, 4), ID: "c1"}
  		c2 := &WSClient{Hub: hub, Send: make(chan []byte, 4), ID: "c2"}
  		hub.Register <- c1
  		hub.Register <- c2
  		time.Sleep(20 * time.Millisecond)

  		hub.Stop()
  		time.Sleep(20 * time.Millisecond)

  		for _, c := range []*WSClient{c1, c2} {
  			select {
  			case _, ok := <-c.Send:
  				if ok {
  					t.Errorf("client %s Send should be closed after Stop", c.ID)
  				}
  			default:
  				t.Errorf("client %s Send should be closed (zero-value receive)", c.ID)
  			}
  		}
  	})

  	t.Run("Stop es idempotente", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		hub.Stop()
  		hub.Stop() // no debe panicar
  	})
  }

  // --- WSBroadcast ---


id: ws_broadcast_go_infra_t1
function_id: ws_broadcast_go_infra
name: TestWSBroadcast
file_path: functions/infra/ws_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestWSBroadcast(t *testing.T) {
  	t.Run("envia mensaje al canal Broadcast del hub", func(t *testing.T) {
  		hub := NewWSHub()
  		err := WSBroadcast(hub, []byte("hola"))
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		select {
  		case msg := <-hub.Broadcast:
  			if string(msg) != "hola" {
  				t.Errorf("got %q, want hola", string(msg))
  			}
  		case <-time.After(time.Second):
  			t.Error("message not in Broadcast channel")
  		}
  	})

  	t.Run("retorna error si hub es nil", func(t *testing.T) {
  		err := WSBroadcast(nil, []byte("x"))
  		if err == nil {
  			t.Error("expected error for nil hub, got nil")
  		}
  	})

  	t.Run("el hub entrega el mensaje a todos los clientes registrados", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		c1 := &WSClient{Hub: hub, Send: make(chan []byte, 4), ID: "c1"}
  		c2 := &WSClient{Hub: hub, Send: make(chan []byte, 4), ID: "c2"}
  		hub.Register <- c1
  		hub.Register <- c2
  		time.Sleep(20 * time.Millisecond)

  		WSBroadcast(hub, []byte("ping"))
  		time.Sleep(20 * time.Millisecond)

  		for _, c := range []*WSClient{c1, c2} {
  			select {
  			case msg := <-c.Send:
  				if string(msg) != "ping" {
  					t.Errorf("client %s got %q, want ping", c.ID, string(msg))
  				}
  			case <-time.After(time.Second):
  				t.Errorf("client %s did not receive broadcast", c.ID)
  			}
  		}
  	})

  	t.Run("cliente lento es desconectado del hub", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		// Cliente con buffer de 1 — el segundo broadcast lo tira
  		slow := &WSClient{Hub: hub, Send: make(chan []byte, 1), ID: "slow"}
  		hub.Register <- slow
  		time.Sleep(10 * time.Millisecond)

  		// Llenar buffer y forzar drop
  		WSBroadcast(hub, []byte("1"))
  		WSBroadcast(hub, []byte("2"))
  		time.Sleep(20 * time.Millisecond)

  		// Drenar Send: deberia tener el primer mensaje y luego estar cerrado
  		got := []string{}
  		for msg := range slow.Send {
  			got = append(got, string(msg))
  		}
  		if len(got) > 1 {
  			t.Errorf("expected at most 1 message in slow client, got %d: %v", len(got), got)
  		}
  	})
  }

  // --- WSSend ---


id: ws_broadcast_go_infra_t2
function_id: ws_broadcast_go_infra
name: TestWSSend
file_path: functions/infra/ws_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestWSSend(t *testing.T) {
  	t.Run("envia mensaje al canal Send del cliente", func(t *testing.T) {
  		client := &WSClient{Send: make(chan []byte, 4), ID: "c1"}
  		err := WSSend(client, []byte("hola"))
  		if err != nil {
  			t.Fatalf("unexpected error: %v", err)
  		}
  		got := <-client.Send
  		if string(got) != "hola" {
  			t.Errorf("got %q, want hola", string(got))
  		}
  	})

  	t.Run("retorna error si client es nil", func(t *testing.T) {
  		err := WSSend(nil, []byte("x"))
  		if err == nil {
  			t.Error("expected error for nil client, got nil")
  		}
  	})

  	t.Run("retorna error si el canal esta lleno", func(t *testing.T) {
  		client := &WSClient{Send: make(chan []byte, 1), ID: "c1"}
  		_ = WSSend(client, []byte("1"))
  		err := WSSend(client, []byte("2"))
  		if err == nil {
  			t.Error("expected error when send channel full, got nil")
  		}
  	})
  }

  // --- WSUpgrader & WSHandler integration ---


id: ws_broadcast_go_infra_t3
function_id: ws_broadcast_go_infra
name: TestWSUpgrader
file_path: functions/infra/ws_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestWSUpgrader(t *testing.T) {
  	t.Run("upgradea conexion valida con `*` en origenes", func(t *testing.T) {
  		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  			conn, err := WSUpgrader(w, r, []string{"*"})
  			if err != nil {
  				return
  			}
  			defer conn.Close(websocket.StatusNormalClosure, "")
  			// Echo
  			_, data, _ := conn.Read(r.Context())
  			conn.Write(r.Context(), websocket.MessageText, data)
  		}))
  		defer ts.Close()

  		wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
  		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
  		defer cancel()

  		c, _, err := websocket.Dial(ctx, wsURL, nil)
  		if err != nil {
  			t.Fatalf("dial failed: %v", err)
  		}
  		defer c.Close(websocket.StatusNormalClosure, "")

  		if err := c.Write(ctx, websocket.MessageText, []byte("ping")); err != nil {
  			t.Fatalf("write failed: %v", err)
  		}
  		_, data, err := c.Read(ctx)
  		if err != nil {
  			t.Fatalf("read failed: %v", err)
  		}
  		if string(data) != "ping" {
  			t.Errorf("got %q, want ping", string(data))
  		}
  	})
  }


id: ws_broadcast_go_infra_t4
function_id: ws_broadcast_go_infra
name: TestWSHandler
file_path: functions/infra/ws_test.go
lang: go
created_at: 2026-04-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  func TestWSHandler(t *testing.T) {
  	t.Run("upgradea conexion y registra cliente en hub", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		ts := httptest.NewServer(WSHandler(hub, []string{"*"}))
  		defer ts.Close()

  		wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
  		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  		defer cancel()

  		c, _, err := websocket.Dial(ctx, wsURL, nil)
  		if err != nil {
  			t.Fatalf("dial failed: %v", err)
  		}
  		defer c.Close(websocket.StatusNormalClosure, "")

  		// Esperar a que el hub procese el Register
  		time.Sleep(50 * time.Millisecond)

  		if hub.ClientCount() != 1 {
  			t.Errorf("expected 1 client in hub, got %d", hub.ClientCount())
  		}
  	})

  	t.Run("broadcast del hub llega al cliente conectado", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		ts := httptest.NewServer(WSHandler(hub, []string{"*"}))
  		defer ts.Close()

  		wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
  		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  		defer cancel()

  		c, _, err := websocket.Dial(ctx, wsURL, nil)
  		if err != nil {
  			t.Fatalf("dial failed: %v", err)
  		}
  		defer c.Close(websocket.StatusNormalClosure, "")

  		time.Sleep(50 * time.Millisecond)

  		WSBroadcast(hub, []byte("hello-all"))

  		_, data, err := c.Read(ctx)
  		if err != nil {
  			t.Fatalf("read failed: %v", err)
  		}
  		if string(data) != "hello-all" {
  			t.Errorf("got %q, want hello-all", string(data))
  		}
  	})

  	t.Run("multiples clientes reciben el broadcast", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		ts := httptest.NewServer(WSHandler(hub, []string{"*"}))
  		defer ts.Close()

  		wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
  		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  		defer cancel()

  		const N = 3
  		conns := make([]*websocket.Conn, N)
  		for i := 0; i < N; i++ {
  			c, _, err := websocket.Dial(ctx, wsURL, nil)
  			if err != nil {
  				t.Fatalf("dial %d failed: %v", i, err)
  			}
  			conns[i] = c
  			defer c.Close(websocket.StatusNormalClosure, "")
  		}

  		time.Sleep(80 * time.Millisecond)

  		WSBroadcast(hub, []byte("multicast"))

  		var wg sync.WaitGroup
  		errs := make(chan error, N)
  		for i := 0; i < N; i++ {
  			wg.Add(1)
  			go func(idx int, conn *websocket.Conn) {
  				defer wg.Done()
  				_, data, err := conn.Read(ctx)
  				if err != nil {
  					errs <- err
  					return
  				}
  				if string(data) != "multicast" {
  					errs <- &readMismatchError{idx: idx, got: string(data)}
  				}
  			}(i, conns[i])
  		}
  		wg.Wait()
  		close(errs)
  		for e := range errs {
  			t.Errorf("client receive error: %v", e)
  		}
  	})

  	t.Run("desregistra cliente al desconectar", func(t *testing.T) {
  		hub := NewWSHub()
  		go hub.Run()
  		defer hub.Stop()

  		ts := httptest.NewServer(WSHandler(hub, []string{"*"}))
  		defer ts.Close()

  		wsURL := "ws" + strings.TrimPrefix(ts.URL, "http")
  		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  		defer cancel()

  		c, _, err := websocket.Dial(ctx, wsURL, nil)
  		if err != nil {
  			t.Fatalf("dial failed: %v", err)
  		}

  		time.Sleep(50 * time.Millisecond)
  		if hub.ClientCount() != 1 {
  			t.Fatalf("expected 1 client after dial, got %d", hub.ClientCount())
  		}

  		c.Close(websocket.StatusNormalClosure, "")
  		time.Sleep(100 * time.Millisecond)

  		if hub.ClientCount() != 0 {
  			t.Errorf("expected 0 clients after close, got %d", hub.ClientCount())
  		}
  	})
  }

  type readMismatchError struct {
  	idx int
  	got string
  }

  func (e *readMismatchError) Error() string {
  	return "client " + string(rune('0'+e.idx)) + " got " + e.got
  }


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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
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-18T15:58:35Z
updated_at: 2026-04-18T15:58:35Z
code:
  def test_5_imagenes_100_lineas_full_page_vlm():
      assert calculate_media_strategy(5, 100) == "full_page_vlm"


