Files
ten31-database/backend/matrix_intake/test_proposals.py
T
Keysat aefb2aa119 Matrix intake: main-timeline nudge, clearer messages, note text in grid
Four bot-side UX fixes surfaced by the live smoke:
- Post a brief pointer in the main timeline (a reply to the user's message)
  alongside the in-thread proposal card, so proposals aren't missed inside a
  thread. Pointer only — approvals still happen in the thread, where the note
  is visible (you can't make an informed yes/no without seeing it).
- A bare yes/no typed in the main timeline while a proposal is pending now
  gets a "reply in the thread" redirect instead of "couldn't tell what to record."
- Clearer commit confirmations: "Created a new grid entry for X" vs
  "Logged a note on X (existing grid entry)."
- Send a blank communication subject when a note is present so the grid's
  one-line note summary shows the note text, not the "(Matrix)" label
  (provenance stays in source="matrix_intake").
2026-06-17 17:14:08 -05:00

114 lines
3.7 KiB
Python

"""Tests for the proposal store + approval state machine (pure logic, no network)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import proposals # noqa: E402
SAMPLE = {"intent": "new_investor", "investor_name": "Acme Capital",
"contact_name": "Jane Doe", "contact_email": "jane@acme.com",
"contact_title": None, "note": "met at conf"}
def test_store_put_get_pop():
s = proposals.ProposalStore()
assert not s.has("$root")
s.put("$root", SAMPLE)
assert s.has("$root")
assert s.get("$root")["investor_name"] == "Acme Capital"
assert s.pop("$root")["investor_name"] == "Acme Capital"
assert not s.has("$root")
assert s.pop("$missing") is None
def test_store_any_pending():
s = proposals.ProposalStore()
assert not s.any_pending()
s.put("$r", SAMPLE)
assert s.any_pending()
s.pop("$r")
assert not s.any_pending()
def test_interpret_yes_variants():
for t in ("yes", "Y", "approve", " ok ", "👍"):
assert proposals.interpret_reply(t)[0] == "approve", t
def test_interpret_no_variants():
for t in ("no", "N", "cancel", "discard", ""):
assert proposals.interpret_reply(t)[0] == "reject", t
def test_interpret_edit_equals():
action, payload = proposals.interpret_reply("edit email=new@acme.com")
assert action == "edit"
assert payload == ("contact_email", "new@acme.com")
def test_interpret_edit_colon_and_alias():
action, payload = proposals.interpret_reply("firm: Acme Capital LLC")
assert action == "edit"
assert payload == ("investor_name", "Acme Capital LLC")
def test_interpret_unknown():
assert proposals.interpret_reply("maybe later")[0] == "unknown"
def test_interpret_edit_colon_value_contains_equals():
# the '=' inside the value must not break parsing — split on ':' first, keep the rest
action, payload = proposals.interpret_reply("note: see deck=v2")
assert action == "edit"
assert payload == ("note", "see deck=v2")
def test_claim_once_pop_guards_double_approve():
# the double-approve guard relies on pop() yielding the proposal exactly once;
# a second claim returns None so a racing second 'yes' is a no-op
s = proposals.ProposalStore()
s.put("$r", SAMPLE)
assert s.pop("$r") is not None
assert s.pop("$r") is None
def test_edit_with_unknown_field_is_not_an_edit():
# an unknown field name must not silently become an edit
assert proposals.interpret_reply("edit zipcode=90210")[0] == "unknown"
def test_apply_edit_is_nondestructive():
updated = proposals.apply_edit(SAMPLE, "contact_email", "x@y.com")
assert updated["contact_email"] == "x@y.com"
assert SAMPLE["contact_email"] == "jane@acme.com" # original untouched
def test_render_includes_fields_and_instructions():
text = proposals.render(SAMPLE)
assert "Acme Capital" in text
assert "jane@acme.com" in text
assert "yes" in text.lower() and "no" in text.lower()
def test_render_meeting_note_variant():
note = dict(SAMPLE, intent="meeting_note")
assert "meeting note" in proposals.render(note).lower()
def test_summary_line_new_vs_note():
new_line = proposals.summary_line(SAMPLE)
assert "Acme Capital" in new_line and "new investor" in new_line.lower()
note_line = proposals.summary_line(dict(SAMPLE, intent="meeting_note"))
assert "Acme Capital" in note_line and "meeting note" in note_line.lower()
# the nudge must point the user to the thread, where the actual action lives
assert "thread" in new_line.lower()
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")