"""Tests del loader NodeGroups kind=Group (issue 0036b). El binario C++ implementa `node_groups_count_for_group` y `node_groups_page_for_group` sobre operations.db. El subcomando `gx-cli group page ` espejea exactamente esa query, asi estos tests verifican el contrato SQL: count + columnas fijas (id, name, type_ref, status, updated_at) + ORDER BY updated_at DESC. Reusa la fixture `env_dirs` del modulo de tests del CLI. """ from __future__ import annotations import sqlite3 import pytest 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, n_children: int): """Inserta un Group + n_children entidades hijas con group_id seteado. Las hijas se crean con updated_at distintos (separados un milisegundo) para que el ORDER BY updated_at DESC tenga orden estable y verificable. """ cn = sqlite3.connect(ops_db) try: # El Group contenedor — type_ref='Group' (definicion en 0035a). 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"), ) # Hijas: Word entities con group_id apuntando al Group. Generamos # updated_at descendente para que el orden ORDER BY DESC sea # determinista (la primera insertada queda al final del ranking). for i in range(n_children): child_id = f"word_{i:02d}" ts = f"2026-05-04T11:{i:02d}:00.000Z" cn.execute( "INSERT INTO entities(id, name, type_ref, status, source, " " metadata, group_id, " " created_at, updated_at) " "VALUES (?, ?, 'Word', 'active', 'manual', '{}', ?, ?, ?)", (child_id, f"word-{i}", group_id, ts, ts), ) # Una entidad sin group_id para verificar que NO aparece en el page. cn.execute( "INSERT INTO entities(id, name, type_ref, status, source, " " metadata, created_at, updated_at) " "VALUES ('orphan_01', 'orphan', 'Word', 'active', 'manual', '{}', " " '2026-05-04T09:00:00.000Z', '2026-05-04T09:00:00.000Z')" ) cn.commit() finally: cn.close() class TestGroupPage: def test_count_matches_children(self, env_dirs): _seed_group_with_children(env_dirs["ops"], "grp_alpha", n_children=5) out = run_gx(env_dirs, "group", "page", "grp_alpha") assert out["total"] == 5, out assert len(out["rows"]) == 5 def test_columns_are_fixed_set(self, env_dirs): _seed_group_with_children(env_dirs["ops"], "grp_alpha", n_children=3) out = run_gx(env_dirs, "group", "page", "grp_alpha") assert out["rows"], out first = out["rows"][0] assert set(first.keys()) == {"id", "name", "type_ref", "status", "updated_at"} def test_orphan_not_listed(self, env_dirs): _seed_group_with_children(env_dirs["ops"], "grp_alpha", n_children=3) out = run_gx(env_dirs, "group", "page", "grp_alpha") ids = {r["id"] for r in out["rows"]} assert "orphan_01" not in ids assert ids == {"word_00", "word_01", "word_02"} def test_order_by_updated_at_desc(self, env_dirs): _seed_group_with_children(env_dirs["ops"], "grp_alpha", n_children=4) out = run_gx(env_dirs, "group", "page", "grp_alpha") ids_in_order = [r["id"] for r in out["rows"]] # word_03 tiene el updated_at mas reciente (11:03), word_00 el mas # antiguo (11:00). El ORDER BY ... DESC los pone en ese orden. assert ids_in_order == ["word_03", "word_02", "word_01", "word_00"] def test_pagination_offset_limit(self, env_dirs): _seed_group_with_children(env_dirs["ops"], "grp_alpha", n_children=10) page1 = run_gx(env_dirs, "group", "page", "grp_alpha", "--limit", "3", "--offset", "0") page2 = run_gx(env_dirs, "group", "page", "grp_alpha", "--limit", "3", "--offset", "3") assert page1["total"] == 10 assert page2["total"] == 10 ids1 = [r["id"] for r in page1["rows"]] ids2 = [r["id"] for r in page2["rows"]] # No deben solaparse y el orden global sigue DESC por updated_at. assert set(ids1).isdisjoint(set(ids2)) assert ids1 == ["word_09", "word_08", "word_07"] assert ids2 == ["word_06", "word_05", "word_04"] def test_unknown_container_returns_empty(self, env_dirs): # Sin seed — la BD esta vacia. out = run_gx(env_dirs, "group", "page", "no_such_group") assert out["total"] == 0 assert out["rows"] == []