424 lines
14 KiB
Python
424 lines
14 KiB
Python
from datetime import datetime, timezone
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from bson import ObjectId
|
|
|
|
from app.services.handoff_delivery import enqueue_handoff_delivery
|
|
from app.services.handoff_mirror_policy import (
|
|
GENERATED_BY_AGENT,
|
|
GENERATED_BY_HANDOFF_BRIDGE,
|
|
MESSAGE_ORIGIN_AGENT,
|
|
MESSAGE_ORIGIN_HUMAN_OWNER,
|
|
RELAY_STATUS_BLOCKED,
|
|
should_mirror_to_human,
|
|
)
|
|
from app.services import human_handoff_service
|
|
from app.services.human_handoff_service import (
|
|
MESSAGE_SCOPE_EXTERNAL,
|
|
MESSAGE_SCOPE_HANDOFF,
|
|
build_handoff_subject,
|
|
maybe_trigger_handoff,
|
|
process_human_reply,
|
|
resolve_handoff_from_inbound,
|
|
)
|
|
from app.services.mailbox_scope import apply_external_scope
|
|
from app.services.external_reply_service import build_external_reply_plan
|
|
|
|
|
|
class FakeCursor:
|
|
def __init__(self, items):
|
|
self.items = items
|
|
|
|
def sort(self, *_args, **_kwargs):
|
|
return self
|
|
|
|
async def to_list(self, length=None):
|
|
return list(self.items)[:length]
|
|
|
|
|
|
class FakeCollection:
|
|
def __init__(self, items=None):
|
|
self.items = list(items or [])
|
|
self.inserted = []
|
|
self.updated = []
|
|
|
|
async def find_one(self, query, projection=None):
|
|
for item in self.items:
|
|
if _matches(item, query):
|
|
return item
|
|
return None
|
|
|
|
def find(self, query=None):
|
|
query = query or {}
|
|
return FakeCursor([item for item in self.items if _matches(item, query)])
|
|
|
|
async def insert_one(self, document):
|
|
document = dict(document)
|
|
document.setdefault("_id", ObjectId())
|
|
self.items.append(document)
|
|
self.inserted.append(document)
|
|
return SimpleNamespace(inserted_id=document["_id"])
|
|
|
|
async def update_one(self, query, update, upsert=False):
|
|
self.updated.append((query, update, upsert))
|
|
existing = await self.find_one(query)
|
|
if existing is None and upsert:
|
|
inserted = {"_id": ObjectId(), **query, **update.get("$setOnInsert", {})}
|
|
self.items.append(inserted)
|
|
return SimpleNamespace(upserted_id=inserted["_id"], modified_count=0)
|
|
if existing is not None:
|
|
existing.update(update.get("$set", {}))
|
|
return SimpleNamespace(upserted_id=None, modified_count=1)
|
|
return SimpleNamespace(upserted_id=None, modified_count=0)
|
|
|
|
async def find_one_and_update(self, query, update, **kwargs):
|
|
existing = await self.find_one(query)
|
|
if existing is None:
|
|
return None
|
|
existing.update(update.get("$set", {}))
|
|
return existing
|
|
|
|
|
|
def _matches(document, query):
|
|
for key, expected in query.items():
|
|
if key == "$or":
|
|
if not any(_matches(document, clause) for clause in expected):
|
|
return False
|
|
continue
|
|
if key == "$ne":
|
|
continue
|
|
actual = document.get(key)
|
|
if isinstance(expected, dict):
|
|
if "$ne" in expected and actual == expected["$ne"]:
|
|
return False
|
|
if "$in" in expected and actual not in expected["$in"]:
|
|
return False
|
|
continue
|
|
if actual != expected:
|
|
return False
|
|
return True
|
|
|
|
|
|
def test_external_mailbox_filter_excludes_handoff_scope():
|
|
match = {"account_id": "account-1", "mailbox": "inbox"}
|
|
apply_external_scope(match)
|
|
assert match["$or"] == [
|
|
{"message_scope": "EXTERNAL"},
|
|
{"message_scope": {"$exists": False}},
|
|
]
|
|
|
|
|
|
def test_bridge_plan_never_references_private_ids():
|
|
customer = {
|
|
"_id": ObjectId(),
|
|
"internet_message_id": "<A@gmail.com>",
|
|
"message_id": "<A@gmail.com>",
|
|
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
|
"references": [],
|
|
"subject": "Invoice 42",
|
|
}
|
|
private_id = "<H1@gentic.email>"
|
|
plan = build_external_reply_plan(
|
|
parent_external_message=customer,
|
|
body_text="Approved",
|
|
recipients=["customer@gmail.com"],
|
|
)
|
|
assert plan.threading.in_reply_to == "<A@gmail.com>"
|
|
assert private_id not in plan.threading.references
|
|
assert plan.subject == "Re: Invoice 42"
|
|
assert "handoff" not in plan.subject.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_existing_handoff_is_sticky_without_reevaluating_rules():
|
|
external_thread_id = ObjectId()
|
|
source_id = ObjectId()
|
|
handoff = {
|
|
"_id": ObjectId(),
|
|
"account_id": "account-1",
|
|
"external_thread_id": external_thread_id,
|
|
"status": "ACTIVE",
|
|
"rule_name": "Finance approvals",
|
|
}
|
|
database = SimpleNamespace(
|
|
human_handoffs=FakeCollection([handoff]),
|
|
human_handoff_rules=FakeCollection(
|
|
[
|
|
{
|
|
"_id": ObjectId(),
|
|
"name": "Finance approvals",
|
|
"priority": 1,
|
|
"labels": ["Finance"],
|
|
"email_types": ["transactional"],
|
|
"instructions": "only invoices",
|
|
"active": True,
|
|
"created_at": datetime.now(timezone.utc),
|
|
}
|
|
]
|
|
),
|
|
handoff_delivery_jobs=FakeCollection(),
|
|
)
|
|
message = {
|
|
"_id": source_id,
|
|
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
|
"direction": "inbound",
|
|
"sender": {"address": "customer@example.com"},
|
|
"subject": "Hello there",
|
|
"body_text": "just checking in",
|
|
"snippet": "just checking in",
|
|
}
|
|
|
|
result = await maybe_trigger_handoff(
|
|
database,
|
|
account_id="account-1",
|
|
agent_email="agent@gentic.email",
|
|
external_message=message,
|
|
external_thread_id=external_thread_id,
|
|
)
|
|
|
|
assert result["_id"] == handoff["_id"]
|
|
assert database.handoff_delivery_jobs.items
|
|
assert (
|
|
database.handoff_delivery_jobs.items[0]["kind"] == "MIRROR_TO_HUMAN"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duplicate_delivery_jobs_share_an_idempotency_key():
|
|
database = SimpleNamespace(handoff_delivery_jobs=FakeCollection())
|
|
handoff_id = ObjectId()
|
|
source_id = ObjectId()
|
|
first = await enqueue_handoff_delivery(
|
|
database,
|
|
kind="MIRROR_TO_HUMAN",
|
|
account_id="account-1",
|
|
handoff_id=handoff_id,
|
|
source_message_id=source_id,
|
|
agent_email="agent@gentic.email",
|
|
)
|
|
second = await enqueue_handoff_delivery(
|
|
database,
|
|
kind="MIRROR_TO_HUMAN",
|
|
account_id="account-1",
|
|
handoff_id=handoff_id,
|
|
source_message_id=source_id,
|
|
agent_email="agent@gentic.email",
|
|
)
|
|
assert first["idempotency_key"] == second["idempotency_key"]
|
|
assert len(database.handoff_delivery_jobs.items) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_human_reply_resolves_only_to_private_handoff():
|
|
parent_id = ObjectId()
|
|
thread_id = ObjectId()
|
|
handoff = {
|
|
"_id": ObjectId(),
|
|
"account_id": "account-1",
|
|
"handoff_thread_id": thread_id,
|
|
"human_email": "owner@example.com",
|
|
"agent_email": "agent@gentic.email",
|
|
"status": "ACTIVE",
|
|
}
|
|
database = SimpleNamespace(
|
|
messages=FakeCollection(
|
|
[
|
|
{
|
|
"_id": parent_id,
|
|
"message_scope": MESSAGE_SCOPE_HANDOFF,
|
|
"message_id": "<H1@gentic.email>",
|
|
"message_id_folded": "<h1@gentic.email>",
|
|
}
|
|
]
|
|
),
|
|
mailbox_entries=FakeCollection(
|
|
[
|
|
{
|
|
"account_id": "account-1",
|
|
"message_id": parent_id,
|
|
"message_scope": MESSAGE_SCOPE_HANDOFF,
|
|
"thread_id": thread_id,
|
|
}
|
|
]
|
|
),
|
|
human_handoffs=FakeCollection([handoff]),
|
|
)
|
|
|
|
resolved = await resolve_handoff_from_inbound(
|
|
database,
|
|
account_id="account-1",
|
|
agent_email="agent@gentic.email",
|
|
sender_address="owner@example.com",
|
|
in_reply_to="<H1@gentic.email>",
|
|
references=["<H1@gentic.email>"],
|
|
)
|
|
assert resolved["_id"] == handoff["_id"]
|
|
|
|
|
|
def test_human_echo_and_agent_mirror_policy():
|
|
assert should_mirror_to_human(
|
|
{
|
|
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
|
"direction": "outbound",
|
|
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
|
|
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
|
|
"generated_from_handoff_message_id": ObjectId(),
|
|
}
|
|
) is False
|
|
assert should_mirror_to_human(
|
|
{
|
|
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
|
"direction": "outbound",
|
|
"generated_by": GENERATED_BY_AGENT,
|
|
"message_origin": MESSAGE_ORIGIN_AGENT,
|
|
}
|
|
) is True
|
|
|
|
|
|
def test_private_subject_stays_off_the_external_plane():
|
|
private = build_handoff_subject(
|
|
sender_name="Parth Dixit",
|
|
external_sender="customer@example.com",
|
|
external_subject="Invoice 42",
|
|
)
|
|
assert private.startswith("handoff · Parth Dixit ·")
|
|
assert "Invoice 42" in private
|
|
assert "Finance approvals" not in private
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_first_matching_rule_creates_a_new_linked_handoff():
|
|
external_thread_id = ObjectId()
|
|
source_id = ObjectId()
|
|
rule = {
|
|
"_id": ObjectId(),
|
|
"account_id": "account-1",
|
|
"name": "Finance approvals",
|
|
"priority": 1,
|
|
"owner_email": "owner@example.com",
|
|
"owner_name": "Owner",
|
|
"labels": ["All mail"],
|
|
"email_types": ["all"],
|
|
"instructions": "",
|
|
"active": True,
|
|
"allow_replies": True,
|
|
"created_at": datetime.now(timezone.utc),
|
|
}
|
|
database = SimpleNamespace(
|
|
human_handoffs=FakeCollection(),
|
|
human_handoff_rules=FakeCollection([rule]),
|
|
handoff_delivery_jobs=FakeCollection(),
|
|
email_threads=FakeCollection(),
|
|
)
|
|
message = {
|
|
"_id": source_id,
|
|
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
|
"direction": "inbound",
|
|
"sender": {"name": "Parth Dixit", "address": "customer@example.com"},
|
|
"subject": "Invoice 42",
|
|
"body_text": "please review this invoice",
|
|
"snippet": "please review this invoice",
|
|
}
|
|
|
|
result = await maybe_trigger_handoff(
|
|
database,
|
|
account_id="account-1",
|
|
agent_email="agent@gentic.email",
|
|
external_message=message,
|
|
external_thread_id=external_thread_id,
|
|
)
|
|
|
|
assert result["rule_name"] == "Finance approvals"
|
|
assert result["external_thread_id"] == external_thread_id
|
|
assert result["handoff_thread_id"]
|
|
assert result["private_subject"].startswith("handoff · Parth Dixit ·")
|
|
assert database.handoff_delivery_jobs.items[0]["kind"] == "MIRROR_TO_HUMAN"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_maybe_trigger_does_not_echo_human_origin_messages():
|
|
database = SimpleNamespace(
|
|
human_handoffs=FakeCollection(
|
|
[
|
|
{
|
|
"_id": ObjectId(),
|
|
"account_id": "account-1",
|
|
"external_thread_id": ObjectId(),
|
|
"status": "ACTIVE",
|
|
}
|
|
]
|
|
),
|
|
handoff_delivery_jobs=FakeCollection(),
|
|
)
|
|
result = await maybe_trigger_handoff(
|
|
database,
|
|
account_id="account-1",
|
|
agent_email="agent@gentic.email",
|
|
external_message={
|
|
"_id": ObjectId(),
|
|
"message_scope": MESSAGE_SCOPE_EXTERNAL,
|
|
"direction": "outbound",
|
|
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
|
|
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
|
|
"generated_from_handoff_message_id": ObjectId(),
|
|
"sender": {"address": "agent@gentic.email"},
|
|
},
|
|
external_thread_id=database.human_handoffs.items[0]["external_thread_id"],
|
|
)
|
|
assert result is None
|
|
assert database.handoff_delivery_jobs.items == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_leaked_private_subject_blocks_human_relay(monkeypatch):
|
|
stored = {}
|
|
|
|
async def store(_database, **kwargs):
|
|
stored.update(kwargs)
|
|
return {"_id": ObjectId(), **kwargs}
|
|
|
|
enqueued = []
|
|
|
|
async def enqueue(*args, **kwargs):
|
|
enqueued.append(kwargs)
|
|
return {}
|
|
|
|
class BrokenStorage:
|
|
async def get(self, *_args, **_kwargs):
|
|
raise RuntimeError("raw mime unavailable")
|
|
|
|
monkeypatch.setattr(human_handoff_service, "store_handoff_message", store)
|
|
monkeypatch.setattr(human_handoff_service, "enqueue_handoff_delivery", enqueue)
|
|
monkeypatch.setattr(human_handoff_service, "get_raw_mime_storage", lambda: BrokenStorage())
|
|
|
|
database = SimpleNamespace(messages=FakeCollection(), human_handoffs=FakeCollection())
|
|
await process_human_reply(
|
|
database,
|
|
account_id="account-1",
|
|
agent_email="agent@gentic.email",
|
|
handoff={
|
|
"_id": ObjectId(),
|
|
"handoff_thread_id": ObjectId(),
|
|
"allow_replies": True,
|
|
},
|
|
parsed={
|
|
"sender": {"name": "Owner", "address": "owner@example.com"},
|
|
"to": [{"address": "agent@gentic.email"}],
|
|
"subject": "Re: Finance approvals · handoff · customer@example.com · Invoice 42",
|
|
"message_id": "<H2@gmail.com>",
|
|
"in_reply_to": "<H1@gentic.email>",
|
|
"references": ["<H1@gentic.email>"],
|
|
"attachments": [],
|
|
"raw_headers": {},
|
|
},
|
|
body_text=(
|
|
"Finance approvals · handoff · customer@example.com · Invoice 42\n"
|
|
"Please approve"
|
|
),
|
|
body_html=None,
|
|
raw_mime={"bucket": "mail", "name": "raw.eml"},
|
|
delivery_key="delivery-1",
|
|
)
|
|
assert stored["relay_status"] == RELAY_STATUS_BLOCKED
|
|
assert enqueued == []
|