"""Tests de la migracion idempotente de operations.db -> + group_id. El binario C++ implementa `project_migrate_schema` en project_manager.cpp: detecta si `entities.group_id` existe via PRAGMA table_info; si no, ejecuta `ALTER TABLE entities ADD COLUMN group_id TEXT`. Es idempotente — al volver a abrir una BD ya migrada NO debe fallar ni duplicar. Como la logica es pequena y deterministica (PRAGMA + ALTER), aqui la replicamos en Python para testear el contrato sin depender del binario. Si el contrato cambia, este mirror tiene que actualizarse junto con project_manager.cpp. """ from __future__ import annotations import sqlite3 from pathlib import Path import pytest def _table_has_column(conn: sqlite3.Connection, table: str, column: str) -> bool: cur = conn.execute(f"PRAGMA table_info({table})") return any(row[1] == column for row in cur.fetchall()) def _migrate_group_id(db_path: Path) -> bool: """Mirror Python de project_migrate_schema (issue 0035a + 0035e). Devuelve True si la migracion se completo (con o sin trabajo), False si la BD no se pudo abrir. """ cn = sqlite3.connect(db_path) try: if not _table_has_column(cn, "entities", "group_id"): cn.execute("ALTER TABLE entities ADD COLUMN group_id TEXT") cn.commit() return True finally: cn.close() # Schema "viejo" — sin la columna group_id. Reproduce el estado de una # operations.db previa al issue 0035a (pre-2026-05-03). LEGACY_SCHEMA = """ CREATE TABLE entities ( id TEXT PRIMARY KEY, name TEXT NOT NULL, type_ref TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', description TEXT NOT NULL DEFAULT '', domain TEXT NOT NULL DEFAULT '', tags TEXT NOT NULL DEFAULT '[]', source TEXT NOT NULL, metadata TEXT NOT NULL DEFAULT '{}', notes TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE relations ( id TEXT PRIMARY KEY, name TEXT NOT NULL, from_entity TEXT NOT NULL DEFAULT '', to_entity TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); """ @pytest.fixture def legacy_db(tmp_path): db = tmp_path / "operations.db" cn = sqlite3.connect(db) cn.executescript(LEGACY_SCHEMA) # Datos previos para verificar que sobreviven la migracion. cn.execute( "INSERT INTO entities(id, name, type_ref, status, source, " " metadata, created_at, updated_at) " "VALUES ('e_pre', 'pre-existing', 'Url', 'active', 'manual', " " '{}', '2025-01-01', '2025-01-01')" ) cn.commit() cn.close() return db def test_schema_migration_idempotent(legacy_db): """1a apertura migra; 2a apertura no rompe ni duplica la columna.""" # Estado inicial: sin group_id. cn = sqlite3.connect(legacy_db) assert not _table_has_column(cn, "entities", "group_id"), \ "fixture ya tenia group_id (schema legacy roto)" cn.close() # 1a migracion: anade la columna. assert _migrate_group_id(legacy_db) is True cn = sqlite3.connect(legacy_db) assert _table_has_column(cn, "entities", "group_id") # Datos previos sobreviven y la columna nueva es NULL por defecto. row = cn.execute( "SELECT id, name, group_id FROM entities WHERE id = 'e_pre'" ).fetchone() assert row == ("e_pre", "pre-existing", None) cn.close() # 2a migracion: idempotente, no debe fallar ni duplicar. assert _migrate_group_id(legacy_db) is True cn = sqlite3.connect(legacy_db) # Una sola columna group_id (no duplicada). cur = cn.execute("PRAGMA table_info(entities)") cols = [row[1] for row in cur.fetchall()] assert cols.count("group_id") == 1, cols # Y los datos siguen intactos. cnt = cn.execute("SELECT COUNT(*) FROM entities").fetchone()[0] assert cnt == 1 cn.close() def test_schema_migration_already_migrated_db_is_noop(tmp_path): """BD ya creada con la columna group_id desde el inicio: noop.""" db = tmp_path / "operations.db" cn = sqlite3.connect(db) cn.executescript(LEGACY_SCHEMA) cn.execute("ALTER TABLE entities ADD COLUMN group_id TEXT") cn.commit() cn.close() # Migrar no debe fallar y la columna sigue siendo unica. assert _migrate_group_id(db) is True cn = sqlite3.connect(db) cur = cn.execute("PRAGMA table_info(entities)") cols = [row[1] for row in cur.fetchall()] assert cols.count("group_id") == 1 cn.close() def test_schema_migration_preserves_existing_group_id_values(tmp_path): """Si una BD ya tiene valores en group_id, la migracion los respeta.""" db = tmp_path / "operations.db" cn = sqlite3.connect(db) cn.executescript(LEGACY_SCHEMA) cn.execute("ALTER TABLE entities ADD COLUMN group_id TEXT") cn.execute( "INSERT INTO entities(id, name, type_ref, status, source, " " metadata, group_id, " " created_at, updated_at) " "VALUES ('child', 'c', 'Url', 'active', 'manual', '{}', " " 'parent_grp', '2026-05-04', '2026-05-04')" ) cn.commit() cn.close() _migrate_group_id(db) cn = sqlite3.connect(db) val = cn.execute( "SELECT group_id FROM entities WHERE id = 'child'" ).fetchone()[0] assert val == "parent_grp" cn.close()