"""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