o2o-plagiarism-ai/tests/test_clustering.py

110 lines
4.3 KiB
Python

"""군집화 기반 표절 판별 단위테스트 (계획서 2단계 고도화).
핵심 시나리오: "본문(lemma)·키워드는 그대로 두고 인물만 바꾼 표절"
element_swap_plagiarism 으로 분해해서 잡는다.
"""
from __future__ import annotations
from app.api.schemas import ExtractedElements
from app.engine.clustering import (
ClusterIndex,
signature_similarity,
_element_signature,
)
def _elem(characters=None, motifs=None, keywords=None, genre=None) -> ExtractedElements:
return ExtractedElements(
characters=characters or [],
motifs=motifs or [],
keywords=keywords or [],
genre=genre,
)
def test_signature_similarity_identical():
a = _element_signature(_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리"], genre="역사"))
assert signature_similarity(a, a) == 1.0
def test_clusters_group_similar_docs():
# 두 홍길동 변형은 한 군집, 어린왕자는 별도 군집
ids = ["hong-a", "hong-b", "prince"]
elems = [
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"], genre="역사"),
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"], genre="역사"),
_elem(characters=["어린왕자", "여우"], keywords=["사막", "장미", ""], genre="동화"),
]
idx = ClusterIndex(ids, elems, link_threshold=0.35)
assert idx.cluster_of("hong-a") == idx.cluster_of("hong-b")
assert idx.cluster_of("prince") != idx.cluster_of("hong-a")
def test_route_to_nearest_cluster():
ids = ["hong", "prince"]
elems = [
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"], genre="역사"),
_elem(characters=["어린왕자"], keywords=["사막", "장미", ""], genre="동화"),
]
idx = ClusterIndex(ids, elems, link_threshold=0.9) # 분리 유지
q = _elem(characters=["홍길동"], keywords=["활빈당", "재물"], genre="역사")
routed = idx.route(q)
assert routed is not None
assert "hong" in routed.members
def test_element_swap_plagiarism_detected():
"""본문 lemma·키워드 유지 + 인물만 교체 → element_swap_plagiarism."""
ids = ["original"]
elems = [_elem(characters=["홍길동"], motifs=["복수"], keywords=["활빈당", "탐관오리", "재물", "의적"])]
lemmas = [["만들다", "빼앗다", "재물", "탐관오리", "활빈당", "나누다"]]
idx = ClusterIndex(ids, elems, doc_lemmas=lemmas, link_threshold=0.35)
# 인물(홍길동→김민수)·모티프만 바꾸고 본문 lemma/키워드는 그대로
q_elem = _elem(characters=["김민수"], motifs=["정의"], keywords=["활빈당", "탐관오리", "재물", "의적"])
q_lemmas = ["만들다", "빼앗다", "재물", "탐관오리", "활빈당", "나누다"]
sig = idx.partial_signal("original", q_elem, q_lemmas)
assert sig is not None
assert sig.verdict == "element_swap_plagiarism", sig.per_element
assert "characters" in sig.changed_elements
assert "lemmas" in sig.retained_elements
def test_near_duplicate_verdict():
ids = ["original"]
elems = [_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"])]
lemmas = [["만들다", "빼앗다", "재물"]]
idx = ClusterIndex(ids, elems, doc_lemmas=lemmas)
sig = idx.partial_signal(
"original",
_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리", "재물"]),
["만들다", "빼앗다", "재물"],
)
assert sig is not None
assert sig.verdict == "near_duplicate"
def test_unrelated_gets_none_or_weak():
ids = ["original"]
elems = [_elem(characters=["홍길동"], keywords=["활빈당", "탐관오리"])]
lemmas = [["만들다", "빼앗다"]]
idx = ClusterIndex(ids, elems, doc_lemmas=lemmas)
sig = idx.partial_signal(
"original",
_elem(characters=["우주비행사"], keywords=["로켓", "행성"]),
["날다", "탐사하다"],
)
assert sig is not None
assert sig.verdict in ("none", "weak")
assert sig.verdict != "element_swap_plagiarism"
def test_empty_index():
idx = ClusterIndex([], [])
assert idx.num_clusters == 0
assert idx.route(_elem(characters=["x"])) is None
assert idx.candidate_ids(_elem(characters=["x"])) == set()