#!/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, 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,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"), ("e2", "Intro", "Good to meet you at the dinner.", "2026-05-01T10:00:00"), ("e3", "Spam", "ignore me", "2026-04-01T10:00:00"), # 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, "includes matched email body") check("Good to meet you" in ctx, "includes a second matched email") check("ignore me" not in ctx, "excludes email not linked to this investor") check(ctx.index("lock-up terms") < ctx.index("Good to meet you"), "newest email first") 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()