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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user