Files
ten31-database/backend/test_grid_email_heal.py
T
Keysat acd316ead4 Review fixes: narrow intake redact predicate to the bot's own nudge + edge tests
reviewer agent flagged the broadened redact_thread predicate (event_id OR in_reply==root)
as over-matching any plain reply to a thread root. Gate the bare-in_reply clause to the bot's
own sender (the nudge is always ours); thread children (cards/acks/human yes-no) still match by
rel_type=m.thread. Add unit edges for _name_similarity's all-generic fallback and a contact_id
NULL orphan case for the grid-blob email heal.
2026-06-20 13:05:13 -05:00

146 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Regression: GET /api/fundraising/state heals blank grid-pill emails from the relational mirror.
The grid blob is canonical for the mobile "Edit investor" sheet, but an email can reach a linked
classic contact (email capture / a contact edit) without ever being written back into the blob pill
— so the edit form showed an empty email for a contact the directory clearly had (Grant, 2026-06-20).
The state handler now fills a blank pill email from fundraising_contacts.email, else the linked
contacts.email, matched by pill order then name. This asserts:
- a blank pill whose linked contact has an email is HEALED on read;
- a blank pill whose linked contact is also blank stays blank;
- a pill that already carries an email in the blob is NEVER overwritten (fill-only).
Synthetic data only.
Run: cd backend && python3 test_grid_email_heal.py
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
from http.server import ThreadingHTTPServer
_DATA = tempfile.mkdtemp()
os.environ["CRM_DATA_DIR"] = _DATA
os.environ["CRM_DB_PATH"] = os.path.join(_DATA, "crm.db")
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import server # noqa: E402
FAILS = []
def check(cond, msg):
print((" PASS " if cond else " FAIL ") + msg)
if not cond:
FAILS.append(msg)
class _Quiet(server.CRMHandler):
def log_message(self, *a):
pass
def _get_state(port, token):
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
conn.request("GET", "/api/fundraising/state", headers={"Authorization": "Bearer " + token})
resp = conn.getresponse()
raw = resp.read().decode("utf-8", "replace")
conn.close()
return resp.status, (json.loads(raw) if raw else None)
GRID = {
"columns": [{"id": "investor_name", "label": "Investor", "type": "text"},
{"id": "contacts", "label": "Contacts", "type": "contacts"}],
"rows": [
{"id": "rowW", "investor_name": "Wyoming", "notes": "",
"contacts": [{"name": "Philip Treick", "email": "", "title": ""},
{"name": "Jose Briones", "email": "", "title": ""}]},
{"id": "rowA", "investor_name": "Acme Capital", "notes": "",
"contacts": [{"name": "Jane Doe", "email": "keep@acme.com", "title": ""}]},
{"id": "rowO", "investor_name": "Orphan LP", "notes": "",
"contacts": [{"name": "No Link", "email": "", "title": ""}]},
],
}
def seed():
c = sqlite3.connect(os.environ["CRM_DB_PATH"])
c.execute("INSERT INTO users (id,username,email,password_hash,full_name,role,is_active) "
"VALUES ('u1','grant','grant@ten31.example','x','Grant','admin',1)")
c.execute("INSERT INTO fundraising_state (id, grid_json, views_json, version) "
"VALUES ('main', ?, '[]', 1) "
"ON CONFLICT(id) DO UPDATE SET grid_json = excluded.grid_json", (json.dumps(GRID),))
# Classic contacts directory: Jose has the captured email the blob never got; Philip is blank.
c.execute("INSERT INTO contacts (id,first_name,last_name,email) VALUES "
"('c-phil','Philip','Treick',''),"
"('c-jose','Jose','Briones','jbriones@uwyo.edu'),"
"('c-jane','Jane','Doe','other@acme.com')") # differs from the blob's keep@acme.com
# Relational mirror (what sync_fundraising_relational would build): blank fc.email, linked contact_id.
c.execute("INSERT INTO fundraising_investors (id,investor_name,source_row_id,total_invested) VALUES "
"('inv-w','Wyoming','rowW',0),('inv-a','Acme Capital','rowA',0),('inv-o','Orphan LP','rowO',0)")
# fc-orphan has contact_id NULL (pre-0004 orphan) and blank email — nothing to heal from.
c.execute("INSERT INTO fundraising_contacts (id,investor_id,full_name,email,sort_order,contact_id) VALUES "
"('fc-phil','inv-w','Philip Treick','',0,'c-phil'),"
"('fc-jose','inv-w','Jose Briones','',1,'c-jose'),"
"('fc-jane','inv-a','Jane Doe','',0,'c-jane'),"
"('fc-orphan','inv-o','No Link','',0,NULL)")
c.commit()
c.close()
def main():
server.init_db()
seed()
token = server.create_token("u1", "grant", "admin")
httpd = ThreadingHTTPServer(("127.0.0.1", 0), _Quiet)
port = httpd.server_address[1]
threading.Thread(target=httpd.serve_forever, daemon=True).start()
try:
st, d = _get_state(port, token)
rows = ((d or {}).get("data", {}).get("grid", {}) or {}).get("rows", [])
by_id = {r.get("id"): r for r in rows}
w = by_id.get("rowW", {})
a = by_id.get("rowA", {})
wc = w.get("contacts", [])
ac = a.get("contacts", [])
print("\n[heal: blank pill email filled from the linked contact (Jose)]")
jose = next((c for c in wc if c.get("name") == "Jose Briones"), {})
check(st == 200 and jose.get("email") == "jbriones@uwyo.edu",
f"Jose pill healed to jbriones@uwyo.edu (got {jose.get('email')!r})")
print("\n[heal: blank pill whose contact is also blank stays blank (Philip)]")
phil = next((c for c in wc if c.get("name") == "Philip Treick"), {})
check(phil.get("email", "") == "",
f"Philip pill stays blank (got {phil.get('email')!r})")
print("\n[heal: a pill that already has an email is never overwritten (Jane)]")
jane = next((c for c in ac if c.get("name") == "Jane Doe"), {})
check(jane.get("email") == "keep@acme.com",
f"Jane pill keeps its blob email, not the contact's (got {jane.get('email')!r})")
print("\n[heal: a pill whose fundraising_contacts row has contact_id NULL stays blank (orphan)]")
o = by_id.get("rowO", {})
orphan = next((c for c in o.get("contacts", []) if c.get("name") == "No Link"), {})
check(orphan.get("email", "") == "",
f"orphan pill (no contact_id, no email source) stays blank (got {orphan.get('email')!r})")
finally:
httpd.shutdown()
print()
if FAILS:
print(f"FAILED ({len(FAILS)}):")
for f in FAILS:
print(f" - {f}")
sys.exit(1)
print("ALL PASS (grid email heal)")
if __name__ == "__main__":
main()