# unit_tests

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


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


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


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


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


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


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


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


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


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


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


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


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

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

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


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


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


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

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

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


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


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


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


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


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


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


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


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


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


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


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


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

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

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


id: docx_to_markdown_py_core_t1
function_id: docx_to_markdown_py_core
name: test_docx_con_tablas_intercaladas
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_docx_con_tablas_intercaladas():
      """docx con tablas intercaladas"""

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

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


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

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

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


id: docx_to_markdown_py_core_t3
function_id: docx_to_markdown_py_core
name: test_docx_vacio
file_path: python/functions/core/docx_to_markdown_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_docx_vacio():
      """docx vacio"""

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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

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


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

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


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

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


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

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

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

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

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


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

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


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


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


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


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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


id: generate_pwa_manifest_py_core_t0
function_id: generate_pwa_manifest_py_core
name: test_valores_por_defecto_generan_manifest_valido_con_icons_placeholder
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_valores_por_defecto_generan_manifest_valido_con_icons_placeholder():
      result = generate_pwa_manifest(name="Mi App", short_name="App")

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

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


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

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


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

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


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


id: generate_pwa_manifest_py_core_t4
function_id: generate_pwa_manifest_py_core
name: test_json_dumps_compatible
file_path: python/functions/core/generate_pwa_manifest_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_json_dumps_compatible():
      import json

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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


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

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


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

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


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

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

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

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

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


id: match_pois_to_interests_py_core_t0
function_id: match_pois_to_interests_py_core
name: test_pois_que_matchean_parcialmente_por_categoria_y_nombre
file_path: python/functions/core/match_pois_to_interests_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pois_que_matchean_parcialmente_por_categoria_y_nombre():
      """pois que matchean parcialmente por categoria y nombre"""
      interests = [{"name": "arte", "keywords": ["museo", "museum", "arte", "galería"], "weight": 1.0}]
      result = match_pois_to_interests(POIS_SAMPLE, interests)

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


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


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


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


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


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

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

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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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


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

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


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


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

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

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

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


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


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

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


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


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

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

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

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


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


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

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

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


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


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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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

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

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


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

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

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


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

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


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

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


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

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

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


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

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

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


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


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


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


id: react_loop_py_core_t0
function_id: react_loop_py_core
name: test_agente_que_usa_1_tool_y_da_respuesta_final
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_agente_que_usa_1_tool_y_da_respuesta_final():
      called_with = {}

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

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

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

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


id: react_loop_py_core_t1
function_id: react_loop_py_core
name: test_agente_que_usa_multiples_tools_iterativamente
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_agente_que_usa_multiples_tools_iterativamente():
      call_log = []

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

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

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

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

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


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

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

      assert result == "42 is the answer."


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

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

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

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

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


id: react_loop_py_core_t4
function_id: react_loop_py_core
name: test_tool_que_falla_error_handling
file_path: python/functions/core/react_loop_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_tool_que_falla_error_handling():
      observations = []

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

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

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

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


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


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


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


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


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

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


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


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


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


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


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

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

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


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

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


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

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

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

      with pytest.raises(RuntimeError):
          flaky()

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


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

      import time

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

      import unittest.mock as mock

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

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

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


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

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

      with pytest.raises(ValueError):
          raises_value_error()

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


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


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

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

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


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

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


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

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

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

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

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


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

      delays: list[float] = []

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

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

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

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


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

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

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

      assert len(calls) == 1


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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

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

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


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


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


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


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


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


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


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


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


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


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


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

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


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

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


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

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


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


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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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


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


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


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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


id: detect_drift_py_datascience_t4
function_id: detect_drift_py_datascience
name: test_threshold_custom
file_path: python/functions/datascience/detect_drift_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_threshold_custom():
      history = [
          {"val": 100.0},
          {"val": 100.0},
          {"val": 110.0},
          {"val": 90.0},
      ]
      # std ~ 7.07, mean = 100
      current = {"val": 115.0}  # z ~ 2.12

      # threshold default 2.0 -> drifted
      results = detect_drift(history, current, ["val"], threshold=2.0)
      assert results[0]["drifted"] is True

      # threshold 3.0 -> no drifted
      results2 = detect_drift(history, current, ["val"], threshold=3.0)
      assert results2[0]["drifted"] is False


  if __name__ == "__main__":
      test_campo_con_drift_claro_z_mayor_threshold()
      test_campo_estable_z_menor_threshold()
      test_historial_con_un_solo_punto_std_0_drifted_False_con_nota()
      test_historial_vacio_todos_drifted_False()
      test_threshold_custom()
      print("All tests passed.")


id: diff_entities_py_datascience_t0
function_id: diff_entities_py_datascience
name: test_entity_anadida
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_entity_anadida():
      before = [{"id": "1", "name": "Alice"}]
      after = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      result = diff_entities(before, after)
      assert len(result["added"]) == 1
      assert result["added"][0]["id"] == "2"
      assert result["removed"] == []
      assert result["modified"] == []
      assert result["unchanged"] == 1
      assert "1 added" in result["summary"]


id: diff_entities_py_datascience_t1
function_id: diff_entities_py_datascience
name: test_entity_eliminada
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_entity_eliminada():
      before = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      after = [{"id": "1", "name": "Alice"}]
      result = diff_entities(before, after)
      assert result["added"] == []
      assert len(result["removed"]) == 1
      assert result["removed"][0]["id"] == "2"
      assert result["unchanged"] == 1
      assert "1 removed" in result["summary"]


id: diff_entities_py_datascience_t2
function_id: diff_entities_py_datascience
name: test_entity_modificada_con_detalle_de_campos
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_entity_modificada_con_detalle_de_campos():
      before = [{"id": "1", "name": "Alice", "status": "active"}]
      after = [{"id": "1", "name": "Alice", "status": "inactive"}]
      result = diff_entities(before, after)
      assert result["added"] == []
      assert result["removed"] == []
      assert len(result["modified"]) == 1
      mod = result["modified"][0]
      assert mod["key"] == "1"
      assert "status" in mod["changes"]
      assert mod["changes"]["status"]["old"] == "active"
      assert mod["changes"]["status"]["new"] == "inactive"
      assert result["unchanged"] == 0


id: diff_entities_py_datascience_t3
function_id: diff_entities_py_datascience
name: test_entities_identicas_unchanged
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_entities_identicas_unchanged():
      before = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      after = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      result = diff_entities(before, after)
      assert result["added"] == []
      assert result["removed"] == []
      assert result["modified"] == []
      assert result["unchanged"] == 2
      assert "2 unchanged" in result["summary"]


id: diff_entities_py_datascience_t4
function_id: diff_entities_py_datascience
name: test_ignore_fields_funciona
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_ignore_fields_funciona():
      before = [{"id": "1", "name": "Alice", "updated_at": "2024-01-01", "created_at": "2023-01-01"}]
      after = [{"id": "1", "name": "Alice", "updated_at": "2024-06-01", "created_at": "2023-01-01"}]
      result = diff_entities(before, after)
      # updated_at se ignora por defecto -> unchanged
      assert result["unchanged"] == 1
      assert result["modified"] == []

      # Si no ignoramos updated_at, debe detectar el cambio
      result2 = diff_entities(before, after, ignore_fields=[])
      assert len(result2["modified"]) == 1
      assert "updated_at" in result2["modified"][0]["changes"]


id: diff_entities_py_datascience_t5
function_id: diff_entities_py_datascience
name: test_compare_fields_filtra_correctamente
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_compare_fields_filtra_correctamente():
      before = [{"id": "1", "name": "Alice", "status": "active", "score": 10}]
      after = [{"id": "1", "name": "Bob", "status": "inactive", "score": 10}]
      # Solo comparar score -> no hay cambio en score, unchanged
      result = diff_entities(before, after, compare_fields=["score"])
      assert result["unchanged"] == 1
      assert result["modified"] == []

      # Solo comparar name -> detecta cambio
      result2 = diff_entities(before, after, compare_fields=["name"])
      assert len(result2["modified"]) == 1
      assert "name" in result2["modified"][0]["changes"]
      assert "status" not in result2["modified"][0]["changes"]


id: diff_entities_py_datascience_t6
function_id: diff_entities_py_datascience
name: test_lista_vacia_vs_lista_con_datos
file_path: python/functions/datascience/diff_entities_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_lista_vacia_vs_lista_con_datos():
      before = []
      after = [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]
      result = diff_entities(before, after)
      assert len(result["added"]) == 2
      assert result["removed"] == []
      assert result["unchanged"] == 0

      # Invertido
      result2 = diff_entities(after, before)
      assert result2["added"] == []
      assert len(result2["removed"]) == 2
      assert result2["unchanged"] == 0


  if __name__ == "__main__":
      test_entity_anadida()
      test_entity_eliminada()
      test_entity_modificada_con_detalle_de_campos()
      test_entities_identicas_unchanged()
      test_ignore_fields_funciona()
      test_compare_fields_filtra_correctamente()
      test_lista_vacia_vs_lista_con_datos()
      print("All tests passed.")


id: diff_relations_py_datascience_t0
function_id: diff_relations_py_datascience
name: test_relacion_anadida
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_relacion_anadida():
      before = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0}]
      after = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "C", "target_id": "D", "relation_type": "owns", "weight": 0.5},
      ]
      result = diff_relations(before, after)
      assert len(result["added"]) == 1
      assert result["added"][0]["source_id"] == "C"
      assert result["removed"] == []
      assert result["unchanged"] == 1
      assert "1 added" in result["summary"]


id: diff_relations_py_datascience_t1
function_id: diff_relations_py_datascience
name: test_relacion_eliminada
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_relacion_eliminada():
      before = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "C", "target_id": "D", "relation_type": "owns", "weight": 0.5},
      ]
      after = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0}]
      result = diff_relations(before, after)
      assert result["added"] == []
      assert len(result["removed"]) == 1
      assert result["removed"][0]["source_id"] == "C"
      assert result["unchanged"] == 1
      assert "1 removed" in result["summary"]


id: diff_relations_py_datascience_t2
function_id: diff_relations_py_datascience
name: test_relacion_con_metadata_modificada_mismo_source_target_type_distinto_weight
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_relacion_con_metadata_modificada_mismo_source_target_type_distinto_weight():
      before = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0}]
      after = [{"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 5.0}]
      result = diff_relations(before, after)
      assert result["added"] == []
      assert result["removed"] == []
      assert len(result["modified"]) == 1
      mod = result["modified"][0]
      assert mod["key"] == "A|B|knows"
      assert "weight" in mod["changes"]
      assert mod["changes"]["weight"]["old"] == 1.0
      assert mod["changes"]["weight"]["new"] == 5.0
      assert result["unchanged"] == 0


id: diff_relations_py_datascience_t3
function_id: diff_relations_py_datascience
name: test_key_compuesta_funciona_correctamente
file_path: python/functions/datascience/diff_relations_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_key_compuesta_funciona_correctamente():
      # Misma pareja A->B pero diferente tipo de relacion -> dos relaciones distintas
      before = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "A", "target_id": "B", "relation_type": "owns", "weight": 0.5},
      ]
      after = [
          {"source_id": "A", "target_id": "B", "relation_type": "knows", "weight": 1.0},
          {"source_id": "A", "target_id": "B", "relation_type": "trusts", "weight": 0.8},
      ]
      result = diff_relations(before, after)
      # owns eliminada, trusts añadida, knows sin cambios
      assert len(result["added"]) == 1
      assert result["added"][0]["relation_type"] == "trusts"
      assert len(result["removed"]) == 1
      assert result["removed"][0]["relation_type"] == "owns"
      assert result["unchanged"] == 1
      assert result["modified"] == []


  if __name__ == "__main__":
      test_relacion_anadida()
      test_relacion_eliminada()
      test_relacion_con_metadata_modificada_mismo_source_target_type_distinto_weight()
      test_key_compuesta_funciona_correctamente()
      print("All tests passed.")


