#71: Prompt-Editor-Tab in der GUI implementieren
Neuer Tab „Prompt" in der GUI-Hauptansicht ermöglicht das Lesen, Bearbeiten und atomare Speichern der konfigurierten KI-Prompt-Datei ohne externen Editor. Änderungen: - PromptSaveResult: neue sealed interface mit Saved, WriteFailed, TargetDirectoryMissing, AtomicMoveFailed als strukturierte Ergebnistypen für savePrompt() - PromptPort: um savePrompt(String) erweitert (nicht mehr funktional – Teststubs angepasst) - FilesystemPromptPortAdapter: savePrompt() mit Temp-Datei im selben Verzeichnis + ATOMIC_MOVE, kein stiller Fallback bei AtomicMoveNotSupportedException - DefaultPromptEditorUseCase: Use-Case-Klasse mit loadPrompt(), savePrompt(), createDefaultPromptIfMissing() als Delegation an PromptPort und ResourceCreationPort - GuiPromptEditorPort: GUI-internes Bridge-Interface (kein hexagonaler Port) - GuiPromptEditorTab: JavaFX-Tab mit TextArea, Dirty-State-Tracking, Speichern/Reset/Anlegen, injizierbare threadFactory + fxDispatcher für Testbarkeit - GuiStartupContext: um promptEditorPort erweitert; alle Backward-Compat-Konstruktoren und blank() mit noOpPromptEditorPort() versorgt - GuiConfigurationEditorWorkspace: promptEditorTab integriert, Tab-Wechsel-Schutz erweitert - BootstrapRunner: buildGuiPromptEditorPort() verdrahtet FilesystemPromptPortAdapter + DefaultPromptEditorUseCase; noOpGuiPromptEditorPort() für Blank-Start-Fälle - Tests: DefaultPromptEditorUseCaseTest, FilesystemPromptPortAdapterTest (savePrompt), GuiPromptEditorTabSmokeTest (headless Monocle), GuiAdapterSmokeTest auf 3 Tabs aktualisiert - docs/betrieb.md: Prompt-Tab dokumentiert, Pfad-Auflösungstabelle ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+51
-31
@@ -1,56 +1,76 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Outbound port for loading external prompt templates.
|
||||
* Outbound-Port zum Laden und Speichern des externen Prompt-Templates.
|
||||
* <p>
|
||||
* This interface abstracts the loading of prompt content from external sources
|
||||
* (files, resources, databases, etc.), allowing the Application layer to remain
|
||||
* independent of how or where prompts are stored.
|
||||
* Dieses Interface abstrahiert den Zugriff auf die Prompt-Datei und erlaubt der
|
||||
* Application-Schicht, unabhängig vom konkreten Speichermedium zu bleiben.
|
||||
* <p>
|
||||
* <strong>Design principles:</strong>
|
||||
* <strong>Designprinzipien:</strong>
|
||||
* <ul>
|
||||
* <li>Prompt is not embedded in code; it is loaded from an external source</li>
|
||||
* <li>Each prompt receives a stable identifier for traceability across batch runs</li>
|
||||
* <li>Results are returned as structured types ({@link PromptLoadingResult}),
|
||||
* never as exceptions</li>
|
||||
* <li>Der Prompt wird nicht im Code fest verdrahtet, sondern aus einer externen Quelle geladen.</li>
|
||||
* <li>Jeder Prompt erhält einen stabilen Identifikator für die lückenlose Nachvollziehbarkeit.</li>
|
||||
* <li>Ergebnisse werden als strukturierte Typen zurückgegeben, niemals als Exceptions.</li>
|
||||
* <li>Der Pfad zur Prompt-Datei ist Implementierungsdetail des Adapters – er erscheint nicht
|
||||
* in der Port-Signatur (hexagonale Regel: keine {@code Path}/{@code File}-Typen).</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Adapter responsibilities:</strong>
|
||||
* <strong>Adapter-Verantwortung:</strong>
|
||||
* <ul>
|
||||
* <li>Locate and read the prompt file/resource from the configured source</li>
|
||||
* <li>Derive a stable prompt identifier (e.g., filename, semantic version, content hash)</li>
|
||||
* <li>Validate that the loaded content is not empty or otherwise invalid</li>
|
||||
* <li>Return either success or a classified failure</li>
|
||||
* <li>Encapsulate all file I/O, resource loading, and configuration details</li>
|
||||
* <li>Prompt-Datei lokalisieren und lesen.</li>
|
||||
* <li>Stabilen Identifikator ableiten (z. B. Dateiname).</li>
|
||||
* <li>Leere oder technisch unbrauchbare Prompts ablehnen.</li>
|
||||
* <li>Beim Speichern: atomares Schreiben via temporäre Datei und {@code ATOMIC_MOVE}.</li>
|
||||
* <li>Alle Datei-I/O-, Ressourcen- und Konfigurationsdetails kapseln.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Non-goals of this port:</strong>
|
||||
* <strong>Nicht-Ziele dieses Ports:</strong>
|
||||
* <ul>
|
||||
* <li>Prompt parsing or templating logic</li>
|
||||
* <li>Combining prompt with document text (Application layer handles this)</li>
|
||||
* <li>Template variable substitution</li>
|
||||
* <li>Validation of prompt content against domain rules</li>
|
||||
* <li>Prompt-Parsing oder Template-Verarbeitung</li>
|
||||
* <li>Kombination von Prompt und Dokumenttext (Application-Schicht)</li>
|
||||
* <li>Validierung des Prompt-Inhalts gegen Domänenregeln</li>
|
||||
* </ul>
|
||||
*/
|
||||
public interface PromptPort {
|
||||
|
||||
/**
|
||||
* Loads the configured external prompt template.
|
||||
* Lädt das konfigurierte externe Prompt-Template.
|
||||
* <p>
|
||||
* This method is called once per batch run to obtain the current prompt.
|
||||
* The prompt content and its stable identifier are returned together.
|
||||
* Diese Methode wird einmal pro Verarbeitungslauf aufgerufen, um den aktuellen Prompt zu laden.
|
||||
* Inhalt und stabiler Identifikator werden gemeinsam zurückgegeben.
|
||||
* <p>
|
||||
* If loading fails for any reason (file not found, I/O error, content validation),
|
||||
* a {@link PromptLoadingFailure} is returned rather than throwing an exception.
|
||||
*
|
||||
* @return a {@link PromptLoadingResult} encoding either:
|
||||
* <ul>
|
||||
* <li>Success: prompt content and identifier loaded successfully</li>
|
||||
* <li>Failure: prompt could not be loaded or is invalid</li>
|
||||
* </ul>
|
||||
* Bei einem technischen Fehler (Datei nicht gefunden, I/O-Fehler, leerer Inhalt) wird
|
||||
* {@link PromptLoadingFailure} zurückgegeben – keine Exception wird geworfen.
|
||||
*
|
||||
* @return {@link PromptLoadingResult} mit Erfolg oder klassifiziertem Fehler; nie {@code null}
|
||||
* @see PromptLoadingSuccess
|
||||
* @see PromptLoadingFailure
|
||||
*/
|
||||
PromptLoadingResult loadPrompt();
|
||||
|
||||
/**
|
||||
* Speichert den übergebenen Inhalt atomar in die konfigurierte Prompt-Datei.
|
||||
* <p>
|
||||
* Der Zielpfad wird intern aus der Konfiguration des Adapters ermittelt und ist
|
||||
* <em>nicht</em> Teil dieser Signatur (hexagonale Regel: keine {@code Path}/{@code File}-Typen
|
||||
* im Port-Vertrag).
|
||||
* <p>
|
||||
* Die Implementierung schreibt zunächst in eine temporäre Datei <em>im selben Verzeichnis</em>
|
||||
* wie die Zieldatei und verschiebt diese danach atomar via {@code ATOMIC_MOVE}.
|
||||
* Bei einem Fehler beim atomaren Verschieben wird <strong>kein stiller Fallback</strong>
|
||||
* auf ein nicht-atomares Schreiben durchgeführt; stattdessen wird
|
||||
* {@link PromptSaveResult.AtomicMoveFailed} zurückgegeben.
|
||||
* <p>
|
||||
* Zeichenkodierung: UTF-8. Zeilenenden werden unverändert übernommen.
|
||||
*
|
||||
* @param content der zu speichernde Prompt-Inhalt; darf leer sein (Entscheidung liegt
|
||||
* beim Aufrufer, ob ein leerer Prompt erwünscht ist)
|
||||
* @return {@link PromptSaveResult} mit Erfolg oder klassifiziertem Fehler; nie {@code null}
|
||||
* @throws NullPointerException wenn {@code content} null ist
|
||||
* @see PromptSaveResult.Saved
|
||||
* @see PromptSaveResult.WriteFailed
|
||||
* @see PromptSaveResult.TargetDirectoryMissing
|
||||
* @see PromptSaveResult.AtomicMoveFailed
|
||||
*/
|
||||
PromptSaveResult savePrompt(String content);
|
||||
}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
|
||||
/**
|
||||
* Versiegeltes Ergebnis-Interface für das Speichern einer Prompt-Datei via
|
||||
* {@link PromptPort#savePrompt(String)}.
|
||||
* <p>
|
||||
* Mögliche Ergebnisse:
|
||||
* <ul>
|
||||
* <li>{@link Saved} – das Speichern war erfolgreich.</li>
|
||||
* <li>{@link WriteFailed} – ein technischer Fehler beim Schreiben ist aufgetreten.</li>
|
||||
* <li>{@link TargetDirectoryMissing} – der konfigurierte Zielordner existiert nicht.</li>
|
||||
* <li>{@link AtomicMoveFailed} – das atomare Verschieben der temporären Datei ist
|
||||
* fehlgeschlagen; kein stiller Fallback.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public sealed interface PromptSaveResult
|
||||
permits PromptSaveResult.Saved,
|
||||
PromptSaveResult.WriteFailed,
|
||||
PromptSaveResult.TargetDirectoryMissing,
|
||||
PromptSaveResult.AtomicMoveFailed {
|
||||
|
||||
/**
|
||||
* Die Prompt-Datei wurde erfolgreich gespeichert.
|
||||
*
|
||||
* @param absolutePath absoluter Pfad der gespeicherten Datei; nie {@code null}
|
||||
*/
|
||||
record Saved(String absolutePath) implements PromptSaveResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein Saved-Ergebnis.
|
||||
*
|
||||
* @param absolutePath absoluter Pfad der gespeicherten Datei; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code absolutePath} null ist
|
||||
*/
|
||||
public Saved {
|
||||
java.util.Objects.requireNonNull(absolutePath, "absolutePath must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Das Schreiben der temporären Datei ist fehlgeschlagen.
|
||||
*
|
||||
* @param message Fehlerbeschreibung; nie {@code null}
|
||||
* @param cause auslösende Ausnahme; kann {@code null} sein
|
||||
*/
|
||||
record WriteFailed(String message, Throwable cause) implements PromptSaveResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein WriteFailed-Ergebnis.
|
||||
*
|
||||
* @param message Fehlerbeschreibung; darf nicht {@code null} sein
|
||||
* @param cause auslösende Ausnahme; kann {@code null} sein
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public WriteFailed {
|
||||
java.util.Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Der konfigurierte Zielordner existiert nicht.
|
||||
*
|
||||
* @param message Beschreibung des fehlenden Ordners; nie {@code null}
|
||||
*/
|
||||
record TargetDirectoryMissing(String message) implements PromptSaveResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein TargetDirectoryMissing-Ergebnis.
|
||||
*
|
||||
* @param message Beschreibung des fehlenden Ordners; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public TargetDirectoryMissing {
|
||||
java.util.Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Das atomare Verschieben der temporären Datei zur Zieldatei ist fehlgeschlagen.
|
||||
* Es wird kein stiller Fallback auf nicht-atomares Schreiben durchgeführt.
|
||||
*
|
||||
* @param message Fehlerbeschreibung; nie {@code null}
|
||||
*/
|
||||
record AtomicMoveFailed(String message) implements PromptSaveResult {
|
||||
|
||||
/**
|
||||
* Erstellt ein AtomicMoveFailed-Ergebnis.
|
||||
*
|
||||
* @param message Fehlerbeschreibung; darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn {@code message} null ist
|
||||
*/
|
||||
public AtomicMoveFailed {
|
||||
java.util.Objects.requireNonNull(message, "message must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;
|
||||
|
||||
/**
|
||||
* Use-Case zur Anzeige und Bearbeitung des KI-Prompt-Templates über die GUI.
|
||||
* <p>
|
||||
* Dieser Use-Case vermittelt zwischen dem GUI-Adapter und dem {@link PromptPort} sowie dem
|
||||
* {@link ResourceCreationPort}. Er kennt keine JavaFX-Typen, kein Dateisystem und keine
|
||||
* HTTP-Kommunikation; alle technischen Details bleiben in den jeweiligen Adaptern.
|
||||
* <p>
|
||||
* <strong>Verantwortung:</strong>
|
||||
* <ul>
|
||||
* <li>Aktuellen Prompt-Inhalt laden und als strukturiertes Ergebnis zurückgeben.</li>
|
||||
* <li>Bearbeiteten Inhalt atomar in die konfigurierte Prompt-Datei speichern.</li>
|
||||
* <li>Anlegen einer Standard-Prompt-Datei delegieren, wenn keine Datei vorhanden ist.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <strong>Abgrenzung:</strong> Dieser Use-Case trifft keine Entscheidungen über
|
||||
* Benutzeroberfläche, Threading oder Dirty-State-Verwaltung. Diese Verantwortung
|
||||
* liegt im GUI-Adapter.
|
||||
*/
|
||||
public class DefaultPromptEditorUseCase {
|
||||
|
||||
private final PromptPort promptPort;
|
||||
private final ResourceCreationPort resourceCreationPort;
|
||||
|
||||
/**
|
||||
* Erstellt den Use-Case mit den erforderlichen Ports.
|
||||
*
|
||||
* @param promptPort Port zum Laden und Speichern des Prompt-Templates;
|
||||
* darf nicht {@code null} sein
|
||||
* @param resourceCreationPort Port zum Anlegen der Standard-Prompt-Datei;
|
||||
* darf nicht {@code null} sein
|
||||
* @throws NullPointerException wenn ein Parameter {@code null} ist
|
||||
*/
|
||||
public DefaultPromptEditorUseCase(PromptPort promptPort, ResourceCreationPort resourceCreationPort) {
|
||||
this.promptPort = Objects.requireNonNull(promptPort, "promptPort must not be null");
|
||||
this.resourceCreationPort = Objects.requireNonNull(resourceCreationPort,
|
||||
"resourceCreationPort must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt den aktuellen Prompt-Inhalt aus der konfigurierten Prompt-Datei.
|
||||
* <p>
|
||||
* Delegiert direkt an {@link PromptPort#loadPrompt()} und gibt das Ergebnis
|
||||
* unverändert zurück.
|
||||
*
|
||||
* @return {@link PromptLoadingResult} mit Inhalt und Identifikator bei Erfolg,
|
||||
* oder einem klassifizierten Fehler; nie {@code null}
|
||||
* @see PromptLoadingSuccess
|
||||
* @see PromptLoadingFailure
|
||||
*/
|
||||
public PromptLoadingResult loadPrompt() {
|
||||
return promptPort.loadPrompt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert den übergebenen Inhalt atomar in die konfigurierte Prompt-Datei.
|
||||
* <p>
|
||||
* Delegiert direkt an {@link PromptPort#savePrompt(String)}. Der Zielpfad ist
|
||||
* Implementierungsdetail des Adapters.
|
||||
*
|
||||
* @param content der zu speichernde Prompt-Inhalt; darf nicht {@code null} sein
|
||||
* @return {@link PromptSaveResult} mit Erfolg oder klassifiziertem Fehler; nie {@code null}
|
||||
* @throws NullPointerException wenn {@code content} null ist
|
||||
* @see PromptSaveResult.Saved
|
||||
* @see PromptSaveResult.WriteFailed
|
||||
* @see PromptSaveResult.TargetDirectoryMissing
|
||||
* @see PromptSaveResult.AtomicMoveFailed
|
||||
*/
|
||||
public PromptSaveResult savePrompt(String content) {
|
||||
Objects.requireNonNull(content, "content must not be null");
|
||||
return promptPort.savePrompt(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine Standard-Prompt-Datei an, wenn noch keine vorhanden ist.
|
||||
* <p>
|
||||
* Delegiert an {@link ResourceCreationPort#createPromptFile(CorrectionSuggestion.CreatePromptFile)}.
|
||||
* Das Ergebnis beschreibt, ob die Datei angelegt wurde, ob sie bereits existierte
|
||||
* oder ob ein Fehler aufgetreten ist.
|
||||
*
|
||||
* @param suggestion Korrekturvorschlag mit dem Zielpfad; darf nicht {@code null} sein
|
||||
* @return {@link CorrectionOutcome} mit dem Ergebnis der Aktion; nie {@code null}
|
||||
* @throws NullPointerException wenn {@code suggestion} null ist
|
||||
*/
|
||||
public CorrectionOutcome createDefaultPromptIfMissing(CorrectionSuggestion.CreatePromptFile suggestion) {
|
||||
Objects.requireNonNull(suggestion, "suggestion must not be null");
|
||||
return resourceCreationPort.createPromptFile(suggestion);
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -1062,8 +1062,16 @@ class BatchRunProcessingUseCaseTest {
|
||||
private static AiNamingService buildStubAiNamingService() {
|
||||
AiInvocationPort stubAiPort = request ->
|
||||
new AiInvocationTechnicalFailure(request, "STUBBED", "Stubbed AI for test");
|
||||
PromptPort stubPromptPort = () ->
|
||||
new PromptLoadingSuccess(new PromptIdentifier("stub-prompt"), "stub prompt content");
|
||||
PromptPort stubPromptPort = new PromptPort() {
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadPrompt() {
|
||||
return new PromptLoadingSuccess(new PromptIdentifier("stub-prompt"), "stub prompt content");
|
||||
}
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult savePrompt(String content) {
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.Saved("stub-path");
|
||||
}
|
||||
};
|
||||
ClockPort stubClock = () -> java.time.Instant.EPOCH;
|
||||
AiResponseValidator validator = new AiResponseValidator(stubClock, TEST_MAX_TITLE_LENGTH);
|
||||
return new AiNamingService(stubAiPort, stubPromptPort, validator, "stub-model", 1000,
|
||||
|
||||
+10
-2
@@ -279,8 +279,16 @@ class BatchRunProgressObservationTest {
|
||||
AiInvocationPort stubAi = req -> {
|
||||
throw new IllegalStateException("AI must not be invoked in these tests");
|
||||
};
|
||||
PromptPort stubPrompt = () -> new PromptLoadingSuccess(
|
||||
new PromptIdentifier("stub-prompt"), "Prompt: {{text}}");
|
||||
PromptPort stubPrompt = new PromptPort() {
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult loadPrompt() {
|
||||
return new PromptLoadingSuccess(new PromptIdentifier("stub-prompt"), "Prompt: {{text}}");
|
||||
}
|
||||
@Override
|
||||
public de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult savePrompt(String content) {
|
||||
return new de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult.Saved("stub-path");
|
||||
}
|
||||
};
|
||||
ClockPort stubClock = () -> Instant.parse("2026-04-22T00:00:00Z");
|
||||
AiResponseValidator validator = new AiResponseValidator(stubClock, TEST_MAX_TITLE);
|
||||
return new AiNamingService(stubAi, stubPrompt, validator, "stub-model", 1000, TEST_MAX_TITLE);
|
||||
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package de.gecheckt.pdf.umbenenner.application.usecase;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingSuccess;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
|
||||
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.PromptIdentifier;
|
||||
|
||||
/**
|
||||
* Unit-Tests für {@link DefaultPromptEditorUseCase}.
|
||||
* <p>
|
||||
* Prüft die Delegation an {@link PromptPort} und {@link ResourceCreationPort}
|
||||
* sowie die Null-Prüfungen am Konstruktor und an den Methoden.
|
||||
*/
|
||||
class DefaultPromptEditorUseCaseTest {
|
||||
|
||||
private static final String STUB_CONTENT = "Mein Test-Prompt";
|
||||
private static final PromptIdentifier STUB_IDENTIFIER = new PromptIdentifier("test-prompt.txt");
|
||||
|
||||
private StubPromptPort stubPromptPort;
|
||||
private StubResourceCreationPort stubResourceCreationPort;
|
||||
private DefaultPromptEditorUseCase useCase;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
stubPromptPort = new StubPromptPort();
|
||||
stubResourceCreationPort = new StubResourceCreationPort();
|
||||
useCase = new DefaultPromptEditorUseCase(stubPromptPort, stubResourceCreationPort);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Konstruktor
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void constructor_shouldThrowNullPointerException_whenPromptPortIsNull() {
|
||||
assertThatThrownBy(() -> new DefaultPromptEditorUseCase(null, stubResourceCreationPort))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageContaining("promptPort");
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_shouldThrowNullPointerException_whenResourceCreationPortIsNull() {
|
||||
assertThatThrownBy(() -> new DefaultPromptEditorUseCase(stubPromptPort, null))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageContaining("resourceCreationPort");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// loadPrompt
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void loadPrompt_shouldDelegateToPromptPort_andReturnSuccess() {
|
||||
// Given
|
||||
stubPromptPort.loadResult = new PromptLoadingSuccess(STUB_IDENTIFIER, STUB_CONTENT);
|
||||
|
||||
// When
|
||||
PromptLoadingResult result = useCase.loadPrompt();
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf(PromptLoadingSuccess.class);
|
||||
PromptLoadingSuccess success = (PromptLoadingSuccess) result;
|
||||
assertThat(success.promptContent()).isEqualTo(STUB_CONTENT);
|
||||
assertThat(success.promptIdentifier()).isEqualTo(STUB_IDENTIFIER);
|
||||
assertThat(stubPromptPort.loadCallCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadPrompt_shouldDelegateToPromptPort_andReturnFailure() {
|
||||
// Given
|
||||
stubPromptPort.loadResult = new PromptLoadingFailure("FILE_NOT_FOUND", "Datei nicht vorhanden");
|
||||
|
||||
// When
|
||||
PromptLoadingResult result = useCase.loadPrompt();
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf(PromptLoadingFailure.class);
|
||||
PromptLoadingFailure failure = (PromptLoadingFailure) result;
|
||||
assertThat(failure.failureReason()).isEqualTo("FILE_NOT_FOUND");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// savePrompt
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void savePrompt_shouldDelegateToPromptPort_andReturnSaved() {
|
||||
// Given
|
||||
stubPromptPort.saveResult = new PromptSaveResult.Saved("/some/path/prompt.txt");
|
||||
|
||||
// When
|
||||
PromptSaveResult result = useCase.savePrompt(STUB_CONTENT);
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf(PromptSaveResult.Saved.class);
|
||||
assertThat(stubPromptPort.lastSavedContent).isEqualTo(STUB_CONTENT);
|
||||
assertThat(stubPromptPort.saveCallCount).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savePrompt_shouldDelegateToPromptPort_andReturnWriteFailed() {
|
||||
// Given
|
||||
stubPromptPort.saveResult = new PromptSaveResult.WriteFailed("Schreibfehler", null);
|
||||
|
||||
// When
|
||||
PromptSaveResult result = useCase.savePrompt(STUB_CONTENT);
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf(PromptSaveResult.WriteFailed.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void savePrompt_shouldThrowNullPointerException_whenContentIsNull() {
|
||||
assertThatThrownBy(() -> useCase.savePrompt(null))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageContaining("content");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// createDefaultPromptIfMissing
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void createDefaultPromptIfMissing_shouldDelegateToResourceCreationPort_andReturnApplied() {
|
||||
// Given
|
||||
CorrectionSuggestion.CreatePromptFile suggestion =
|
||||
new CorrectionSuggestion.CreatePromptFile(
|
||||
"/some/prompt.txt", "Standard anlegen", 60);
|
||||
CorrectionOutcome.Applied applied = new CorrectionOutcome.Applied(
|
||||
suggestion, "Standard-Prompt-Datei wurde angelegt.");
|
||||
stubResourceCreationPort.createPromptFileResult = applied;
|
||||
|
||||
// When
|
||||
CorrectionOutcome result = useCase.createDefaultPromptIfMissing(suggestion);
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf(CorrectionOutcome.Applied.class);
|
||||
assertThat(stubResourceCreationPort.createPromptFileCallCount).isEqualTo(1);
|
||||
assertThat(stubResourceCreationPort.lastSuggestion).isSameAs(suggestion);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDefaultPromptIfMissing_shouldThrowNullPointerException_whenSuggestionIsNull() {
|
||||
assertThatThrownBy(() -> useCase.createDefaultPromptIfMissing(null))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessageContaining("suggestion");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Test-Stubs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static class StubPromptPort implements PromptPort {
|
||||
PromptLoadingResult loadResult = new PromptLoadingSuccess(
|
||||
new PromptIdentifier("stub.txt"), "Stub-Inhalt");
|
||||
PromptSaveResult saveResult = new PromptSaveResult.Saved("/stub/path.txt");
|
||||
int loadCallCount = 0;
|
||||
int saveCallCount = 0;
|
||||
String lastSavedContent = null;
|
||||
|
||||
@Override
|
||||
public PromptLoadingResult loadPrompt() {
|
||||
loadCallCount++;
|
||||
return loadResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PromptSaveResult savePrompt(String content) {
|
||||
saveCallCount++;
|
||||
lastSavedContent = content;
|
||||
return saveResult;
|
||||
}
|
||||
}
|
||||
|
||||
private static class StubResourceCreationPort implements ResourceCreationPort {
|
||||
CorrectionOutcome createPromptFileResult = new CorrectionOutcome.Applied(
|
||||
new CorrectionSuggestion.CreatePromptFile("/stub.txt", "Stub", 60),
|
||||
"Angelegt.");
|
||||
int createPromptFileCallCount = 0;
|
||||
CorrectionSuggestion.CreatePromptFile lastSuggestion = null;
|
||||
|
||||
@Override
|
||||
public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory suggestion) {
|
||||
return new CorrectionOutcome.NotAttempted(suggestion, "Nicht implementiert im Stub.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile suggestion) {
|
||||
createPromptFileCallCount++;
|
||||
lastSuggestion = suggestion;
|
||||
return createPromptFileResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath suggestion) {
|
||||
return new CorrectionOutcome.NotAttempted(suggestion, "Nicht implementiert im Stub.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user