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.
This commit is contained in:
Keysat
2026-06-18 11:59:38 -05:00
parent 48bd29aaa3
commit a10889b10b
9 changed files with 136 additions and 51 deletions
+18 -12
View File
@@ -1,5 +1,5 @@
"""Offline tests for the email-proposal review logic (card render, reply grammar, note revision).
The network/Matrix wiring lives in bot.py (live-smoke only); this covers the pure functions."""
"""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
@@ -11,7 +11,8 @@ 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": "✉ Received: asked about terms",
"snippet": "thanks for the deck — one question on terms",
"proposed_note": "✉ Jane Doe emailed the team: asked about terms",
}
@@ -25,18 +26,28 @@ def test_interpret_yes_no_else():
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 and "Received" in card
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 "✉ Received: asked about terms" 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_sent_direction():
assert "(Sent)" in email_proposals.render_card(dict(ITEM, direction="sent"))
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():
@@ -59,11 +70,6 @@ def test_revise_note_noop_or_empty_returns_none():
assert email_proposals.revise_note("n", "y", parse_fn=lambda *a, **k: None) is None
def test_closure_line_reflects_status():
assert "approved" in email_proposals.closure_line("approved").lower()
assert "dismiss" in email_proposals.closure_line("dismissed").lower()
if __name__ == "__main__":
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for fn in fns: