107 lines
4.0 KiB
Python
107 lines
4.0 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, "에피소드")
|
|
|
|
|
|
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
|