id: extract_entities_llm_py_datascience_t0
function_id: extract_entities_llm_py_datascience
name: test_texto_con_entidades_claras_retorna_entity_candidate
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_con_entidades_claras_retorna_entity_candidate():
      """texto con entidades claras retorna EntityCandidate"""
      llm = make_llm({
          "entities": [
              {
                  "name": "John Smith",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {"full_name": "John Smith", "nationality": "US"},
                  "confidence": 0.95,
              },
              {
                  "name": "evil-corp.com",
                  "type_ref": "osint_domain_go_cybersecurity",
                  "attributes": {"fqdn": "evil-corp.com"},
                  "confidence": 0.88,
              },
          ]
      })

      result = extract_entities_llm(
          "John Smith, US citizen, linked to evil-corp.com.", SCHEMA, llm
      )

      assert len(result) == 2

      person = next(e for e in result if e.name == "John Smith")
      assert person.type_ref == "osint_person_go_cybersecurity"
      assert person.type_label == "Person"
      assert person.attributes["full_name"] == "John Smith"
      assert person.confidence == 0.95

      domain = next(e for e in result if e.name == "evil-corp.com")
      assert domain.type_ref == "osint_domain_go_cybersecurity"
      assert domain.type_label == "Domain"
      assert domain.attributes["fqdn"] == "evil-corp.com"
      assert domain.confidence == 0.88


id: extract_entities_llm_py_datascience_t1
function_id: extract_entities_llm_py_datascience
name: test_texto_sin_entidades_retorna_lista_vacia
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_sin_entidades_retorna_lista_vacia():
      """texto sin entidades retorna lista vacia"""
      llm = make_llm({"entities": []})

      result = extract_entities_llm(
          "The sky is blue and the grass is green.", SCHEMA, llm
      )

      assert result == []


id: extract_entities_llm_py_datascience_t2
function_id: extract_entities_llm_py_datascience
name: test_llm_retorna_json_mal_formado_retorna_lista_vacia_con_warning
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_llm_retorna_json_mal_formado_retorna_lista_vacia_con_warning():
      """llm retorna json mal formado retorna lista vacia con warning"""
      def bad_llm(messages: list[dict]) -> dict:
          raise ValueError("JSON decode error")

      with warnings.catch_warnings(record=True) as caught:
          warnings.simplefilter("always")
          result = extract_entities_llm("Some text with entities.", SCHEMA, bad_llm)

      assert result == []
      assert len(caught) == 1
      assert "error llamando al LLM" in str(caught[0].message)


id: extract_entities_llm_py_datascience_t3
function_id: extract_entities_llm_py_datascience
name: test_type_ref_invalido_en_respuesta_se_descarta_con_warning
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_type_ref_invalido_en_respuesta_se_descarta_con_warning():
      """type_ref invalido en respuesta se descarta con warning"""
      llm = make_llm({
          "entities": [
              {
                  "name": "Valid Person",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {},
                  "confidence": 0.9,
              },
              {
                  "name": "Unknown Thing",
                  "type_ref": "nonexistent_type_ref",
                  "attributes": {},
                  "confidence": 0.8,
              },
          ]
      })

      with warnings.catch_warnings(record=True) as caught:
          warnings.simplefilter("always")
          result = extract_entities_llm("Text with entities.", SCHEMA, llm)

      assert len(result) == 1
      assert result[0].name == "Valid Person"
      assert any("nonexistent_type_ref" in str(w.message) for w in caught)


id: extract_entities_llm_py_datascience_t4
function_id: extract_entities_llm_py_datascience
name: test_confidence_se_propaga_correctamente
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_confidence_se_propaga_correctamente():
      """confidence se propaga correctamente"""
      llm = make_llm({
          "entities": [
              {
                  "name": "Implied Person",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {},
                  "confidence": 0.7,
              },
              {
                  "name": "Weakly Implied Domain",
                  "type_ref": "osint_domain_go_cybersecurity",
                  "attributes": {},
                  "confidence": 0.5,
              },
              {
                  "name": "Explicit Entity",
                  "type_ref": "osint_person_go_cybersecurity",
                  "attributes": {},
                  "confidence": 1.0,
              },
          ]
      })

      result = extract_entities_llm("Some text.", SCHEMA, llm)

      assert len(result) == 3
      confidences = {e.name: e.confidence for e in result}
      assert confidences["Implied Person"] == 0.7
      assert confidences["Weakly Implied Domain"] == 0.5
      assert confidences["Explicit Entity"] == 1.0


id: extract_entities_llm_py_datascience_t5
function_id: extract_entities_llm_py_datascience
name: test_schema_vacio_lanza_value_error
file_path: python/functions/datascience/extract_entities_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_schema_vacio_lanza_value_error():
      """schema vacio lanza ValueError"""
      llm = make_llm({"entities": []})

      with pytest.raises(ValueError, match="entity_schema no puede estar vacio"):
          extract_entities_llm("Some text.", [], llm)


