|
|
"""연합 시나리오 통합 테스트 — 캡처→분류→확인→tasks 등장→risks→dashboard 집계.
|
|
|
LLM은 FakeLLMProvider로 고정(conftest, 결정론적). golden case 기준.
|
|
|
phase-16+: 작업 시드 제거 → 리스크 테스트는 작업을 직접 생성.
|
|
|
"""
|
|
|
from datetime import date
|
|
|
|
|
|
from tests._factories import make_task
|
|
|
|
|
|
GOLDEN_RAW = "다음 주에 한국 놀러가는 비행기 티켓 사기"
|
|
|
|
|
|
|
|
|
def _seed_risk(s):
|
|
|
make_task(s, id="k1", title="분기 리포트 초안 마무리", project_id="biz-report",
|
|
|
status="doing", due=date(2026, 6, 8), assignee_id="jiwoo")
|
|
|
make_task(s, id="dep-a", title="예산 섹션 작성", project_id="biz-report", status="todo")
|
|
|
make_task(s, id="dep-b", title="경영진 검토 요청 메일", project_id="biz-report",
|
|
|
status="todo", due=date(2026, 6, 10))
|
|
|
|
|
|
|
|
|
def _flatten(nodes):
|
|
|
out = []
|
|
|
for n in nodes:
|
|
|
out.append(n)
|
|
|
out.extend(_flatten(n.get("children", []) or []))
|
|
|
return out
|
|
|
|
|
|
|
|
|
def test_health(client):
|
|
|
assert client.get("/api/health").json() == {"status": "ok"}
|
|
|
|
|
|
|
|
|
def test_capture_classifies_to_task_life_travel(client):
|
|
|
"""① 캡처 → 분류: 비행기 티켓 = 작업 / life / 개인 › 여행 — 한국"""
|
|
|
r = client.post("/api/inbox/capture", json={"kind": "text", "raw": GOLDEN_RAW})
|
|
|
assert r.status_code == 200
|
|
|
c = r.json()["classification"]
|
|
|
assert c["type"] == "task"
|
|
|
assert c["sphere"] == "life"
|
|
|
assert "여행" in c["proj_label"] and "한국" in c["proj_label"]
|
|
|
assert "작업" in c["reason"]
|
|
|
assert 0.0 <= c["confidence"] <= 1.0
|
|
|
assert r.json()["item"]["status"] == "classified"
|
|
|
|
|
|
|
|
|
def test_confirm_materializes_into_task_tree(client):
|
|
|
"""① 확인(실체화): confirm 시 task 생성 + tasks 트리·life 필터에 등장"""
|
|
|
cap = client.post("/api/inbox/capture", json={"kind": "text", "raw": GOLDEN_RAW}).json()
|
|
|
item_id = cap["item"]["id"]
|
|
|
confirmed = client.post(f"/api/inbox/{item_id}/confirm").json()
|
|
|
new_task_id = confirmed["task"]["id"]
|
|
|
assert new_task_id
|
|
|
|
|
|
inbox = client.get("/api/inbox").json()
|
|
|
target = next(i for i in inbox if i["id"] == item_id)
|
|
|
assert target["status"] == "confirmed"
|
|
|
assert target["materialized_task_id"] == new_task_id
|
|
|
|
|
|
life = client.get("/api/tasks", params={"area": "life"}).json()
|
|
|
flat = _flatten(life)
|
|
|
created = next(t for t in flat if t["id"] == new_task_id)
|
|
|
assert created["project_id"] == "life-trip"
|
|
|
assert "비행기 티켓" in created["title"]
|
|
|
assert created["status"] == "todo"
|
|
|
assert "가격 추적" in created["notes"]
|
|
|
|
|
|
|
|
|
def test_confirm_idempotent(client):
|
|
|
"""confirm 두 번 호출 — 동일 materialized_task_id, 중복 task 생성 금지"""
|
|
|
cap = client.post("/api/inbox/capture", json={"kind": "text", "raw": GOLDEN_RAW}).json()
|
|
|
iid = cap["item"]["id"]
|
|
|
first = client.post(f"/api/inbox/{iid}/confirm").json()
|
|
|
first_tid = first["task"]["id"]
|
|
|
# 두 번째 confirm: 이미 confirmed → task 미생성(없음), materialized_task_id 유지
|
|
|
second = client.post(f"/api/inbox/{iid}/confirm").json()
|
|
|
assert second["task"] is None
|
|
|
inbox = {i["id"]: i for i in client.get("/api/inbox").json()}
|
|
|
assert inbox[iid]["materialized_task_id"] == first_tid
|
|
|
|
|
|
|
|
|
def test_risk_recompute_after_task_change(client, session):
|
|
|
"""② 작업 데이터 → 리스크 레이더 자동 계산(최대 3건, TODAY=8)"""
|
|
|
s, _ = session
|
|
|
_seed_risk(s)
|
|
|
risks = client.get("/api/risks", params={"area": "work"}).json()
|
|
|
assert len(risks) <= 3
|
|
|
kinds = [r["kind"] for r in risks]
|
|
|
assert "지연 위험" in kinds
|
|
|
delay = next(r for r in risks if r["kind"] == "지연 위험")
|
|
|
assert delay["tone"] == "coral" and delay["icon"] == "clock"
|
|
|
if "의존성" in kinds:
|
|
|
dep = next(r for r in risks if r["kind"] == "의존성")
|
|
|
assert dep["tone"] == "violet" and dep["icon"] == "link"
|
|
|
|
|
|
|
|
|
def test_risk_disappears_when_done(client, session):
|
|
|
"""k1(분기 리포트)을 done 으로 PATCH 후 → 지연 위험에서 k1 사라짐(재계산)"""
|
|
|
s, _ = session
|
|
|
_seed_risk(s)
|
|
|
before = client.get("/api/risks", params={"area": "work"}).json()
|
|
|
delay_before = next((r for r in before if r["kind"] == "지연 위험"), None)
|
|
|
assert delay_before and delay_before["task_id"] == "k1"
|
|
|
client.patch("/api/tasks/k1", json={"status": "done"})
|
|
|
after = client.get("/api/risks", params={"area": "work"}).json()
|
|
|
delay_after = next((r for r in after if r["kind"] == "지연 위험"), None)
|
|
|
# k1 이 done → 지연 위험이 없거나 다른 작업으로 바뀜(k1 아님)
|
|
|
assert delay_after is None or delay_after["task_id"] != "k1"
|
|
|
|
|
|
|
|
|
def test_dashboard_aggregates_tasks_inbox_approvals(client):
|
|
|
"""③ 대시보드 집계 — 작업/인박스/결재함 요약 + 배지"""
|
|
|
cap = client.post("/api/inbox/capture", json={"kind": "text", "raw": GOLDEN_RAW}).json()
|
|
|
client.post(f"/api/inbox/{cap['item']['id']}/confirm")
|
|
|
|
|
|
d = client.get("/api/dashboard").json()
|
|
|
assert "user" in d and d["user"]["name"]
|
|
|
assert "briefing" in d and "schedule" in d
|
|
|
assert "task_summary" in d and "goals" in d
|
|
|
assert "approvals_summary" in d and "inbox_recent" in d
|
|
|
assert "open_count" in d["task_summary"] and "items" in d["task_summary"]
|
|
|
assert len(d["approvals_summary"]) <= 3
|
|
|
assert set(d["badges"]) == {"appr", "task", "noti"}
|
|
|
assert isinstance(d["inbox_recent"], list)
|