Guide d'utilisateur GUI ; Recoverable statement/Persp Gemini generation

This commit is contained in:
sebastien committed 2026-10-05 09:04:01 +02:00
1 parent 2f8422f6d5
commit 94ca8b8256
9 files changed
+1333 -170

No files matched your search

+123 -7
View File
@@ -74,7 +74,8 @@ class SelectiveGeminiTests(unittest.TestCase):
(self.root / folder).mkdir()
for label in self.labels:
suffix = ".tex" if folder.endswith("2") else ""
(self.root / folder / (label + suffix)).write_text(f"Personal {folder}: {label}")
(self.root / folder / (label + suffix)).write_text(
label + '\n' if folder == 'Persp' else f"Personal {folder}: {label}")
(self.root / "Persp" / "Ex 1").write_text("Old aggregate rubric")
self.client = SimpleNamespace(models=SimpleNamespace(generate_content=Mock()))
@@ -113,8 +114,11 @@ class SelectiveGeminiTests(unittest.TestCase):
self.assertEqual(gemini.refine_existing(self.workspace, "persp", api_client=self.client), ExitCode.SUCCESS)
after = self.snapshot()
self.assertEqual({key: value for key, value in before.items() if not key.startswith("Persp/")},
{key: value for key, value in after.items() if not key.startswith("Persp/")})
self.assertFalse((self.root / "Persp" / "Ex 1").exists())
{key: value for key, value in after.items()
if not key.startswith(("Persp/", "Cache/"))})
responses = list((self.root / 'Cache' / 'rubric_responses').glob('*.json'))
self.assertEqual(len(responses), 2)
self.assertEqual((self.root / "Persp" / "Ex 1").read_bytes(), before['Persp/Ex 1'])
for label in self.labels:
self.assertIn("Barème Gemini", (self.root / "Persp" / label).read_text())
for call in self.client.models.generate_content.call_args_list:
@@ -126,13 +130,22 @@ class SelectiveGeminiTests(unittest.TestCase):
self.assertIn("caractères mathématiques Unicode", gemini.PROMPT_4)
self.assertIn(r"$\lfloor \sqrt{k} \rfloor$", gemini.PROMPT_4)
def test_incomplete_or_failed_rubrics_preserve_entire_persp(self):
def test_incomplete_or_failed_rubrics_keep_completed_groups(self):
before = self.snapshot()
for last in (self.response({"rubrics": []}), RuntimeError("API unavailable")):
for label in self.labels[:2]:
(self.root / 'Persp' / label).write_bytes(before[f'Persp/{label}'])
self.client.models.generate_content.side_effect = [self.rubric_response(self.labels[:2]), last]
with self.assertRaises((CliError, RuntimeError)):
gemini.refine_existing(self.workspace, "persp", api_client=self.client)
self.assertEqual(self.snapshot(), before)
after = self.snapshot()
for label in self.labels[:2]:
self.assertIn(b'Bar\xc3\xa8me Gemini', after[f'Persp/{label}'])
self.assertEqual(after['Persp/Ex 2'], before['Persp/Ex 2'])
self.assertEqual(after['Persp/Ex 1'], before['Persp/Ex 1'])
self.assertEqual({key: value for key, value in before.items() if not key.startswith('Persp/')},
{key: value for key, value in after.items()
if not key.startswith(('Persp/', 'Cache/'))})
def test_rubric_latex_and_line_breaks_survive_json_and_file_writes(self):
expected = (
@@ -161,15 +174,96 @@ class SelectiveGeminiTests(unittest.TestCase):
{'Ex 2': '$' + command + '$'})
self.assertEqual(self.client.models.generate_content.call_count, 1)
def test_unrepairable_control_character_preserves_all_existing_rubrics(self):
def test_control_character_keeps_progress_and_saves_diagnostics(self):
before = self.snapshot()
broken = self.response({'rubrics': [{'label': 'Ex 2', 'rubric_content': 'invalid \x01 control'}]})
self.client.models.generate_content.side_effect = [
self.rubric_response(self.labels[:2]), broken]
with self.assertRaisesRegex(ValueError, 'control characters'):
output = io.StringIO()
with patch('sys.stdout', output), self.assertRaisesRegex(ValueError, 'control characters'):
gemini.refine_existing(self.workspace, 'persp', api_client=self.client)
after = self.snapshot()
self.assertEqual(after['Persp/Ex 2'], before['Persp/Ex 2'])
for label in self.labels[:2]:
self.assertIn(b'Bar\xc3\xa8me Gemini', after[f'Persp/{label}'])
self.assertIn('Question Ex 2', output.getvalue())
self.assertIn('U+0001 at offset 8', output.getvalue())
self.assertIn(r'invalid \x01 control', output.getvalue())
responses = list((self.root / 'Cache' / 'rubric_responses').glob('*.json'))
self.assertEqual(len(responses), 2)
self.assertIn(broken.text, [path.read_text() for path in responses])
log = (self.root / 'Cache' / 'rubric_errors.log').read_text()
self.assertIn('Question Ex 2', log)
self.assertIn(r'invalid \x01 control', log)
def test_invalid_json_is_saved_exactly_and_reported(self):
raw = '{"rubrics": [this is malformed JSON'
self.client.models.generate_content.return_value = SimpleNamespace(text=raw)
output = io.StringIO()
with patch('sys.stdout', output), self.assertRaises(ValueError):
gemini.generate_rubrics(self.client, 'context', workspace=self.workspace,
expected_labels=['Ex 2'])
responses = list((self.root / 'Cache' / 'rubric_responses').glob('*.json'))
self.assertEqual(len(responses), 1)
self.assertEqual(responses[0].read_text(), raw)
self.assertIn(raw, output.getvalue())
self.assertIn(str(responses[0]), output.getvalue())
def test_missing_question_only_preserves_existing_and_keeps_group_context(self):
kept = self.root / 'Persp' / self.labels[0]
kept.write_text(self.labels[0] + '\nManually reviewed rubric')
before = kept.read_bytes()
(self.root / 'Persp' / self.labels[1]).write_text('')
(self.root / 'Persp' / self.labels[2]).unlink()
self.client.models.generate_content.side_effect = [
self.rubric_response([self.labels[1]]), self.rubric_response([self.labels[2]])]
gemini.refine_existing(self.workspace, 'persp', api_client=self.client)
self.assertEqual(kept.read_bytes(), before)
call = self.client.models.generate_content.call_args_list[0]
text = call.kwargs['contents'][0].parts[1].text
self.assertIn('UNIQUEMENT pour les labels suivants : ' + self.labels[1] + '\n', text)
self.assertIn('Question [' + self.labels[0] + ']', text)
self.assertEqual(self.client.models.generate_content.call_count, 2)
def test_retry_requests_only_failed_group(self):
self.client.models.generate_content.side_effect = [
self.rubric_response(self.labels[:2]), RuntimeError('API unavailable')]
with self.assertRaises(RuntimeError):
gemini.refine_existing(self.workspace, 'persp', api_client=self.client)
before = {label: (self.root / 'Persp' / label).read_bytes() for label in self.labels[:2]}
self.client.models.generate_content.reset_mock()
self.client.models.generate_content.side_effect = [self.rubric_response(self.labels[2:])]
gemini.refine_existing(self.workspace, 'persp', api_client=self.client)
self.assertEqual(self.client.models.generate_content.call_count, 1)
for label, content in before.items():
self.assertEqual((self.root / 'Persp' / label).read_bytes(), content)
def test_complete_persp_needs_no_api_key_or_requests(self):
for label in self.labels:
(self.root / 'Persp' / label).write_text(label + '\nReviewed rubric')
before = self.snapshot()
with patch.object(gemini, 'api_key', ''), patch.object(gemini.genai, 'Client') as client:
self.assertEqual(gemini.refine_existing(self.workspace, 'persp'), ExitCode.SUCCESS)
client.assert_not_called()
self.assertEqual(self.snapshot(), before)
def test_delete_persp_forces_regeneration_and_removes_old_files(self):
for label in self.labels:
(self.root / 'Persp' / label).write_text(label + '\nReviewed rubric')
self.client.models.generate_content.side_effect = [
self.rubric_response(self.labels[:2]), self.rubric_response(self.labels[2:])]
gemini.refine_existing(self.workspace, 'persp', api_client=self.client, delete_persp=True)
self.assertEqual(self.client.models.generate_content.call_count, 2)
self.assertEqual({path.name for path in (self.root / 'Persp').iterdir()}, set(self.labels))
def test_cli_delete_persp_dispatch(self):
with patch.object(gemini, 'refine_existing', return_value=ExitCode.SUCCESS) as refine:
self.assertEqual(gemini.main([str(self.root), '--persp-only', '--delete-persp']), ExitCode.SUCCESS)
self.assertEqual(refine.call_args.kwargs, {'delete_persp': True})
refine.reset_mock()
self.assertEqual(gemini.main([str(self.root), '--delete-persp']), ExitCode.INVALID_ARGUMENTS)
refine.assert_not_called()
def test_cli_dispatches_selective_modes_without_full_extraction(self):
with patch.object(gemini, "refine_existing", return_value=ExitCode.SUCCESS) as refine, patch.object(
gemini, "process_exam"
@@ -251,6 +345,28 @@ class PersonalStatementGuiTests(unittest.TestCase):
self.assertIn(flag, app.command_var.get())
self.assertTrue(app.current_step.optional)
self.assertFalse(app.runner.running)
(evaluation / 'labels').write_text('Ex 1\nEx 2\n')
(evaluation / 'Persp').mkdir()
(evaluation / 'Persp' / 'Ex 1').write_text('Ex 1\nReviewed rubric')
(evaluation / 'Persp' / 'Ex 2').write_text('Ex 2\n')
app._refresh_persp_inventory()
self.assertIn('Barèmes présents : 1/2', app.persp_inventory_label.cget('text'))
self.assertIn('Manquants : Ex 2', app.persp_inventory_label.cget('text'))
self.assertFalse(app.arg_vars['delete_persp'].get())
app.arg_vars['delete_persp'].set(True)
self.assertIn('--delete-persp', app.command_var.get())
app._save_current_form()
app._render_step()
self.assertFalse(app.arg_vars['delete_persp'].get())
self.assertNotIn('--delete-persp', app.command_var.get())
app.tree.selection_set('statement')
app.update()
app._select_variant(1)
app.arg_vars['restart'].set(True)
app._save_current_form()
app._render_step()
self.assertFalse(app.arg_vars['restart'].get())
self.assertNotIn('--restart', app.command_var.get())
finally:
for callback in app.tk.splitlist(app.tk.call("after", "info")):
app.after_cancel(callback)