You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
155 lines
9.5 KiB
Python
155 lines
9.5 KiB
Python
# backend/tests/test_triage_baseline.py
|
|
# 아리 정리(비서) 기준 시각: smart 보기/분석은 '기준 이후 도착한 새 메일'만 다룬다.
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from app.models import AppSetting, TriageEvent
|
|
from tests._factories import make_email, make_mail_account
|
|
|
|
|
|
def _set_baseline(s, dt: datetime) -> None:
|
|
"""기준 시각(mail.triage_since)을 직접 설정 — 수동 리셋 엔드포인트가 없어졌으므로 테스트에서 주입."""
|
|
row = s.get(AppSetting, "mail.triage_since")
|
|
if row:
|
|
row.value = dt.isoformat()
|
|
s.add(row)
|
|
else:
|
|
s.add(AppSetting(key="mail.triage_since", value=dt.isoformat()))
|
|
s.commit()
|
|
|
|
|
|
def _seed_old_new(s, since: datetime):
|
|
"""기준 시각 전/후로 도착한 집중(focused) 안읽음 메일을 하나씩 생성."""
|
|
make_mail_account(s, id="work", name="회사", tone="coral", sort_order=0)
|
|
make_email(s, id="old", account="work", subject="지난 메일",
|
|
received_at=since - timedelta(hours=2))
|
|
make_email(s, id="new", account="work", subject="새 메일",
|
|
received_at=since + timedelta(hours=2))
|
|
|
|
|
|
def test_smart_folder_shows_only_new_mail(client, session):
|
|
s, _ = session
|
|
since = datetime.fromisoformat(client.get("/api/mail/triage/baseline").json()["since"])
|
|
_seed_old_new(s, since)
|
|
# 아리 정리(smart): 기준 이후 도착한 'new'만.
|
|
smart = [r["id"] for r in client.get("/api/mail?folder=smart").json()]
|
|
assert smart == ["new"]
|
|
# 사이드바 배지(새 메일 수, 읽음 무관)도 기준 이후 새 메일만 센다.
|
|
assert client.get("/api/mail/triage/baseline").json()["count"] == 1
|
|
# 일반 받은편지함(inbox)은 둘 다 보인다(기준 무관).
|
|
inbox = sorted(r["id"] for r in client.get("/api/mail?folder=inbox").json())
|
|
assert inbox == ["new", "old"]
|
|
|
|
|
|
def test_baseline_get_initializes(client, session):
|
|
"""기준 시각은 첫 GET 조회 때 now 로 lazy-init 되고 이후 안정적으로 유지된다.
|
|
(수동 재설정 POST 는 제거 — 새 메일 정리는 전부 자동이라 사용자가 기준을 만질 일이 없다.)"""
|
|
s, _ = session
|
|
first = client.get("/api/mail/triage/baseline").json()["since"]
|
|
assert first # 첫 조회 시 now 로 lazy-init
|
|
assert client.get("/api/mail/triage/baseline").json()["since"] == first # 재조회 시 유지
|
|
|
|
|
|
def test_smart_count_excludes_analyzed_informational(client, session):
|
|
"""분석이 끝난 '정보성'(액션 없음·낮음) 새 메일은 아리 정리 카드가 안 생기므로 배지에서 제외 —
|
|
배지(count)와 아리 정리 패널(카드) 숫자를 일치시킨다(배지 1인데 패널은 비어 보이는 혼선 방지)."""
|
|
s, _ = session
|
|
since = datetime.fromisoformat(client.get("/api/mail/triage/baseline").json()["since"])
|
|
make_mail_account(s, id="work", name="회사", tone="coral", sort_order=0)
|
|
make_email(s, id="info", account="work", subject="운임 변경 알림", read=True,
|
|
received_at=since + timedelta(hours=1),
|
|
ai_json={"summary": "안내", "priority": "낮음",
|
|
"tasks": [], "events": [], "replies": []})
|
|
# 카드가 없으므로 배지 0 — 단, 요약은 '아리가 1통을 읽었다'를 보고한다(빈 화면≠고장).
|
|
b = client.get("/api/mail/triage/baseline").json()
|
|
assert b["count"] == 0 and b["new_total"] == 1 and b["screened"] == 0
|
|
# 단, 받은편지함/smart 목록에는 그대로 남는다(분석만 끝났을 뿐 — 패널이 카드로 필터).
|
|
assert [r["id"] for r in client.get("/api/mail?folder=smart").json()] == ["info"]
|
|
|
|
|
|
def test_triage_summary_reports_screened(client, session):
|
|
"""홍보·스팸 등 집중이 아닌 새 메일은 screened 로 집계 — 패널이 '아리가 N통 걸렀어요'를 보여준다."""
|
|
s, _ = session
|
|
since = datetime.fromisoformat(client.get("/api/mail/triage/baseline").json()["since"])
|
|
make_mail_account(s, id="work", name="회사", tone="coral", sort_order=0)
|
|
# 집중(primary) 새 메일 1통 + 홍보 새 메일 1통
|
|
make_email(s, id="focus", account="work", subject="검토 요청",
|
|
received_at=since + timedelta(hours=1),
|
|
ai_json={"summary": "검토", "priority": "보통",
|
|
"tasks": [{"text": "검토"}], "events": [], "replies": []})
|
|
make_email(s, id="promo", account="work", subject="여름 세일", labels=["프로모션"],
|
|
received_at=since + timedelta(hours=1))
|
|
b = client.get("/api/mail/triage/baseline").json()
|
|
assert b["new_total"] == 2 # 새 메일 총 2통
|
|
assert b["screened"] == 1 # 그중 홍보 1통은 집중이 아니라 자동으로 걸러짐
|
|
assert b["count"] == 1 # 카드는 집중 액션 메일 1통만
|
|
|
|
|
|
def test_smart_count_includes_actionable(client, session):
|
|
"""할 일/일정/답장이 있거나 중요(높음)인 분석 메일은 카드가 생기므로 배지에 포함(읽음 무관)."""
|
|
s, _ = session
|
|
since = datetime.fromisoformat(client.get("/api/mail/triage/baseline").json()["since"])
|
|
make_mail_account(s, id="work", name="회사", tone="coral", sort_order=0)
|
|
make_email(s, id="todo", account="work", subject="검토 요청", read=True,
|
|
received_at=since + timedelta(hours=1),
|
|
ai_json={"summary": "검토", "priority": "보통",
|
|
"tasks": [{"text": "문서 검토"}], "events": [], "replies": []})
|
|
assert client.get("/api/mail/triage/baseline").json()["count"] == 1
|
|
|
|
|
|
def test_summary_new_total_counts_today_only(client, session):
|
|
"""new_total/screened 는 day_start(클라 로컬 자정) 이후 도착분만 — 기준 시각이 며칠 전이어도
|
|
누적('새 메일 60통')되지 않고 '오늘 온 새 메일'만 표시. 단, 배지(count)는 기준 이후 전체라
|
|
어제 도착한 챙길 거리(카드)도 유지한다."""
|
|
s, _ = session
|
|
day_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) # 오늘 자정
|
|
_set_baseline(s, day_start - timedelta(days=3)) # 기준 시각은 3일 전
|
|
make_mail_account(s, id="work", name="회사", tone="coral", sort_order=0)
|
|
actionable = {"summary": "검토", "priority": "보통", "tasks": [{"text": "검토"}],
|
|
"events": [], "replies": []}
|
|
# 어제 도착(집중·액션) + 오늘 도착(집중·액션) + 오늘 도착(홍보)
|
|
make_email(s, id="yday", account="work", subject="어제 검토",
|
|
received_at=day_start - timedelta(hours=5), ai_json=actionable)
|
|
make_email(s, id="tfocus", account="work", subject="오늘 검토",
|
|
received_at=day_start + timedelta(hours=2), ai_json=actionable)
|
|
make_email(s, id="tpromo", account="work", subject="오늘 세일", labels=["프로모션"],
|
|
received_at=day_start + timedelta(hours=3))
|
|
b = client.get("/api/mail/triage/baseline", params={"day_start": day_start.isoformat()}).json()
|
|
assert b["new_total"] == 2 # 오늘 도착분(집중+홍보)만 — 어제(yday)는 제외
|
|
assert b["screened"] == 1 # 오늘 홍보 1통
|
|
assert b["count"] == 2 # 배지=기준 이후 전체 카드(어제+오늘 집중) — 어제 챙길 거리도 유지
|
|
|
|
|
|
def test_triage_log_returns_today_events_newest_first(client, session):
|
|
"""자동 정리 실행 로그: 오늘(day_start 이후) 이벤트만 최신순 — 분석 통수·에러를 그대로 노출."""
|
|
s, _ = session
|
|
day_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
s.add(TriageEvent(at=day_start - timedelta(hours=2), analyzed=2, cards=1)) # 어제(범위 밖)
|
|
s.add(TriageEvent(at=day_start + timedelta(hours=1), analyzed=3, cards=2)) # 오늘
|
|
s.add(TriageEvent(at=day_start + timedelta(hours=3), analyzed=0, cards=0, error="모델 응답 없음"))
|
|
s.commit()
|
|
rows = client.get("/api/mail/triage/log", params={"day_start": day_start.isoformat()}).json()
|
|
assert len(rows) == 2 # 오늘 것만(어제 제외)
|
|
assert rows[0]["error"] == "모델 응답 없음" # 최신순 — 에러가 먼저
|
|
assert rows[1]["analyzed"] == 3 and rows[1]["cards"] == 2
|
|
|
|
|
|
def test_triage_classified_lists_today_mail_with_bucket(client, session):
|
|
"""상세 로그: 오늘 새 메일을 버킷(집중/홍보)·분석 결과와 함께 하나씩 — 어제 것은 제외."""
|
|
s, _ = session
|
|
day_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0)
|
|
_set_baseline(s, day_start - timedelta(days=3))
|
|
make_mail_account(s, id="work", name="회사", tone="coral", sort_order=0)
|
|
make_email(s, id="yday", account="work", subject="어제", received_at=day_start - timedelta(hours=5))
|
|
make_email(s, id="focus", account="work", subject="검토 요청",
|
|
received_at=day_start + timedelta(hours=2),
|
|
ai_json={"summary": "검토", "priority": "보통",
|
|
"tasks": [{"text": "검토"}], "events": [], "replies": []})
|
|
make_email(s, id="promo", account="work", subject="세일", labels=["프로모션"],
|
|
received_at=day_start + timedelta(hours=1))
|
|
rows = client.get("/api/mail/triage/classified", params={"day_start": day_start.isoformat()}).json()
|
|
byid = {r["id"]: r for r in rows}
|
|
assert set(byid) == {"focus", "promo"} # 오늘 둘만(어제 yday 제외)
|
|
assert byid["focus"]["bucket"] == "집중" and byid["focus"]["focused"] is True
|
|
assert byid["focus"]["analyzed"] is True and byid["focus"]["tasks"] == 1
|
|
assert byid["promo"]["bucket"] == "홍보" and byid["promo"]["focused"] is False
|