o2o-plagiarism-ai/tests/test_generate_ai_samples.py

111 lines
4.2 KiB
Python

"""generate_ai_samples 단위테스트. API 를 호출하지 않는다."""
from __future__ import annotations
import importlib.util
import json
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
_spec = importlib.util.spec_from_file_location(
"generate_ai_samples", ROOT / "scripts" / "generate_ai_samples.py"
)
gen = importlib.util.module_from_spec(_spec)
sys.modules["generate_ai_samples"] = gen
_spec.loader.exec_module(gen)
class TestManuscriptNeverLeaves:
"""이 프로젝트에서 가장 깨지면 안 되는 성질 — 원고는 외부로 나가지 않는다."""
def test_meta_columns_contain_no_body_column(self):
for column in gen.META_COLUMNS:
assert not any(bad in column.lower() for bad in gen.TEXT_COLUMN_BLOCKLIST)
def test_guard_rejects_body_column(self):
"""META_COLUMNS 에 본문 컬럼을 넣는 수정은 실행 시점에 막혀야 한다."""
with pytest.raises(SystemExit) as err:
gen.assert_no_body_columns(["gpt_keyword", "에피소드"])
assert "에피소드" in str(err.value)
def test_guard_passes_clean_columns(self):
cols = ["gpt_keyword", "gpt_title"]
assert gen.assert_no_body_columns(cols) == cols
def test_episode_title_is_allowed_but_episode_body_is_not(self):
assert gen.assert_no_body_columns(["에피소드 제목"]) == ["에피소드 제목"]
with pytest.raises(SystemExit):
gen.assert_no_body_columns(["에피소드 본문"])
def test_prompt_contains_only_given_meta(self):
"""프롬프트에 넣지 않은 문자열은 절대 나타나지 않는다."""
secret = "이것은 저자의 실제 원고 본문이다"
prompt = gen._build_prompt(
{"gpt_keyword": "습관", "gpt_title": "변화"}, 950, "담담하게"
)
assert secret not in prompt
assert "습관" in prompt and "변화" in prompt
def test_build_prompt_signature_has_no_text_param(self):
"""본문을 넘길 자리 자체가 없어야 한다."""
import inspect
params = set(inspect.signature(gen._build_prompt).parameters)
assert params == {"meta", "target_chars", "style"}
class TestLengthBand:
def test_band_from_distribution(self, tmp_path):
import pandas as pd
frame = pd.DataFrame({"에피소드": ["" * n for n in range(200, 1200, 10)]})
lengths, low, high = gen.load_human_length_band(frame, "에피소드")
assert low < high
assert min(lengths) >= 120 # 120자 미만은 채점 불가라 제외된다
assert low >= min(lengths) and high <= max(lengths)
def test_rejects_empty_column(self):
import pandas as pd
frame = pd.DataFrame({"에피소드": ["짧음"] * 5})
with pytest.raises(SystemExit):
gen.load_human_length_band(frame, "에피소드")
def test_hangul_quality_gate(self):
assert gen._hangul_ratio("오늘은 시장에 갔다.") > 0.9
assert gen._hangul_ratio("This is an English model failure.") == 0.0
class TestResume:
def test_reads_done_ids(self, tmp_path):
out = tmp_path / "s.jsonl"
out.write_text(
json.dumps({"prompt_id": "aaa", "text": "x"}, ensure_ascii=False) + "\n"
+ json.dumps({"prompt_id": "bbb", "text": "y"}, ensure_ascii=False) + "\n",
encoding="utf-8",
)
assert gen.load_done_ids(out) == {"aaa", "bbb"}
def test_missing_file_is_empty(self, tmp_path):
assert gen.load_done_ids(tmp_path / "nope.jsonl") == set()
def test_broken_line_does_not_crash(self, tmp_path):
"""중간에 죽어 잘린 행이 있어도 재개는 되어야 한다."""
out = tmp_path / "s.jsonl"
out.write_text('{"prompt_id": "aaa"}\n{"prompt_id": "bro\n', encoding="utf-8")
assert gen.load_done_ids(out) == {"aaa"}
class TestDiversity:
def test_style_variants_are_distinct(self):
assert len(set(gen.STYLE_VARIANTS)) == len(gen.STYLE_VARIANTS)
assert len(gen.STYLE_VARIANTS) >= 4
def test_style_assignment_is_deterministic(self):
a = gen._stable_int("abc") % len(gen.STYLE_VARIANTS)
b = gen._stable_int("abc") % len(gen.STYLE_VARIANTS)
assert a == b