diff --git a/copienator_gui/app.py b/copienator_gui/app.py index 4aea8a0..ce605ae 100644 --- a/copienator_gui/app.py +++ b/copienator_gui/app.py @@ -1591,12 +1591,7 @@ class CopienatorApp(tk.Tk): self._save_current_form() run_values = self._values() - ordered_ids = self._progression_ids(step.id) - if step.id == "correction" and variant.id == "integrate": - # Integration consumes the retrieved batches; it does not invalidate them. - ordered_ids = [step_id for step_id in ordered_ids - if not self.step_by_id[step_id].skip_for_live_correction] - self.state_store.invalidate_after(ordered_ids, step.id) + self._invalidate_after_step(step, variant.id) self.state_store.update_step( step.id, status="running", @@ -1646,13 +1641,24 @@ class CopienatorApp(tk.Tk): self._populate_tree() self._update_controls() + def _invalidate_after_step(self, step: StepDefinition, variant_id: str) -> None: + # Checking remote job status does not change any downstream input. + if step.id == "batch_status": + return + ordered_ids = self._progression_ids(step.id) + if step.id == "correction" and variant_id in {"live", "refaire", "integrate"}: + # These modes reuse correction inputs without generating new batches. + ordered_ids = [step_id for step_id in ordered_ids + if not self.step_by_id[step_id].skip_for_live_correction] + self.state_store.invalidate_after(ordered_ids, step.id) + def _mark_step( self, status: str, *, automatic: bool = False, reason: str | None = None ) -> None: if not self.current_step or not self.state_store.evaluation or self.active_step_id: return self._save_current_form() - self.state_store.invalidate_after(self._progression_ids(self.current_step.id), self.current_step.id) + self._invalidate_after_step(self.current_step, self._current_variant().id) self.state_store.update_step(self.current_step.id, status=status) history: dict[str, object] = { "step": self.current_step.id, diff --git a/tests/test_gui_convenience.py b/tests/test_gui_convenience.py index 48378e5..f29d08a 100644 --- a/tests/test_gui_convenience.py +++ b/tests/test_gui_convenience.py @@ -1,6 +1,7 @@ import os import tempfile import unittest +from itertools import product from pathlib import Path from tkinter import ttk from unittest.mock import patch @@ -280,9 +281,11 @@ class GuiConvenienceTests(unittest.TestCase): self.assertEqual(self.app.state_store.step("correction")["variant"], "integrate") self.assertIn("--deal-with-batched", self.app._make_command()) - def test_integration_preserves_batch_statuses_but_invalidates_downstream_steps(self): - for variant in ("integrate", "batch"): - with self.subTest(variant=variant): + def test_correction_invalidates_batches_only_when_regenerating_or_resetting(self): + for variant, action in product( + ("integrate", "live", "refaire", "batch", "hybrid", "reset"), ("run", "mark") + ): + with self.subTest(variant=variant, action=action): self.app.active_step_id = None self.app.current_step = None self.app.state_store.update_step("correction", variant=variant, batch_started=True) @@ -294,14 +297,36 @@ class GuiConvenienceTests(unittest.TestCase): self.app.tree.selection_set("correction") self.app.update() with patch.object(self.app.runner, "start"), \ - patch.object(self.app, "_missing_requirements", return_value=[]): - self.app._run_current_step() - self.assertEqual(self.app.active_step_id, "correction") + patch.object(self.app, "_missing_requirements", return_value=[]), \ + patch("copienator_gui.app.messagebox.askyesno", return_value=True): + if action == "run": + self.app._run_current_step() + self.assertEqual(self.app.active_step_id, "correction") + else: + self.app._mark_step("success") for step_id, status in statuses.items(): - expected = status if variant == "integrate" and step_id != "annotation" else "stale" + preserve = variant in {"integrate", "live", "refaire"} and step_id != "annotation" + expected = status if preserve else "stale" self.assertEqual(self.app.state_store.step(step_id)["status"], expected) self.app.active_step_id = None + def test_checking_batch_status_preserves_downstream_statuses(self): + self.app.state_store.update_step("correction", batch_started=True) + statuses = {"fetch_batches": "success", "annotation": "detected"} + for step_id, status in statuses.items(): + self.app.state_store.update_step(step_id, status=status) + self.app._populate_tree() + self.app.tree.selection_set("batch_status") + self.app.update() + with patch.object(self.app.runner, "start"), \ + patch.object(self.app, "_missing_requirements", return_value=[]): + self.app._run_current_step() + self.assertEqual(self.app.active_step_id, "batch_status") + self.app.active_step_id = None + self.app._mark_step("success") + for step_id, status in statuses.items(): + self.assertEqual(self.app.state_store.step(step_id)["status"], status) + def test_unsuccessful_batch_retrieval_stays_on_retrieval(self): self.app.state_store.update_step("correction", variant="batch", last_run_variant="batch") self.app._populate_tree()