"""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", "city": "", "linkedin_url": "", "phone": "", "mobile": ""} assert out["body"] == "met at conf" assert out["source"] == "matrix_intake" def test_contact_carries_card_fields_when_present(): p = {"intent": "new_investor", "investor_name": "Acme Capital", "contact_name": "Jane Doe", "contact_email": "jane@acme.com", "city": "New York", "linkedin_url": "linkedin.com/in/janedoe", "phone": "212-555-0100", "mobile": "917-555-0199", "note": "met at conf"} out = crm_client.build_commit_payload(p) assert out["contact"]["city"] == "New York" assert out["contact"]["linkedin_url"] == "linkedin.com/in/janedoe" assert out["contact"]["phone"] == "212-555-0100" # office/main line assert out["contact"]["mobile"] == "917-555-0199" # cell 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 test_source_defaults_to_intake_and_card_overrides(): # Provenance: a typed note tags source="matrix_intake"; a scanned card rides in on # _source="matrix_card" (set by the bot's image handler) so the audit log distinguishes them. typed = crm_client.build_commit_payload( {"intent": "new_investor", "investor_name": "Acme", "note": "x"}) assert typed["source"] == "matrix_intake" card = crm_client.build_commit_payload( {"intent": "new_investor", "investor_name": "Acme", "note": "x", "_source": "matrix_card"}) assert card["source"] == "matrix_card" 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": []} def test_nl_query_returns_endpoint_data(): cap = {} orig = _with_stub_authed( (200, {"data": {"intent": "top_investors_committed", "rows": [], "summary": "ok"}}), cap) try: res = crm_client.nl_query("top investors") finally: crm_client._authed = orig assert res["intent"] == "top_investors_committed" assert cap["path"] == "/api/query/nl" def test_nl_query_passes_through_soft_503(): # Model-down still carries a structured body (the endpoint 503s with the error in `data`) — # return it for the renderer to surface, don't raise. orig = _with_stub_authed((503, {"data": {"error": "model_unavailable"}})) try: res = crm_client.nl_query("anything") finally: crm_client._authed = orig assert res["error"] == "model_unavailable" def test_nl_query_raises_on_auth_failure(): orig = _with_stub_authed((403, {"error": "Bot or admin required"})) raised = False try: crm_client.nl_query("x") except RuntimeError: raised = True finally: crm_client._authed = orig assert raised 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")