#!/usr/bin/env python3 """Unit tests for P1d: consent gating, preference parsing, and walker behaviour. Tests ----- 1. Bootstrap runner skips when ``knowledgeIngestionEnabled=False``. 2. ``loadConnectionPrefs`` returns safe defaults when preferences are absent. 3. ``loadConnectionPrefs`` maps all ยง2.6 keys correctly from a full prefs dict. 4. Gmail walker passes ``neutralize=True`` and ``mailContentDepth`` to IngestionJob. 5. Gmail walker produces only a header content-object when depth="metadata". 6. ClickUp walker skips description when scope="titles". """ from __future__ import annotations import asyncio import os import sys import types import unittest from typing import Any, Dict, Optional from unittest.mock import AsyncMock, MagicMock, patch sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../..")) # --------------------------------------------------------------------------- # 1. Bootstrap runner consent gate # --------------------------------------------------------------------------- class TestBootstrapConsentGate(unittest.TestCase): """_bootstrapJobHandler must no-op when knowledgeIngestionEnabled is False.""" def _makeJob(self, connectionId="c-test", authority="google"): return {"payload": {"connectionId": connectionId, "authority": authority}} def _makeConn(self, enabled: bool): conn = MagicMock() conn.knowledgeIngestionEnabled = enabled return conn def test_skips_when_consent_disabled(self): from modules.serviceCenter.services.serviceKnowledge import subConnectorIngestConsumer as sut fake_root = MagicMock() fake_root.getUserConnectionById.return_value = self._makeConn(False) with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=fake_root): result = asyncio.get_event_loop().run_until_complete( sut._bootstrapJobHandler(self._makeJob(), lambda *a: None) ) assert result.get("skipped") is True assert result.get("reason") == "consent_disabled" fake_root.getUserConnectionById.assert_called_once_with("c-test") def test_proceeds_when_consent_enabled(self): """When consent is enabled, the handler should call at least one walker.""" from modules.serviceCenter.services.serviceKnowledge import subConnectorIngestConsumer as sut fake_root = MagicMock() fake_root.getUserConnectionById.return_value = self._makeConn(True) # Patch the inner walker so it doesn't do real I/O. async def _fakeBootstrap(**kwargs): return {"indexed": 0} with ( patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=fake_root), patch( "modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGdrive.bootstrapGdrive", new=AsyncMock(return_value={"indexed": 0}), ), patch( "modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail.bootstrapGmail", new=AsyncMock(return_value={"indexed": 0}), ), ): result = asyncio.get_event_loop().run_until_complete( sut._bootstrapJobHandler(self._makeJob(authority="google"), lambda *a: None) ) # Should not have 'skipped' at the top level. assert result.get("skipped") is not True assert result.get("authority") == "google" # --------------------------------------------------------------------------- # 2 + 3. loadConnectionPrefs # --------------------------------------------------------------------------- class TestLoadConnectionPrefs(unittest.TestCase): def _makeConn(self, prefs: Optional[Dict[str, Any]]): conn = MagicMock() conn.knowledgePreferences = prefs return conn def _mockRoot(self, prefs): root = MagicMock() root.getUserConnectionById.return_value = self._makeConn(prefs) return root def test_returns_safe_defaults_when_prefs_none(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorPrefs import ( ConnectionIngestionPrefs, loadConnectionPrefs, ) with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=self._mockRoot(None)): prefs = loadConnectionPrefs("x") assert prefs.neutralizeBeforeEmbed is False assert prefs.mailContentDepth == "full" assert prefs.mailIndexAttachments is False assert prefs.maxAgeDays == 90 assert prefs.clickupScope == "title_description" assert prefs.gmailEnabled is True assert prefs.driveEnabled is True def test_maps_all_keys(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorPrefs import loadConnectionPrefs raw = { "neutralizeBeforeEmbed": True, "mailContentDepth": "metadata", "mailIndexAttachments": True, "filesIndexBinaries": False, "clickupScope": "with_comments", "maxAgeDays": 30, "surfaceToggles": { "google": {"gmail": False, "drive": True}, "msft": {"sharepoint": False, "outlook": True}, }, } with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=self._mockRoot(raw)): prefs = loadConnectionPrefs("x") assert prefs.neutralizeBeforeEmbed is True assert prefs.mailContentDepth == "metadata" assert prefs.mailIndexAttachments is True assert prefs.filesIndexBinaries is False assert prefs.clickupScope == "with_comments" assert prefs.maxAgeDays == 30 assert prefs.gmailEnabled is False assert prefs.driveEnabled is True assert prefs.sharepointEnabled is False assert prefs.outlookEnabled is True def test_invalid_depth_falls_back_to_default(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorPrefs import loadConnectionPrefs raw = {"mailContentDepth": "everything_please"} with patch("modules.interfaces.interfaceDbApp.getRootInterface", return_value=self._mockRoot(raw)): prefs = loadConnectionPrefs("x") assert prefs.mailContentDepth == "full" # --------------------------------------------------------------------------- # 4. Gmail walker passes neutralize + mailContentDepth to IngestionJob # --------------------------------------------------------------------------- class TestGmailWalkerPrefs(unittest.TestCase): def _make_message(self, *, subject="Test", snippet="hello", body_text="full body"): import base64 encoded = base64.urlsafe_b64encode(body_text.encode()).decode() return { "id": "msg-1", "historyId": "h-42", "threadId": "t-1", "snippet": snippet, "payload": { "mimeType": "multipart/alternative", "headers": [ {"name": "Subject", "value": subject}, {"name": "From", "value": "alice@example.com"}, {"name": "To", "value": "bob@example.com"}, {"name": "Date", "value": "Mon, 20 Apr 2026 10:00:00 +0000"}, ], "parts": [ { "mimeType": "text/plain", "body": {"data": encoded}, } ], }, } def test_neutralize_flag_forwarded(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail import ( GmailBootstrapLimits, _ingestMessage, GmailBootstrapResult, ) from modules.serviceCenter.services.serviceKnowledge.mainServiceKnowledge import IngestionJob captured_jobs = [] async def fake_requestIngestion(job: IngestionJob): captured_jobs.append(job) return MagicMock(status="indexed", error=None) ks = MagicMock() ks.requestIngestion = fake_requestIngestion limits = GmailBootstrapLimits(neutralize=True, mailContentDepth="full") result = GmailBootstrapResult(connectionId="c-1") asyncio.get_event_loop().run_until_complete( _ingestMessage( googleGetFn=AsyncMock(return_value={}), knowledgeService=ks, connectionId="c-1", mandateId="", userId="u-1", labelId="INBOX", message=self._make_message(), limits=limits, result=result, progressCb=None, ) ) assert len(captured_jobs) == 1 assert captured_jobs[0].neutralize is True def test_metadata_depth_yields_only_header(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail import ( _buildContentObjects, ) message = self._make_message(snippet="hi", body_text="should be excluded") parts = _buildContentObjects(message, maxBodyChars=4000, mailContentDepth="metadata") ids = [p["contentObjectId"] for p in parts] assert ids == ["header"] def test_snippet_depth_yields_header_and_snippet(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncGmail import ( _buildContentObjects, ) message = self._make_message(snippet="hi", body_text="should be excluded") parts = _buildContentObjects(message, maxBodyChars=4000, mailContentDepth="snippet") ids = [p["contentObjectId"] for p in parts] assert "header" in ids assert "snippet" in ids assert "body" not in ids # --------------------------------------------------------------------------- # 5. ClickUp walker respects clickupScope="titles" # --------------------------------------------------------------------------- class TestClickupWalkerScope(unittest.TestCase): def _make_task(self): return { "id": "task-1", "name": "Ship feature X", "date_updated": "1713888000000", "description": "This should be omitted", "text_content": "Also omitted", "status": {"status": "open"}, "assignees": [], "tags": [], "list": {"name": "Backlog"}, "folder": {}, "space": {"name": "Engineering"}, } def test_titles_scope_omits_description(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncClickup import ( ClickupBootstrapLimits, _buildContentObjects, ) limits = ClickupBootstrapLimits(clickupScope="titles") parts = _buildContentObjects(self._make_task(), limits) ids = [p["contentObjectId"] for p in parts] assert ids == ["header"] assert "description" not in ids def test_with_description_scope_includes_description(self): from modules.serviceCenter.services.serviceKnowledge.subConnectorSyncClickup import ( ClickupBootstrapLimits, _buildContentObjects, ) limits = ClickupBootstrapLimits(clickupScope="title_description") parts = _buildContentObjects(self._make_task(), limits) ids = [p["contentObjectId"] for p in parts] assert "header" in ids assert "description" in ids if __name__ == "__main__": unittest.main()