Files
ten31-database/backend/matrix_intake/test_crm_client.py
T
Keysat 0b893295e1 Matrix intake: fuzzy investor matching + conversational in-thread edits (v0.1.0:86)
Close the two locked post-deploy enhancements for the Matrix intake bot.

Fuzzy matching (server-side, ships in the s9pk): new find_intake_candidates in
server.py returns ranked deterministic near-matches (difflib name similarity +
token-set Jaccard, legal-suffix-aware, + email Levenshtein <= 2); GET
/api/intake/match now returns {match, candidates}. The bot surfaces a numbered
shortlist so a near-duplicate (Charlie/Charles, Acme Capital vs Acme Capital LLC,
a one-char email typo) is confirmed by a human instead of silently creating a
second investor. Exact match still auto-attaches; fuzzy candidates are never
auto-attached. The optional LLM-judge re-rank is deferred.

Conversational edits (bot-side, ships on the Spark): any in-thread reply that
isn't yes/no/edit field=value is treated as a natural-language revision and
re-run through local Qwen (parse.revise). Email integrity is preserved -- a
changed address must literally appear in the instruction; the model's email
field is structurally unreachable. No-op revisions re-prompt.

Docs/current-state brought current; 27/27 backend tests green.
2026-06-17 18:50:58 -05:00

122 lines
4.7 KiB
Python

"""Tests for the CRM client's payload builder (pure logic, no network)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import crm_client # noqa: E402
def test_new_investor_payload():
p = {"intent": "new_investor", "investor_name": "Acme Capital",
"contact_name": "Jane Doe", "contact_email": "jane@acme.com",
"contact_title": "GP", "note": "met at conf"}
out = crm_client.build_commit_payload(p)
assert out["investor_name"] == "Acme Capital"
assert out["create_investor_if_missing"] is True
assert "row_id" not in out
assert out["contact"] == {"name": "Jane Doe", "email": "jane@acme.com", "title": "GP"}
assert out["body"] == "met at conf"
assert out["source"] == "matrix_intake"
def test_existing_investor_uses_row_id_not_create():
p = {"intent": "meeting_note", "investor_name": "Acme Capital",
"contact_name": "Jane Doe", "contact_email": None, "note": "wants Q3 deck",
"_match_id": "rowAcme"}
out = crm_client.build_commit_payload(p)
assert out["row_id"] == "rowAcme"
assert "create_investor_if_missing" not in out
assert "investor_name" not in out # targeted by row id, never re-matched by name
assert out["body"] == "wants Q3 deck"
def test_contact_falls_back_to_investor_name_when_no_person():
p = {"intent": "new_investor", "investor_name": "Delta Fund",
"contact_name": None, "contact_email": None, "note": None}
out = crm_client.build_commit_payload(p)
assert out["contact"]["name"] == "Delta Fund"
assert out["body"] == ""
def test_no_email_sends_empty_string_not_none():
p = {"intent": "new_investor", "investor_name": "Gamma", "contact_name": "Bob",
"contact_email": None, "note": "x"}
out = crm_client.build_commit_payload(p)
assert out["contact"]["email"] == ""
def test_subject_blank_when_note_present_else_provenance_label():
# The CRM's grid note line uses subject-or-body, so a blank subject lets the note text show.
with_note = crm_client.build_commit_payload(
{"intent": "meeting_note", "investor_name": "Acme", "note": "sent the deck", "_match_id": "r1"})
assert with_note["subject"] == ""
assert with_note["body"] == "sent the deck"
# no note text → fall back to a provenance label so the grid line isn't empty
no_note = crm_client.build_commit_payload(
{"intent": "new_investor", "investor_name": "Beta", "contact_name": "X", "note": None})
assert no_note["subject"] == "Intake (Matrix)"
def _with_stub_authed(reply, capture=None):
"""Swap crm_client._authed for a canned (status, data); return a restorer."""
orig = crm_client._authed
def fake(method, path, body=None):
if capture is not None:
capture["path"] = path
return reply
crm_client._authed = fake
return orig
def test_match_parses_exact_match():
cap = {}
orig = _with_stub_authed((200, {"data": {
"match": {"id": "rowAcme", "investor_name": "Acme Capital", "matched_on": "name"},
"candidates": [],
}}), cap)
try:
res = crm_client.match({"investor_name": "Acme Capital", "contact_email": ""})
finally:
crm_client._authed = orig
assert res["match"] == {"id": "rowAcme", "name": "Acme Capital"}
assert res["candidates"] == []
assert "q=Acme" in cap["path"] # the query was forwarded
def test_match_returns_ranked_candidates_when_no_exact():
orig = _with_stub_authed((200, {"data": {"match": None, "candidates": [
{"id": "rowCharlie", "investor_name": "Charlie Brown", "score": 0.92, "matched_on": "name"},
{"id": "rowBeta", "investor_name": "Beta Capital LLC", "score": 0.86, "matched_on": "name"},
]}}))
try:
res = crm_client.match({"investor_name": "Charles Brown"})
finally:
crm_client._authed = orig
assert res["match"] is None
assert [c["id"] for c in res["candidates"]] == ["rowCharlie", "rowBeta"]
assert res["candidates"][0]["name"] == "Charlie Brown"
assert res["candidates"][0]["matched_on"] == "name"
def test_match_no_query_skips_network():
def boom(*a, **k):
raise AssertionError("should not hit the network when there's nothing to match on")
orig = crm_client._authed
crm_client._authed = boom
try:
res = crm_client.match({"investor_name": None, "contact_name": None, "contact_email": None})
finally:
crm_client._authed = orig
assert res == {"match": None, "candidates": []}
if __name__ == "__main__":
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for fn in fns:
fn()
print(f"ok {fn.__name__}")
print(f"\n{len(fns)} passed")