id: extract_relations_llm_py_datascience_t0
function_id: extract_relations_llm_py_datascience
name: test_texto_con_dos_entidades_relacionadas
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_con_dos_entidades_relacionadas():
      entities = [_make_entity("Acme Corp", "Organization"), _make_entity("John Smith", "Person")]
      relation_types = ["employs", "funds", "related_to"]

      llm_response = {
          "relations": [
              {
                  "from_name": "Acme Corp",
                  "to_name": "John Smith",
                  "relation_type": "employs",
                  "description": "Acme Corp employs John Smith as CEO",
                  "confidence": 0.9,
              }
          ]
      }

      result = extract_relations_llm(
          text="Acme Corp employs John Smith as CEO.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      assert len(result) == 1
      rel = result[0]
      assert rel.from_name == "Acme Corp"
      assert rel.to_name == "John Smith"
      assert rel.relation_type == "employs"
      assert rel.confidence == 0.9
      assert "CEO" in rel.description


id: extract_relations_llm_py_datascience_t1
function_id: extract_relations_llm_py_datascience
name: test_texto_con_entidades_pero_sin_relacion
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_con_entidades_pero_sin_relacion():
      entities = [_make_entity("Alice", "Person"), _make_entity("Bob", "Person")]
      relation_types = ["funds", "employs"]

      llm_response = {"relations": []}

      result = extract_relations_llm(
          text="Alice and Bob both attended the conference.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      assert result == []


id: extract_relations_llm_py_datascience_t2
function_id: extract_relations_llm_py_datascience
name: test_menos_de_dos_entidades_retorna_lista_vacia
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_menos_de_dos_entidades_retorna_lista_vacia():
      entities = [_make_entity("Solo Corp", "Organization")]
      relation_types = ["employs", "funds"]

      # El LLM nunca deberia ser llamado, pero si lo fuera retornaria relaciones
      llm_response = {
          "relations": [
              {"from_name": "Solo Corp", "to_name": "Nobody", "relation_type": "employs", "confidence": 0.9}
          ]
      }

      result = extract_relations_llm(
          text="Solo Corp is a company.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      assert result == []


id: extract_relations_llm_py_datascience_t3
function_id: extract_relations_llm_py_datascience
name: test_llm_inventa_entidad_que_no_existe_se_descarta
file_path: python/functions/datascience/extract_relations_llm_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_llm_inventa_entidad_que_no_existe_se_descarta():
      entities = [_make_entity("Alice", "Person"), _make_entity("Bob", "Person")]
      relation_types = ["funds", "employs", "related_to"]

      llm_response = {
          "relations": [
              # Valida — Alice y Bob existen
              {
                  "from_name": "Alice",
                  "to_name": "Bob",
                  "relation_type": "funds",
                  "description": "Alice funds Bob",
                  "confidence": 0.8,
              },
              # Invalida — "Charlie" no esta en entities
              {
                  "from_name": "Alice",
                  "to_name": "Charlie",
                  "relation_type": "employs",
                  "description": "Alice employs Charlie",
                  "confidence": 0.7,
              },
              # Invalida — "Unknown Corp" no esta en entities
              {
                  "from_name": "Unknown Corp",
                  "to_name": "Bob",
                  "relation_type": "related_to",
                  "description": "...",
                  "confidence": 0.6,
              },
          ]
      }

      result = extract_relations_llm(
          text="Alice funds Bob. Alice also employs Charlie from Unknown Corp.",
          entities=entities,
          relation_types=relation_types,
          llm_chat_json=_make_llm(llm_response),
      )

      # Solo la primera relacion es valida
      assert len(result) == 1
      assert result[0].from_name == "Alice"
      assert result[0].to_name == "Bob"
      assert result[0].relation_type == "funds"


id: hotness_score_py_datascience_t0
function_id: hotness_score_py_datascience
name: test_active_count_zero_updated_at_reciente
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_active_count_zero_updated_at_reciente():
      """active_count=0, updated_at reciente."""
      updated_at = NOW - timedelta(hours=1)
      score = hotness_score(0, updated_at, now=NOW)
      # freq = sigmoid(log1p(0)) = sigmoid(0) = 0.5
      # recency ~ 1.0 (casi nuevo)
      assert 0.45 < score < 0.55, f"Expected ~0.5, got {score}"


id: hotness_score_py_datascience_t1
function_id: hotness_score_py_datascience
name: test_active_count_alto_updated_at_reciente
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_active_count_alto_updated_at_reciente():
      """active_count=100, updated_at reciente (score alto)."""
      updated_at = NOW - timedelta(hours=1)
      score = hotness_score(100, updated_at, now=NOW)
      # freq = sigmoid(log1p(100)) = sigmoid(4.615) ~ 0.99
      # recency ~ 1.0
      assert score > 0.95, f"Expected > 0.95, got {score}"


id: hotness_score_py_datascience_t2
function_id: hotness_score_py_datascience
name: test_active_count_alto_updated_at_hace_30_dias
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_active_count_alto_updated_at_hace_30_dias():
      """active_count=100, updated_at hace 30 dias (score bajo)."""
      updated_at = NOW - timedelta(days=30)
      score = hotness_score(100, updated_at, now=NOW)
      # recency = exp(-ln2/7 * 30) = exp(-2.97) ~ 0.051
      # score ~ 0.99 * 0.051 ~ 0.05
      assert score < 0.1, f"Expected < 0.1, got {score}"


id: hotness_score_py_datascience_t3
function_id: hotness_score_py_datascience
name: test_updated_at_none_retorna_cero
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_updated_at_none_retorna_cero():
      """updated_at=None (retorna 0.0)."""
      score = hotness_score(100, None, now=NOW)
      assert score == 0.0, f"Expected 0.0, got {score}"


id: hotness_score_py_datascience_t4
function_id: hotness_score_py_datascience
name: test_now_explicito
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_now_explicito():
      """now explicito (determinista para tests)."""
      updated_at = NOW - timedelta(days=7)
      score = hotness_score(50, updated_at, now=NOW)
      # recency = exp(-ln2/7 * 7) = 0.5
      # freq = sigmoid(log1p(50)) ~ sigmoid(3.93) ~ 0.981
      expected = (1.0 / (1.0 + math.exp(-math.log1p(50)))) * 0.5
      assert abs(score - expected) < 1e-9, f"Expected {expected}, got {score}"


id: hotness_score_py_datascience_t5
function_id: hotness_score_py_datascience
name: test_half_life_days_custom
file_path: python/functions/datascience/hotness_score_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_half_life_days_custom():
      """half_life_days custom."""
      updated_at = NOW - timedelta(days=1)
      # Con half_life=1 dia, despues de 1 dia recency = 0.5
      score = hotness_score(50, updated_at, now=NOW, half_life_days=1.0)
      freq = 1.0 / (1.0 + math.exp(-math.log1p(50)))
      expected = freq * 0.5
      assert abs(score - expected) < 1e-6, f"Expected {expected}, got {score}"


id: melt_py_datascience_t0
function_id: melt_py_datascience
name: test_melt_basico
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_melt_basico():
      """Melt basico."""
      rows = [{"region": "US", "q1": 10, "q2": 20}]
      result = melt(rows, id_vars=["region"], value_vars=["q1", "q2"])
      assert len(result) == 2
      assert result[0] == {"region": "US", "variable": "q1", "value": 10}
      assert result[1] == {"region": "US", "variable": "q2", "value": 20}


id: melt_py_datascience_t1
function_id: melt_py_datascience
name: test_melt_multiples_id_vars
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_melt_multiples_id_vars():
      """Multiples id_vars."""
      rows = [{"region": "US", "year": 2023, "q1": 10, "q2": 20}]
      result = melt(rows, id_vars=["region", "year"], value_vars=["q1", "q2"])
      assert len(result) == 2
      assert result[0]["region"] == "US"
      assert result[0]["year"] == 2023
      assert result[0]["variable"] == "q1"
      assert result[0]["value"] == 10
      assert result[1]["variable"] == "q2"
      assert result[1]["value"] == 20


id: melt_py_datascience_t2
function_id: melt_py_datascience
name: test_melt_value_vars_none_derrite_todas_las_columnas_no_id
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_melt_value_vars_none_derrite_todas_las_columnas_no_id():
      """value_vars None derrite todas las columnas no-id."""
      rows = [{"id": 1, "a": 10, "b": 20, "c": 30}]
      result = melt(rows, id_vars=["id"])
      assert len(result) == 3
      vars_found = {r["variable"] for r in result}
      assert vars_found == {"a", "b", "c"}
      values_found = {r["value"] for r in result}
      assert values_found == {10, 20, 30}


id: melt_py_datascience_t3
function_id: melt_py_datascience
name: test_melt_fila_con_campo_faltante_en_value_vars
file_path: python/functions/datascience/melt_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_melt_fila_con_campo_faltante_en_value_vars():
      """Fila con campo faltante en value_vars."""
      rows = [{"region": "US", "q1": 10}]  # q2 no existe
      result = melt(rows, id_vars=["region"], value_vars=["q1", "q2"])
      assert len(result) == 2
      q2_row = next(r for r in result if r["variable"] == "q2")
      assert q2_row["value"] is None


id: merge_graphs_py_datascience_t0
function_id: merge_graphs_py_datascience
name: test_dos_grafos_con_entity_duplicada_merge
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dos_grafos_con_entity_duplicada_merge():
      g1 = {
          "entities": [{"id": "1", "name": "Alice Corp", "type": "company"}],
          "relations": [],
      }
      g2 = {
          "entities": [{"id": "2", "name": "Alice Corp", "type": "company", "country": "US"}],
          "relations": [],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.95)
      # Nombres identicos -> similitud 1.0 -> deben mergearse
      assert len(result["entities"]) == 1
      assert len(result["merge_log"]) == 1
      merged = result["entities"][0]
      # El merge debe preservar "country" aunque el canonical no lo tuviera
      assert merged.get("country") == "US" or merged.get("name") == "Alice Corp"


id: merge_graphs_py_datascience_t1
function_id: merge_graphs_py_datascience
name: test_entities_similares_pero_bajo_threshold_no_merge
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_entities_similares_pero_bajo_threshold_no_merge():
      g1 = {
          "entities": [{"id": "1", "name": "Alice"}],
          "relations": [],
      }
      g2 = {
          "entities": [{"id": "2", "name": "Bob"}],
          "relations": [],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.85)
      # Alice y Bob son muy distintos -> no merge
      assert len(result["entities"]) == 2
      assert len(result["merge_log"]) == 0


id: merge_graphs_py_datascience_t2
function_id: merge_graphs_py_datascience
name: test_relaciones_re_apuntadas_correctamente
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_relaciones_re_apuntadas_correctamente():
      g1 = {
          "entities": [
              {"id": "1", "name": "Alice Corp"},
              {"id": "2", "name": "Bob"},
          ],
          "relations": [
              {"source_id": "2", "target_id": "1", "relation_type": "works_at"},
          ],
      }
      g2 = {
          "entities": [
              {"id": "3", "name": "Alice Corp"},  # duplicada de id=1
          ],
          "relations": [
              {"source_id": "3", "target_id": "2", "relation_type": "knows"},
          ],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.95)
      # Entity 3 mergeada en 1 -> relacion source_id=3 debe apuntar al canonical de 1
      assert len(result["entities"]) == 2  # Alice Corp + Bob
      # Verificar que las relaciones tienen IDs canonicos (no "3")
      for rel in result["relations"]:
          assert rel["source_id"] != "3"
          assert rel["target_id"] != "3"


id: merge_graphs_py_datascience_t3
function_id: merge_graphs_py_datascience
name: test_merge_log_registra_cada_merge
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_merge_log_registra_cada_merge():
      g1 = {
          "entities": [{"id": "1", "name": "OpenAI"}],
          "relations": [],
      }
      g2 = {
          "entities": [{"id": "2", "name": "OpenAI"}],
          "relations": [],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.9)
      assert len(result["merge_log"]) == 1
      log = result["merge_log"][0]
      assert "merged" in log
      assert "into" in log
      assert "similarity" in log
      assert log["similarity"] == 1.0


id: merge_graphs_py_datascience_t4
function_id: merge_graphs_py_datascience
name: test_tres_grafos_merge_transitivo
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_tres_grafos_merge_transitivo():
      # A~B y B~C -> A, B, C deben mergearse en uno
      g1 = {"entities": [{"id": "1", "name": "Acme Corp"}], "relations": []}
      g2 = {"entities": [{"id": "2", "name": "Acme Corp"}], "relations": []}
      g3 = {"entities": [{"id": "3", "name": "Acme Corp"}], "relations": []}
      result = merge_graphs([g1, g2, g3], similarity_threshold=0.9)
      assert len(result["entities"]) == 1


id: merge_graphs_py_datascience_t5
function_id: merge_graphs_py_datascience
name: test_grafos_sin_overlap_concatenacion_simple
file_path: python/functions/datascience/merge_graphs_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_grafos_sin_overlap_concatenacion_simple():
      g1 = {
          "entities": [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}],
          "relations": [{"source_id": "1", "target_id": "2", "relation_type": "knows"}],
      }
      g2 = {
          "entities": [{"id": "3", "name": "Carol"}, {"id": "4", "name": "Dave"}],
          "relations": [{"source_id": "3", "target_id": "4", "relation_type": "knows"}],
      }
      result = merge_graphs([g1, g2], similarity_threshold=0.85)
      # Ninguna entity similar -> concatenacion directa
      assert len(result["entities"]) == 4
      assert len(result["relations"]) == 2
      assert len(result["merge_log"]) == 0


  if __name__ == "__main__":
      test_dos_grafos_con_entity_duplicada_merge()
      test_entities_similares_pero_bajo_threshold_no_merge()
      test_relaciones_re_apuntadas_correctamente()
      test_merge_log_registra_cada_merge()
      test_tres_grafos_merge_transitivo()
      test_grafos_sin_overlap_concatenacion_simple()
      print("All tests passed.")


id: pivot_py_datascience_t0
function_id: pivot_py_datascience
name: test_pivot_basico_con_sum
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pivot_basico_con_sum():
      """Pivot basico con sum."""
      rows = [
          {"region": "US", "product": "A", "sales": 10},
          {"region": "US", "product": "B", "sales": 20},
          {"region": "EU", "product": "A", "sales": 15},
      ]
      result = pivot(rows, index="region", columns="product", values="sales")
      assert len(result) == 2
      us = next(r for r in result if r["region"] == "US")
      eu = next(r for r in result if r["region"] == "EU")
      assert us["A"] == 10
      assert us["B"] == 20
      assert eu["A"] == 15
      assert eu["B"] == 0


id: pivot_py_datascience_t1
function_id: pivot_py_datascience
name: test_pivot_con_count_y_mean
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pivot_con_count_y_mean():
      """Pivot con count y mean."""
      rows = [
          {"region": "US", "product": "A", "sales": 10},
          {"region": "US", "product": "A", "sales": 20},
          {"region": "EU", "product": "A", "sales": 15},
      ]
      result_count = pivot(rows, index="region", columns="product", values="sales", agg="count")
      us_count = next(r for r in result_count if r["region"] == "US")
      assert us_count["A"] == 2

      result_mean = pivot(rows, index="region", columns="product", values="sales", agg="mean")
      us_mean = next(r for r in result_mean if r["region"] == "US")
      assert us_mean["A"] == 15.0


id: pivot_py_datascience_t2
function_id: pivot_py_datascience
name: test_pivot_valores_faltantes_rellenados_con_0
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pivot_valores_faltantes_rellenados_con_0():
      """Valores faltantes rellenados con 0."""
      rows = [
          {"region": "US", "product": "A", "sales": 5},
          {"region": "EU", "product": "B", "sales": 8},
      ]
      result = pivot(rows, index="region", columns="product", values="sales")
      us = next(r for r in result if r["region"] == "US")
      eu = next(r for r in result if r["region"] == "EU")
      assert us["B"] == 0
      assert eu["A"] == 0


id: pivot_py_datascience_t3
function_id: pivot_py_datascience
name: test_pivot_una_sola_fila
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pivot_una_sola_fila():
      """Una sola fila."""
      rows = [{"region": "US", "product": "A", "sales": 42}]
      result = pivot(rows, index="region", columns="product", values="sales")
      assert len(result) == 1
      assert result[0]["region"] == "US"
      assert result[0]["A"] == 42


id: pivot_py_datascience_t4
function_id: pivot_py_datascience
name: test_pivot_multiples_valores_por_celda_requieren_agregacion
file_path: python/functions/datascience/pivot_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pivot_multiples_valores_por_celda_requieren_agregacion():
      """Multiples valores por celda requieren agregacion."""
      rows = [
          {"region": "US", "product": "A", "sales": 10},
          {"region": "US", "product": "A", "sales": 30},
      ]
      result_sum = pivot(rows, index="region", columns="product", values="sales", agg="sum")
      assert result_sum[0]["A"] == 40

      result_min = pivot(rows, index="region", columns="product", values="sales", agg="min")
      assert result_min[0]["A"] == 10

      result_max = pivot(rows, index="region", columns="product", values="sales", agg="max")
      assert result_max[0]["A"] == 30


id: cache_to_file_py_infra_t0
function_id: cache_to_file_py_infra
name: test_set_y_get_basico
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_set_y_get_basico(store):
      store.set("hello", {"x": 42})
      assert store.get("hello") == {"x": 42}


id: cache_to_file_py_infra_t1
function_id: cache_to_file_py_infra
name: test_ttl_expirado_retorna_none
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_ttl_expirado_retorna_none(store):
      store.set("temp", "val", ttl=0.05)
      time.sleep(0.1)
      assert store.get("temp") is None


id: cache_to_file_py_infra_t2
function_id: cache_to_file_py_infra
name: test_archivo_meta_con_metadata_correcta
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_meta_con_metadata_correcta(tmp_path):
      s = cache_to_file(str(tmp_path), "ns")
      s.set("mykey", "myval", ttl=60)
      ns_dir = os.path.join(str(tmp_path), "ns")
      meta_files = [f for f in os.listdir(ns_dir) if f.endswith(".meta")]
      assert len(meta_files) == 1
      with open(os.path.join(ns_dir, meta_files[0])) as f:
          meta = json.load(f)
      assert meta["original_key"] == "mykey"
      assert meta["expires_at"] is not None
      assert meta["created_at"] > 0


id: cache_to_file_py_infra_t3
function_id: cache_to_file_py_infra
name: test_clear_elimina_directorio_del_namespace
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_clear_elimina_directorio_del_namespace(tmp_path):
      s = cache_to_file(str(tmp_path), "mynamespace")
      s.set("a", 1)
      s.set("b", 2)
      removed = s.clear()
      assert removed == 2
      assert s.get("a") is None
      assert s.get("b") is None


id: cache_to_file_py_infra_t4
function_id: cache_to_file_py_infra
name: test_key_con_caracteres_especiales_hash_seguro
file_path: python/functions/infra/cache_to_file_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_key_con_caracteres_especiales_hash_seguro(store):
      key = "https://example.com/path?q=1&r=2 <special>#hash"
      store.set(key, "safe")
      assert store.get(key) == "safe"


id: cache_to_sqlite_py_infra_t0
function_id: cache_to_sqlite_py_infra
name: test_set_y_get_basico
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_set_y_get_basico(store):
      store.set("foo", {"x": 1})
      assert store.get("foo") == {"x": 1}


id: cache_to_sqlite_py_infra_t1
function_id: cache_to_sqlite_py_infra
name: test_ttl_expirado_retorna_none
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_ttl_expirado_retorna_none(store):
      store.set("expiring", "hello", ttl=0.05)
      time.sleep(0.1)
      assert store.get("expiring") is None


id: cache_to_sqlite_py_infra_t2
function_id: cache_to_sqlite_py_infra
name: test_ttl_cero_nunca_expira
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_ttl_cero_nunca_expira(store):
      store.set("forever", 42, ttl=0)
      time.sleep(0.05)
      assert store.get("forever") == 42


id: cache_to_sqlite_py_infra_t3
function_id: cache_to_sqlite_py_infra
name: test_get_or_set_factory_solo_se_llama_en_miss
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_get_or_set_factory_solo_se_llama_en_miss(store):
      calls = []

      def factory():
          calls.append(1)
          return "computed"

      result1 = store.get_or_set("key", factory, ttl=10)
      result2 = store.get_or_set("key", factory, ttl=10)
      assert result1 == "computed"
      assert result2 == "computed"
      assert len(calls) == 1


id: cache_to_sqlite_py_infra_t4
function_id: cache_to_sqlite_py_infra
name: test_namespaces_independientes
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_namespaces_independientes(store_and_other):
      s1, s2 = store_and_other
      s1.set("k", "from_ns1")
      assert s2.get("k") is None
      s2.set("k", "from_ns2")
      assert s1.get("k") == "from_ns1"
      assert s2.get("k") == "from_ns2"


id: cache_to_sqlite_py_infra_t5
function_id: cache_to_sqlite_py_infra
name: test_clear_elimina_solo_el_namespace
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_clear_elimina_solo_el_namespace(store_and_other):
      s1, s2 = store_and_other
      s1.set("a", 1)
      s2.set("b", 2)
      removed = s1.clear()
      assert removed == 1
      assert s1.get("a") is None
      assert s2.get("b") == 2


id: cache_to_sqlite_py_infra_t6
function_id: cache_to_sqlite_py_infra
name: test_stats_contadores_correctos
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_stats_contadores_correctos(store):
      store.set("x", 10)
      store.get("x")   # hit
      store.get("x")   # hit
      store.get("z")   # miss
      s = store.stats()
      assert s["hits"] == 2
      assert s["misses"] == 1
      assert s["size"] == 1


id: cache_to_sqlite_py_infra_t7
function_id: cache_to_sqlite_py_infra
name: test_concurrencia
file_path: python/functions/infra/cache_to_sqlite_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_concurrencia(tmp_path):
      db = str(tmp_path / "concurrent.db")
      s = cache_to_sqlite(db, "parallel")
      errors = []

      def worker(i):
          try:
              s.set(f"key_{i}", i)
              val = s.get(f"key_{i}")
              assert val == i
          except Exception as e:
              errors.append(e)

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

      assert errors == [], f"Errors in threads: {errors}"


id: config_from_env_py_infra_t0
function_id: config_from_env_py_infra
name: test_populate_string_desde_env
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_populate_string_desde_env():
      os.environ["CFE_PY_HOST"] = "myhost"
      try:
          cfg = config_from_env(SimpleConfig)
          assert cfg.host == "myhost"
      finally:
          _clean("CFE_PY_HOST")


id: config_from_env_py_infra_t1
function_id: config_from_env_py_infra
name: test_populate_int_con_conversion
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_populate_int_con_conversion():
      os.environ["CFE_PY_PORT"] = "9090"
      try:
          cfg = config_from_env(SimpleConfig)
          assert cfg.port == 9090
      finally:
          _clean("CFE_PY_PORT")


id: config_from_env_py_infra_t2
function_id: config_from_env_py_infra
name: test_populate_bool_true
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_populate_bool_true():
      os.environ["CFE_PY_DEBUG"] = "true"
      try:
          cfg = config_from_env(SimpleConfig)
          assert cfg.debug is True
      finally:
          _clean("CFE_PY_DEBUG")


id: config_from_env_py_infra_t3
function_id: config_from_env_py_infra
name: test_populate_float_field
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_populate_float_field():
      os.environ["CFE_PY_RATIO"] = "3.14"
      try:
          cfg = config_from_env(SimpleConfig)
          assert abs(cfg.ratio - 3.14) < 1e-9
      finally:
          _clean("CFE_PY_RATIO")


id: config_from_env_py_infra_t4
function_id: config_from_env_py_infra
name: test_populate_list_comma_separated
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_populate_list_comma_separated():
      os.environ["CFE_PY_TAGS"] = "x,y,z"
      try:
          cfg = config_from_env(SimpleConfig)
          assert cfg.tags == ["x", "y", "z"]
      finally:
          _clean("CFE_PY_TAGS")


id: config_from_env_py_infra_t5
function_id: config_from_env_py_infra
name: test_default_usado_cuando_env_no_seteada
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_default_usado_cuando_env_no_seteada():
      _clean("CFE_PY_PORT")
      cfg = config_from_env(SimpleConfig)
      assert cfg.port == 8080


id: config_from_env_py_infra_t6
function_id: config_from_env_py_infra
name: test_required_sin_valor_lanza_error
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_required_sin_valor_lanza_error():
      _clean("CFE_PY_API_KEY")
      with pytest.raises(ValueError, match="required"):
          config_from_env(RequiredConfig)


id: config_from_env_py_infra_t7
function_id: config_from_env_py_infra
name: test_no_dataclass_lanza_type_error
file_path: python/functions/infra/config_from_env_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_no_dataclass_lanza_type_error():
      class NotADataclass:
          pass

      with pytest.raises(TypeError):
          config_from_env(NotADataclass)


id: dotenv_load_py_infra_t0
function_id: dotenv_load_py_infra
name: test_parsea_key_value_y_setea_variable
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_parsea_key_value_y_setea_variable():
      path = write_env("DL_PY_FOO=hello\n")
      _clean("DL_PY_FOO")
      try:
          dotenv_load(path)
          assert os.environ.get("DL_PY_FOO") == "hello"
      finally:
          _clean("DL_PY_FOO")
          os.unlink(path)


id: dotenv_load_py_infra_t1
function_id: dotenv_load_py_infra
name: test_ignora_comentarios_y_lineas_vacias
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_ignora_comentarios_y_lineas_vacias():
      content = "# comment\n\nDL_PY_BAR=world\n"
      path = write_env(content)
      _clean("DL_PY_BAR")
      try:
          dotenv_load(path)
          assert os.environ.get("DL_PY_BAR") == "world"
      finally:
          _clean("DL_PY_BAR")
          os.unlink(path)


id: dotenv_load_py_infra_t2
function_id: dotenv_load_py_infra
name: test_no_sobreescribe_variables_existentes
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_no_sobreescribe_variables_existentes():
      path = write_env("DL_PY_EXIST=new\n")
      os.environ["DL_PY_EXIST"] = "original"
      try:
          dotenv_load(path)
          assert os.environ["DL_PY_EXIST"] == "original"
      finally:
          _clean("DL_PY_EXIST")
          os.unlink(path)


id: dotenv_load_py_infra_t3
function_id: dotenv_load_py_infra
name: test_valores_con_comillas_dobles_se_stripean
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_valores_con_comillas_dobles_se_stripean():
      path = write_env('DL_PY_QUOTED="quoted value"\n')
      _clean("DL_PY_QUOTED")
      try:
          dotenv_load(path)
          assert os.environ.get("DL_PY_QUOTED") == "quoted value"
      finally:
          _clean("DL_PY_QUOTED")
          os.unlink(path)


id: dotenv_load_py_infra_t4
function_id: dotenv_load_py_infra
name: test_valores_con_comillas_simples_se_stripean
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_valores_con_comillas_simples_se_stripean():
      path = write_env("DL_PY_SINGLE='single quoted'\n")
      _clean("DL_PY_SINGLE")
      try:
          dotenv_load(path)
          assert os.environ.get("DL_PY_SINGLE") == "single quoted"
      finally:
          _clean("DL_PY_SINGLE")
          os.unlink(path)


id: dotenv_load_py_infra_t5
function_id: dotenv_load_py_infra
name: test_archivo_inexistente_lanza_error
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_inexistente_lanza_error():
      with pytest.raises(FileNotFoundError):
          dotenv_load("/nonexistent/.env.does.not.exist")


id: dotenv_load_py_infra_t6
function_id: dotenv_load_py_infra
name: test_linea_sin_igual_lanza_value_error
file_path: python/functions/infra/dotenv_load_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_linea_sin_igual_lanza_value_error():
      path = write_env("INVALID_LINE\n")
      try:
          with pytest.raises(ValueError, match="missing '='"):
              dotenv_load(path)
      finally:
          os.unlink(path)


id: email_build_html_py_infra_t0
function_id: email_build_html_py_infra
name: test_construye_mensaje_html_con_campos_correctos
file_path: python/functions/infra/email_build_html_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_construye_mensaje_html_con_campos_correctos():
      msg = email_build_html(
          from_addr="alice@example.com",
          to=["bob@example.com"],
          subject="Hola",
          body_html="<b>Hola Bob</b>",
      )
      assert msg.from_addr == "alice@example.com"
      assert msg.subject == "Hola"
      assert msg.body_html == "<b>Hola Bob</b>"
      assert "bob@example.com" in msg.to


id: email_build_html_py_infra_t1
function_id: email_build_html_py_infra
name: test_convierte_lista_to_a_tupla_inmutable
file_path: python/functions/infra/email_build_html_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_convierte_lista_to_a_tupla_inmutable():
      to_list = ["a@example.com", "b@example.com"]
      msg = email_build_html("f@x.com", to_list, "s", "h")
      assert isinstance(msg.to, tuple)
      # Mutating the original list must not affect the message
      to_list[0] = "mutated@example.com"
      assert msg.to[0] == "a@example.com"


id: email_build_html_py_infra_t2
function_id: email_build_html_py_infra
name: test_body_text_queda_vacio
file_path: python/functions/infra/email_build_html_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_body_text_queda_vacio():
      msg = email_build_html("f@x.com", ["t@x.com"], "s", "<p>html</p>")
      assert msg.body_text == ""


id: email_build_html_py_infra_t3
function_id: email_build_html_py_infra
name: test_mensaje_es_inmutable
file_path: python/functions/infra/email_build_html_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_mensaje_es_inmutable():
      msg = email_build_html("f@x.com", ["t@x.com"], "s", "h")
      try:
          msg.subject = "changed"
          assert False, "Should have raised FrozenInstanceError"
      except Exception:
          pass  # frozen dataclass raises FrozenInstanceError


id: get_logger_py_infra_t0
function_id: get_logger_py_infra
name: test_logger_tiene_dos_handlers
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_logger_tiene_dos_handlers():
      with tempfile.TemporaryDirectory() as log_dir:
          logger = setup_logger(name="test_two_handlers", log_dir=log_dir)
          assert len(logger.handlers) == 2
          # limpiar para no contaminar otros tests
          logger.handlers.clear()


id: get_logger_py_infra_t1
function_id: get_logger_py_infra
name: test_segundo_call_no_duplica_handlers
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_segundo_call_no_duplica_handlers():
      with tempfile.TemporaryDirectory() as log_dir:
          logger1 = setup_logger(name="test_idempotent", log_dir=log_dir)
          handler_count_after_first = len(logger1.handlers)
          logger2 = setup_logger(name="test_idempotent", log_dir=log_dir)
          assert logger1 is logger2
          assert len(logger2.handlers) == handler_count_after_first
          logger1.handlers.clear()


id: get_logger_py_infra_t2
function_id: get_logger_py_infra
name: test_archivo_se_crea_en_log_dir
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_se_crea_en_log_dir():
      with tempfile.TemporaryDirectory() as log_dir:
          logger = setup_logger(name="test_file_created", log_dir=log_dir)
          log_files = [f for f in os.listdir(log_dir) if f.endswith(".log")]
          assert len(log_files) == 1
          logger.handlers.clear()


id: get_logger_py_infra_t3
function_id: get_logger_py_infra
name: test_get_logger_retorna_logger_configurado
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_get_logger_retorna_logger_configurado():
      with tempfile.TemporaryDirectory() as log_dir:
          # Primero configurar para que get_logger encuentre handlers
          setup_logger(name="test_get_logger", log_dir=log_dir)
          logger = get_logger(name="test_get_logger")
          assert len(logger.handlers) == 2
          logger.handlers.clear()


id: get_logger_py_infra_t4
function_id: get_logger_py_infra
name: test_logger_level_es_debug
file_path: python/functions/infra/setup_logger_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_logger_level_es_debug():
      with tempfile.TemporaryDirectory() as log_dir:
          logger = setup_logger(name="test_level_debug", log_dir=log_dir, level=logging.DEBUG)
          assert logger.level == logging.DEBUG
          logger.handlers.clear()


id: normalize_zip_filenames_py_infra_t0
function_id: normalize_zip_filenames_py_infra
name: test_zip_normal
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_zip_normal():
      """ZIP normal extrae correctamente dentro del destino."""
      zip_path = _make_zip({"hello.txt": b"hello world", "subdir/file.py": b"# code"})
      try:
          with tempfile.TemporaryDirectory() as dest:
              safe_extract_zip(zip_path, dest)
              assert os.path.isfile(os.path.join(dest, "hello.txt"))
              assert os.path.isfile(os.path.join(dest, "subdir", "file.py"))
              with open(os.path.join(dest, "hello.txt"), "rb") as f:
                  assert f.read() == b"hello world"
      finally:
          os.unlink(zip_path)


id: normalize_zip_filenames_py_infra_t1
function_id: normalize_zip_filenames_py_infra
name: test_zip_con_path_traversal
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_zip_con_path_traversal():
      """ZIP con path traversal lanza ValueError."""
      zip_path = _make_zip_with_traversal("../../etc/passwd")
      try:
          with tempfile.TemporaryDirectory() as dest:
              raised = False
              try:
                  safe_extract_zip(zip_path, dest)
              except ValueError as e:
                  raised = True
                  assert "Zip Slip" in str(e)
              assert raised, "Expected ValueError for path traversal"
      finally:
          os.unlink(zip_path)


id: normalize_zip_filenames_py_infra_t2
function_id: normalize_zip_filenames_py_infra
name: test_zip_con_paths_absolutos
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_zip_con_paths_absolutos():
      """ZIP con paths absolutos lanza ValueError."""
      zip_path = _make_zip_with_traversal("/etc/passwd")
      try:
          with tempfile.TemporaryDirectory() as dest:
              raised = False
              try:
                  safe_extract_zip(zip_path, dest)
              except ValueError as e:
                  raised = True
                  assert "Zip Slip" in str(e)
              assert raised, "Expected ValueError for absolute path"
      finally:
          os.unlink(zip_path)


id: normalize_zip_filenames_py_infra_t3
function_id: normalize_zip_filenames_py_infra
name: test_normalize_utf8_correctos_no_cambian
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_normalize_utf8_correctos_no_cambian():
      """ZIP con nombres UTF-8 correctos (flag 0x800) no se modifican."""
      buf = io.BytesIO()
      with zipfile.ZipFile(buf, "w") as zipf:
          info = zipfile.ZipInfo("archivo_normal.txt")
          info.flag_bits |= 0x800  # marcar como UTF-8
          zipf.writestr(info, b"content")
      buf.seek(0)
      with zipfile.ZipFile(buf, "r") as zipf:
          original_name = zipf.infolist()[0].filename
          normalize_zip_filenames(zipf)
          assert zipf.infolist()[0].filename == original_name


id: normalize_zip_filenames_py_infra_t4
function_id: normalize_zip_filenames_py_infra
name: test_normalize_cjk_mojibake_repara
file_path: python/functions/infra/safe_extract_zip_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_normalize_cjk_mojibake_repara():
      """ZIP con nombres CJK en mojibake (UTF-8 bytes leidos como CP437) se reparan.

      Simula un ZIP donde los bytes del filename son UTF-8 valido de un nombre CJK,
      pero el flag 0x800 no esta seteado, asi que zipfile los decodifica como CP437
      produciendo mojibake. normalize_zip_filenames debe detectarlo y repararlo.
      """
      cjk_name = "\u6587\u4ef6.txt"  # 文件.txt

      # Construir ZIP con bytes UTF-8 crudos en el campo filename, sin flag 0x800.
      # Python no permite esto via ZipInfo (fuerza 0x800 para non-ASCII), por eso
      # construimos el ZIP manualmente con _make_zip_with_raw_filename.
      utf8_bytes = cjk_name.encode("utf-8")
      zip_bytes = _make_zip_with_raw_filename(utf8_bytes, b"cjk content")

      with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zipf:
          member = zipf.infolist()[0]
          # Sin el flag, zipfile lee los bytes como CP437: debe ser mojibake
          assert not (member.flag_bits & 0x800), "Flag 0x800 no deberia estar seteado"
          assert member.filename != cjk_name, "El nombre aun no debe estar reparado"

          normalize_zip_filenames(zipf)
          repaired = zipf.infolist()[0].filename
          has_cjk = any(
              "\u4e00" <= c <= "\u9fff" or "\u3400" <= c <= "\u4dbf" for c in repaired
          )
          assert has_cjk, f"Esperaba CJK en nombre reparado, got: {repaired!r}"


  if __name__ == "__main__":
      test_zip_normal()
      print("PASS: ZIP normal extrae correctamente dentro del destino")

      test_zip_con_path_traversal()
      print("PASS: ZIP con path traversal lanza ValueError")

      test_zip_con_paths_absolutos()
      print("PASS: ZIP con paths absolutos lanza ValueError")

      test_normalize_utf8_correctos_no_cambian()
      print("PASS: ZIP con nombres UTF-8 correctos no se modifican")

      test_normalize_cjk_mojibake_repara()
      print("PASS: ZIP con nombres CJK mojibake se reparan")

      print("\nAll tests passed.")


id: pdf_add_page_py_infra_t0
function_id: pdf_add_page_py_infra
name: test_anadir_pagina_portrait_a4
file_path: python/functions/infra/pdf_add_page_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_anadir_pagina_portrait_a4():
      doc = pdf_create()
      doc = pdf_add_page(doc, orientation="portrait")
      assert len(doc.pages) == 1
      assert doc.pages[0]["orientation"] == "portrait"
      assert doc.fpdf.page == 1


id: pdf_add_page_py_infra_t1
function_id: pdf_add_page_py_infra
name: test_anadir_pagina_landscape
file_path: python/functions/infra/pdf_add_page_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_anadir_pagina_landscape():
      doc = pdf_create()
      doc = pdf_add_page(doc, orientation="landscape")
      assert len(doc.pages) == 1
      assert doc.pages[0]["orientation"] == "landscape"


id: pdf_add_page_py_infra_t2
function_id: pdf_add_page_py_infra
name: test_pagina_con_dimensiones_personalizadas
file_path: python/functions/infra/pdf_add_page_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pagina_con_dimensiones_personalizadas():
      doc = pdf_create()
      doc = pdf_add_page(doc, width=148.0, height=210.0)  # A5
      assert len(doc.pages) == 1
      # Verificar que se añadio la pagina
      assert doc.fpdf.page == 1


id: pdf_add_table_py_infra_t0
function_id: pdf_add_table_py_infra
name: test_tabla_con_headers_y_datos
file_path: python/functions/infra/pdf_add_table_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_tabla_con_headers_y_datos():
      doc = pdf_create()
      doc = pdf_add_page(doc)
      headers = ["Dominio", "Funciones", "Tipos"]
      rows = [
          ["core", "45", "12"],
          ["infra", "38", "8"],
          ["finance", "22", "6"],
      ]
      doc = pdf_add_table(doc, headers, rows)
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"
      assert len(pdf_bytes) > 200


id: pdf_add_table_py_infra_t1
function_id: pdf_add_table_py_infra
name: test_tabla_con_col_widths_personalizados
file_path: python/functions/infra/pdf_add_table_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_tabla_con_col_widths_personalizados():
      doc = pdf_create()
      doc = pdf_add_page(doc)
      headers = ["Nombre", "Valor"]
      rows = [["alpha", "100"], ["beta", "200"]]
      doc = pdf_add_table(doc, headers, rows, col_widths=[100.0, 70.0])
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"


id: pdf_add_table_py_infra_t2
function_id: pdf_add_table_py_infra
name: test_tabla_sin_borde
file_path: python/functions/infra/pdf_add_table_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_tabla_sin_borde():
      doc = pdf_create()
      doc = pdf_add_page(doc)
      headers = ["A", "B"]
      rows = [["1", "2"]]
      doc = pdf_add_table(doc, headers, rows, border=0)
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"


id: pdf_add_text_py_infra_t0
function_id: pdf_add_text_py_infra
name: test_texto_simple_con_estilo_defecto
file_path: python/functions/infra/pdf_add_text_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_simple_con_estilo_defecto():
      doc = pdf_create(title="Test")
      doc = pdf_add_page(doc)
      doc = pdf_add_text(doc, "Hola mundo")
      assert isinstance(doc, PDFDoc)
      # El PDF debe ser valido
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"


id: pdf_add_text_py_infra_t1
function_id: pdf_add_text_py_infra
name: test_texto_con_estilo_personalizado_bold_centrado
file_path: python/functions/infra/pdf_add_text_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_con_estilo_personalizado_bold_centrado():
      doc = pdf_create()
      doc = pdf_add_page(doc)
      style = PDFStyle(font_family="Helvetica-Bold", font_size=18.0, alignment="center", color="#FF0000")
      doc = pdf_add_text(doc, "Titulo en Rojo", style=style)
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"


id: pdf_add_text_py_infra_t2
function_id: pdf_add_text_py_infra
name: test_texto_multilinea_con_saltos
file_path: python/functions/infra/pdf_add_text_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_texto_multilinea_con_saltos():
      doc = pdf_create()
      doc = pdf_add_page(doc)
      doc = pdf_add_text(doc, "Linea 1\nLinea 2\nLinea 3")
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"
      assert len(pdf_bytes) > 100


id: pdf_create_py_infra_t0
function_id: pdf_create_py_infra
name: test_crear_documento_con_titulo_y_autor
file_path: python/functions/infra/pdf_create_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_crear_documento_con_titulo_y_autor():
      doc = pdf_create(title="Test Title", author="Test Author")
      assert isinstance(doc, PDFDoc)
      assert doc.title == "Test Title"
      assert doc.author == "Test Author"
      assert doc.fpdf is not None


id: pdf_create_py_infra_t1
function_id: pdf_create_py_infra
name: test_margenes_personalizados_aplicados
file_path: python/functions/infra/pdf_create_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_margenes_personalizados_aplicados():
      doc = pdf_create(margin_left=15.0, margin_right=15.0, margin_top=25.0, margin_bottom=25.0)
      assert doc.margin_left == 15.0
      assert doc.margin_right == 15.0
      assert doc.margin_top == 25.0
      assert doc.margin_bottom == 25.0
      # Verificar que fpdf tiene los margenes aplicados
      assert abs(doc.fpdf.l_margin - 15.0) < 0.1
      assert abs(doc.fpdf.r_margin - 15.0) < 0.1


id: pdf_merge_py_infra_t0
function_id: pdf_merge_py_infra
name: test_fusionar_dos_pdfs
file_path: python/functions/infra/pdf_merge_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_fusionar_dos_pdfs():
      with tempfile.TemporaryDirectory() as tmpdir:
          p1 = _create_pdf(os.path.join(tmpdir, "a.pdf"), "Documento A")
          p2 = _create_pdf(os.path.join(tmpdir, "b.pdf"), "Documento B")
          out = os.path.join(tmpdir, "merged.pdf")
          result = pdf_merge([p1, p2], out)
          assert result == out
          assert os.path.exists(out)
          # Verificar que el PDF combinado es valido y mas grande que cada parte
          with open(out, "rb") as f:
              content = f.read()
          assert content[:4] == b"%PDF"


id: pdf_merge_py_infra_t1
function_id: pdf_merge_py_infra
name: test_error_si_archivo_no_existe
file_path: python/functions/infra/pdf_merge_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_error_si_archivo_no_existe():
      with tempfile.TemporaryDirectory() as tmpdir:
          out = os.path.join(tmpdir, "merged.pdf")
          with pytest.raises(FileNotFoundError):
              pdf_merge(["/no/existe.pdf"], out)


id: pdf_merge_py_infra_t2
function_id: pdf_merge_py_infra
name: test_error_si_lista_vacia
file_path: python/functions/infra/pdf_merge_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_error_si_lista_vacia():
      with tempfile.TemporaryDirectory() as tmpdir:
          out = os.path.join(tmpdir, "merged.pdf")
          with pytest.raises(ValueError):
              pdf_merge([], out)


id: pdf_save_py_infra_t0
function_id: pdf_save_py_infra
name: test_guardar_pdf_a_archivo
file_path: python/functions/infra/pdf_save_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_guardar_pdf_a_archivo():
      doc = _make_simple_doc()
      with tempfile.TemporaryDirectory() as tmpdir:
          path = os.path.join(tmpdir, "test.pdf")
          result = pdf_save(doc, path)
          assert result == path
          assert os.path.exists(path)
          assert os.path.getsize(path) > 0


id: pdf_save_py_infra_t1
function_id: pdf_save_py_infra
name: test_retornar_bytes_del_pdf
file_path: python/functions/infra/pdf_save_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_retornar_bytes_del_pdf():
      doc = _make_simple_doc()
      result = pdf_save(doc)
      assert isinstance(result, bytes)
      assert len(result) > 0


id: pdf_save_py_infra_t2
function_id: pdf_save_py_infra
name: test_pdf_valido_comienza_con_pdf
file_path: python/functions/infra/pdf_save_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_pdf_valido_comienza_con_pdf():
      doc = _make_simple_doc()
      pdf_bytes = pdf_save(doc)
      assert pdf_bytes[:4] == b"%PDF"


id: read_file_with_encoding_py_infra_t0
function_id: read_file_with_encoding_py_infra
name: test_archivo_utf8
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_utf8():
      texto = "Hola mundo con acentos: áéíóú"
      path = _write_bytes(texto.encode("utf-8"))
      try:
          result = read_file_with_encoding(path)
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t1
function_id: read_file_with_encoding_py_infra
name: test_archivo_utf8_con_bom
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_utf8_con_bom():
      texto = "Contenido con BOM"
      path = _write_bytes(texto.encode("utf-8-sig"))
      try:
          # Usando utf-8-sig explicitamente para que el BOM sea eliminado
          result = read_file_with_encoding(path, encodings=["utf-8-sig"])
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t2
function_id: read_file_with_encoding_py_infra
name: test_archivo_latin1
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_latin1():
      texto = "Texto en Latin-1: café"
      path = _write_bytes(texto.encode("latin-1"))
      try:
          result = read_file_with_encoding(path)
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t3
function_id: read_file_with_encoding_py_infra
name: test_archivo_binario_falla
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_binario_falla():
      # Bytes que no son validos en ningun encoding de texto comun
      path = _write_bytes(bytes([0x80, 0x81, 0x82, 0x83, 0xFF, 0xFE, 0x00, 0x01]))
      try:
          with pytest.raises(ValueError, match="Unable to decode file"):
              # Forzar solo encodings estrictos para que falle con binario puro
              read_file_with_encoding(path, encodings=["utf-8", "utf-8-sig"])
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t4
function_id: read_file_with_encoding_py_infra
name: test_encodings_personalizados
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_encodings_personalizados():
      texto = "Windows text: Ñoño"
      path = _write_bytes(texto.encode("cp1252"))
      try:
          result = read_file_with_encoding(path, encodings=["cp1252"])
          assert result == texto
      finally:
          os.unlink(path)


id: read_file_with_encoding_py_infra_t5
function_id: read_file_with_encoding_py_infra
name: test_archivo_no_existe
file_path: python/functions/infra/read_file_with_encoding_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_no_existe():
      with pytest.raises(FileNotFoundError):
          read_file_with_encoding("/tmp/archivo_que_no_existe_12345.txt")


id: scan_directory_py_infra_t0
function_id: scan_directory_py_infra
name: test_directorio_con_mezcla_de_archivos
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_directorio_con_mezcla_de_archivos():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "report.pdf": "pdf content",
              "notes.md": "# Notes",
              "image.png": "png content",
              "data.csv": "a,b,c",
          })

          result = scan_directory(str(root), supported_extensions={".pdf", ".md"})

          rel_paths = [f.rel_path for f in result.processable]
          assert "notes.md" in rel_paths, f"notes.md no en processable: {rel_paths}"
          assert "report.pdf" in rel_paths, f"report.pdf no en processable: {rel_paths}"

          unsup_paths = [f.rel_path for f in result.unsupported]
          assert "image.png" in unsup_paths, f"image.png no en unsupported: {unsup_paths}"
          assert "data.csv" in unsup_paths, f"data.csv no en unsupported: {unsup_paths}"

          assert all(f.classification == "processable" for f in result.processable)
          assert all(f.classification == "unsupported" for f in result.unsupported)


  # ---------------------------------------------------------------------------
  # Test: directorio con dot files
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t1
function_id: scan_directory_py_infra
name: test_directorio_con_dot_files
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_directorio_con_dot_files():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "visible.txt": "content",
              ".hidden": "hidden content",
              ".env": "SECRET=x",
          })

          result = scan_directory(str(root))

          all_paths = [f.rel_path for f in result.processable + result.unsupported]
          assert ".hidden" not in all_paths, f".hidden no deberia aparecer: {all_paths}"
          assert ".env" not in all_paths, f".env no deberia aparecer: {all_paths}"
          assert "visible.txt" in all_paths, f"visible.txt deberia aparecer: {all_paths}"

          skipped_paths = " ".join(result.skipped)
          assert ".hidden" in skipped_paths or ".env" in skipped_paths


  # ---------------------------------------------------------------------------
  # Test: directorio con subdirs ignorados
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t2
function_id: scan_directory_py_infra
name: test_directorio_con_subdirs_ignorados
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_directorio_con_subdirs_ignorados():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "main.py": "print('hello')",
              "__pycache__/module.pyc": "bytecode",
              "node_modules/lib/index.js": "// js",
              ".git/config": "[core]",
              "src/utils.py": "def f(): pass",
          })

          result = scan_directory(str(root))

          all_rels = [f.rel_path for f in result.processable + result.unsupported]

          # Archivos dentro de dirs ignorados no deben aparecer
          assert not any("__pycache__" in r for r in all_rels), \
              f"__pycache__ no deberia estar en resultados: {all_rels}"
          assert not any("node_modules" in r for r in all_rels), \
              f"node_modules no deberia estar en resultados: {all_rels}"
          assert not any(".git" in r for r in all_rels), \
              f".git no deberia estar en resultados: {all_rels}"

          # Archivos fuera de dirs ignorados si deben aparecer
          assert "main.py" in all_rels, f"main.py deberia estar: {all_rels}"
          assert "src/utils.py" in all_rels, f"src/utils.py deberia estar: {all_rels}"


  # ---------------------------------------------------------------------------
  # Test: filtros include/exclude
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t3
function_id: scan_directory_py_infra
name: test_filtros_include_exclude
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_filtros_include_exclude():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "report.pdf": "content",
              "notes.md": "notes",
              "image.png": "image",
              "drafts/draft.md": "draft",
              "temp.tmp": "tmp",
          })

          # Solo incluir .pdf y .md
          result = scan_directory(str(root), include="*.pdf,*.md")
          all_rels = [f.rel_path for f in result.processable + result.unsupported]
          assert "image.png" not in all_rels, f"image.png no deberia incluirse: {all_rels}"
          assert "temp.tmp" not in all_rels, f"temp.tmp no deberia incluirse: {all_rels}"
          assert "report.pdf" in all_rels
          assert "notes.md" in all_rels

          # Excluir path prefix drafts/ y extension .tmp
          result2 = scan_directory(str(root), exclude="drafts/,*.tmp")
          all_rels2 = [f.rel_path for f in result2.processable + result2.unsupported]
          assert "drafts/draft.md" not in all_rels2, \
              f"drafts/draft.md no deberia incluirse: {all_rels2}"
          assert "temp.tmp" not in all_rels2, f"temp.tmp no deberia incluirse: {all_rels2}"
          assert "report.pdf" in all_rels2


  # ---------------------------------------------------------------------------
  # Test: modo strict
  # ---------------------------------------------------------------------------


