Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Node.js dependencies
/sumdays-backend/node_modules
/sumdays-backend/coverage

# Byte-compiled / optimal files
**/.__pytest_cache/
Expand All @@ -13,5 +14,5 @@
# env
**/.env
**/.coverage
**/coverage/
**/coverage
**/tmp/
3 changes: 3 additions & 0 deletions Sumdays/.idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions sumdays-backend/ai/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
--extra-index-url https://download.pytorch.org/whl/cpu
torch
sentence-transformers

langchain==0.3.27
langchain-openai==0.3.35
langchain-core==0.3.79
Expand All @@ -10,5 +14,4 @@ flask
google-cloud-vision
pytest
coverage
pytest-cov
sentence-transformers
pytest-cov
8 changes: 0 additions & 8 deletions sumdays-backend/ai/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@

@pytest.fixture(scope="session", autouse=True)
def load_env():
"""
.env 파일을 명시적으로 로드 (테스트 실행 시 환경 변수 누락 방지)
"""
env_path = os.path.join(os.path.dirname(__file__), "..", "..", ".env")
if os.path.exists(env_path):
load_dotenv(dotenv_path=env_path)
Expand All @@ -26,11 +23,6 @@ def load_env():

@pytest.fixture
def client():
"""
Flask 애플리케이션의 테스트 클라이언트를 반환합니다.
- 실제 app.py를 import하여 모든 blueprint를 등록한 상태로 실행됩니다.
- 내부적으로 요청 시 실제 OpenAI/Google Vision API가 호출됩니다.
"""
app.config["TESTING"] = True
app.config["WTF_CSRF_ENABLED"] = False
return app.test_client()
4 changes: 0 additions & 4 deletions sumdays-backend/ai/tests/test_diary_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def test_analyze_diary_integration(client):
assert response.status_code == 200
data = response.get_json()

# JSON 필드 검증
assert "analysis" in data
assert "icon" in data
assert "ai_comment" in data
Expand All @@ -25,9 +24,6 @@ def test_analyze_diary_integration(client):
assert "emotion_score" in data["analysis"]

def test_analysis_diary_missing_field(client):
"""
[에러 테스트] /analysis/diary - 빈 바디 전달
"""
res = client.post("/analysis/diary", data=json.dumps({}),
content_type="application/json")
data = res.get_json()
Expand Down
21 changes: 0 additions & 21 deletions sumdays-backend/ai/tests/test_extract_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
from PIL import Image


# ============================================
# 1) extract_service.py 모든 함수 단독 테스트
# ============================================
def test_l2norm_basic():
from ai.services.extract.extract_service import l2norm
x = np.array([[3.0, 4.0]])
Expand Down Expand Up @@ -65,19 +62,13 @@ def test_extract_style_full():
assert "style_examples" in result


# ============================================
# 2) compute_style_profile_text → 실제 OpenAI 호출 테스트
# ============================================
def test_compute_style_profile_text():
from ai.services.extract.extract_style_profile import compute_style_profile_text
diaries = ["오늘은 기분이 좋았다.", "점심은 맛있었다.", "밤에는 산책을 했다.", "잤다", "일어났다"]
text = compute_style_profile_text(diaries)
assert len(text) >= 5


# ============================================
# 3) /extract/style API 테스트 — JSON 입력
# ============================================
def test_extract_style_route_json(client):
diaries = [
"오늘은 좋은 날이었다.",
Expand All @@ -97,10 +88,6 @@ def test_extract_style_route_json(client):
assert "style_examples" in data
assert "style_prompt" in data


# ============================================
# 4) /extract/style API 테스트 — diaries 부족 → 에러분기
# ============================================
def test_extract_style_route_too_few(client):
diaries = ["하나", "둘"] # 최소 3개 → MIN_DIARY_NUM=3

Expand All @@ -114,18 +101,12 @@ def test_extract_style_route_too_few(client):
assert response.status_code == 400


# ============================================
# 5) /extract/style API 테스트 — form-data + 이미지 OCR 경로
# ============================================
def test_extract_style_route_with_images(client):
# 더미 이미지를 메모리에서 생성 → Vision OCR 호출됨
img = Image.new("RGB", (200, 60), color=(255, 255, 255))
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)

