Files
graph_explorer/tests/test_group_visual_inheritance.py
egutierrez deb86b24ec test(0035e): cobertura del visual heredado, threshold override y migracion idempotente
- 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.
2026-05-04 14:25:03 +02:00

122 lines
5.2 KiB
Python

"""Tests del visual heredado del Group (issue 0035e).
El binario C++ implementa `apply_group_inherited_visuals` en data.cpp:
para cada nodo Group del grafo, consulta `SELECT DISTINCT type_ref
FROM entities WHERE group_id = ? AND type_ref != 'Group'`. Si la
familia es homogenea (un solo tipo), reasigna el `type_id` del nodo
Group al de ese tipo y fija `shape_override = SHAPE_SQUARE`. Si es
heterogenea o vacia, conserva el visual generico.
El subcomando `gx-cli group visual <id>` espejea exactamente esa SQL,
asi estos tests verifican el contrato (homogeneo vs heterogeneo,
type heredado y shape=square preservado) sin depender del binario.
"""
from __future__ import annotations
import sqlite3
from test_gx_cli import OPS_SCHEMA, APP_SCHEMA, env_dirs, run_gx # noqa: F401
def _seed_group_with_children(ops_db, group_id: str,
child_specs: list[tuple[str, str]]):
"""Inserta el contenedor Group + cada hijo (id, type_ref).
`child_specs` = [(child_id, type_ref), ...]. Se anaden con group_id
apuntando al contenedor.
"""
cn = sqlite3.connect(ops_db)
try:
cn.execute(
"INSERT INTO entities(id, name, type_ref, status, source, "
" metadata, created_at, updated_at) "
"VALUES (?, ?, 'Group', 'active', 'manual', '{}', "
" '2026-05-04T10:00:00.000Z', '2026-05-04T10:00:00.000Z')",
(group_id, "test-group"),
)
for i, (cid, type_ref) in enumerate(child_specs):
cn.execute(
"INSERT INTO entities(id, name, type_ref, status, source, "
" metadata, group_id, "
" created_at, updated_at) "
"VALUES (?, ?, ?, 'active', 'manual', '{}', ?, ?, ?)",
(cid, f"name-{i}", type_ref, group_id,
f"2026-05-04T11:{i:02d}:00.000Z",
f"2026-05-04T11:{i:02d}:00.000Z"),
)
cn.commit()
finally:
cn.close()
def test_group_inherits_visual_from_homogeneous_children(env_dirs):
"""5 Urls como hijos -> visual heredado a 'Url' (homogeneo)."""
children = [(f"u_{i:02d}", "Url") for i in range(5)]
_seed_group_with_children(env_dirs["ops"], "G_homogeneous", children)
out = run_gx(env_dirs, "group", "visual", "G_homogeneous")
assert out["homogeneous"] is True, out
assert out["inherited"] == "Url", out
assert out["child_types"] == ["Url"], out
# La forma siempre se queda como square — distintivo de contenedor.
assert out["shape"] == "square", out
def test_group_falls_back_to_generic_for_heterogeneous(env_dirs):
"""Url + Email en el mismo Group -> visual generico Group."""
children = [
("u_00", "Url"), ("u_01", "Url"), ("u_02", "Url"),
("e_00", "Email"), ("e_01", "Email"),
]
_seed_group_with_children(env_dirs["ops"], "G_heterogeneous", children)
out = run_gx(env_dirs, "group", "visual", "G_heterogeneous")
assert out["homogeneous"] is False, out
assert out["inherited"] == "Group", out
# child_types ordenado alfabeticamente — verifica ambos presentes.
assert out["child_types"] == ["Email", "Url"], out
assert out["shape"] == "square", out
def test_group_with_no_children_falls_back_to_generic(env_dirs):
"""Group vacio (sin hijos con group_id apuntando a el) -> generico."""
_seed_group_with_children(env_dirs["ops"], "G_empty", [])
out = run_gx(env_dirs, "group", "visual", "G_empty")
assert out["homogeneous"] is False, out
assert out["inherited"] == "Group", out
assert out["child_types"] == [], out
def test_group_visual_ignores_nested_subgroups(env_dirs):
"""Subgrupos anidados (type_ref='Group') no cuentan — siguen scope fase 1."""
cn = sqlite3.connect(env_dirs["ops"])
try:
cn.execute(
"INSERT INTO entities(id, name, type_ref, status, source, "
" metadata, created_at, updated_at) "
"VALUES ('G_outer', 'outer', 'Group', 'active', 'manual', '{}', "
" '2026-05-04T10:00:00.000Z', '2026-05-04T10:00:00.000Z')"
)
for i in range(3):
cn.execute(
"INSERT INTO entities(id, name, type_ref, status, source, "
" metadata, group_id, "
" created_at, updated_at) "
"VALUES (?, ?, 'Url', 'active', 'manual', '{}', 'G_outer', "
" '2026-05-04T11:00:00.000Z', '2026-05-04T11:00:00.000Z')",
(f"u_{i}", f"url-{i}"),
)
# Subgrupo anidado — el resolver lo excluye via type_ref != 'Group'.
cn.execute(
"INSERT INTO entities(id, name, type_ref, status, source, "
" metadata, group_id, "
" created_at, updated_at) "
"VALUES ('G_nested', 'nested', 'Group', 'active', 'manual', "
" '{}', 'G_outer', "
" '2026-05-04T11:00:00.000Z', '2026-05-04T11:00:00.000Z')"
)
cn.commit()
finally:
cn.close()
out = run_gx(env_dirs, "group", "visual", "G_outer")
assert out["homogeneous"] is True, out
assert out["inherited"] == "Url", out