id: scan_directory_py_infra_t4
function_id: scan_directory_py_infra
name: test_modo_strict
file_path: python/functions/infra/scan_directory_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_modo_strict():
      with tempfile.TemporaryDirectory() as tmp:
          root = Path(tmp)
          _make_tree(root, {
              "doc.pdf": "content",
              "image.png": "image",
          })

          # strict=False no lanza error aunque haya unsupported
          result = scan_directory(str(root), supported_extensions={".pdf"}, strict=False)
          assert len(result.unsupported) == 1

          # strict=True lanza ValueError
          raised = False
          try:
              scan_directory(str(root), supported_extensions={".pdf"}, strict=True)
          except ValueError:
              raised = True
          assert raised, "strict=True deberia lanzar ValueError cuando hay unsupported"


  if __name__ == "__main__":
      test_directorio_con_mezcla_de_archivos()
      print("PASS: directorio con mezcla de archivos")

      test_directorio_con_dot_files()
      print("PASS: directorio con dot files")

      test_directorio_con_subdirs_ignorados()
      print("PASS: directorio con subdirs ignorados")

      test_filtros_include_exclude()
      print("PASS: filtros include/exclude")

      test_modo_strict()
      print("PASS: modo strict")

      print("\nAll tests passed.")


id: smtp_send_py_infra_t0
function_id: smtp_send_py_infra
name: test_envia_texto_plano_via_mock_smtpd
file_path: python/functions/infra/smtp_send_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_envia_texto_plano_via_mock_smtpd():
      srv = _MockSMTPServer().start()
      try:
          smtp_send(
              srv.cfg(),
              from_addr="alice@example.com",
              to=["bob@example.com"],
              subject="Test plain",
              body_text="Hello Bob",
          )
      finally:
          srv.stop()


