2929 LLMUpstreamUnavailableError ,
3030)
3131from app .services .tagging .exceptions import TaggingValidationError
32+ from app .services .tagging .v2_postscore import (
33+ _extract_json_payload as _v2_extract_json_payload ,
34+ )
3235from app .services .tagging .v2_postscore import (
3336 _parse_and_validate as _v2_parse_and_validate ,
3437)
5154
5255_PII_MARKERS = ("MAGICPII-ST-1234" , "MAGICPII-A-5678" , "MAGICPII-R-9012" )
5356
54- # v2_postscore happy LLM 응답 — 5개 후보(모두 풀 내·중복 없음). DEVELOPER 가중치로
55- # 5개 모두 High(3) 동점 → tie-break 입력 순서 top3 = ["#원인분석","#검증및테스트","#반복개선"].
56- _HAPPY_LLM_RAW = '{"detailTags": ["#원인분석","#검증및테스트","#반복개선","#문제해결","#디버깅"]}'
57+ # v2_postscore happy LLM 응답 — assistant prefill('{') 적용 가정 (issue #18).
58+ # 코드가 prefill + raw 로 합쳐 `{"detailTags": [...]}` JSON 을 완성한다.
59+ # 5개 후보 모두 풀 내·중복 없음. DEVELOPER 가중치로 5개 모두 High(3) 동점 →
60+ # tie-break 입력 순서 top3 = ["#원인분석","#검증및테스트","#반복개선"].
61+ _HAPPY_LLM_RAW = '"detailTags": ["#원인분석","#검증및테스트","#반복개선","#문제해결","#디버깅"]}'
5762_HAPPY_TOP3 = ["#원인분석" , "#검증및테스트" , "#반복개선" ]
5863
5964
@@ -137,9 +142,10 @@ def test_v1_tagging_happy_path(client: TestClient, token: str) -> None:
137142@respx .mock
138143def test_v1_tagging_corrective_retry_recovers (client : TestClient , token : str ) -> None :
139144 # 1차: 풀-외 + 갯수 미달 → 검증 실패. 2차: 풀 내 5개 → top3 자르고 응답.
145+ # mock 응답은 모두 assistant prefill('{') 가정 (issue #18).
140146 route = respx .post (ANTHROPIC_URL ).mock (
141147 side_effect = [
142- Response (200 , json = _anthropic_msg ('{ "detailTags": ["#존재하지않는태그"]}' )),
148+ Response (200 , json = _anthropic_msg ('"detailTags": ["#존재하지않는태그"]}' )),
143149 Response (200 , json = _anthropic_msg (_HAPPY_LLM_RAW )),
144150 ]
145151 )
@@ -156,8 +162,8 @@ def test_v1_tagging_corrective_retry_recovers(client: TestClient, token: str) ->
156162def test_v1_tagging_double_validation_failure_returns_422 (client : TestClient , token : str ) -> None :
157163 route = respx .post (ANTHROPIC_URL ).mock (
158164 side_effect = [
159- Response (200 , json = _anthropic_msg ('{ "detailTags": ["#없는태그A"]}' )),
160- Response (200 , json = _anthropic_msg ('{ "detailTags": ["#없는태그B"]}' )),
165+ Response (200 , json = _anthropic_msg ('"detailTags": ["#없는태그A"]}' )),
166+ Response (200 , json = _anthropic_msg ('"detailTags": ["#없는태그B"]}' )),
161167 ]
162168 )
163169 r = client .post ("/v1/tagging" , json = _PAYLOAD , headers = {"X-Internal-Token" : token })
@@ -173,9 +179,9 @@ def test_v1_tagging_double_validation_failure_returns_422(client: TestClient, to
173179@pytest .mark .parametrize (
174180 "bad_response" ,
175181 [
176- '{ "detailTags": []}' ,
177- '{ "detailTags": ["#원인분석","#검증및테스트","#반복개선","#문제해결"]}' ,
178- '{ "detailTags": ["#A","#B","#C","#D","#E","#F","#G","#H"]}' ,
182+ '"detailTags": []}' ,
183+ '"detailTags": ["#원인분석","#검증및테스트","#반복개선","#문제해결"]}' ,
184+ '"detailTags": ["#A","#B","#C","#D","#E","#F","#G","#H"]}' ,
179185 ],
180186 ids = ["empty_under_min" , "four_tags_under_min" , "eight_tags_over_max" ],
181187)
@@ -266,6 +272,50 @@ def test_v2_postscore_strips_all_backtick_widths(wrapped: str) -> None:
266272 assert _v2_strip_code_fence (wrapped ) == '{"detailTags": ["#문제해결"]}'
267273
268274
275+ @pytest .mark .parametrize (
276+ ("raw" , "expected" ),
277+ [
278+ ('{"detailTags": ["#문제해결"]}' , '{"detailTags": ["#문제해결"]}' ),
279+ ('다음과 같습니다: {"detailTags": ["#문제해결"]}' , '{"detailTags": ["#문제해결"]}' ),
280+ (
281+ '# 결과\n {"detailTags": ["#문제해결"]}\n 참고: ...' ,
282+ '{"detailTags": ["#문제해결"]}' ,
283+ ),
284+ (' \n {"detailTags": ["#문제해결"]} ' , '{"detailTags": ["#문제해결"]}' ),
285+ (
286+ '```json\n {"detailTags": ["#문제해결"]}\n ```' ,
287+ '{"detailTags": ["#문제해결"]}' ,
288+ ),
289+ ("브레이스없음" , "브레이스없음" ),
290+ ],
291+ ids = [
292+ "clean" ,
293+ "korean_prefix" ,
294+ "markdown_prefix_and_suffix" ,
295+ "whitespace" ,
296+ "code_fence" ,
297+ "no_braces_passthrough" ,
298+ ],
299+ )
300+ def test_v2_postscore_extracts_json_payload_from_messy_responses (raw : str , expected : str ) -> None :
301+ """도입어 / 헤딩 / 공백 / 코드펜스 prefix·suffix 를 흡수해 JSON object 본문만 슬라이스.
302+
303+ issue #18 — Haiku 가 ``다음과 같습니다:`` 같은 도입어를 자주 붙여 ``^{`` 만 허용하면
304+ 운영 실패율이 과도하게 높아짐. ``{`` ~ ``}`` 슬라이스 fallback 으로 흡수.
305+ """
306+ assert _v2_extract_json_payload (raw ) == expected
307+
308+
309+ def test_v2_postscore_parse_and_validate_handles_intro_prefix () -> None :
310+ """도입어가 붙은 응답도 정상 파싱 — _extract_json_payload 가 prefix 흡수."""
311+ raw = (
312+ "다음과 같습니다: "
313+ '{"detailTags": ["#원인분석","#검증및테스트","#반복개선","#문제해결","#디버깅"]}'
314+ )
315+ tags = _v2_parse_and_validate (raw )
316+ assert tags == ["#원인분석" , "#검증및테스트" , "#반복개선" , "#문제해결" , "#디버깅" ]
317+
318+
269319def test_v2_postscore_parse_accepts_5_to_7_candidates () -> None :
270320 """후보 갯수 5~7 범위는 통과."""
271321 five = '{"detailTags": ["#원인분석","#검증및테스트","#반복개선","#문제해결","#디버깅"]}'
@@ -349,8 +399,8 @@ def test_v1_tagging_does_not_log_star_body_on_validation_failure(
349399) -> None :
350400 respx .post (ANTHROPIC_URL ).mock (
351401 side_effect = [
352- Response (200 , json = _anthropic_msg ('{ "detailTags": ["#없는1"]}' )),
353- Response (200 , json = _anthropic_msg ('{ "detailTags": ["#없는2"]}' )),
402+ Response (200 , json = _anthropic_msg ('"detailTags": ["#없는1"]}' )),
403+ Response (200 , json = _anthropic_msg ('"detailTags": ["#없는2"]}' )),
354404 ]
355405 )
356406 caplog .set_level (logging .INFO )
0 commit comments