Spaces:
Sleeping
Sleeping
Download tests/test_prospection_cli.py from Viney/readthrough: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/Viney/readthrough/resolve/main/tests/test_prospection_cli.py
- Command line
-
hf download hf://spaces/Viney/readthrough/tests/test_prospection_cli.py
-
curl -L -o test_prospection_cli.py https://huggingface.co/spaces/Viney/readthrough/resolve/main/tests/test_prospection_cli.py
10.5 kB
| # tests/test_prospection_cli.py | |
| """tests/test_prospection_cli.py — export .eml et surface de la CLI.""" | |
| from __future__ import annotations | |
| import email | |
| from pathlib import Path | |
| import pytest | |
| from prospection import cli | |
| from prospection.enrich.base import hash_email | |
| from prospection.outreach import compliance, eml | |
| from prospection.store import prospects_db as db | |
| def tmp_db(tmp_path, monkeypatch): | |
| monkeypatch.setattr(db, "DB_PATH", tmp_path / "prospects.db") | |
| monkeypatch.setenv("PROSPECTION_SENDER_NAME", "Vincent Durey") | |
| monkeypatch.setenv("PROSPECTION_SENDER_EMAIL", "vincent@example.fr") | |
| monkeypatch.setenv("PROSPECTION_OPTOUT_EMAIL", "stop@example.fr") | |
| db.init_db() | |
| def test_render_eml_produces_a_parsable_message(): | |
| raw = eml.render_eml("Sujet", "Corps du message", "marie@x.fr") | |
| parsed = email.message_from_bytes(raw) | |
| assert parsed["Subject"] == "Sujet" | |
| assert parsed["To"] == "marie@x.fr" | |
| assert parsed["From"] == "Vincent Durey <vincent@example.fr>" | |
| def test_render_eml_attaches_the_pdf_as_a_second_part(tmp_path): | |
| pdf = tmp_path / "brief.pdf" | |
| pdf.write_bytes(b"%PDF-1.4 fake") | |
| parsed = email.message_from_bytes(eml.render_eml("S", "B", "m@x.fr", attachment=pdf)) | |
| names = [part.get_filename() for part in parsed.walk()] | |
| assert "brief.pdf" in names | |
| def test_render_eml_has_no_tracking_pixel_and_no_html_part(): | |
| parsed = email.message_from_bytes(eml.render_eml("S", "B", "m@x.fr")) | |
| types = [part.get_content_type() for part in parsed.walk()] | |
| assert "text/html" not in types | |
| def test_cli_rejects_an_unknown_command(capsys): | |
| with pytest.raises(SystemExit): | |
| cli.main(["inexistant"]) | |
| def test_cli_report_runs_on_an_empty_database(capsys): | |
| assert cli.main(["report"]) == 0 | |
| assert "Tier 1" in capsys.readouterr().out | |
| def test_cli_optout_add_records_the_hash_and_is_idempotent(): | |
| assert cli.main(["optout", "add", "marie@x.fr"]) == 0 | |
| assert cli.main(["optout", "add", "marie@x.fr"]) == 0 | |
| with db.connect() as conn: | |
| assert conn.execute("SELECT COUNT(*) c FROM suppression").fetchone()["c"] == 1 | |
| def _approved_touch(email, **contact_overrides): | |
| """Compte + contact + touche approuvée, prêts à l'export.""" | |
| account_id = db.upsert_account({"no_amf": "GP-1", "nom": "B", "statut": "vivant", | |
| "independante": 1}) | |
| row = {"account_id": account_id, "prenom": "Marie", "nom": "Durand", | |
| "titre": "Gérante", "seniorite": "c-suite", "email": email, | |
| "email_sha256": hash_email(email) if email else None, | |
| "email_status": "probable", "telephone": None, "linkedin_url": None, | |
| "source": "pattern", "confidence": "probable", | |
| "retrieved_at": "2026-08-29T09:00:00", "is_generic": 0} | |
| row.update(contact_overrides) | |
| contact_id = db.upsert_contact(row) | |
| touch_id = db.upsert_touch({"contact_id": contact_id, "channel": "email", | |
| "template_id": "email_1", "subject": "S", "body": "B", | |
| "status": "approved"}) | |
| return account_id, contact_id, touch_id | |
| def test_cli_campaign_export_refuses_a_contact_who_opted_out(tmp_path): | |
| """La dernière barrière avant qu'un fichier quitte la machine. Sans ce test, | |
| supprimer entièrement l'appel à `assert_sendable` ne ferait échouer aucun | |
| test de la suite.""" | |
| _approved_touch("marie.durand@x.fr") | |
| compliance.record_optout("marie.durand@x.fr") | |
| assert cli.main(["campaign", "export", "--out", str(tmp_path)]) == 0 | |
| assert list(tmp_path.glob("*.eml")) == [] | |
| def test_cli_campaign_export_writes_an_eml_for_a_clean_contact(tmp_path): | |
| """Le pendant du test précédent : sans lui, une barrière qui refuse tout | |
| passerait pour une barrière qui fonctionne.""" | |
| _approved_touch("marie.durand@x.fr") | |
| assert cli.main(["campaign", "export", "--out", str(tmp_path)]) == 0 | |
| assert len(list(tmp_path.glob("*.eml"))) == 1 | |
| def test_cli_campaign_export_skips_a_contact_with_no_address(tmp_path): | |
| """Un contact issu de LinkedIn n'a pas d'adresse et n'est pas générique : | |
| rien en amont ne l'écarte, et `EmailMessage` écrirait « To: None ».""" | |
| _approved_touch(None) | |
| assert cli.main(["campaign", "export", "--out", str(tmp_path)]) == 0 | |
| assert list(tmp_path.glob("*.eml")) == [] | |
| def test_render_eml_refuses_an_empty_recipient(): | |
| with pytest.raises(ValueError): | |
| eml.render_eml("S", "B", "") | |
| def test_cli_campaign_export_refuses_a_draft_that_is_not_approved(tmp_path): | |
| account_id = db.upsert_account({"no_amf": "GP-1", "nom": "B", "statut": "vivant", | |
| "independante": 1}) | |
| contact_id = db.upsert_contact({"account_id": account_id, "email": "m@x.fr", | |
| "email_sha256": "d" * 64, "email_status": "probable", | |
| "source": "pattern", "confidence": "probable", | |
| "retrieved_at": "2026-08-29T09:00:00", "is_generic": 0}) | |
| db.upsert_touch({"contact_id": contact_id, "channel": "email", "template_id": "email_1", | |
| "subject": "S", "body": "B", "status": "draft"}) | |
| assert cli.main(["campaign", "export", "--out", str(tmp_path)]) == 0 | |
| assert list(tmp_path.glob("*.eml")) == [] | |
| def test_contact_rank_puts_the_decision_maker_first(): | |
| manager = {"titre": "Gérante fondatrice", "email_status": "probable"} | |
| research = {"titre": "Head of Research", "email_status": "verified"} | |
| compliance_officer = {"titre": "RCCI", "email_status": "verified"} | |
| ranked = sorted([compliance_officer, research, manager], key=cli._contact_rank) | |
| assert [c["titre"] for c in ranked] == ["Gérante fondatrice", "Head of Research", "RCCI"] | |
| def test_contact_rank_breaks_ties_on_address_quality(): | |
| """À rang égal, l'adresse vérifiée passe devant la probable : la règle « un | |
| seul contact » rend ce départage décisif.""" | |
| probable = {"titre": "Head of Research", "email_status": "probable"} | |
| verified = {"titre": "Head of Research", "email_status": "verified"} | |
| assert sorted([probable, verified], key=cli._contact_rank)[0] is verified | |
| def _tier1_ready_account(no_amf: str = "GP-1") -> int: | |
| """Un compte que `score --final` promeut réellement en Tier 1. | |
| Sans poche actions, sans position confirmée et sans contact nommé, un compte | |
| finit en Tier 0 quoi qu'il arrive : un test du veto de démo construit sur une | |
| coquille passerait sans rien vérifier. | |
| """ | |
| account_id = db.upsert_account({ | |
| "no_amf": no_amf, "nom": "Boutique Gestion SAS", "statut": "vivant", | |
| "independante": 1, "effectif": 18, "type_activite": "Actions", | |
| "dirigeants": ["Marie Durand"]}) | |
| holding_id = db.upsert_holding({ | |
| "account_id": account_id, "fund_name": "Fonds Actions Internationales", | |
| "raw_label": "Microsoft Corp", "ticker": "MSFT", "weight_pct": 5.4, | |
| "as_of": "2026-07-31", "source_url": "https://x.fr/r.pdf", | |
| "confidence": "probable", "retrieved_at": "2026-08-29T09:00:00"}) | |
| db.confirm_holding(holding_id, "2026-08-29T09:00:00") | |
| db.upsert_contact({ | |
| "account_id": account_id, "prenom": "Marie", "nom": "Durand", | |
| "titre": "Gérante fondatrice", "seniorite": "c-suite", | |
| "email": "marie.durand@x.fr", "email_sha256": hash_email("marie.durand@x.fr"), | |
| "email_status": "probable", "source": "pattern", "confidence": "probable", | |
| "retrieved_at": "2026-08-29T09:00:00", "is_generic": 0}) | |
| return account_id | |
| def test_best_unconfirmed_holding_is_the_heaviest_line_nobody_confirmed(): | |
| """`universe_inferred` signifie exactement qu'aucune position n'est | |
| confirmée. Sans cet accesseur, `campaign draft` passait `confirmed[0]` — | |
| donc `None` — et le prompt présentait la chaîne « None » comme un fait.""" | |
| account_id = db.upsert_account({"no_amf": "GP-2", "nom": "B2", "statut": "vivant", | |
| "independante": 1}) | |
| for label, weight in (("Apple Inc", 2.0), ("Microsoft Corp", 7.5)): | |
| db.upsert_holding({"account_id": account_id, "fund_name": "Fonds A", | |
| "raw_label": label, "ticker": None, "weight_pct": weight, | |
| "as_of": "2026-07-31", "source_url": "https://x.fr/r.pdf", | |
| "confidence": "probable", | |
| "retrieved_at": "2026-08-29T09:00:00"}) | |
| heaviest = db.upsert_holding({"account_id": account_id, "fund_name": "Fonds A", | |
| "raw_label": "Nvidia Corp", "ticker": "NVDA", | |
| "weight_pct": 9.9, "as_of": "2026-07-31", | |
| "source_url": "https://x.fr/r.pdf", | |
| "confidence": "probable", | |
| "retrieved_at": "2026-08-29T09:00:00"}) | |
| db.confirm_holding(heaviest, "2026-08-29T09:00:00") | |
| best = db.best_unconfirmed_holding(account_id) | |
| assert best["raw_label"] == "Microsoft Corp" | |
| def test_a_rescore_promotes_an_account_with_nothing_against_it(): | |
| """Le témoin du test suivant : sans lui, un veto qui refuse tout passerait | |
| pour un veto qui fonctionne.""" | |
| account_id = _tier1_ready_account() | |
| assert cli.main(["score", "--final"]) == 0 | |
| assert db.get_account_by_id(account_id)["tier"] == 1 | |
| def test_a_blocked_demo_survives_a_rescore(): | |
| """`assign_final` ne sait rien de la démo : sans persistance, le rescore | |
| suivant annulerait le constat qu'aucun brief n'est livrable.""" | |
| account_id = _tier1_ready_account() | |
| db.block_demo(account_id, "2026-08-29T09:00:00") | |
| assert cli.main(["score", "--final"]) == 0 | |
| assert db.get_account_by_id(account_id)["tier"] == 2 | |
| def test_build_demo_blocks_the_account_when_no_brief_ships(tmp_path): | |
| """La marque doit être posée par la démo elle-même, pas seulement lisible.""" | |
| from prospection.demo import build_demo as demo_module | |
| account_id = _tier1_ready_account() | |
| assert demo_module.build_demo( | |
| account_id, tmp_path, | |
| brief_runner=lambda ticker: {"status": "PARTIAL", "evidence_coverage": {}}, | |
| pdf_builder=lambda brief, ticker: b"%PDF") is None | |
| assert db.get_account_by_id(account_id)["demo_blocked_at"] | |
| assert cli.main(["score", "--final"]) == 0 | |
| assert db.get_account_by_id(account_id)["tier"] == 2 | |