id: smtp_send_py_infra_t1
function_id: smtp_send_py_infra
name: test_envia_html_via_mock_smtpd
file_path: python/functions/infra/smtp_send_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_envia_html_via_mock_smtpd():
      srv = _MockSMTPServer().start()
      try:
          smtp_send(
              srv.cfg(),
              from_addr="alice@example.com",
              to=["bob@example.com"],
              subject="Test HTML",
              body_html="<b>Hello</b>",
          )
      finally:
          srv.stop()


id: smtp_send_py_infra_t2
function_id: smtp_send_py_infra
name: test_envia_con_adjunto_via_mock_smtpd
file_path: python/functions/infra/smtp_send_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_envia_con_adjunto_via_mock_smtpd():
      srv = _MockSMTPServer().start()
      try:
          att = EmailAttachmentPy(filename="f.txt", content_type="text/plain", data=b"file data")
          smtp_send(
              srv.cfg(),
              from_addr="alice@example.com",
              to=["bob@example.com"],
              subject="Test attachment",
              body_text="See attachment",
              attachments=[att],
          )
      finally:
          srv.stop()


id: smtp_send_py_infra_t3
function_id: smtp_send_py_infra
name: test_error_si_host_no_existe
file_path: python/functions/infra/smtp_send_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_error_si_host_no_existe():
      port = _find_free_port()
      # Port is free but nothing listens — should get connection refused
      cfg = SMTPConfigPy(host="127.0.0.1", port=port, tls_mode="")
      try:
          smtp_send(cfg, "a@x.com", ["b@x.com"], "sub", body_text="hi")
          assert False, "Expected RuntimeError"
      except RuntimeError:
          pass


