#!/usr/bin/env python3 """Test the outreach agent's context assembly: it pulls the investor's CRM notes + recent matched email into the de-identifiable context block. Synthetic data only (guardrail #9). The scrub/Claude/rehydrate round-trip is exercised live in the preview. Run: cd backend && python3 mcp/test_outreach.py """ import os import sqlite3 import sys import tempfile sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import outreach_agent as oa # noqa: E402 FAILS = [] def check(cond, msg): print((" PASS " if cond else " FAIL ") + msg) if not cond: FAILS.append(msg) def main(): db = os.path.join(tempfile.mkdtemp(), "t.db") c = sqlite3.connect(db) c.row_factory = sqlite3.Row c.executescript(""" CREATE TABLE fundraising_investors (id TEXT PRIMARY KEY, investor_name TEXT, notes TEXT); CREATE TABLE emails (id TEXT PRIMARY KEY, subject TEXT, body_text TEXT, snippet TEXT, sent_at TEXT, from_email TEXT, to_emails_json TEXT, thread_id TEXT, is_matched INT); CREATE TABLE email_investor_links (id TEXT, email_id TEXT, fundraising_investor_id TEXT); """) c.execute("INSERT INTO fundraising_investors VALUES ('inv1','Harbor & Vine','Met at the conference; interested in Fund III.')") c.executemany("INSERT INTO emails (id,subject,body_text,sent_at,thread_id,is_matched) VALUES (?,?,?,?,?,1)", [ ("e1", "Re: Fund III", "Thanks for the call. We are still weighing the lock-up terms.", "2026-06-02T10:00:00", "t1"), ("e2", "Intro", "Good to meet you at the dinner.", "2026-05-01T10:00:00", "t0"), ("e3", "Spam", "ignore me", "2026-04-01T10:00:00", "t9"), # not linked -> excluded ]) c.executemany("INSERT INTO email_investor_links (id,email_id,fundraising_investor_id) VALUES (?,?, 'inv1')", [("l1", "e1"), ("l2", "e2")]) c.commit() name, ctx = oa._context(c, "inv1") check(name == "Harbor & Vine", f"resolves investor name (got {name!r})") check("Met at the conference" in ctx, "includes CRM notes") check("lock-up terms" in ctx, "active-thread email present") check("Good to meet you" in ctx, "earlier email present as background") check("ignore me" not in ctx, "excludes email not linked to this investor") check("Active conversation" in ctx and "Earlier emails" in ctx and ctx.index("lock-up terms") < ctx.index("Good to meet you"), "active thread is separated from background, active first") # voice examples: the sender's own sent emails (few-shot + transparency) c.execute("INSERT INTO emails (id,subject,body_text,sent_at,from_email,to_emails_json,thread_id,is_matched) " "VALUES ('v1','My note','Hi there, quick update on the fund. Best, Grant'," "'2026-06-01T10:00:00','grant@ten31.xyz','[{\"email\":\"lp@x.example\"}]','tv',1)") c.commit() blocks, meta = oa._voice_examples(c, "grant@ten31.xyz") check(len(blocks) == 1 and "quick update on the fund" in blocks[0], "voice example pulls the sender's own email") check(len(meta) == 1 and meta[0]["subject"] == "My note" and meta[0]["to"] == "lp@x.example", "voice meta carries subject + recipient for transparency") check(oa._voice_examples(c, None) == ([], []), "no sender -> no voice examples") n2, c2 = oa._context(c, "missing") check(n2 is None and c2 is None, "unknown investor -> (None, None)") # type catalogue is intact check(set(["intro", "follow_up", "fund_update", "meeting_follow_up", "nurture"]) <= set(oa.OUTREACH_TYPES), "outreach types catalogue present") # ── follow-up radar ── rc = sqlite3.connect(os.path.join(tempfile.mkdtemp(), "radar.db")) rc.row_factory = sqlite3.Row rc.executescript(""" CREATE TABLE fundraising_investors (id TEXT PRIMARY KEY, investor_name TEXT, follow_up TEXT, graveyard TEXT); CREATE TABLE emails (id TEXT PRIMARY KEY, from_email TEXT, sent_at TEXT, is_matched INT); CREATE TABLE email_investor_links (id TEXT, email_id TEXT, fundraising_investor_id TEXT); """) rc.executemany("INSERT INTO fundraising_investors (id,investor_name,follow_up,graveyard) VALUES (?,?,?,?)", [ ("owe", "Owe Reply LP", None, None), # they emailed last, 5d ago -> tier 0 ("warm", "Warm Quiet LP", None, None), # we emailed last, 60d ago -> tier 2 ("fresh", "Fresh LP", None, None), # we emailed 4d ago -> not surfaced ("buried", "Buried LP", None, "1"), # graveyard -> excluded ]) OURS = "grant@ten31.xyz" em = [ ("o1", "lp@owe.example", "2026-06-04T10:00:00", "owe"), # inbound, 5 days before 06-09 ("w1", OURS, "2026-04-10T10:00:00", "warm"), # outbound, ~60 days ("w0", "lp@warm.example", "2026-04-01T10:00:00", "warm"), # 2nd email for history ("f1", OURS, "2026-06-05T10:00:00", "fresh"), # outbound, 4 days -> too recent ("b1", "lp@buried.example", "2026-01-01T10:00:00", "buried"), ] for eid, frm, sent, inv in em: rc.execute("INSERT INTO emails (id,from_email,sent_at,is_matched) VALUES (?,?,?,1)", (eid, frm, sent)) rc.execute("INSERT INTO email_investor_links (id,email_id,fundraising_investor_id) VALUES (?,?,?)", (eid + "l", eid, inv)) rc.commit() radar = oa.follow_up_radar(rc, [OURS], "2026-06-09T00:00:00", warm_days=45) names = [x["name"] for x in radar] check("Owe Reply LP" in names and "Warm Quiet LP" in names, f"surfaces owe-reply + warm-quiet (got {names})") check("Fresh LP" not in names, "recent contact not surfaced") check("Buried LP" not in names, "graveyard excluded") check(radar[0]["name"] == "Owe Reply LP" and radar[0]["tier"] == 0, "owe-a-reply ranked first (tier 0)") owe = next(x for x in radar if x["name"] == "Owe Reply LP") check("owe a reply" in owe["reason"] and owe["suggested_type"] == "follow_up", "owe-reply reason + suggested type") warm = next(x for x in radar if x["name"] == "Warm Quiet LP") check(warm["tier"] == 2 and warm["suggested_type"] == "nurture", "warm-quiet is tier 2, suggests nurture") if FAILS: print(f"\nFAILED ({len(FAILS)})") for f in FAILS: print(" - " + f) sys.exit(1) print("\nALL PASS (outreach context assembly)") if __name__ == "__main__": main()