"""연합 시나리오 통합 테스트 — 캡처→분류→확인→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)