id: metabase_mbql_validate_py_core_t0
function_id: metabase_mbql_validate_py_core
name: test_valid_dq_returns_no_errors
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_valid_dq_returns_no_errors():
      """DQ valido retorna lista vacia."""
      errors = metabase_mbql_validate(VALID_DQ)
      assert errors == [], f"Esperaba 0 errores, got: {errors}"


  # ---------------------------------------------------------------------------
  # Check 1: UUID duplicado
  # ---------------------------------------------------------------------------


id: metabase_mbql_validate_py_core_t1
function_id: metabase_mbql_validate_py_core
name: test_duplicate_uuid_detected
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_duplicate_uuid_detected():
      """UUID repetido en el arbol MBQL genera error."""
      dq = {
          "stages": [
              {
                  "lib/type": "mbql.stage/mbql",
                  "expressions": [
                      [
                          "-",
                          {"lib/uuid": "dup-uuid-001", "lib/expression-name": "ExprA"},
                          ["field", {"base-type": "type/Decimal", "lib/uuid": "dup-uuid-001"}, "CampoX"],
                          ["field", {"base-type": "type/Decimal", "lib/uuid": "unique-uuid-002"}, "CampoY"],
                      ]
                  ],
              }
          ]
      }
      errors = metabase_mbql_validate(dq)
      assert any("dup-uuid-001" in e for e in errors), f"Esperaba error de UUID duplicado, got: {errors}"


  # ---------------------------------------------------------------------------
  # Check 2: Stage mixing — expressions referencian slots de aggregation
  # ---------------------------------------------------------------------------


