"""Tests for ``app.services.stack_resolver.StackResolver`` (v49). Covers Section 4 of the unified-stack design: * 4.1 API surface — browse / stack / is_required / add_to_stack / remove_from_stack / memory_item_is_required * 4.2 Resolution algorithm — grants ∪ subscriptions * 4.3 Required precedence — OR across grants * 4.4 Memory item-level Required precedence — per-group MEMORY_ITEM override, then global item.is_required """ import duckdb import pytest from fastapi import HTTPException from app.resource_types import ResourceType from app.services.stack_resolver import ResourceEntry, StackResolver from src.db import _ensure_schema # -------- Fixtures --------------------------------------------------------- @pytest.fixture def conn(): c = duckdb.connect(":memory:") _ensure_schema(c) # Seed two groups c.execute("INSERT INTO user_groups(id, name) VALUES ('g_sales', 'Sales')") c.execute("INSERT INTO user_groups(id, name) VALUES ('g_eng', 'Engineering')") # User is in both groups c.execute("INSERT INTO user_group_members(user_id, group_id, source) " "VALUES ('u1', 'g_sales', 'admin')") c.execute("INSERT INTO user_group_members(user_id, group_id, source) " "VALUES ('u1', 'g_eng', 'admin')") # Two data packages c.execute( "INSERT INTO data_packages(id, slug, name, description, icon, color) " "VALUES ('pkg_sales', 'sales', 'Sales bundle', 'Sales', '📦', '#abc')" ) c.execute( "INSERT INTO data_packages(id, slug, name, description, icon, color) " "VALUES ('pkg_compliance', 'compliance', 'Compliance', 'Mandatory', '⚖️', '#fbb')" ) return c def _grant(conn, group_id, resource_type, resource_id, requirement="available"): """Insert one resource_grant row.""" import uuid conn.execute( "INSERT INTO resource_grants(id, group_id, resource_type, resource_id, " "requirement, assigned_at, assigned_by) " "VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, 'test')", [str(uuid.uuid4()), group_id, resource_type, resource_id, requirement], ) # -------- Browse + Stack --------------------------------------------------- class TestBrowse: def test_browse_includes_available_grants(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) entries = resolver.browse("u1", ResourceType.DATA_PACKAGE) ids = {e.id for e in entries} assert "pkg_sales" in ids def test_browse_includes_required_grants(self, conn): _grant(conn, "g_sales", "data_package", "pkg_compliance", "required") resolver = StackResolver(conn) entries = resolver.browse("u1", ResourceType.DATA_PACKAGE) comp = next(e for e in entries if e.id == "pkg_compliance") assert comp.requirement == "required" assert comp.in_stack is True # required entries are always in_stack def test_browse_in_stack_reflects_subscription(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) # No subscription yet → in_stack = False entries = resolver.browse("u1", ResourceType.DATA_PACKAGE) e = next(x for x in entries if x.id == "pkg_sales") assert e.in_stack is False # Add subscription → in_stack flips to True resolver.add_to_stack("u1", ResourceType.DATA_PACKAGE, "pkg_sales") entries = resolver.browse("u1", ResourceType.DATA_PACKAGE) e = next(x for x in entries if x.id == "pkg_sales") assert e.in_stack is True class TestStack: def test_stack_excludes_available_without_subscription(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) ids = {e.id for e in resolver.stack("u1", ResourceType.DATA_PACKAGE)} assert "pkg_sales" not in ids def test_stack_includes_available_with_subscription(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) resolver.add_to_stack("u1", ResourceType.DATA_PACKAGE, "pkg_sales") ids = {e.id for e in resolver.stack("u1", ResourceType.DATA_PACKAGE)} assert "pkg_sales" in ids def test_stack_includes_required_grants(self, conn): _grant(conn, "g_sales", "data_package", "pkg_compliance", "required") resolver = StackResolver(conn) ids = {e.id for e in resolver.stack("u1", ResourceType.DATA_PACKAGE)} assert "pkg_compliance" in ids def test_zombie_subscription_filtered_when_no_grant(self, conn): """Subscription exists but the grant has been revoked — the entry is excluded from stack() (it's not effective).""" resolver = StackResolver(conn) # Hand-insert a subscription without a corresponding grant. conn.execute( "INSERT INTO user_stack_subscriptions(user_id, resource_type, resource_id) " "VALUES ('u1', 'data_package', 'pkg_zombie')" ) ids = {e.id for e in resolver.stack("u1", ResourceType.DATA_PACKAGE)} assert "pkg_zombie" not in ids # -------- Required precedence --------------------------------------------- class TestRequiredPrecedence: def test_required_wins_when_any_grant_requires(self, conn): # g_sales says available; g_eng says required → required wins. _grant(conn, "g_sales", "data_package", "pkg_sales", "available") _grant(conn, "g_eng", "data_package", "pkg_sales", "required") resolver = StackResolver(conn) assert resolver.is_required("u1", ResourceType.DATA_PACKAGE, "pkg_sales") is True def test_available_only_when_no_required_grant(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) assert resolver.is_required("u1", ResourceType.DATA_PACKAGE, "pkg_sales") is False def test_no_grant_means_not_required(self, conn): resolver = StackResolver(conn) assert resolver.is_required("u1", ResourceType.DATA_PACKAGE, "pkg_other") is False # -------- add / remove from stack ------------------------------------------ class TestAddToStack: def test_add_to_stack_creates_subscription(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) resolver.add_to_stack("u1", ResourceType.DATA_PACKAGE, "pkg_sales") row = conn.execute( "SELECT 1 FROM user_stack_subscriptions " "WHERE user_id='u1' AND resource_type='data_package' " "AND resource_id='pkg_sales'" ).fetchone() assert row is not None def test_add_to_required_rejected(self, conn): _grant(conn, "g_sales", "data_package", "pkg_compliance", "required") resolver = StackResolver(conn) with pytest.raises(HTTPException) as exc_info: resolver.add_to_stack("u1", ResourceType.DATA_PACKAGE, "pkg_compliance") assert exc_info.value.status_code == 400 class TestRemoveFromStack: def test_remove_drops_subscription(self, conn): _grant(conn, "g_sales", "data_package", "pkg_sales", "available") resolver = StackResolver(conn) resolver.add_to_stack("u1", ResourceType.DATA_PACKAGE, "pkg_sales") resolver.remove_from_stack("u1", ResourceType.DATA_PACKAGE, "pkg_sales") row = conn.execute( "SELECT 1 FROM user_stack_subscriptions " "WHERE user_id='u1' AND resource_type='data_package' " "AND resource_id='pkg_sales'" ).fetchone() assert row is None def test_remove_required_rejected(self, conn): _grant(conn, "g_sales", "data_package", "pkg_compliance", "required") resolver = StackResolver(conn) with pytest.raises(HTTPException) as exc_info: resolver.remove_from_stack( "u1", ResourceType.DATA_PACKAGE, "pkg_compliance" ) assert exc_info.value.status_code == 400 # -------- Memory item-level Required precedence (Section 4.4) -------------- class TestMemoryItemIsRequired: """Top-down precedence: 1) any grant(MEMORY_ITEM, requirement='required') → True 2) any grant(MEMORY_ITEM, requirement='available') → False 3) item.is_required = TRUE → True 4) otherwise → False """ def test_global_flag_true_makes_item_required(self, conn): resolver = StackResolver(conn) assert resolver.memory_item_is_required("u1", "k1", True) is True def test_global_flag_false_keeps_item_not_required(self, conn): resolver = StackResolver(conn) assert resolver.memory_item_is_required("u1", "k1", False) is False def test_per_group_required_overrides_global_false(self, conn): _grant(conn, "g_sales", "memory_item", "k1", "required") resolver = StackResolver(conn) assert resolver.memory_item_is_required("u1", "k1", False) is True def test_per_group_available_overrides_global_true(self, conn): # Per-group 'available' means "force-NOT-required for this group" # — overrides the global is_required=TRUE flag. _grant(conn, "g_sales", "memory_item", "k1", "available") resolver = StackResolver(conn) assert resolver.memory_item_is_required("u1", "k1", True) is False def test_required_grant_wins_over_available_grant(self, conn): # Same precedence rule as DATA_PACKAGE — OR across grants. _grant(conn, "g_sales", "memory_item", "k1", "available") _grant(conn, "g_eng", "memory_item", "k1", "required") resolver = StackResolver(conn) assert resolver.memory_item_is_required("u1", "k1", False) is True # -------- Browse for MEMORY_DOMAIN ---------------------------------------- class TestBrowseMemoryDomain: def test_memory_domain_entries_carry_metadata(self, conn): md_id = conn.execute( "SELECT id FROM memory_domains WHERE slug='finance'" ).fetchone()[0] _grant(conn, "g_sales", "memory_domain", md_id, "required") resolver = StackResolver(conn) entries = resolver.browse("u1", ResourceType.MEMORY_DOMAIN) e = next(x for x in entries if x.id == md_id) assert e.name == "Finance" assert e.requirement == "required" # -------- ResourceEntry shape ---------------------------------------------- class TestResourceEntryShape: def test_dataclass_default_in_stack_false(self): e = ResourceEntry(id="x", name="X") assert e.in_stack is False assert e.requirement == "available"