"""Tests del override `auto_group_threshold` desde manifest (issue 0035e). El manifest YAML puede declarar un campo top-level `auto_group_threshold: `. enrichers.cpp lo parsea (EnricherSpec) y jobs.cpp lo inyecta como campo del JSON stdin (`auto_group_threshold`). Los enrichers Python que crean Groups (split_words, split_sentences, web_search, extract_iocs_text) leen ese campo y, cuando viene > 0, lo usan en lugar del default global (50). Como los tests corren los run.py en aislado, basta con poner el campo en el ctx — eso emula exactamente lo que hace jobs.cpp en la app. """ from __future__ import annotations import sqlite3 from conftest import ( base_ctx, list_entities, make_node, run_enricher, ) # Texto largo (~85 unicas con dedupe) — sobrepasa el default 50 pero # no llega a 100. Mismo cuerpo que test_split_words.LONG_TEXT. LONG_TEXT = ( "Las estrellas brillan suavemente sobre el horizonte mientras " "la marea retrocede dejando huellas mojadas en la arena fina. " "Caminamos lentamente conversando sobre proyectos antiguos, " "ideas frescas, libros leidos durante el invierno pasado, " "viajes pendientes hacia tierras lejanas con culturas vibrantes. " "Recordamos infancias compartidas, amigos perdidos, victorias " "modestas, fracasos instructivos. Cada palabra dibuja un mapa " "diferente del territorio interno que habitamos. Los nombres de " "ciudades antiguas resuenan: Estambul, Marrakech, Kioto, Lisboa, " "Praga, Budapest, Cuzco, Cartagena. Tambien tecnologia: servidores, " "bases datos, redes neuronales, modelos linguisticos, sistemas " "distribuidos, criptografia moderna. La conversacion fluye sin " "esfuerzo aparente entre dominios completamente distintos." ) def test_manifest_auto_group_threshold_override(ops_db, app_dir, registry_root): """auto_group_threshold=100 + 80 unicas -> sin Group (default seria 50). Sin el override, 80 >= 50 dispararia agrupacion. Con override 100, 80 < 100 y todos los Words quedan sueltos (grouped=False). """ make_node(ops_db, node_id="t1", name="largo", type_ref="text", notes=LONG_TEXT) ctx = base_ctx(ops_db=ops_db, app_dir=app_dir, registry_root=registry_root, node_id="t1", node_name="largo", node_type="text") ctx["auto_group_threshold"] = 100 rc, out, err = run_enricher("split_words", ctx, timeout=60) assert rc == 0, err # El texto produce >50 unicas (sino el test no es valido) pero <100. assert 50 <= out["words"] < 100, out # Override toma efecto: no se crea Group. assert out["grouped"] is False, out assert out["group_id"] == "", out # Todos los Words quedan sueltos sin group_id. words = list_entities(ops_db, type_ref="Word") assert len(words) == out["words"] assert all(w["group_id"] is None for w in words), words[:5] def test_manifest_threshold_override_below_default_still_groups( ops_db, app_dir, registry_root): """Override mas BAJO que el default tambien debe respetarse. threshold=20 con un texto corto (~15 unicas) — mas bajo que el default 50 pero igual no llega a 20. Para verificar la direccion contraria: 25 unicas >= 20 -> SI agrupa aunque < 50. """ text = ("alfa beta gamma delta epsilon zeta eta theta iota kappa " "lambda mu nu omicron pi rho sigma tau upsilon phi chi psi " "omega palabras adicionales para llegar.") make_node(ops_db, node_id="t1", name="corto", type_ref="text", notes=text) ctx = base_ctx(ops_db=ops_db, app_dir=app_dir, registry_root=registry_root, node_id="t1", node_name="corto", node_type="text") ctx["auto_group_threshold"] = 20 rc, out, err = run_enricher("split_words", ctx, timeout=30) assert rc == 0, err if out["words"] >= 20: assert out["grouped"] is True, out assert out["group_id"], out else: # Defensa por si el tokenizer cuenta menos — el test sigue siendo # informativo aunque no dispare la direccion principal. assert out["grouped"] is False, out def test_manifest_threshold_zero_uses_default(ops_db, app_dir, registry_root): """auto_group_threshold=0 debe caer al default 50 (no desactivar).""" make_node(ops_db, node_id="t1", name="largo", type_ref="text", notes=LONG_TEXT) ctx = base_ctx(ops_db=ops_db, app_dir=app_dir, registry_root=registry_root, node_id="t1", node_name="largo", node_type="text") ctx["auto_group_threshold"] = 0 rc, out, err = run_enricher("split_words", ctx, timeout=60) assert rc == 0, err # Con default 50 y 80+ unicas, debe agrupar. assert out["words"] >= 50, out assert out["grouped"] is True, out def test_cpp_manifest_parser_reads_auto_group_threshold(tmp_path): """Parser ad-hoc Python que replica enrichers.cpp parse_manifest. Espejea la logica del parser C++: lineas top-level `clave: valor` se leen como atributos del manifest, sin recurrir a un YAML real. Verifica que el campo `auto_group_threshold` se extrae como int. """ # Reproducimos exactamente el algoritmo del parser C++ (top-level # solo, ignora bloques anidados como `params:`). def parse(text: str) -> dict: out: dict = {} in_skip = False for raw in text.splitlines(): line = raw.rstrip("\r") s = line.strip() if not s or s.startswith("#"): continue indented = line and line[0].isspace() if not indented: in_skip = False if in_skip: continue if ":" not in s: continue key, _, val = s.partition(":") key, val = key.strip(), val.strip() if val and val[0] in ('"', "'") and val[-1] == val[0]: val = val[1:-1] if key == "params" and not val: in_skip = True continue out[key] = val return out manifest = ( "id: split_words\n" "name: \"Split into words\"\n" "applies_to: [text]\n" "auto_group_threshold: 100\n" "params:\n" " - { name: max_words, type: int, default: 500 }\n" ) parsed = parse(manifest) assert parsed.get("id") == "split_words" assert parsed.get("auto_group_threshold") == "100" assert int(parsed["auto_group_threshold"]) == 100