id: metabase_mbql_validate_py_core_t2
function_id: metabase_mbql_validate_py_core
name: test_stage_mixing_detected
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_stage_mixing_detected():
      """Stage con aggregations y expressions que referencian slots generados."""
      dq = {
          "stages": [
              {
                  "lib/type": "mbql.stage/mbql",
                  "aggregation": [
                      ["sum", {"lib/uuid": "agg1"}, ["field", {"base-type": "type/Decimal", "lib/uuid": "f1"}, "Precio"]]
                  ],
                  "expressions": [
                      [
                          "*",
                          {"lib/uuid": "expr2", "lib/expression-name": "DobleSum"},
                          # field sin base-type y nombre 'sum' = slot ref de aggregation
                          ["field", {"lib/uuid": "ref-slot"}, "sum"],
                          2,
                      ]
                  ],
              }
          ]
      }
      errors = metabase_mbql_validate(dq)
      assert any("mezcla" in e.lower() or "mixing" in e.lower() or "stage" in e.lower() for e in errors), \
          f"Esperaba error de stage mixing, got: {errors}"


  # ---------------------------------------------------------------------------
  # Check 3: Slot ref rota — sum_99 inexistente
  # ---------------------------------------------------------------------------


id: metabase_mbql_validate_py_core_t3
function_id: metabase_mbql_validate_py_core
name: test_broken_slot_ref_detected
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_broken_slot_ref_detected():
      """Expression que referencia sum_99 cuando solo hay 1 sum genera error."""
      dq = {
          "stages": [
              {
                  "lib/type": "mbql.stage/mbql",
                  "aggregation": [
                      ["sum", {"lib/uuid": "agg-s1"}, ["field", {"base-type": "type/Decimal", "lib/uuid": "f-s1"}, "Precio"]]
                  ],
                  "expressions": [
                      [
                          "+",
                          {"lib/uuid": "expr-broken", "lib/expression-name": "SumRota"},
                          # sum_99 no existe (solo hay 1 sum = sum_0)
                          ["field", {"lib/uuid": "ref-broken"}, "sum_99"],
                          1,
                      ]
                  ],
              }
          ]
      }
      errors = metabase_mbql_validate(dq)
      assert any("sum_99" in e for e in errors), f"Esperaba error de slot ref roto, got: {errors}"


  # ---------------------------------------------------------------------------
  # Check 4: Case con estructura invalida
  # ---------------------------------------------------------------------------


id: metabase_mbql_validate_py_core_t4
function_id: metabase_mbql_validate_py_core
name: test_case_malformed_cases_not_pairs
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_case_malformed_cases_not_pairs():
      """Case con casos que no son pares [cond, result] genera error."""
      dq = {
          "stages": [
              {
                  "lib/type": "mbql.stage/mbql",
                  "expressions": [
                      [
                          "case",
                          {"lib/uuid": "case-bad", "lib/expression-name": "CaseMalo"},
                          # lista con un solo elemento (no par)
                          [["solo_elemento"]],
                      ]
                  ],
              }
          ]
      }
      errors = metabase_mbql_validate(dq)
      assert any("case" in e.lower() for e in errors), f"Esperaba error de case structure, got: {errors}"


  # ---------------------------------------------------------------------------
  # Check 5: Name collision en expressions
  # ---------------------------------------------------------------------------


id: metabase_mbql_validate_py_core_t5
function_id: metabase_mbql_validate_py_core
name: test_name_collision_detected
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_name_collision_detected():
      """Dos expressions con mismo lib/expression-name en la misma stage generan error."""
      dq = {
          "stages": [
              {
                  "lib/type": "mbql.stage/mbql",
                  "expressions": [
                      ["-", {"lib/uuid": "e1", "lib/expression-name": "MiCalculo"},
                       ["field", {"base-type": "type/Decimal", "lib/uuid": "f1"}, "A"],
                       ["field", {"base-type": "type/Decimal", "lib/uuid": "f2"}, "B"]],
                      ["+", {"lib/uuid": "e2", "lib/expression-name": "MiCalculo"},
                       ["field", {"base-type": "type/Decimal", "lib/uuid": "f3"}, "C"],
                       1],
                  ],
              }
          ]
      }
      errors = metabase_mbql_validate(dq)
      assert any("MiCalculo" in e for e in errors), f"Esperaba error de name collision, got: {errors}"


  # ---------------------------------------------------------------------------
  # Check: stages ausente
  # ---------------------------------------------------------------------------


id: metabase_mbql_validate_py_core_t6
function_id: metabase_mbql_validate_py_core
name: test_missing_stages_returns_error
file_path: python/functions/metabase/test_metabase_mbql_validate.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_missing_stages_returns_error():
      """dataset_query sin 'stages' devuelve error de estructura."""
      errors = metabase_mbql_validate({"database": 1})
      assert any("stages" in e.lower() for e in errors), f"Esperaba error de stages ausente, got: {errors}"


id: metabase_validate_card_payload_py_infra_t0
function_id: metabase_validate_card_payload_py_infra
name: test_card_valido_retorna_lista_vacia
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_valido_retorna_lista_vacia():
      issues = metabase_validate_card_payload(_base_card())
      assert issues == [], f"Se esperaba lista vacia, got: {issues}"


id: metabase_validate_card_payload_py_infra_t1
function_id: metabase_validate_card_payload_py_infra
name: test_card_display_invalido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_display_invalido():
      payload = _base_card()
      payload["display"] = "foobar"
      issues = metabase_validate_card_payload(payload)
      assert any("display" in i and "foobar" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t2
function_id: metabase_validate_card_payload_py_infra
name: test_card_display_ausente
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_display_ausente():
      payload = _base_card()
      del payload["display"]
      issues = metabase_validate_card_payload(payload)
      assert any("display" in i and "ausente" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t3
function_id: metabase_validate_card_payload_py_infra
name: test_card_name_ausente
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_name_ausente():
      payload = _base_card()
      del payload["name"]
      issues = metabase_validate_card_payload(payload)
      assert any("name" in i and "ausente" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t4
function_id: metabase_validate_card_payload_py_infra
name: test_card_name_vacio
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_name_vacio():
      payload = _base_card()
      payload["name"] = "   "
      issues = metabase_validate_card_payload(payload)
      assert any("name" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t5
function_id: metabase_validate_card_payload_py_infra
name: test_card_dataset_query_ausente
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_dataset_query_ausente():
      payload = _base_card()
      del payload["dataset_query"]
      issues = metabase_validate_card_payload(payload)
      assert any("dataset_query" in i and "ausente" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t6
function_id: metabase_validate_card_payload_py_infra
name: test_card_dataset_query_sin_database
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_dataset_query_sin_database():
      payload = _base_card()
      del payload["dataset_query"]["database"]
      issues = metabase_validate_card_payload(payload)
      assert any("database" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t7
function_id: metabase_validate_card_payload_py_infra
name: test_card_nativa_sin_sql
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_nativa_sin_sql():
      payload = _base_card()
      payload["dataset_query"]["native"]["query"] = ""
      issues = metabase_validate_card_payload(payload)
      assert any("SQL" in i or "native" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t8
function_id: metabase_validate_card_payload_py_infra
name: test_card_nativa_mbql5
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_nativa_mbql5():
      """Acepta SQL en stages[0].native (formato MBQL5)."""
      payload = {
          "name": "MBQL5 card",
          "display": "table",
          "dataset_query": {
              "database": 2,
              "stages": [{"native": "SELECT 1"}],
          },
      }
      issues = metabase_validate_card_payload(payload)
      assert issues == [], f"Se esperaba lista vacia, got: {issues}"


id: metabase_validate_card_payload_py_infra_t9
function_id: metabase_validate_card_payload_py_infra
name: test_card_type_invalido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_type_invalido():
      payload = _base_card()
      payload["type"] = "unknown_type"
      issues = metabase_validate_card_payload(payload)
      assert any("type" in i and "unknown_type" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t10
function_id: metabase_validate_card_payload_py_infra
name: test_card_type_valido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_type_valido():
      for t in ("question", "model", "metric"):
          payload = _base_card()
          payload["type"] = t
          issues = metabase_validate_card_payload(payload)
          assert issues == [], f"type={t!r} no deberia dar issues: {issues}"


id: metabase_validate_card_payload_py_infra_t11
function_id: metabase_validate_card_payload_py_infra
name: test_card_visualization_settings_no_dict
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_visualization_settings_no_dict():
      payload = _base_card()
      payload["visualization_settings"] = "no es un dict"
      issues = metabase_validate_card_payload(payload)
      assert any("visualization_settings" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t12
function_id: metabase_validate_card_payload_py_infra
name: test_card_parameters_no_list
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_parameters_no_list():
      payload = _base_card()
      payload["parameters"] = {"not": "a list"}
      issues = metabase_validate_card_payload(payload)
      assert any("parameters" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t13
function_id: metabase_validate_card_payload_py_infra
name: test_card_archived_no_bool
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_archived_no_bool():
      payload = _base_card()
      payload["archived"] = "yes"
      issues = metabase_validate_card_payload(payload)
      assert any("archived" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t14
function_id: metabase_validate_card_payload_py_infra
name: test_card_acumula_multiples_errores
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_card_acumula_multiples_errores():
      """Un payload con varios errores debe retornar todos los issues, no solo el primero."""
      payload = {
          "display": "invalid_display",
          "dataset_query": "not a dict",
          "archived": "yes",
      }
      issues = metabase_validate_card_payload(payload)
      assert len(issues) >= 3, f"Se esperaban >= 3 issues, got {len(issues)}: {issues}"


  # ---------------------------------------------------------------------------
  # metabase_validate_dashboard_payload
  # ---------------------------------------------------------------------------

  def _base_dashboard() -> dict:
      return {"name": "My Dashboard"}


id: metabase_validate_card_payload_py_infra_t15
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_valido_sin_dashcards
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_valido_sin_dashcards():
      issues = metabase_validate_dashboard_payload(_base_dashboard(), known_card_ids=set())
      assert issues == [], issues


id: metabase_validate_card_payload_py_infra_t16
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_valido_con_dashcards
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_valido_con_dashcards():
      payload = {
          "name": "KPIs",
          "dashcards": [
              {"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 4},
              {"card_id": 2, "row": 0, "col": 6, "size_x": 6, "size_y": 4},
          ],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2})
      assert issues == [], issues


id: metabase_validate_card_payload_py_infra_t17
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_card_id_desconocido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_card_id_desconocido():
      payload = {
          "name": "Dashboard",
          "dashcards": [{"card_id": 999, "row": 0, "col": 0, "size_x": 6, "size_y": 4}],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2})
      assert any("999" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t18
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_card_virtual_null_permitido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_card_virtual_null_permitido():
      """card_id null = dashcard virtual (texto/heading), siempre permitido."""
      payload = {
          "name": "Dashboard",
          "dashcards": [{"card_id": None, "row": 0, "col": 0, "size_x": 6, "size_y": 2}],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids=set())
      assert issues == [], issues


id: metabase_validate_card_payload_py_infra_t19
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_dashcards_solapadas
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_dashcards_solapadas():
      payload = {
          "name": "Dashboard",
          "dashcards": [
              {"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 4},
              {"card_id": 2, "row": 2, "col": 2, "size_x": 4, "size_y": 4},
          ],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2})
      assert any("solapan" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t20
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_dashcards_adyacentes_no_solapan
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_dashcards_adyacentes_no_solapan():
      """Dos dashcards que se tocan en el borde NO solapan."""
      payload = {
          "name": "Dashboard",
          "dashcards": [
              {"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 4},
              {"card_id": 2, "row": 0, "col": 6, "size_x": 6, "size_y": 4},
          ],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2})
      assert not any("solapan" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t21
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_col_fuera_de_bounds
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_col_fuera_de_bounds():
      payload = {
          "name": "Dashboard",
          "dashcards": [{"card_id": 1, "row": 0, "col": 25, "size_x": 1, "size_y": 1}],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1})
      assert any("col" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t22
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_col_mas_size_x_excede_grid
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_col_mas_size_x_excede_grid():
      payload = {
          "name": "Dashboard",
          "dashcards": [{"card_id": 1, "row": 0, "col": 20, "size_x": 6, "size_y": 4}],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1})
      assert any("24" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t23
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_size_y_fuera_de_bounds
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_size_y_fuera_de_bounds():
      payload = {
          "name": "Dashboard",
          "dashcards": [{"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 0}],
      }
      issues = metabase_validate_dashboard_payload(payload, known_card_ids={1})
      assert any("size_y" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t24
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_name_ausente
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_name_ausente():
      issues = metabase_validate_dashboard_payload({}, known_card_ids=set())
      assert any("name" in i and "ausente" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t25
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_tabs_invalidos
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_tabs_invalidos():
      payload = {"name": "Dashboard", "tabs": [{"name": "Tab1"}]}  # falta id
      issues = metabase_validate_dashboard_payload(payload, known_card_ids=set())
      assert any("id" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t26
function_id: metabase_validate_card_payload_py_infra
name: test_dashboard_parameters_no_list
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_dashboard_parameters_no_list():
      payload = {"name": "Dashboard", "parameters": "not a list"}
      issues = metabase_validate_dashboard_payload(payload, known_card_ids=set())
      assert any("parameters" in i for i in issues), issues


  # ---------------------------------------------------------------------------
  # metabase_validate_document_payload
  # ---------------------------------------------------------------------------

  from metabase.validation import metabase_validate_document_payload


  def _base_doc(content):
      return {"name": "Notas", "document": {"type": "doc", "content": content}}


id: metabase_validate_card_payload_py_infra_t27
function_id: metabase_validate_card_payload_py_infra
name: test_document_valido_minimo
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_valido_minimo():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "paragraph", "content": [{"type": "text", "text": "hola"}]}
      ]))
      assert issues == [], issues


id: metabase_validate_card_payload_py_infra_t28
function_id: metabase_validate_card_payload_py_infra
name: test_document_name_ausente
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_name_ausente():
      issues = metabase_validate_document_payload({"document": {"type": "doc", "content": []}})
      assert any("name" in i and "ausente" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t29
function_id: metabase_validate_card_payload_py_infra
name: test_document_nodo_desconocido_callout
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_nodo_desconocido_callout():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "callout", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "x"}]}]}
      ]))
      assert any("callout" in i and "no soportado" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t30
function_id: metabase_validate_card_payload_py_infra
name: test_document_nodo_desconocido_taskList
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_nodo_desconocido_taskList():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "taskList", "content": []}
      ]))
      assert any("taskList" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t31
function_id: metabase_validate_card_payload_py_infra
name: test_document_mark_desconocido_underline
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_mark_desconocido_underline():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "paragraph", "content": [
              {"type": "text", "marks": [{"type": "underline"}], "text": "x"}
          ]}
      ]))
      assert any("underline" in i and "no soportado" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t32
function_id: metabase_validate_card_payload_py_infra
name: test_document_heading_level_invalido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_heading_level_invalido():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "heading", "attrs": {"level": 9}, "content": [{"type": "text", "text": "x"}]}
      ]))
      assert any("level" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t33
function_id: metabase_validate_card_payload_py_infra
name: test_document_cardEmbed_sin_id_ni_slug
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_cardEmbed_sin_id_ni_slug():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "cardEmbed", "attrs": {}}
      ]))
      assert any("cardEmbed" in i and "id" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t34
function_id: metabase_validate_card_payload_py_infra
name: test_document_cardEmbed_con_id_valido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_cardEmbed_con_id_valido():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "cardEmbed", "attrs": {"id": 42}}
      ]))
      assert issues == [], issues


