298 lines
11 KiB
Python
298 lines
11 KiB
Python
#!/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()
|