# diaries는 1개만 넣고, OCR 결과에서 최소 2개 이상 텍스트가 나오도록 기대
# (실제 Vision 결과에 따라 다르지만, 호출 자체가 성공하면 커버리지 충족됨)
data = {
"diaries": json.dumps(["오늘 일기를 썼다."]),
}
Expand All @@ -139,6 +120,4 @@ def test_extract_style_route_with_images(client):
content_type="multipart/form-data"
)

# OCR이 실패하더라도 detect_texts_from_diaries 내부에서 포맷은 반환됨
# diaries ≥ 3 이면 200, 아니면 400
assert response.status_code in (200, 400)
23 changes: 0 additions & 23 deletions sumdays-backend/ai/tests/test_image_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@
import os
import pytest

# 실제 이미지 파일 경로 설정
# ai/tests/sample.jpg, ai/tests/diary_page1.jpg, ai/tests/diary_page2.jpg 준비 필요
SAMPLE_IMG = os.path.join("ai", "tests", "images", "sample.jpg")
DIARY_IMG_1 = os.path.join("ai", "tests", "images", "diary_page1.jpg")
DIARY_IMG_2 = os.path.join("ai", "tests", "images", "diary_page2.jpg")


def test_image_memo_extract(client):
"""
[통합 테스트] /image/memo (type=extract)
- 실제 이미지 파일로 OCR 추출
"""
with open(SAMPLE_IMG, "rb") as f:
data = {"type": "extract", "image": (io.BytesIO(f.read()), "sample.jpg")}

Expand All @@ -32,10 +26,6 @@ def test_image_memo_extract(client):


def test_image_memo_describe(client):
"""
[통합 테스트] /image/memo (type=describe)
- 실제 이미지 파일로 GPT Vision 호출
"""
with open(SAMPLE_IMG, "rb") as f:
data = {"type": "describe", "image": (io.BytesIO(f.read()), "sample.jpg")}

Expand All @@ -54,9 +44,6 @@ def test_image_memo_describe(client):


def test_image_memo_invalid_type(client):
"""
[에러 테스트] /image/memo invalid type
"""
with open(SAMPLE_IMG, "rb") as f:
data = {"type": "nonsense", "image": (io.BytesIO(f.read()), "sample.jpg")}

Expand All @@ -68,9 +55,6 @@ def test_image_memo_invalid_type(client):


def test_image_memo_no_file(client):
"""
[에러 테스트] /image/memo without image
"""
data = {"type": "extract"}
res = client.post("/image/memo", data=data, content_type="multipart/form-data")
data = res.get_json()
Expand All @@ -80,10 +64,6 @@ def test_image_memo_no_file(client):


def test_image_diary_multiple(client):
"""
[통합 테스트] /image/diary
- 실제 이미지 2장 업로드, Vision + GPT refine 경로 모두 실행
"""
with open(DIARY_IMG_1, "rb") as f1, open(DIARY_IMG_2, "rb") as f2:
files = [("image", (io.BytesIO(f1.read()), "page1.jpg")),
("image", (io.BytesIO(f2.read()), "page2.jpg"))]
Expand All @@ -102,9 +82,6 @@ def test_image_diary_multiple(client):


def test_image_diary_no_files(client):
"""
[에러 테스트] /image/diary without files
"""
res = client.post("/image/diary", data={}, content_type="multipart/form-data")
data = res.get_json()

Expand Down
85 changes: 0 additions & 85 deletions sumdays-backend/ai/tests/test_merge_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,8 @@
merge_rerank,
)

# ============================================================
# 0) analysis_service + OpenAI 전체 mock (라우트 400 방지)
# ============================================================
@pytest.fixture(autouse=True)
def mock_all(monkeypatch):
"""
merge 라우트에 사용되는 모든 외부 요소를 mock:
- analysis_service.analyze
- OpenAI completions.create
"""

