1212)
1313from pywikitools .translationreview .openai_client import TIMEOUT_SENTINEL
1414from pywikitools .translationreview .prompts import (
15- build_best_translation_prompt ,
1615 build_review_prompt ,
16+ build_translate_prompt ,
1717 language_name ,
1818)
1919
@@ -25,7 +25,7 @@ def make_config(temp_dir: str) -> ConfigParser:
2525 "Paths" : {"temp" : temp_dir },
2626 "translationreview" : {
2727 "site" : "4training" ,
28- "openai_api_key " : "test-key" ,
28+ "api_key " : "test-key" ,
2929 "model" : "gpt-4o" ,
3030 "reference_language" : "de" ,
3131 "request_delay" : "0" ,
@@ -74,8 +74,8 @@ def test_review_prompt_contains_all_texts(self):
7474 self .assertIn ("teyze" , prompt )
7575 self .assertIn ("do NOT provide a final improved translation" , prompt )
7676
77- def test_best_translation_prompt_includes_review (self ):
78- prompt = build_best_translation_prompt (
77+ def test_translate_after_review_prompt_includes_review_and_guidelines (self ):
78+ prompt = build_translate_prompt (
7979 language_code = "tr" ,
8080 english = "Pray for others" ,
8181 reference = "Betet für andere" ,
@@ -84,6 +84,19 @@ def test_best_translation_prompt_includes_review(self):
8484 )
8585 self .assertIn ("ONLY the best improved translation" , prompt )
8686 self .assertIn ("The translation is good but could be more natural." , prompt )
87+ self .assertIn ("teyze" , prompt )
88+
89+ def test_translate_prompt (self ):
90+ prompt = build_translate_prompt (
91+ language_code = "tr" ,
92+ english = "Pray for others" ,
93+ reference = "Betet für andere" ,
94+ target = "Başkaları için dua et" ,
95+ )
96+ self .assertIn ("ONLY the best improved translation" , prompt )
97+ self .assertIn ("Pray for others" , prompt )
98+ self .assertIn ("teyze" , prompt )
99+ self .assertNotIn ("do NOT provide a final improved translation" , prompt )
87100
88101 def test_default_guidelines_for_unknown_language (self ):
89102 prompt = build_review_prompt (
@@ -140,6 +153,56 @@ def test_model_cli_override(
140153 mock_openai_cls .assert_called_once ()
141154 self .assertEqual (mock_openai_cls .call_args .kwargs ["model" ], "gpt-4.1" )
142155
156+ @patch ("pywikitools.translationreview.bot.OpenAIClient" )
157+ @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
158+ @patch ("pywikitools.translationreview.bot.Family" )
159+ def test_api_key_cli_override (
160+ self , mock_family , mock_fortraininglib_cls , mock_openai_cls
161+ ):
162+ TranslationReviewBot (self .config , api_key = "cli-key" )
163+ mock_openai_cls .assert_called_once ()
164+ self .assertEqual (mock_openai_cls .call_args .kwargs ["api_key" ], "cli-key" )
165+
166+ @patch ("pywikitools.translationreview.bot.OpenAIClient" )
167+ @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
168+ @patch ("pywikitools.translationreview.bot.Family" )
169+ def test_base_url_cli_override (
170+ self , mock_family , mock_fortraininglib_cls , mock_openai_cls
171+ ):
172+ TranslationReviewBot (
173+ self .config ,
174+ base_url = "https://openrouter.ai/api/v1" ,
175+ )
176+ mock_openai_cls .assert_called_once ()
177+ self .assertEqual (
178+ mock_openai_cls .call_args .kwargs ["base_url" ],
179+ "https://openrouter.ai/api/v1" ,
180+ )
181+
182+ @patch ("pywikitools.translationreview.bot.OpenAIClient" )
183+ @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
184+ @patch ("pywikitools.translationreview.bot.Family" )
185+ def test_missing_api_key_raises (
186+ self , mock_family , mock_fortraininglib_cls , mock_openai_cls
187+ ):
188+ config = make_config (self .temp_dir )
189+ config .remove_option ("translationreview" , "api_key" )
190+ with self .assertRaisesRegex (RuntimeError , "Missing API key" ):
191+ TranslationReviewBot (config )
192+ mock_openai_cls .assert_not_called ()
193+
194+ @patch ("pywikitools.translationreview.bot.OpenAIClient" )
195+ @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
196+ @patch ("pywikitools.translationreview.bot.Family" )
197+ def test_api_key_cli_works_without_config_key (
198+ self , mock_family , mock_fortraininglib_cls , mock_openai_cls
199+ ):
200+ config = make_config (self .temp_dir )
201+ config .remove_option ("translationreview" , "api_key" )
202+ TranslationReviewBot (config , api_key = "cli-only-key" )
203+ mock_openai_cls .assert_called_once ()
204+ self .assertEqual (mock_openai_cls .call_args .kwargs ["api_key" ], "cli-only-key" )
205+
143206 @patch ("pywikitools.translationreview.bot.OpenAIClient" )
144207 @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
145208 @patch ("pywikitools.translationreview.bot.Family" )
@@ -164,13 +227,13 @@ def test_default_output_dir_includes_model_and_date(
164227 "translation_review/Forgiving_Step_by_Step_tr_openai-gpt-4o_" ,
165228 out_dir ,
166229 )
167- date_part = out_dir .rstrip ("/" ).rsplit ( "_" , 1 )[- 1 ]
168- self .assertRegex (date_part , r"^\d{4}-\d{2}-\d{2}$" )
230+ date_part = out_dir .rstrip ("/" ).split ( "openai-gpt-4o_" )[- 1 ]
231+ self .assertRegex (date_part , r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2} $" )
169232
170233 @patch ("pywikitools.translationreview.bot.OpenAIClient" )
171234 @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
172235 @patch ("pywikitools.translationreview.bot.Family" )
173- def test_run_processes_units_with_two_api_calls (
236+ def test_run_default_one_api_call_per_unit (
174237 self , mock_family , mock_fortraininglib_cls , mock_openai_cls
175238 ):
176239 target_units = [
@@ -194,36 +257,134 @@ def test_run_processes_units_with_two_api_calls(
194257 mock_client = MagicMock ()
195258 mock_openai_cls .return_value = mock_client
196259 mock_client .complete .side_effect = [
197- "Review for unit 1" ,
198260 "Improved translation 1" ,
199- "Review for unit 2" ,
200261 "Improved translation 2" ,
201262 ]
202263
203264 bot = TranslationReviewBot (self .config )
204- summary = bot .run ("Test" , "tr" , output_dir = self .temp_dir )
265+ summary = bot .run (
266+ "Test" ,
267+ "tr" ,
268+ output_dir = self .temp_dir ,
269+ run_options = {"loglevel" : "warning" , "with_review" : "no" },
270+ )
205271
206272 self .assertEqual (summary .processed , 2 )
207273 self .assertEqual (summary .skipped , 0 )
208274 self .assertEqual (summary .failed , 0 )
209- self .assertEqual (mock_client .complete .call_count , 4 )
275+ self .assertFalse (summary .with_review )
276+ self .assertEqual (mock_client .complete .call_count , 2 )
210277
211278 review_path = os .path .join (self .temp_dir , "review_report.md" )
212279 best_path = os .path .join (self .temp_dir , "best_translations.wiki" )
213280 summary_path = os .path .join (self .temp_dir , "summary.txt" )
214281
215- with open (review_path , encoding = "utf-8" ) as handle :
216- review_content = handle .read ()
282+ self .assertFalse (os .path .isfile (review_path ))
217283 with open (best_path , encoding = "utf-8" ) as handle :
218284 best_content = handle .read ()
219285 with open (summary_path , encoding = "utf-8" ) as handle :
220286 summary_content = handle .read ()
221287
222- self .assertIn ("## Prayer/1" , review_content )
223- self .assertIn ("Review for unit 1" , review_content )
224288 self .assertIn ("=== Prayer/1 ===" , best_content )
225289 self .assertIn ("Improved translation 1" , best_content )
226290 self .assertIn ("Processed: 2" , summary_content )
291+ self .assertIn ("with_review: no" , summary_content )
292+ self .assertIn ("loglevel: warning" , summary_content )
293+ self .assertIn ("model: gpt-4o" , summary_content )
294+
295+ @patch ("pywikitools.translationreview.bot.OpenAIClient" )
296+ @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
297+ @patch ("pywikitools.translationreview.bot.Family" )
298+ def test_run_with_review_two_api_calls_per_unit (
299+ self , mock_family , mock_fortraininglib_cls , mock_openai_cls
300+ ):
301+ target_units = [
302+ TranslationUnit (
303+ "Prayer/1" , "tr" , "Pray for others" , "Başkaları için dua et"
304+ ),
305+ TranslationUnit ("Prayer/2" , "tr" , "Trust God" , "Tanrı'ya güven" ),
306+ ]
307+ reference_units = [
308+ TranslationUnit ("Prayer/1" , "de" , "Pray for others" , "Betet für andere" ),
309+ TranslationUnit ("Prayer/2" , "de" , "Trust God" , "Vertraut Gott" ),
310+ ]
311+
312+ mock_lib = MagicMock ()
313+ mock_fortraininglib_cls .return_value = mock_lib
314+ mock_lib .get_translation_units .side_effect = [
315+ TranslatedPage ("Test" , "tr" , target_units ),
316+ TranslatedPage ("Test" , "de" , reference_units ),
317+ ]
318+
319+ mock_client = MagicMock ()
320+ mock_openai_cls .return_value = mock_client
321+ mock_client .complete .side_effect = [
322+ "Review for unit 1" ,
323+ "Improved translation 1" ,
324+ "Review for unit 2" ,
325+ "Improved translation 2" ,
326+ ]
327+
328+ bot = TranslationReviewBot (self .config )
329+ summary = bot .run ("Test" , "tr" , output_dir = self .temp_dir , with_review = True )
330+
331+ self .assertEqual (summary .processed , 2 )
332+ self .assertTrue (summary .with_review )
333+ self .assertEqual (mock_client .complete .call_count , 4 )
334+
335+ review_path = os .path .join (self .temp_dir , "review_report.md" )
336+ with open (review_path , encoding = "utf-8" ) as handle :
337+ review_content = handle .read ()
338+
339+ self .assertIn ("## Prayer/1" , review_content )
340+ self .assertIn ("Review for unit 1" , review_content )
341+
342+ @patch ("pywikitools.translationreview.odt_export.export_suggested_odt" )
343+ @patch ("pywikitools.translationreview.pdf_report.build_pdf_report" )
344+ @patch ("pywikitools.translationreview.bot.OpenAIClient" )
345+ @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
346+ @patch ("pywikitools.translationreview.bot.Family" )
347+ def test_run_generate_pdf_and_odt (
348+ self ,
349+ mock_family ,
350+ mock_fortraininglib_cls ,
351+ mock_openai_cls ,
352+ mock_build_pdf_report ,
353+ mock_export_suggested_odt ,
354+ ):
355+ target_units = [
356+ TranslationUnit (
357+ "Prayer/1" , "tr" , "Pray for others" , "Başkaları için dua et"
358+ ),
359+ ]
360+ reference_units = [
361+ TranslationUnit ("Prayer/1" , "de" , "Pray for others" , "Betet für andere" ),
362+ ]
363+
364+ mock_lib = MagicMock ()
365+ mock_fortraininglib_cls .return_value = mock_lib
366+ mock_lib .get_translation_units .side_effect = [
367+ TranslatedPage ("Test" , "tr" , target_units ),
368+ TranslatedPage ("Test" , "de" , reference_units ),
369+ ]
370+
371+ mock_client = MagicMock ()
372+ mock_openai_cls .return_value = mock_client
373+ mock_client .complete .side_effect = [
374+ "Improved translation 1" ,
375+ ]
376+ mock_export_suggested_odt .return_value = os .path .join (
377+ self .temp_dir , "gpt-4o_suggested.odt"
378+ )
379+
380+ bot = TranslationReviewBot (self .config , model = "gpt-4o" )
381+ bot .run ("Test" , "tr" , output_dir = self .temp_dir , generate = {"pdf" , "odt" })
382+
383+ mock_build_pdf_report .assert_called_once ()
384+ self .assertFalse (
385+ mock_build_pdf_report .call_args .kwargs .get ("include_review" , True )
386+ )
387+ mock_export_suggested_odt .assert_called_once ()
227388
228389 @patch ("pywikitools.translationreview.bot.OpenAIClient" )
229390 @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
@@ -258,7 +419,7 @@ def test_run_logs_analysis_and_best_translation(
258419 with self .assertLogs (
259420 "pywikitools.translationreview" , level = logging .INFO
260421 ) as log_context :
261- bot .run ("Test" , "tr" , output_dir = self .temp_dir )
422+ bot .run ("Test" , "tr" , output_dir = self .temp_dir , with_review = True )
262423
263424 messages = [record .getMessage () for record in log_context .records ]
264425 info_messages = [
@@ -305,7 +466,6 @@ def test_skips_units_without_reference(
305466 mock_client = MagicMock ()
306467 mock_openai_cls .return_value = mock_client
307468 mock_client .complete .side_effect = [
308- "Review for unit 1" ,
309469 "Improved translation 1" ,
310470 ]
311471
@@ -315,7 +475,7 @@ def test_skips_units_without_reference(
315475 self .assertEqual (summary .processed , 1 )
316476 self .assertEqual (summary .skipped , 1 )
317477 self .assertIn ("Prayer/2" , summary .skipped_units )
318- self .assertEqual (mock_client .complete .call_count , 2 )
478+ self .assertEqual (mock_client .complete .call_count , 1 )
319479
320480 @patch ("pywikitools.translationreview.bot.OpenAIClient" )
321481 @patch ("pywikitools.translationreview.bot.ForTrainingLib" )
@@ -343,13 +503,13 @@ def test_limit_and_unit_filter(
343503
344504 mock_client = MagicMock ()
345505 mock_openai_cls .return_value = mock_client
346- mock_client .complete .side_effect = ["Review" , " Best" ]
506+ mock_client .complete .side_effect = ["Best" ]
347507
348508 bot = TranslationReviewBot (self .config )
349509 summary = bot .run ("Test" , "tr" , unit_id = "Prayer/2" , output_dir = self .temp_dir )
350510
351511 self .assertEqual (summary .processed , 1 )
352- self .assertEqual (mock_client .complete .call_count , 2 )
512+ self .assertEqual (mock_client .complete .call_count , 1 )
353513
354514 with open (
355515 os .path .join (self .temp_dir , "best_translations.wiki" ), encoding = "utf-8"
@@ -392,16 +552,14 @@ def test_run_marks_timeout_as_failed_and_writes_reports(
392552 self .assertEqual (summary .failed_units , ["Prayer/1" ])
393553 mock_client .complete .assert_called_once ()
394554
395- with open (
396- os .path .join (self .temp_dir , "review_report.md" ), encoding = "utf-8"
397- ) as handle :
398- review_content = handle .read ()
555+ self .assertFalse (
556+ os .path .isfile (os .path .join (self .temp_dir , "review_report.md" ))
557+ )
399558 with open (
400559 os .path .join (self .temp_dir , "best_translations.wiki" ), encoding = "utf-8"
401560 ) as handle :
402561 best_content = handle .read ()
403562
404- self .assertIn ("TIMEOUT" , review_content )
405563 self .assertIn ("=== Prayer/1 ===" , best_content )
406564 self .assertIn ("TIMEOUT" , best_content )
407565
@@ -432,7 +590,7 @@ def test_run_marks_partial_timeout_as_failed(
432590 mock_client .complete .side_effect = ["Review for unit 1" , TIMEOUT_SENTINEL ]
433591
434592 bot = TranslationReviewBot (self .config )
435- summary = bot .run ("Test" , "tr" , output_dir = self .temp_dir )
593+ summary = bot .run ("Test" , "tr" , output_dir = self .temp_dir , with_review = True )
436594
437595 self .assertEqual (summary .processed , 0 )
438596 self .assertEqual (summary .failed , 1 )
0 commit comments