Files
ten31-database/backend/matrix_intake/test_email_proposals.py
T
Keysat a10889b10b Refine email-proposal review UX (v0.1.0:91)
Three post-smoke refinements to the Matrix email-proposal review:

1. Dash separators (bot): every card/reply is framed with a dash rule top and
   bottom so threads stop bleeding together vertically on mobile.

2. Remove decided threads (bot): on a conclusive approve/dismiss from either
   surface, the bot redacts the card (client.room_redact) so the room clears
   down to only undecided items. Redacting the bot's own card needs no power;
   the web->Matrix path now redacts instead of posting a closure note.

3. Clearer note wording (server v91 + bot): the proposed grid note now names who
   emailed whom -- "{teammate} emailed {investor}" (outbound) / "{sender} emailed
   the team" (inbound) -- instead of an ambiguous "Sent"/"Received". Outbound
   detection also matches our corporate domain (public providers excluded), so a
   teammate's mail from a non-enrolled @ten31.xyz address no longer reads as
   "Received". Going-forward only; no schema change. The card drops its bare
   direction label since the note now carries the relationship.

Tests updated; 30/30 green, render-smoke green.
2026-06-18 11:59:38 -05:00

79 lines
3.2 KiB
Python

"""Offline tests for the email-proposal review logic (card render, framing, reply grammar, note
revision). The network/Matrix wiring lives in bot.py (live-smoke only); this covers pure functions."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import email_proposals # noqa: E402
ITEM = {
"id": "p1", "investor_name": "Acme Capital", "direction": "received",
"from_name": "Jane Doe", "from_email": "jane@acme.com",
"email_subject": "Re: Fund III", "email_date": "2026-06-02",
"snippet": "thanks for the deck — one question on terms",
"proposed_note": "✉ Jane Doe emailed the team: asked about terms",
}
def test_interpret_yes_no_else():
assert email_proposals.interpret("yes") == "approve"
assert email_proposals.interpret(" Y ") == "approve"
assert email_proposals.interpret("") == "approve"
assert email_proposals.interpret("no") == "reject"
assert email_proposals.interpret("skip") == "reject"
# anything that isn't a clear yes/no is treated as a revision instruction
assert email_proposals.interpret("say we discussed the Q3 raise") == "revise"
def test_frame_wraps_with_rules():
out = email_proposals.frame("hello")
lines = out.split("\n")
assert lines[0] == email_proposals.RULE and lines[-1] == email_proposals.RULE
assert "hello" in out
def test_render_card_has_context_note_and_actions():
card = email_proposals.render_card(ITEM)
assert "Acme Capital" in card
assert "Jane Doe" in card
assert "Re: Fund III" in card and "2026-06-02" in card
assert "thanks for the deck" in card
assert "Jane Doe emailed the team: asked about terms" in card # the clear, named note
assert "yes" in card.lower() and "no" in card.lower()
def test_render_card_is_framed_and_dropless_direction():
card = email_proposals.render_card(ITEM)
assert card.startswith(email_proposals.RULE) and card.rstrip().endswith(email_proposals.RULE)
# the bare Sent/Received label is gone — the note itself names who emailed whom
assert "(Received)" not in card and "(Sent)" not in card
def test_render_card_truncates_long_snippet():
card = email_proposals.render_card(dict(ITEM, snippet="x" * 1000))
assert "" in card and len(card) < 1000
def test_revise_note_applies_model_output():
out = email_proposals.revise_note(
"old note", "make it about the Q3 raise",
parse_fn=lambda prompt, system=None, max_tokens=400: {"note": "Discussed the Q3 raise."})
assert out == "Discussed the Q3 raise."
def test_revise_note_noop_or_empty_returns_none():
# model echoes the same note unchanged -> None so the caller re-prompts (not "Updated")
assert email_proposals.revise_note("same", "x", parse_fn=lambda *a, **k: {"note": "same"}) is None
# model returns nothing usable -> None
assert email_proposals.revise_note("n", "y", parse_fn=lambda *a, **k: {}) is None
assert email_proposals.revise_note("n", "y", parse_fn=lambda *a, **k: None) is None
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")