# ---------------------------
# 1) diary 분석 mock
# ---------------------------
class FakeAnalysis:
def analyze(self, diary):
return {
Expand All @@ -38,9 +26,6 @@ def analyze(self, diary):
merge_routes.analysis_service, "analyze", FakeAnalysis().analyze
)

# ---------------------------
# 2) OpenAI completions mock
# ---------------------------
import ai.services.merge.merge_service as ms

class FakeMessage:
Expand All @@ -55,7 +40,6 @@ class FakeResponse:
def __init__(self, content):
self.choices = [FakeChoice(content)]

# merge_rerank가 반드시 텍스트를 생성할 수 있도록 한다
def fake_create(*args, **kwargs):
return FakeResponse("첫번째 단락입니다.\n###\n두번째 단락입니다.")

Expand All @@ -64,9 +48,6 @@ def fake_create(*args, **kwargs):
)


# ============================================================
# 공용 payload
# ============================================================
def _build_base_payload(end_flag: bool, advanced_flag: bool = False) -> dict:
return {
"memos": [
Expand All @@ -89,16 +70,8 @@ def _build_base_payload(end_flag: bool, advanced_flag: bool = False) -> dict:
}


# ============================================================
# 1) /merge/ 라우트 테스트
# ============================================================

def test_merge_only_streaming_route(client):
"""
Case A:
end_flag=False AND advanced_flag=False
→ merge_stream → text/plain
"""
payload = _build_base_payload(end_flag=False, advanced_flag=False)

res = client.post("/merge/", data=json.dumps(payload), content_type="application/json")
Expand All @@ -109,52 +82,7 @@ def test_merge_only_streaming_route(client):
body = res.get_data(as_text=True).strip()
assert body != ""


# def test_merge_rerank_route(client):
# """
# Case B:
# end_flag=False AND advanced_flag=True
# → merge_rerank → string
# """
# payload = _build_base_payload(end_flag=False, advanced_flag=True)

# res = client.post("/merge/", data=json.dumps(payload), content_type="application/json")

# assert res.status_code == 200

# body = res.get_data(as_text=True)
# assert isinstance(body, str)
# assert body.strip() != ""


# def test_merge_and_analyze_route(client):
# """
# Case C:
# end_flag=True → merge 후 analysis → JSON
# """
# payload = _build_base_payload(end_flag=True, advanced_flag=True)
# payload["entry_date"] = "2025-10-29"
# payload["user_id"] = 123

# res = client.post("/merge/", data=json.dumps(payload), content_type="application/json")

# assert res.status_code == 200

# data = res.get_json()
# assert "entry_date" in data
# assert "user_id" in data
# assert "diary" in data
# assert "icon" in data
# assert "ai_comment" in data
# assert "analysis" in data

# assert data["diary"].strip() != ""


def test_merge_bad_request_route(client):
"""
잘못된 요청 → KeyError → except → 400
"""
bad_payload = {
# memos 누락
"end_flag": False,
Expand All @@ -171,10 +99,6 @@ def test_merge_bad_request_route(client):
assert "error" in res.get_json()


# ============================================================
# 2) helper 함수 테스트
# ============================================================

def test_l2norm_basic_and_zero():
vec = np.array([[3.0, 4.0]], dtype=np.float32)
out = l2norm(vec)
Expand Down Expand Up @@ -219,17 +143,8 @@ def test_embed_sentences_shape_and_norm():
assert np.allclose(np.linalg.norm(E, axis=-1), 1.0, atol=1e-3)


# ============================================================
# 3) merge_rerank 전체 로직 커버
# ============================================================

def test_merge_rerank_full_flow(monkeypatch):
"""
merge_rerank 내부 전체 흐름 커버 테스트
- 첫번째 메모 → 단락 선택됨
- 두번째 메모 → blocks 없음 → skip
- 세번째 메모 → 중복 단락 skip
"""
from ai.services.merge import merge_service as ms

def fake_embed(sentences):
Expand Down
Loading
Loading