id: metabase_validate_card_payload_py_infra_t35
function_id: metabase_validate_card_payload_py_infra
name: test_document_cardEmbed_slug_desconocido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_cardEmbed_slug_desconocido():
      issues = metabase_validate_document_payload(
          _base_doc([{"type": "cardEmbed", "attrs": {"card": "inventado"}}]),
          known_card_slugs={"real_one", "real_two"},
      )
      assert any("inventado" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t36
function_id: metabase_validate_card_payload_py_infra
name: test_document_flexContainer_demasiados_hijos
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_flexContainer_demasiados_hijos():
      children = [{"type": "cardEmbed", "attrs": {"id": 1}} for _ in range(4)]
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "flexContainer", "attrs": {"columnWidths": [25, 25, 25, 25]}, "content": children}
      ]))
      assert any("1-3" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t37
function_id: metabase_validate_card_payload_py_infra
name: test_document_flexContainer_hijo_invalido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_flexContainer_hijo_invalido():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "flexContainer", "attrs": {"columnWidths": [100]}, "content": [
              {"type": "paragraph", "content": [{"type": "text", "text": "x"}]}
          ]}
      ]))
      assert any("flexContainer solo acepta" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t38
function_id: metabase_validate_card_payload_py_infra
name: test_document_flexContainer_columnWidths_mismatch
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_flexContainer_columnWidths_mismatch():
      issues = metabase_validate_document_payload(_base_doc([
          {"type": "flexContainer", "attrs": {"columnWidths": [50, 50]}, "content": [
              {"type": "cardEmbed", "attrs": {"id": 1}}
          ]}
      ]))
      assert any("columnWidths" in i for i in issues), issues


id: metabase_validate_card_payload_py_infra_t39
function_id: metabase_validate_card_payload_py_infra
name: test_document_vacio_string_es_valido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_vacio_string_es_valido():
      issues = metabase_validate_document_payload({"name": "vacio", "document": ""})
      assert issues == [], issues


id: metabase_validate_card_payload_py_infra_t40
function_id: metabase_validate_card_payload_py_infra
name: test_document_kitchen_sink_valido
file_path: python/functions/metabase/validation_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_document_kitchen_sink_valido():
      """Simula el kitchen_sink real y debe pasar sin issues."""
      content = [
          {"type": "heading", "attrs": {"level": 1}, "content": [{"type": "text", "text": "T"}]},
          {"type": "paragraph", "content": [
              {"type": "text", "text": "a "},
              {"type": "text", "marks": [{"type": "bold"}], "text": "b"},
              {"type": "text", "marks": [{"type": "link", "attrs": {"href": "https://x"}}], "text": "l"},
          ]},
          {"type": "bulletList", "content": [
              {"type": "listItem", "content": [
                  {"type": "paragraph", "content": [{"type": "text", "text": "i"}]}
              ]}
          ]},
          {"type": "blockquote", "content": [
              {"type": "paragraph", "content": [{"type": "text", "text": "q"}]}
          ]},
          {"type": "codeBlock", "attrs": {"language": "py"}, "content": [{"type": "text", "text": "x=1"}]},
          {"type": "horizontalRule"},
          {"type": "cardEmbed", "attrs": {"id": 1}},
          {"type": "flexContainer", "attrs": {"columnWidths": [50, 50]}, "content": [
              {"type": "cardEmbed", "attrs": {"id": 1}},
              {"type": "cardEmbed", "attrs": {"id": 2}},
          ]},
      ]
      issues = metabase_validate_document_payload(_base_doc(content))
      assert issues == [], issues


id: extraction_pipeline_py_pipelines_t0
function_id: extraction_pipeline_py_pipelines
name: test_documento_con_entidades_y_relaciones
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_documento_con_entidades_y_relaciones():
      """documento con entidades y relaciones retorna ExtractionResult completo"""
      text = (
          "John Smith, a US national, operates the domain evil-corp.com. "
          "He was identified as the main administrator of the infrastructure."
      )
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_with_entities,
              chunk_size=500,
              chunk_overlap=50,
              confidence_threshold=0.5,
              dedup_threshold=0.85,
          )
          assert result is not None
          assert len(result.entities) >= 1
          assert result.stats.total_chunks >= 1
          assert result.stats.total_chars > 0
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t1
function_id: extraction_pipeline_py_pipelines
name: test_documento_vacio
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_documento_vacio():
      """documento vacio retorna ExtractionResult con listas vacias"""
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write("")
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_empty,
          )
          assert result is not None
          assert result.entities == []
          assert result.relations == []
          assert result.stats.total_chunks == 0
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t2
function_id: extraction_pipeline_py_pipelines
name: test_documento_sin_entidades_detectables
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_documento_sin_entidades_detectables():
      """documento sin entidades detectables retorna listas vacias"""
      text = "The weather is nice today. The sun shines brightly over the mountains."
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_empty,
              confidence_threshold=0.5,
          )
          assert result is not None
          assert result.entities == []
          assert result.relations == []
          assert result.stats.raw_entities_count == 0
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t3
function_id: extraction_pipeline_py_pipelines
name: test_archivo_no_encontrado_lanza_filenotfounderror
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_archivo_no_encontrado_lanza_filenotfounderror():
      """archivo no encontrado lanza FileNotFoundError"""
      import pytest
      with pytest.raises(FileNotFoundError):
          extraction_pipeline(
              file_path="/tmp/no_existe_para_test_extraccion_pipeline.txt",
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_empty,
          )


id: extraction_pipeline_py_pipelines_t4
function_id: extraction_pipeline_py_pipelines
name: test_entity_presets_vacio_lanza_valueerror
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_entity_presets_vacio_lanza_valueerror():
      """entity presets vacio lanza ValueError"""
      import pytest
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write("some text")
          tmp_path = f.name

      try:
          with pytest.raises(ValueError):
              extraction_pipeline(
                  file_path=tmp_path,
                  entity_presets=[],
                  relation_types=RELATION_TYPES,
                  llm_chat_json=_llm_empty,
              )
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t5
function_id: extraction_pipeline_py_pipelines
name: test_progress_callback_se_invoca
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_progress_callback_se_invoca():
      """progress callback se invoca durante la ejecucion"""
      calls: list[tuple[str, float]] = []

      def _on_progress(msg: str, pct: float) -> None:
          calls.append((msg, pct))

      text = "John Smith operates evil-corp.com."
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_with_entities,
              on_progress=_on_progress,
          )
          assert len(calls) > 0
          messages = [c[0] for c in calls]
          assert any("Extracting" in m or "Done" in m or "Dedup" in m for m in messages)
      finally:
          os.unlink(tmp_path)


id: extraction_pipeline_py_pipelines_t6
function_id: extraction_pipeline_py_pipelines
name: test_stats_se_rellenan_correctamente
file_path: python/functions/pipelines/extraction_pipeline_test.py
lang: py
created_at: 2026-04-18T15:33:38Z
updated_at: 2026-04-18T15:33:38Z
code:
  def test_stats_se_rellenan_correctamente():
      """stats se rellenan correctamente con conteos y tiempo"""
      text = "John Smith, a US national, operates the domain evil-corp.com."
      with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
          f.write(text)
          tmp_path = f.name

      try:
          result = extraction_pipeline(
              file_path=tmp_path,
              entity_presets=ENTITY_PRESETS,
              relation_types=RELATION_TYPES,
              llm_chat_json=_llm_with_entities,
          )
          assert result.stats.total_chars > 0
          assert result.stats.total_chunks >= 1
          assert result.stats.processing_time_seconds >= 0.0
      finally:
          os.unlink(tmp_path)


