"""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.""" 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": "✉ Received: 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_render_card_has_context_note_and_actions(): card = email_proposals.render_card(ITEM) assert "Acme Capital" in card and "Received" 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 "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_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 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: fn() print(f"ok {fn.__name__}") print(f"\n{len(fns)} passed")