# unit_tests

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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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_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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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


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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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-13T11:11:46Z
updated_at: 2026-04-13T11:11:46Z
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)


# projects

id: app_turismo
name: app_turismo
tags: ["turismo","spain","app","datascience","mobile"]
dir_path: projects/app_turismo
content_hash: 7865e4287a5485a823f8c627006753ed99e47e8bf4ee5e1189dcdecb35060540
created_at: 2026-04-13T00:06:12Z
updated_at: 2026-04-13T00:06:12Z
description:
  App de turismo para Espana: asistente contextual, descubrimiento local y offline-first para turistas internacionales.

notes:
  Proyecto de app movil nativa (Kotlin/Swift) para turistas internacionales en Espana.
  MVP con tres pilares: asistente diario contextual (GPS+clima+hora), descubrimiento de zonas fuera del radar, y offline-first con datos pre-descargados.

  Mercado: 96.8M turistas internacionales (2025), 134.710M EUR gasto. 5 idiomas (EN/FR/DE/IT/NL) cubren el 62% del mercado.

  ### Componentes

  - **analysis/turismo_spain/** — Exploracion Jupyter con datos INE, FRONTUR, EGATUR, Turespaña
  - **vaults/turismo_spain/** — Datos crudos y procesados (symlink a /home/lucas/vaults/turismo_spain)
  - **apps/** — (pendiente) App movil y backend


id: element_agents
name: element_agents
tags: ["agents","matrix","element","bots","llm","chat","selfhosted","devops"]
dir_path: projects/element_agents
content_hash: e19b58804611d7f17baaceaa49c7f9b8a2c6892bbd8d05371736d3d1a6f0eaa1
created_at: 2026-04-13T00:06:12Z
updated_at: 2026-04-13T00:06:12Z
description:
  Suite de agentes autonomos Matrix (Element) con infraestructura de chat autohosteada. Agents & Robots + Element Matrix Chat.

notes:
  Proyecto que agrupa la plataforma de bots autonomos Matrix y la infraestructura de chat Element autohosteada.

  ### Componentes

  - **apps/agents_and_robots/** — Plataforma Go de bots autonomos Matrix (launcher + agentctl + dashboard + register). Arquitectura pure core / impure shell. Agentes con LLM, tools, E2EE, cron, skills.
  - **apps/element_matrix_chat/** — Stack completo Matrix autohosteado con Docker Compose (Synapse, Element Web, MAS, LiveKit, PostgreSQL, Synapse Admin).

  ### VPS: organic-machine.com

  - **SSH:** `organic-machine.com` (user: ubuntu, key: ~/.ssh/organic-machine)
  - **OS:** Ubuntu 24.04 LTS, 6 CPU, 12GB RAM, 96GB disco (40% usado)
  - **Orquestador:** Coolify (proxy en puertos 80/443/8080)
  - **agents_and_robots:** desplegado en `/home/ubuntu/CodeProyects/agents_and_robots/`, launcher corriendo como proceso (PID)
  - **element_matrix_chat:** desplegado en `/home/ubuntu/CodeProyects/element_matrix_chat/`, 10 containers Docker activos (5 semanas uptime)

  ### Puertos en uso

  | Servicio | Puerto |
  |----------|--------|
  | Coolify proxy | 80, 443, 8080 |
  | Coolify app | 8000 |
  | Coolify realtime | 6001-6002 |
  | Element Web | 8081 |
  | Synapse Admin | 8082 |
  | MAS (auth) | 8083 |
  | Synapse (Matrix) | 8008 (internal) |
  | LiveKit | 7880-7882, 50000-50200/udp |
  | Gitea SSH | 22222 |
  | Kanboard | 38060 |
  | KeeWeb | 38679 |
  | PostgreSQL (Matrix) | 5432 |


id: fn_monitoring
name: fn_monitoring
tags: ["monitoring","api","dashboard","sqlite","visualization"]
dir_path: projects/fn_monitoring
content_hash: 124049f04d0c7e4f6cce62983c5ffdbf7394bcf6740e573b2a256f4986eebe2c
created_at: 2026-04-13T00:03:20Z
updated_at: 2026-04-13T00:03:20Z
description:
  Monitoreo y visualizacion del estado del fn_registry. API HTTP read-only sobre las bases de datos SQLite y dashboard ImGui que consume la API.

documentation:
  ## Apps

  | App | Descripcion |
  |-----|-------------|
  | sqlite_api | API REST HTTP read-only para registry.db y operations.db (Go, net/http, puerto 8484) |
  | registry_dashboard | Dashboard ImGui con KPIs, charts y tablas del registry (C++, consume sqlite_api) |

  ## Arquitectura

  ```
  sqlite_api (Go, :8484)
    └── registry.db, apps/*/operations.db (read-only)
         ↑
  registry_dashboard (C++/ImGui)
    └── HTTP GET/POST → sqlite_api
    └── Fallback: SQLite directo si API no disponible
  ```


