Scheduler: Autostart-Feature entfernen

Der Scheduler startet niemals automatisch beim Programmstart. Der Nutzer
startet ihn ausschliesslich bewusst ueber den Start-Button im
Scheduler-Tab. scheduler.enabled wird nicht mehr gelesen oder geschrieben;
das Property ist obsolet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 12:57:54 +02:00
parent 719cc50d16
commit 13141f9638
11 changed files with 79 additions and 486 deletions
@@ -25,7 +25,6 @@ public interface SchedulerControlUseCase {
* folgende Sequenz gestartet:
* <ol>
* <li>Zustand auf {@code STARTING} setzen</li>
* <li>{@code scheduler.enabled=true} persistieren</li>
* <li>Exklusiven OS-Lock auf Konfigurationsdatei erwerben</li>
* <li>Scheduler-Adapter starten (erster Tick sofort)</li>
* <li>Zustand auf {@code RUNNING_IDLE} setzen</li>
@@ -45,8 +44,7 @@ public interface SchedulerControlUseCase {
* Ist der Scheduler bereits gestoppt, hat dieser Aufruf keine Wirkung.
* Läuft gerade ein Tick, wechselt der Zustand zu
* {@code STOPPING_BATCH_ACTIVE}; der laufende Batch wird regulär
* zu Ende geführt. Danach werden {@code scheduler.enabled=false}
* persistiert und der OS-Lock freigegeben.
* zu Ende geführt. Danach wird der OS-Lock freigegeben.
*/
void stop();
@@ -85,15 +83,4 @@ public interface SchedulerControlUseCase {
* @param seconds Intervall in Sekunden; sollte &ge; 30 sein
*/
void saveIntervalSeconds(int seconds);
/**
* Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}.
* <p>
* Wird vom Scheduler-Tab aufgerufen, wenn der Benutzer den fehlgeschlagenen
* Autostart dauerhaft deaktivieren möchte. Sicher aufzurufen wenn der Scheduler
* gestoppt ist.
* <p>
* Muss auf einem Hintergrund-Thread aufgerufen werden.
*/
void disableAutostart();
}
@@ -3,9 +3,8 @@ package de.gecheckt.pdf.umbenenner.application.port.in;
/**
* Wird geworfen, wenn der Start des automatischen Schedulers fehlschlägt.
* <p>
* Mögliche Ursachen sind: Fehler beim Erwerb des Konfigurations-Datei-Locks,
* Fehler beim Persistieren von {@code scheduler.enabled=true} oder
* technische Fehler beim Starten des Scheduler-Adapters.
* Mögliche Ursachen sind: Fehler beim Erwerb des Konfigurations-Datei-Locks
* oder technische Fehler beim Starten des Scheduler-Adapters.
* <p>
* Diese Ausnahme ist ungeprüft (extends {@link RuntimeException}) und
* wird in der Callchain bis zum GUI-Layer weitergeleitet, der eine
@@ -26,16 +26,13 @@ import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
* @param lastError letzte aufgetretene deutsche Fehlermeldung;
* wird bei erfolgreichem Lauf gelöscht,
* bei {@code SkippedBusy} unverändert gelassen
* @param autostartFailed {@code true}, wenn ein konfigurierter Autostart
* beim Programmstart fehlgeschlagen ist
*/
public record SchedulerStatus(
SchedulerState state,
Optional<Instant> lastRunEndedAt,
Optional<RunSummary> lastRunSummary,
Optional<Instant> nextTickAt,
Optional<String> lastError,
boolean autostartFailed
Optional<String> lastError
) {
/**
@@ -62,8 +59,8 @@ public record SchedulerStatus(
/**
* Erzeugt den initialen Scheduler-Status beim Programmstart.
* <p>
* Zustand ist {@link SchedulerState#STOPPED}, alle optionalen Felder
* sind leer und {@code autostartFailed} ist {@code false}.
* Zustand ist {@link SchedulerState#STOPPED} und alle optionalen Felder
* sind leer.
*
* @return initialer Scheduler-Status
*/
@@ -73,8 +70,7 @@ public record SchedulerStatus(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
false
Optional.empty()
);
}
}
@@ -3,23 +3,16 @@ package de.gecheckt.pdf.umbenenner.application.port.out;
/**
* Persistierte Scheduler-Einstellungen aus der {@code .properties}-Datei.
* <p>
* Dieses DTO repräsentiert die beiden Scheduler-Properties
* {@code scheduler.enabled} und {@code scheduler.interval.seconds},
* wie sie aus der Konfigurationsdatei gelesen werden. Es wird von
* {@link SchedulerSettingsPort#loadSettings()} zurückgegeben und
* dient als Eingabe für die Autostart-Entscheidung und die
* Scheduler-Tab-Anzeige.
* Dieses DTO repräsentiert die Scheduler-Property
* {@code scheduler.interval.seconds}, wie sie aus der Konfigurationsdatei
* gelesen wird. Es wird von {@link SchedulerSettingsPort#loadSettings()}
* zurückgegeben und dient als Eingabe für die Scheduler-Tab-Anzeige.
*
* @param enabled {@code true}, wenn der Scheduler beim nächsten
* Programmstart automatisch gestartet werden soll
* @param intervalSeconds konfigurierte Wartezeit in Sekunden zwischen
* Läufen; entspricht dem gelesenen Rohwert
* ohne weitere Validierung
*/
public record SchedulerSettings(boolean enabled, int intervalSeconds) {
/** Standardwert für {@code scheduler.enabled}, wenn der Key fehlt oder leer ist. */
public static final boolean DEFAULT_ENABLED = false;
public record SchedulerSettings(int intervalSeconds) {
/** Standardwert für {@code scheduler.interval.seconds}, wenn der Key fehlt oder leer ist. */
public static final int DEFAULT_INTERVAL_SECONDS = 180;
@@ -27,9 +20,9 @@ public record SchedulerSettings(boolean enabled, int intervalSeconds) {
/**
* Erzeugt eine {@code SchedulerSettings}-Instanz mit Standardwerten.
*
* @return Instanz mit {@code enabled=false} und {@code intervalSeconds=180}
* @return Instanz mit {@code intervalSeconds=180}
*/
public static SchedulerSettings defaults() {
return new SchedulerSettings(DEFAULT_ENABLED, DEFAULT_INTERVAL_SECONDS);
return new SchedulerSettings(DEFAULT_INTERVAL_SECONDS);
}
}
@@ -4,9 +4,8 @@ package de.gecheckt.pdf.umbenenner.application.port.out;
* Outbound-Port zum Lesen und Schreiben der Scheduler-Einstellungen
* in der {@code .properties}-Konfigurationsdatei.
* <p>
* Schreiboperationen aktualisieren ausschließlich die beiden
* Scheduler-Keys ({@code scheduler.enabled} und
* {@code scheduler.interval.seconds}). Alle übrigen Zeilen, Kommentare
* Schreiboperationen aktualisieren ausschließlich den Scheduler-Key
* {@code scheduler.interval.seconds}. Alle übrigen Zeilen, Kommentare
* und unbekannten Properties bleiben unverändert erhalten.
* <p>
* Schreibvorgänge sind atomar: Sie erfolgen über eine temporäre Datei,
@@ -33,17 +32,6 @@ public interface SchedulerSettingsPort {
*/
SchedulerSettings loadSettings();
/**
* Schreibt den Wert von {@code scheduler.enabled} in die
* Konfigurationsdatei.
* <p>
* Alle übrigen Inhalte der Datei bleiben unverändert.
*
* @param enabled neuer Wert für {@code scheduler.enabled}
* @throws SchedulerSettingsWriteException wenn der Schreibvorgang fehlschlägt
*/
void saveEnabled(boolean enabled) throws SchedulerSettingsWriteException;
/**
* Schreibt den Wert von {@code scheduler.interval.seconds} in die
* Konfigurationsdatei.
@@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicReference;
* Dieser Use Case:
* <ul>
* <li>Verwaltet den Scheduler-Lebenszyklus (Start, Stop) über einen
* {@link SchedulerPort} und persistiert dabei den {@code enabled}-Wert.</li>
* {@link SchedulerPort}.</li>
* <li>Hält den exklusiven OS-Lock auf die Konfigurationsdatei über
* {@link ConfigurationFileLockPort}, solange der Scheduler aktiv ist.</li>
* <li>Liest beim Erstellen das konfigurierte Intervall via
@@ -111,40 +111,28 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
// Schritt 1: Zustand auf STARTING setzen
statusRef.set(withState(current, SchedulerState.STARTING, Optional.empty()));
// Schritt 2: scheduler.enabled=true persistieren
try {
settingsPort.saveEnabled(true);
} catch (Exception e) {
logger.error("Scheduler konnte nicht gestartet werden: Einstellung nicht persistierbar.", e);
statusRef.set(SchedulerStatus.initial());
throw new SchedulerStartException(
"Scheduler-Einstellung konnte nicht gespeichert werden.", e);
}
// Schritt 3: OS-Lock erwerben
// Schritt 2: OS-Lock erwerben
try {
lockPort.acquireLock();
} catch (Exception e) {
logger.error("Scheduler konnte nicht gestartet werden: Lock nicht erwerbbar.", e);
tryPersistDisabled();
statusRef.set(SchedulerStatus.initial());
throw new SchedulerStartException(
"Konfigurationsdatei konnte nicht gesperrt werden.", e);
}
// Schritt 4: Scheduler-Adapter starten
// Schritt 3: Scheduler-Adapter starten
try {
schedulerPort.startScheduler(new SchedulerConfig(intervalSeconds), this::executeWrappedTick);
} catch (Exception e) {
logger.error("Scheduler konnte nicht gestartet werden: Adapter-Start fehlgeschlagen.", e);
lockPort.releaseLock();
tryPersistDisabled();
statusRef.set(SchedulerStatus.initial());
throw new SchedulerStartException(
"Scheduler-Adapter konnte nicht gestartet werden.", e);
}
// Schritt 5: Zustand auf RUNNING_IDLE setzen
// Schritt 4: Zustand auf RUNNING_IDLE setzen
Instant nextTick = Instant.now().plusSeconds(intervalSeconds);
statusRef.updateAndGet(s -> withState(s, SchedulerState.RUNNING_IDLE, Optional.of(nextTick)));
logger.info("Scheduler gestartet. Intervall: {} Sekunden.", intervalSeconds);
@@ -179,7 +167,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
schedulerPort.stopScheduler();
if (!batchWasRunning) {
tryPersistDisabled();
lockPort.releaseLock();
logger.info("Scheduler gestoppt.");
} else {
@@ -220,36 +207,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
settingsPort.saveIntervalSeconds(seconds);
}
/**
* Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}.
*/
@Override
public void disableAutostart() {
try {
settingsPort.saveEnabled(false);
logger.info("Autostart deaktiviert.");
} catch (Exception e) {
logger.warn("Fehler beim Deaktivieren des Autostarts.", e);
}
}
/**
* Markiert den Autostart als fehlgeschlagen.
* <p>
* Wird von der Bootstrap-Schicht aufgerufen, wenn ein konfigurierter Autostart
* beim Programmstart fehlgeschlagen ist. Alle übrigen Statusfelder bleiben erhalten;
* lediglich {@link SchedulerStatus#autostartFailed()} wird auf {@code true} gesetzt.
* <p>
* Diese Methode darf nur aufgerufen werden, wenn der Scheduler noch gestoppt ist
* (unmittelbar nach einem fehlgeschlagenen {@link #start()}).
*/
public void markAutostartFailed() {
statusRef.updateAndGet(s -> new SchedulerStatus(
s.state(), s.lastRunEndedAt(), s.lastRunSummary(),
s.nextTickAt(), s.lastError(), true));
logger.warn("Scheduler-Status: Autostart als fehlgeschlagen markiert.");
}
// -------------------------------------------------------------------------
// Tick-Wrapper (package-private für Testbarkeit)
// -------------------------------------------------------------------------
@@ -265,8 +222,7 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
* <li>{@link SchedulerState#RUNNING_IDLE} bei normalem Weiterlauf</li>
* <li>{@link SchedulerState#STOPPED} wenn ein Stopp-Befehl empfangen wurde</li>
* </ul>
* Im Fall {@link SchedulerState#STOPPED} werden außerdem {@code enabled=false}
* persistiert und der OS-Lock freigegeben.
* Im Fall {@link SchedulerState#STOPPED} wird außerdem der OS-Lock freigegeben.
* <p>
* Package-private, damit Unit-Tests den Tick-Wrapper direkt aufrufen können
* ohne den Scheduler-Executor zu starten.
@@ -315,11 +271,9 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
lastRunEndedAt,
lastRunSummary,
nextTickAt,
lastError,
afterBatch.autostartFailed()));
lastError));
if (stopping) {
tryPersistDisabled();
lockPort.releaseLock();
logger.info("Scheduler gestoppt nach Abschluss des laufenden Batches.");
}
@@ -331,14 +285,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
// Hilfsmethoden
// -------------------------------------------------------------------------
private void tryPersistDisabled() {
try {
settingsPort.saveEnabled(false);
} catch (Exception e) {
logger.warn("Fehler beim Zurücksetzen von scheduler.enabled auf false.", e);
}
}
private static SchedulerStatus withState(
SchedulerStatus base, SchedulerState state, Optional<Instant> nextTickAt) {
return new SchedulerStatus(
@@ -346,7 +292,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
base.lastRunEndedAt(),
base.lastRunSummary(),
nextTickAt,
base.lastError(),
base.autostartFailed());
base.lastError());
}
}
@@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -32,21 +31,9 @@ import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerConfig;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerPort;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettings;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettingsPort;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettingsWriteException;
/**
* Unit-Tests für {@link DefaultSchedulerControlUseCase}.
* <p>
* Teststrategien:
* <ul>
* <li>Lifecycle-Tests (Start, Stop, Idempotenz) prüfen Zustandsübergänge
* und Interaktionen mit gemockten Ports.</li>
* <li>Tick-Tests rufen {@code executeWrappedTick()} direkt auf (package-private)
* und verifizieren die Zustandsübergänge für alle drei
* {@link BatchRunTriggerResult}-Varianten.</li>
* <li>Rollback-Tests prüfen, dass fehlgeschlagene Startschritte den Zustand
* vollständig auf {@link SchedulerState#STOPPED} zurücksetzen.</li>
* </ul>
*/
@ExtendWith(MockitoExtension.class)
class DefaultSchedulerControlUseCaseTest {
@@ -62,13 +49,9 @@ class DefaultSchedulerControlUseCaseTest {
@BeforeEach
void setUp() {
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(false, 180));
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(180));
}
// =========================================================================
// Initialer Zustand
// =========================================================================
@Test
void getStatus_returnsInitialStatusOnCreation() {
DefaultSchedulerControlUseCase useCase = createUseCase();
@@ -80,15 +63,13 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(status.lastRunSummary()).isEmpty();
assertThat(status.nextTickAt()).isEmpty();
assertThat(status.lastError()).isEmpty();
assertThat(status.autostartFailed()).isFalse();
}
@Test
void constructor_loadsSettingsIntervalSeconds() {
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(false, 300));
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(300));
DefaultSchedulerControlUseCase useCase = createUseCase();
// Verify interval is 300 by starting and checking the SchedulerConfig passed to schedulerPort
useCase.start();
ArgumentCaptor<SchedulerConfig> configCaptor = ArgumentCaptor.forClass(SchedulerConfig.class);
verify(schedulerPort).startScheduler(configCaptor.capture(), any());
@@ -97,7 +78,7 @@ class DefaultSchedulerControlUseCaseTest {
@Test
void constructor_clampsIntervalBelowMinimumTo30() {
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(false, 10));
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(10));
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
@@ -106,10 +87,6 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(30);
}
// =========================================================================
// start(): Normalfall
// =========================================================================
@Test
void start_setsStateToRunningIdle() {
DefaultSchedulerControlUseCase useCase = createUseCase();
@@ -133,13 +110,6 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(nextTickAt.get()).isBeforeOrEqualTo(after.plusSeconds(181));
}
@Test
void start_persistsEnabledTrue() {
createUseCase().start();
verify(settingsPort).saveEnabled(true);
}
@Test
void start_acquiresLock() {
createUseCase().start();
@@ -156,37 +126,14 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(180);
}
// =========================================================================
// start(): Idempotenz
// =========================================================================
@Test
void start_whenAlreadyActive_isIdempotent() {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
assertThatCode(useCase::start).doesNotThrowAnyException();
// Second start must NOT persist, acquire lock or start scheduler again
verify(settingsPort).saveEnabled(true); // only once
verify(lockPort).acquireLock(); // only once
verify(schedulerPort).startScheduler(any(), any()); // only once
}
// =========================================================================
// start(): Rollback bei Fehlern
// =========================================================================
@Test
void start_rollsBackToStoppedWhenSaveEnabledFails() {
doThrow(new SchedulerSettingsWriteException("Schreibfehler"))
.when(settingsPort).saveEnabled(true);
DefaultSchedulerControlUseCase useCase = createUseCase();
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
verify(lockPort, never()).acquireLock();
verify(schedulerPort, never()).startScheduler(any(), any());
verify(lockPort).acquireLock();
verify(schedulerPort).startScheduler(any(), any());
}
@Test
@@ -198,7 +145,6 @@ class DefaultSchedulerControlUseCaseTest {
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
verify(settingsPort).saveEnabled(false); // rollback
verify(schedulerPort, never()).startScheduler(any(), any());
}
@@ -212,27 +158,8 @@ class DefaultSchedulerControlUseCaseTest {
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
verify(lockPort).releaseLock();
verify(settingsPort).saveEnabled(false); // rollback
}
@Test
void start_rollbackPersistsEnabledFalseEvenWhenRollbackFails() {
doThrow(new ConfigurationFileLockException("Timeout"))
.when(lockPort).acquireLock();
doThrow(new SchedulerSettingsWriteException("Rollback-Fehler"))
.when(settingsPort).saveEnabled(false);
DefaultSchedulerControlUseCase useCase = createUseCase();
// Even if rollback persist fails, SchedulerStartException is thrown and state is STOPPED
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
}
// =========================================================================
// stop(): Normalfall (kein laufender Batch)
// =========================================================================
@Test
void stop_whenRunningIdle_stopsImmediately() {
DefaultSchedulerControlUseCase useCase = createUseCase();
@@ -243,16 +170,6 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
}
@Test
void stop_persistsEnabledFalse() {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
useCase.stop();
verify(settingsPort).saveEnabled(false);
}
@Test
void stop_releasesLock() {
DefaultSchedulerControlUseCase useCase = createUseCase();
@@ -283,17 +200,12 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(useCase.getStatus().nextTickAt()).isEmpty();
}
// =========================================================================
// stop(): Idempotenz
// =========================================================================
@Test
void stop_whenAlreadyStopped_isIdempotent() {
DefaultSchedulerControlUseCase useCase = createUseCase();
assertThatCode(useCase::stop).doesNotThrowAnyException();
verify(schedulerPort, never()).stopScheduler();
verify(settingsPort, never()).saveEnabled(anyBoolean());
}
@Test
@@ -303,44 +215,28 @@ class DefaultSchedulerControlUseCaseTest {
useCase.stop();
assertThatCode(useCase::stop).doesNotThrowAnyException();
verify(schedulerPort).stopScheduler(); // only once from the first stop
verify(schedulerPort).stopScheduler();
}
// =========================================================================
// stop(): STOPPING_BATCH_ACTIVE-Szenario (via executeWrappedTick direkt)
// =========================================================================
@Test
void stop_whenBatchRunning_setsStoppingBatchActiveAndDefersCleaup() {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
// Während des triggerRun()-Aufrufs ist der Zustand RUNNING_BATCH_ACTIVE.
// Ein dazwischen gerufenes stop() setzt STOPPING_BATCH_ACTIVE.
// executeWrappedTick() erkennt das danach und setzt STOPPED.
when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
// stop() während RUNNING_BATCH_ACTIVE → setzt STOPPING_BATCH_ACTIVE
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_BATCH_ACTIVE);
useCase.stop();
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPING_BATCH_ACTIVE);
// Lock und saveEnabled(false) dürfen hier noch nicht aufgerufen worden sein
verify(lockPort, never()).releaseLock();
verify(settingsPort, never()).saveEnabled(false);
return new BatchRunTriggerResult.SkippedBusy();
});
useCase.executeWrappedTick();
// Erst nach Abschluss des Batches: STOPPED und Cleanup
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
verify(lockPort).releaseLock();
verify(settingsPort).saveEnabled(false);
}
// =========================================================================
// executeWrappedTick(): Zustandsübergänge für alle Ergebnisvarianten
// =========================================================================
@Test
void tick_started_setsRunningIdleWithLastRunData() {
DefaultSchedulerControlUseCase useCase = createUseCase();
@@ -364,13 +260,11 @@ class DefaultSchedulerControlUseCaseTest {
void tick_started_clearsLastError() {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
// Erst einen fehlgeschlagenen Tick erzeugen
when(batchRunTrigger.triggerRun())
.thenReturn(new BatchRunTriggerResult.Failed("GUI-Fehler", "technisch"));
useCase.executeWrappedTick();
assertThat(useCase.getStatus().lastError()).isPresent();
// Nun erfolgreich
when(batchRunTrigger.triggerRun())
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp()));
useCase.executeWrappedTick();
@@ -397,13 +291,11 @@ class DefaultSchedulerControlUseCaseTest {
void tick_skippedBusy_preservesLastError() {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
// Erst einen Fehler setzen
when(batchRunTrigger.triggerRun())
.thenReturn(new BatchRunTriggerResult.Failed("Vorheriger Fehler", "technisch"));
useCase.executeWrappedTick();
assertThat(useCase.getStatus().lastError()).isPresent();
// Dann SkippedBusy
when(batchRunTrigger.triggerRun()).thenReturn(new BatchRunTriggerResult.SkippedBusy());
useCase.executeWrappedTick();
@@ -431,7 +323,6 @@ class DefaultSchedulerControlUseCaseTest {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
// Innerhalb des Batch-Aufrufs muss der Zustand RUNNING_BATCH_ACTIVE sein
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_BATCH_ACTIVE);
return new BatchRunTriggerResult.SkippedBusy();
});
@@ -439,25 +330,12 @@ class DefaultSchedulerControlUseCaseTest {
useCase.executeWrappedTick();
}
// =========================================================================
// executeWrappedTick(): STOPPING_BATCH_ACTIVE → STOPPED
// =========================================================================
@Test
void tick_whenStoppingBatchActive_setsStoppedAndCleansUp() {
DefaultSchedulerControlUseCase useCase = createUseCase();
useCase.start();
// Simuliere: stop() wurde aufgerufen während ein Batch lief.
// stop() setzt den Zustand auf STOPPING_BATCH_ACTIVE wenn isBatchRunning().
// Da kein echter Executor läuft, testen wir den Wrapper direkt:
// Wir müssen den Zustand auf STOPPING_BATCH_ACTIVE bringen.
// Dazu nutzen wir, dass executeWrappedTick() zuerst RUNNING_BATCH_ACTIVE setzt,
// dann den Trigger aufruft. Wenn stop() dazwischen aufgerufen würde,
// würde es STOPPING_BATCH_ACTIVE setzen.
// Wir simulieren das, indem wir stop() innerhalb des triggerRun()-Aufrufs rufen:
when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
// stop() während laufendem Batch aufrufen
useCase.stop();
return new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp());
});
@@ -466,7 +344,6 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
assertThat(useCase.getStatus().nextTickAt()).isEmpty();
verify(settingsPort).saveEnabled(false);
verify(lockPort).releaseLock();
}
@@ -489,10 +366,6 @@ class DefaultSchedulerControlUseCaseTest {
assertThat(useCase.getStatus().lastRunSummary()).contains(summary);
}
// =========================================================================
// Hilfsmethoden
// =========================================================================
private DefaultSchedulerControlUseCase createUseCase() {
return new DefaultSchedulerControlUseCase(schedulerPort, lockPort, settingsPort, batchRunTrigger);
}