deb86b24ec
- test_group_visual_inheritance.py (4 tests): homogeneo->Url heredado, heterogeneo->generico Group, vacio->generico, subgrupos anidados ignorados. - test_manifest_threshold_override.py (4 tests): override 100 con 80 unicas no agrupa; override bajo (20) si agrupa cuando se supera; threshold=0 cae al default 50; mirror Python del parser de manifest C++ confirma el campo se extrae como int. - test_schema_migration_group_id.py (3 tests): mirror Python de project_migrate_schema, verifica idempotencia (1a y 2a apertura no duplican columna), no-op sobre BD ya migrada, datos previos sobreviven la migracion.
154 lines
6.4 KiB
Python
154 lines
6.4 KiB
Python
"""Tests del override `auto_group_threshold` desde manifest (issue 0035e).
|
|
|
|
El manifest YAML puede declarar un campo top-level
|
|
`auto_group_threshold: <int>`. 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
|