feat(projekt-matching): flow-2 stages with gate, CRM verify, notification, langfuse trace
This commit is contained in:
102
projekt-matching/tests/test_stages.py
Normal file
102
projekt-matching/tests/test_stages.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import datetime as dt
|
||||
from unittest import mock
|
||||
|
||||
from projektmatch import stages
|
||||
from projektmatch.config import Cfg
|
||||
|
||||
CFG = Cfg(imap_password="pw", espo_base="https://crm/api/v1",
|
||||
espo_api_key="k", notify_to="n@x", alert_to="a@x",
|
||||
vllm_base="http://v/v1", vllm_model="m", data_dir="/tmp")
|
||||
|
||||
EXTRACT = {
|
||||
"projectName": "Python Entwickler KI",
|
||||
"offerType": "Projekt", "buyerType": "agency",
|
||||
"companyName": "Aristo Group", "contactPerson": "Max Mann",
|
||||
"requirements": [
|
||||
{"text": "Python", "kat": "Must", "miscType": "other",
|
||||
"startDate": None, "workloadPercent": None, "remotePercent": None,
|
||||
"onsiteLocation": None},
|
||||
{"text": "K8s", "kat": "Nice", "miscType": "other",
|
||||
"startDate": None, "workloadPercent": None, "remotePercent": None,
|
||||
"onsiteLocation": None},
|
||||
{"text": "100 % remote", "kat": "Misc", "miscType": "location",
|
||||
"startDate": None, "workloadPercent": None, "remotePercent": 100,
|
||||
"onsiteLocation": None},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def ctx_after_match(ratings):
|
||||
ctx = {"canonical": "https://x/projekt/p", "title": "t", "status": "ok",
|
||||
"extract": dict(EXTRACT)}
|
||||
ctx["ratings"] = ratings
|
||||
return ctx
|
||||
|
||||
|
||||
def test_run_stage_catches_and_skips():
|
||||
def boom(ctx, cfg):
|
||||
raise ValueError("kaputt")
|
||||
ctx = stages.run_stage("fetch", boom, {"status": "ok"}, CFG)
|
||||
assert ctx["status"] == "failed" and "fetch: kaputt" in ctx["error"]
|
||||
untouched = stages.run_stage("extract", boom, dict(ctx), CFG)
|
||||
assert untouched["status"] == "failed" # boom not called again
|
||||
|
||||
|
||||
def test_stage_rules_consider_and_reject():
|
||||
yes = [{"nr": 1, "rating": "yes", "reason": "ok"},
|
||||
{"nr": 2, "rating": "yes", "reason": "ok"}]
|
||||
ctx = stages.stage_rules(ctx_after_match(yes), CFG)
|
||||
assert ctx["decision"] == "consider" and ctx["mustMatch"] == 100
|
||||
assert ctx["status"] == "ok"
|
||||
assert "| 1 | Must | ✅ | Python |" in ctx["description"]
|
||||
no = [{"nr": 1, "rating": "no", "reason": "fehlt"},
|
||||
{"nr": 2, "rating": "yes", "reason": "ok"}]
|
||||
ctx2 = stages.stage_rules(ctx_after_match(no), CFG)
|
||||
assert ctx2["status"] == "rejected" and ctx2["decision"] == "rejected"
|
||||
assert ctx2["mustMatch"] == 0
|
||||
|
||||
|
||||
def test_stage_crm_agency_linking_and_verify():
|
||||
ctx = ctx_after_match([{"nr": 1, "rating": "yes", "reason": "ok"},
|
||||
{"nr": 2, "rating": "yes", "reason": "ok"}])
|
||||
ctx = stages.stage_rules(ctx, CFG)
|
||||
espo = mock.Mock()
|
||||
espo.team_id.return_value = "T1"
|
||||
espo.ensure_account.return_value = "A1"
|
||||
espo.ensure_contact.return_value = "C1"
|
||||
espo.unique_opportunity_name.return_value = "Python Entwickler KI"
|
||||
espo.create_opportunity.return_value = {"id": "O1"}
|
||||
espo.get_opportunity.return_value = {
|
||||
"id": "O1", "name": "Python Entwickler KI",
|
||||
"cProjektlink": "https://x/projekt/p",
|
||||
"description": ctx["description"], "cAccount1Id": "A1",
|
||||
"accountId": None, "teamsIds": ["T1"]}
|
||||
out = stages.stage_crm(ctx, CFG, espo=espo)
|
||||
assert out["status"] == "created" and out["opportunityId"] == "O1"
|
||||
assert out["crmUrl"].endswith("#Opportunity/view/O1")
|
||||
payload = espo.create_opportunity.call_args.args[0]
|
||||
assert payload["cAccount1Id"] == "A1" and "accountId" not in payload
|
||||
assert payload["teamsIds"] == ["T1"]
|
||||
espo.ensure_account.assert_called_with("Aristo Group", "Reseller")
|
||||
|
||||
|
||||
def test_stage_crm_skips_when_rejected():
|
||||
ctx = {"status": "rejected", "decision": "rejected"}
|
||||
espo = mock.Mock()
|
||||
assert stages.stage_crm(ctx, CFG, espo=espo)["status"] == "rejected"
|
||||
espo.team_id.assert_not_called()
|
||||
|
||||
|
||||
def test_stage_notify_only_on_created():
|
||||
sent = []
|
||||
with mock.patch("projektmatch.stages.mailer.send_mail",
|
||||
lambda *a: sent.append(a)), \
|
||||
mock.patch("projektmatch.stages.pm_trace.post_trace", lambda c: None):
|
||||
ctx = {"status": "created", "projectName": "P", "mustMatch": 90,
|
||||
"niceMatch": None, "crmUrl": "https://crm/#Opportunity/view/O1"}
|
||||
stages.stage_notify(dict(ctx), CFG)
|
||||
assert sent[0][2] == "n@x"
|
||||
assert sent[0][3] == "[Projekt-Match] Freelancermap — 90 % — P"
|
||||
sent.clear()
|
||||
stages.stage_notify({"status": "rejected"}, CFG)
|
||||
assert not sent
|
||||
Reference in New Issue
Block a user