acd316ead4
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.
146 lines
6.1 KiB
Python
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()
|