M5 komplett umgesetzt
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Carries the AI-related traceability data produced during an AI naming attempt.
|
||||
* <p>
|
||||
* This record aggregates the metadata required to persist full AI traceability in
|
||||
* the processing attempt history:
|
||||
* <ul>
|
||||
* <li>AI infrastructure details (model name, prompt identifier)</li>
|
||||
* <li>Request size metrics (processed pages, sent character count)</li>
|
||||
* <li>Raw AI output (for audit and diagnostics; stored in SQLite, not in log files)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* This context is produced whenever an AI call is attempted, regardless of whether
|
||||
* the call succeeded or failed. Fields that could not be determined (e.g. raw response
|
||||
* on connection failure) may be {@code null}.
|
||||
*
|
||||
* @param modelName the AI model name used in the request; never null
|
||||
* @param promptIdentifier stable identifier of the prompt template; never null
|
||||
* @param processedPageCount number of PDF pages included in the extraction; must be >= 1
|
||||
* @param sentCharacterCount number of document-text characters sent to the AI; must be >= 0
|
||||
* @param aiRawResponse the complete raw AI response body; {@code null} if the call did
|
||||
* not return a response body (e.g. timeout or connection error)
|
||||
*/
|
||||
public record AiAttemptContext(
|
||||
String modelName,
|
||||
String promptIdentifier,
|
||||
int processedPageCount,
|
||||
int sentCharacterCount,
|
||||
String aiRawResponse) {
|
||||
|
||||
/**
|
||||
* Compact constructor validating mandatory fields.
|
||||
*
|
||||
* @throws NullPointerException if {@code modelName} or {@code promptIdentifier} is null
|
||||
* @throws IllegalArgumentException if {@code processedPageCount} < 1 or
|
||||
* {@code sentCharacterCount} < 0
|
||||
*/
|
||||
public AiAttemptContext {
|
||||
Objects.requireNonNull(modelName, "modelName must not be null");
|
||||
Objects.requireNonNull(promptIdentifier, "promptIdentifier must not be null");
|
||||
if (processedPageCount < 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"processedPageCount must be >= 1, but was: " + processedPageCount);
|
||||
}
|
||||
if (sentCharacterCount < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"sentCharacterCount must be >= 0, but was: " + sentCharacterCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Outcome indicating a deterministic functional (content) failure in the AI naming pipeline.
|
||||
* <p>
|
||||
* Functional failures occur when the AI returns a structurally valid response but the
|
||||
* content violates the applicable fachliche rules, for example:
|
||||
* <ul>
|
||||
* <li>Title exceeds 20 characters</li>
|
||||
* <li>Title contains prohibited special characters</li>
|
||||
* <li>Title is a generic placeholder (e.g., "Dokument", "Scan")</li>
|
||||
* <li>AI-provided date is present but not a valid YYYY-MM-DD string</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* These failures are deterministic: retrying the same document against the same AI
|
||||
* and prompt is unlikely to resolve the issue without a document or prompt change.
|
||||
* The content error counter is incremented, and the standard one-retry rule applies.
|
||||
*
|
||||
* @param candidate the source document candidate; never null
|
||||
* @param errorMessage human-readable description of the validation failure; never null
|
||||
* @param aiContext AI traceability context for the attempt record; never null
|
||||
*/
|
||||
public record AiFunctionalFailure(
|
||||
SourceDocumentCandidate candidate,
|
||||
String errorMessage,
|
||||
AiAttemptContext aiContext) implements DocumentProcessingOutcome {
|
||||
|
||||
/**
|
||||
* Compact constructor validating mandatory fields.
|
||||
*
|
||||
* @throws NullPointerException if any field is null
|
||||
*/
|
||||
public AiFunctionalFailure {
|
||||
Objects.requireNonNull(candidate, "candidate must not be null");
|
||||
Objects.requireNonNull(errorMessage, "errorMessage must not be null");
|
||||
Objects.requireNonNull(aiContext, "aiContext must not be null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Outcome indicating a transient technical failure during the AI naming pipeline.
|
||||
* <p>
|
||||
* Technical failures include:
|
||||
* <ul>
|
||||
* <li>AI service not reachable</li>
|
||||
* <li>HTTP timeout</li>
|
||||
* <li>Connection error</li>
|
||||
* <li>Unparseable or structurally invalid AI response (missing mandatory fields, invalid JSON)</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* These failures are retryable. The transient error counter is incremented.
|
||||
*
|
||||
* @param candidate the source document candidate; never null
|
||||
* @param errorMessage human-readable description of the failure; never null
|
||||
* @param cause the underlying exception, or {@code null} if not applicable
|
||||
* @param aiContext AI traceability context captured before or during the failure; never null
|
||||
*/
|
||||
public record AiTechnicalFailure(
|
||||
SourceDocumentCandidate candidate,
|
||||
String errorMessage,
|
||||
Throwable cause,
|
||||
AiAttemptContext aiContext) implements DocumentProcessingOutcome {
|
||||
|
||||
/**
|
||||
* Compact constructor validating mandatory fields.
|
||||
*
|
||||
* @throws NullPointerException if {@code candidate}, {@code errorMessage}, or
|
||||
* {@code aiContext} is null
|
||||
*/
|
||||
public AiTechnicalFailure {
|
||||
Objects.requireNonNull(candidate, "candidate must not be null");
|
||||
Objects.requireNonNull(errorMessage, "errorMessage must not be null");
|
||||
Objects.requireNonNull(aiContext, "aiContext must not be null");
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
* </ul>
|
||||
*/
|
||||
public sealed interface DocumentProcessingOutcome
|
||||
permits PreCheckPassed, PreCheckFailed, TechnicalDocumentError {
|
||||
permits PreCheckPassed, PreCheckFailed, TechnicalDocumentError,
|
||||
NamingProposalReady, AiTechnicalFailure, AiFunctionalFailure {
|
||||
// Marker interface; concrete implementations define structure
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Outcome indicating that an AI naming pipeline completed successfully and produced
|
||||
* a validated naming proposal ready for persistence.
|
||||
* <p>
|
||||
* This outcome is returned when:
|
||||
* <ol>
|
||||
* <li>PDF text extraction and pre-checks passed.</li>
|
||||
* <li>The AI was invoked and returned a parseable response.</li>
|
||||
* <li>The response passed all semantic validation rules.</li>
|
||||
* <li>A {@link NamingProposal} was produced.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* The document master record will be updated to {@link ProcessingStatus#PROPOSAL_READY};
|
||||
* a physical target copy is not yet produced at this stage.
|
||||
*
|
||||
* @param candidate the source document candidate; never null
|
||||
* @param proposal the validated naming proposal ready for persistence; never null
|
||||
* @param aiContext AI traceability data required for the processing attempt record; never null
|
||||
*/
|
||||
public record NamingProposalReady(
|
||||
SourceDocumentCandidate candidate,
|
||||
NamingProposal proposal,
|
||||
AiAttemptContext aiContext) implements DocumentProcessingOutcome {
|
||||
|
||||
/**
|
||||
* Compact constructor validating all fields.
|
||||
*
|
||||
* @throws NullPointerException if any field is null
|
||||
*/
|
||||
public NamingProposalReady {
|
||||
Objects.requireNonNull(candidate, "candidate must not be null");
|
||||
Objects.requireNonNull(proposal, "proposal must not be null");
|
||||
Objects.requireNonNull(aiContext, "aiContext must not be null");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package de.gecheckt.pdf.umbenenner.domain.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for AI-related domain types:
|
||||
* {@link ParsedAiResponse}, {@link AiRequestRepresentation},
|
||||
* {@link AiResponseParsingFailure}, {@link AiResponseParsingSuccess},
|
||||
* and {@link AiErrorClassification}.
|
||||
*/
|
||||
class AiResponseTypesTest {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ParsedAiResponse
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_withDate_fieldsAreSet() {
|
||||
var parsed = new ParsedAiResponse("Rechnung", "AI reasoning", Optional.of("2026-01-15"));
|
||||
|
||||
assertEquals("Rechnung", parsed.title());
|
||||
assertEquals("AI reasoning", parsed.reasoning());
|
||||
assertTrue(parsed.dateString().isPresent());
|
||||
assertEquals("2026-01-15", parsed.dateString().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_withoutDate_dateStringIsEmpty() {
|
||||
var parsed = new ParsedAiResponse("Rechnung", "AI reasoning", Optional.empty());
|
||||
|
||||
assertFalse(parsed.dateString().isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_factoryMethod_withDate_wrapsDate() {
|
||||
var parsed = ParsedAiResponse.of("Vertrag", "reasoning", "2026-03-01");
|
||||
|
||||
assertEquals("Vertrag", parsed.title());
|
||||
assertTrue(parsed.dateString().isPresent());
|
||||
assertEquals("2026-03-01", parsed.dateString().get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_factoryMethod_withNullDate_producesEmpty() {
|
||||
var parsed = ParsedAiResponse.of("Vertrag", "reasoning", null);
|
||||
|
||||
assertFalse(parsed.dateString().isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_nullTitle_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new ParsedAiResponse(null, "reasoning", Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_nullReasoning_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new ParsedAiResponse("title", null, Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsedAiResponse_nullDateString_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new ParsedAiResponse("title", "reasoning", null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiRequestRepresentation
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_validConstruction_fieldsAreSet() {
|
||||
var promptId = new PromptIdentifier("prompt-v1");
|
||||
var repr = new AiRequestRepresentation(promptId, "Prompt text", "Document content", 16);
|
||||
|
||||
assertEquals(promptId, repr.promptIdentifier());
|
||||
assertEquals("Prompt text", repr.promptContent());
|
||||
assertEquals("Document content", repr.documentText());
|
||||
assertEquals(16, repr.sentCharacterCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_zeroSentCharacterCount_isValid() {
|
||||
var promptId = new PromptIdentifier("p");
|
||||
var repr = new AiRequestRepresentation(promptId, "prompt", "text", 0);
|
||||
assertEquals(0, repr.sentCharacterCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_sentCharCountEqualsTextLength_isValid() {
|
||||
var promptId = new PromptIdentifier("p");
|
||||
String text = "hello";
|
||||
var repr = new AiRequestRepresentation(promptId, "prompt", text, text.length());
|
||||
assertEquals(text.length(), repr.sentCharacterCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_negativeSentCharCount_throwsIllegalArgument() {
|
||||
var promptId = new PromptIdentifier("p");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new AiRequestRepresentation(promptId, "prompt", "text", -1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_sentCharCountExceedsTextLength_throwsIllegalArgument() {
|
||||
var promptId = new PromptIdentifier("p");
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new AiRequestRepresentation(promptId, "prompt", "short", 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_nullPromptIdentifier_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiRequestRepresentation(null, "prompt", "text", 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_nullPromptContent_throwsNPE() {
|
||||
var promptId = new PromptIdentifier("p");
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiRequestRepresentation(promptId, null, "text", 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiRequestRepresentation_nullDocumentText_throwsNPE() {
|
||||
var promptId = new PromptIdentifier("p");
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiRequestRepresentation(promptId, "prompt", null, 0));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiResponseParsingFailure
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiResponseParsingFailure_fieldsAreSet() {
|
||||
var failure = new AiResponseParsingFailure("MISSING_TITLE", "AI response missing mandatory field 'title'");
|
||||
|
||||
assertEquals("MISSING_TITLE", failure.failureReason());
|
||||
assertEquals("AI response missing mandatory field 'title'", failure.failureMessage());
|
||||
assertInstanceOf(AiResponseParsingResult.class, failure);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiResponseParsingFailure_nullFailureReason_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiResponseParsingFailure(null, "message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiResponseParsingFailure_nullFailureMessage_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiResponseParsingFailure("REASON", null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiResponseParsingSuccess
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiResponseParsingSuccess_fieldsAreSet() {
|
||||
var parsed = ParsedAiResponse.of("Rechnung", "reasoning", "2026-01-01");
|
||||
var success = new AiResponseParsingSuccess(parsed);
|
||||
|
||||
assertEquals(parsed, success.response());
|
||||
assertInstanceOf(AiResponseParsingResult.class, success);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiResponseParsingSuccess_nullResponse_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiResponseParsingSuccess(null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiErrorClassification
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiErrorClassification_hasTwoValues() {
|
||||
AiErrorClassification[] values = AiErrorClassification.values();
|
||||
assertEquals(2, values.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiErrorClassification_technical_isEnumValue() {
|
||||
assertEquals(AiErrorClassification.TECHNICAL,
|
||||
AiErrorClassification.valueOf("TECHNICAL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiErrorClassification_functional_isEnumValue() {
|
||||
assertEquals(AiErrorClassification.FUNCTIONAL,
|
||||
AiErrorClassification.valueOf("FUNCTIONAL"));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -108,4 +109,166 @@ class DocumentProcessingOutcomeTest {
|
||||
assertInstanceOf(DocumentProcessingOutcome.class, outcome);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiAttemptContext
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiAttemptContext_validConstruction_fieldsAreSet() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 2, 500, "{\"title\":\"Test\"}");
|
||||
|
||||
assertEquals("gpt-4", ctx.modelName());
|
||||
assertEquals("prompt-v1", ctx.promptIdentifier());
|
||||
assertEquals(2, ctx.processedPageCount());
|
||||
assertEquals(500, ctx.sentCharacterCount());
|
||||
assertEquals("{\"title\":\"Test\"}", ctx.aiRawResponse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiAttemptContext_nullRawResponse_isAllowed() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 0, null);
|
||||
assertNull(ctx.aiRawResponse());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiAttemptContext_nullModelName_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiAttemptContext(null, "prompt-v1", 1, 0, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiAttemptContext_nullPromptIdentifier_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiAttemptContext("gpt-4", null, 1, 0, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiAttemptContext_zeroPageCount_throwsIllegalArgument() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new AiAttemptContext("gpt-4", "prompt-v1", 0, 0, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiAttemptContext_negativeCharCount_throwsIllegalArgument() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new AiAttemptContext("gpt-4", "prompt-v1", 1, -1, null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiFunctionalFailure
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiFunctionalFailure_validConstruction_fieldsAreSet() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 100, "{}");
|
||||
var failure = new AiFunctionalFailure(candidate, "Title too long", ctx);
|
||||
|
||||
assertEquals(candidate, failure.candidate());
|
||||
assertEquals("Title too long", failure.errorMessage());
|
||||
assertEquals(ctx, failure.aiContext());
|
||||
assertInstanceOf(DocumentProcessingOutcome.class, failure);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiFunctionalFailure_nullCandidate_throwsNPE() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiFunctionalFailure(null, "error", ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiFunctionalFailure_nullErrorMessage_throwsNPE() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiFunctionalFailure(candidate, null, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiFunctionalFailure_nullAiContext_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiFunctionalFailure(candidate, "error", null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AiTechnicalFailure
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void aiTechnicalFailure_validConstruction_fieldsAreSet() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 100, null);
|
||||
var cause = new RuntimeException("timeout");
|
||||
var failure = new AiTechnicalFailure(candidate, "HTTP timeout", cause, ctx);
|
||||
|
||||
assertEquals(candidate, failure.candidate());
|
||||
assertEquals("HTTP timeout", failure.errorMessage());
|
||||
assertEquals(cause, failure.cause());
|
||||
assertEquals(ctx, failure.aiContext());
|
||||
assertInstanceOf(DocumentProcessingOutcome.class, failure);
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiTechnicalFailure_nullCause_isAllowed() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
var failure = new AiTechnicalFailure(candidate, "error", null, ctx);
|
||||
assertNull(failure.cause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiTechnicalFailure_nullCandidate_throwsNPE() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiTechnicalFailure(null, "error", null, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiTechnicalFailure_nullErrorMessage_throwsNPE() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiTechnicalFailure(candidate, null, null, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aiTechnicalFailure_nullAiContext_throwsNPE() {
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new AiTechnicalFailure(candidate, "error", null, null));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// NamingProposalReady
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void namingProposalReady_validConstruction_fieldsAreSet() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "prompt-v1", 1, 100, "{\"title\":\"Rechnung\"}");
|
||||
var proposal = new NamingProposal(LocalDate.of(2026, 1, 15), DateSource.AI_PROVIDED, "Rechnung", "AI reasoning");
|
||||
var ready = new NamingProposalReady(candidate, proposal, ctx);
|
||||
|
||||
assertEquals(candidate, ready.candidate());
|
||||
assertEquals(proposal, ready.proposal());
|
||||
assertEquals(ctx, ready.aiContext());
|
||||
assertInstanceOf(DocumentProcessingOutcome.class, ready);
|
||||
}
|
||||
|
||||
@Test
|
||||
void namingProposalReady_nullCandidate_throwsNPE() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
var proposal = new NamingProposal(LocalDate.now(), DateSource.AI_PROVIDED, "Test", "r");
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new NamingProposalReady(null, proposal, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
void namingProposalReady_nullProposal_throwsNPE() {
|
||||
var ctx = new AiAttemptContext("gpt-4", "p", 1, 0, null);
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new NamingProposalReady(candidate, null, ctx));
|
||||
}
|
||||
|
||||
@Test
|
||||
void namingProposalReady_nullAiContext_throwsNPE() {
|
||||
var proposal = new NamingProposal(LocalDate.now(), DateSource.AI_PROVIDED, "Test", "r");
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> new NamingProposalReady(candidate, proposal, null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user